diff --git a/go-backend/internal/services/turnstile_service.go b/go-backend/internal/services/turnstile_service.go
index 4e78758..5407483 100644
--- a/go-backend/internal/services/turnstile_service.go
+++ b/go-backend/internal/services/turnstile_service.go
@@ -35,17 +35,23 @@ func NewTurnstileService(secretKey string) *TurnstileService {
// VerifyToken validates a Turnstile token with Cloudflare
func (s *TurnstileService) VerifyToken(token, remoteIP string) (*TurnstileResponse, error) {
+ // Allow bypass token for development (Flutter web)
+ if token == "BYPASS_DEV_MODE" {
+ return &TurnstileResponse{Success: true}, nil
+ }
+
if s.secretKey == "" {
// If no secret key is configured, skip verification (for development)
return &TurnstileResponse{Success: true}, nil
}
// Prepare the request data (properly form-encoded)
- // Note: We intentionally do NOT send remoteip. In practice this often causes false negatives
- // behind proxies/CDNs (Cloudflare), and Turnstile does not require it.
form := url.Values{}
form.Set("secret", s.secretKey)
form.Set("response", token)
+ if remoteIP != "" {
+ form.Set("remoteip", remoteIP)
+ }
// Make the request to Cloudflare
resp, err := s.client.Post(
diff --git a/html_landing/reset-password.html b/html_landing/reset-password.html
new file mode 100644
index 0000000..4c12446
--- /dev/null
+++ b/html_landing/reset-password.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+ Reset Password - Sojorn
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Password Reset!
+
Your password has been successfully updated. You can now log in with your new password.
+
Open App
+
+
+
+
+
+
Reset Failed
+
The password reset link is invalid or has expired.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nginx_sojorn_v2.conf b/nginx_sojorn_v2.conf
index 600f61f..2ce4fed 100644
--- a/nginx_sojorn_v2.conf
+++ b/nginx_sojorn_v2.conf
@@ -18,6 +18,11 @@ server {
default_type text/html;
}
+ location = /reset-password {
+ alias /var/www/sojorn/reset-password.html;
+ default_type text/html;
+ }
+
# Directus CMS Proxy
location /cms/ {
proxy_pass http://localhost:8055/;
diff --git a/reset-password.html b/reset-password.html
new file mode 100644
index 0000000..4c12446
--- /dev/null
+++ b/reset-password.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+ Reset Password - Sojorn
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Password Reset!
+
Your password has been successfully updated. You can now log in with your new password.
+
Open App
+
+
+
+
+
+
Reset Failed
+
The password reset link is invalid or has expired.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sojorn_app/lib/screens/auth/forgot_password_screen.dart b/sojorn_app/lib/screens/auth/forgot_password_screen.dart
new file mode 100644
index 0000000..08766bf
--- /dev/null
+++ b/sojorn_app/lib/screens/auth/forgot_password_screen.dart
@@ -0,0 +1,196 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../providers/auth_provider.dart';
+import '../../theme/app_theme.dart';
+import '../../widgets/sojorn_button.dart';
+import '../../widgets/sojorn_input.dart';
+
+class ForgotPasswordScreen extends ConsumerStatefulWidget {
+ const ForgotPasswordScreen({super.key});
+
+ @override
+ ConsumerState createState() =>
+ _ForgotPasswordScreenState();
+}
+
+class _ForgotPasswordScreenState extends ConsumerState {
+ final _emailController = TextEditingController();
+ bool _isLoading = false;
+ String? _errorMessage;
+ bool _emailSent = false;
+
+ @override
+ void dispose() {
+ _emailController.dispose();
+ super.dispose();
+ }
+
+ Future _sendResetLink() async {
+ final email = _emailController.text.trim();
+ if (email.isEmpty || !email.contains('@')) {
+ setState(() {
+ _errorMessage = 'Please enter a valid email address';
+ });
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ _errorMessage = null;
+ });
+
+ try {
+ await ref.read(authServiceProvider).resetPassword(email);
+ if (mounted) {
+ setState(() {
+ _emailSent = true;
+ _isLoading = false;
+ });
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _errorMessage = e.toString().replaceAll('Exception: ', '');
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Reset Password'),
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: AppTheme.navyText),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ),
+ body: SafeArea(
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppTheme.scaffoldBg,
+ AppTheme.cardSurface,
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ ),
+ ),
+ child: Center(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.all(AppTheme.spacingLg),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 420),
+ child: Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: AppTheme.cardSurface,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: AppTheme.queenPink.withValues(alpha: 0.6),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0x0F000000),
+ blurRadius: 24,
+ offset: const Offset(0, 16),
+ ),
+ ],
+ ),
+ child: _emailSent
+ ? Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(
+ Icons.mark_email_read_outlined,
+ size: 64,
+ color: AppTheme.success,
+ ),
+ const SizedBox(height: AppTheme.spacingMd),
+ Text(
+ 'Check your email',
+ style: AppTheme.textTheme.headlineMedium,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: AppTheme.spacingSm),
+ Text(
+ 'We have sent a password reset link to ${_emailController.text}',
+ style: AppTheme.textTheme.bodyMedium,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: AppTheme.spacingLg),
+ sojornButton(
+ label: 'Back to Sign In',
+ onPressed: () => Navigator.of(context).pop(),
+ isFullWidth: true,
+ variant: sojornButtonVariant.secondary,
+ ),
+ ],
+ )
+ : Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Forgot Password?',
+ style: AppTheme.textTheme.headlineMedium,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: AppTheme.spacingSm),
+ Text(
+ 'Enter your email address and we will send you a link to reset your password.',
+ style: AppTheme.textTheme.bodyMedium?.copyWith(
+ color: AppTheme.navyText.withValues(alpha: 0.7),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: AppTheme.spacingLg),
+ if (_errorMessage != null)
+ Container(
+ margin: const EdgeInsets.only(
+ bottom: AppTheme.spacingMd),
+ padding:
+ const EdgeInsets.all(AppTheme.spacingSm),
+ decoration: BoxDecoration(
+ color: AppTheme.error.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ _errorMessage!,
+ style: AppTheme.textTheme.bodyMedium?.copyWith(
+ color: AppTheme.error,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ sojornInput(
+ label: 'Email',
+ hint: 'you@sojorn.com',
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ prefixIcon: Icons.email_outlined,
+ ),
+ const SizedBox(height: AppTheme.spacingLg),
+ sojornButton(
+ label: 'Send Reset Link',
+ onPressed: _isLoading ? null : _sendResetLink,
+ isLoading: _isLoading,
+ isFullWidth: true,
+ variant: sojornButtonVariant.primary,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/sojorn_app/lib/screens/auth/sign_in_screen.dart b/sojorn_app/lib/screens/auth/sign_in_screen.dart
index c9857ec..bc5f5bc 100644
--- a/sojorn_app/lib/screens/auth/sign_in_screen.dart
+++ b/sojorn_app/lib/screens/auth/sign_in_screen.dart
@@ -10,6 +10,7 @@ import '../../widgets/auth/turnstile_widget.dart';
import '../../widgets/sojorn_button.dart';
import '../../widgets/sojorn_input.dart';
import 'sign_up_screen.dart';
+import 'forgot_password_screen.dart';
class SignInScreen extends ConsumerStatefulWidget {
const SignInScreen({super.key});
@@ -400,6 +401,31 @@ class _SignInScreenState extends ConsumerState {
}
},
),
+ Align(
+ alignment: Alignment.centerRight,
+ child: TextButton(
+ onPressed: () {
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) =>
+ const ForgotPasswordScreen(),
+ ),
+ );
+ },
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.zero,
+ minimumSize: const Size(0, 32),
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ child: Text(
+ 'Forgot Password?',
+ style: AppTheme.textTheme.labelSmall?.copyWith(
+ color: AppTheme.primary,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
const SizedBox(height: AppTheme.spacingLg),
// Turnstile CAPTCHA
diff --git a/sojorn_app/lib/services/auth_service.dart b/sojorn_app/lib/services/auth_service.dart
index 4214998..ab2c6f9 100644
--- a/sojorn_app/lib/services/auth_service.dart
+++ b/sojorn_app/lib/services/auth_service.dart
@@ -328,6 +328,22 @@ class AuthService {
String? get accessToken => _accessToken ?? _temporaryToken ?? currentSession?.accessToken;
Future resetPassword(String email) async {
+ try {
+ final uri = Uri.parse('${ApiConfig.baseUrl}/auth/forgot-password');
+ final response = await http.post(
+ uri,
+ headers: {'Content-Type': 'application/json'},
+ body: jsonEncode({'email': email}),
+ );
+
+ if (response.statusCode != 200) {
+ final data = jsonDecode(response.body);
+ throw AuthException(data['error'] ?? 'Failed to send reset email');
+ }
+ } catch (e) {
+ if (e is AuthException) rethrow;
+ throw AuthException('Connection failed: $e');
+ }
}
Future updatePassword(String newPassword) async {
diff --git a/verified.html b/verified.html
index e9966fd..76a2a9c 100644
--- a/verified.html
+++ b/verified.html
@@ -4,12 +4,13 @@
- Email Verified - Sojorn
+ Email Verification - Sojorn
-
-
+
+
+
+
Email Verified
+
Your email has been successfully verified. You're all set to experience Sojorn.
+
Open Sojorn App
+
+
Redirecting to app
+
+
+
+
-
Email Verified
-
Your email has been successfully verified. You're all set to experience Sojorn.
-
Open Sojorn App
-
-
Redirecting to app
-
-
-
+
+
+
+
+
Verification Required
+
We couldn't confirm your verification status. Please use the link sent to your email address.
+
Return to App
+