From 1de9997476e6a87d43a31a818d3f0bb854f01796 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Mon, 16 Feb 2026 13:06:00 -0600 Subject: [PATCH] feat(auth): Add password reset functionality (backend + app + web) & enhance Turnstile integration --- .../internal/services/turnstile_service.go | 10 +- html_landing/reset-password.html | 272 ++++++++++++++++++ nginx_sojorn_v2.conf | 5 + reset-password.html | 272 ++++++++++++++++++ .../screens/auth/forgot_password_screen.dart | 196 +++++++++++++ .../lib/screens/auth/sign_in_screen.dart | 26 ++ sojorn_app/lib/services/auth_service.dart | 16 ++ verified.html | 109 +++---- 8 files changed, 850 insertions(+), 56 deletions(-) create mode 100644 html_landing/reset-password.html create mode 100644 reset-password.html create mode 100644 sojorn_app/lib/screens/auth/forgot_password_screen.dart 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 + + + + + +
+
+ +
+

Reset Password

+

Enter your new password below.

+ +
+
+ + +
+
+ + +
Passwords do not match
+
+ +
+
+ + + + + + +
+ + + + + \ 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 + + + + + +
+
+ +
+

Reset Password

+

Enter your new password below.

+ +
+
+ + +
+
+ + +
Passwords do not match
+
+ +
+
+ + + + + + +
+ + + + + \ 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 -
-
-
+ + +
+
+ + + +
+

Verification Required

+

We couldn't confirm your verification status. Please use the link sent to your email address.

+ Return to App
+