mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Added mechanism of required password change of new user's first login (#272)
* Deprecate login scenarios that support pre-web era * refactor and simplify setup * Added user info to change password form * change isFistLogin column to shouldChangePassword * Implemented change user password * Implement the change password page for mobile * Change label * Added changes log and up minor version * Fixed typo in the release note * Up server version
This commit is contained in:
		| @@ -0,0 +1,3 @@ | ||||
| * Fixed app does not resume back up when reopening a closed app | ||||
| * Fixed wrong asset count on the upload page | ||||
| * Added mechanism to change the password of new user on the first login (except Admin) | ||||
| @@ -19,7 +19,7 @@ platform :ios do | ||||
|   desc "iOS Beta" | ||||
|   lane :beta do | ||||
|     increment_version_number( | ||||
|       version_number: "1.13.0" | ||||
|       version_number: "1.14.0" | ||||
|     ) | ||||
|     increment_build_number( | ||||
|       build_number: latest_testflight_build_number + 1, | ||||
|   | ||||
| @@ -191,7 +191,7 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|                 ), | ||||
|                 onTap: () async { | ||||
|                   bool res = | ||||
|                       await ref.read(authenticationProvider.notifier).logout(); | ||||
|                       await ref.watch(authenticationProvider.notifier).logout(); | ||||
|  | ||||
|                   if (res) { | ||||
|                     ref.watch(backupProvider.notifier).cancelBackup(); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class AuthenticationState { | ||||
|   final String firstName; | ||||
|   final String lastName; | ||||
|   final bool isAdmin; | ||||
|   final bool isFirstLogin; | ||||
|   final bool shouldChangePassword; | ||||
|   final String profileImagePath; | ||||
|   final DeviceInfoRemote deviceInfo; | ||||
|  | ||||
| @@ -24,7 +24,7 @@ class AuthenticationState { | ||||
|     required this.firstName, | ||||
|     required this.lastName, | ||||
|     required this.isAdmin, | ||||
|     required this.isFirstLogin, | ||||
|     required this.shouldChangePassword, | ||||
|     required this.profileImagePath, | ||||
|     required this.deviceInfo, | ||||
|   }); | ||||
| @@ -38,7 +38,7 @@ class AuthenticationState { | ||||
|     String? firstName, | ||||
|     String? lastName, | ||||
|     bool? isAdmin, | ||||
|     bool? isFirstLoggedIn, | ||||
|     bool? shouldChangePassword, | ||||
|     String? profileImagePath, | ||||
|     DeviceInfoRemote? deviceInfo, | ||||
|   }) { | ||||
| @@ -51,17 +51,12 @@ class AuthenticationState { | ||||
|       firstName: firstName ?? this.firstName, | ||||
|       lastName: lastName ?? this.lastName, | ||||
|       isAdmin: isAdmin ?? this.isAdmin, | ||||
|       isFirstLogin: isFirstLoggedIn ?? isFirstLogin, | ||||
|       shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword, | ||||
|       profileImagePath: profileImagePath ?? this.profileImagePath, | ||||
|       deviceInfo: deviceInfo ?? this.deviceInfo, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)'; | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     final result = <String, dynamic>{}; | ||||
|  | ||||
| @@ -73,7 +68,7 @@ class AuthenticationState { | ||||
|     result.addAll({'firstName': firstName}); | ||||
|     result.addAll({'lastName': lastName}); | ||||
|     result.addAll({'isAdmin': isAdmin}); | ||||
|     result.addAll({'isFirstLogin': isFirstLogin}); | ||||
|     result.addAll({'shouldChangePassword': shouldChangePassword}); | ||||
|     result.addAll({'profileImagePath': profileImagePath}); | ||||
|     result.addAll({'deviceInfo': deviceInfo.toMap()}); | ||||
|  | ||||
| @@ -90,7 +85,7 @@ class AuthenticationState { | ||||
|       firstName: map['firstName'] ?? '', | ||||
|       lastName: map['lastName'] ?? '', | ||||
|       isAdmin: map['isAdmin'] ?? false, | ||||
|       isFirstLogin: map['isFirstLogin'] ?? false, | ||||
|       shouldChangePassword: map['shouldChangePassword'] ?? false, | ||||
|       profileImagePath: map['profileImagePath'] ?? '', | ||||
|       deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']), | ||||
|     ); | ||||
| @@ -101,6 +96,11 @@ class AuthenticationState { | ||||
|   factory AuthenticationState.fromJson(String source) => | ||||
|       AuthenticationState.fromMap(json.decode(source)); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
| @@ -114,7 +114,7 @@ class AuthenticationState { | ||||
|         other.firstName == firstName && | ||||
|         other.lastName == lastName && | ||||
|         other.isAdmin == isAdmin && | ||||
|         other.isFirstLogin == isFirstLogin && | ||||
|         other.shouldChangePassword == shouldChangePassword && | ||||
|         other.profileImagePath == profileImagePath && | ||||
|         other.deviceInfo == deviceInfo; | ||||
|   } | ||||
| @@ -129,7 +129,7 @@ class AuthenticationState { | ||||
|         firstName.hashCode ^ | ||||
|         lastName.hashCode ^ | ||||
|         isAdmin.hashCode ^ | ||||
|         isFirstLogin.hashCode ^ | ||||
|         shouldChangePassword.hashCode ^ | ||||
|         profileImagePath.hashCode ^ | ||||
|         deviceInfo.hashCode; | ||||
|   } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ class LogInReponse { | ||||
|   final String lastName; | ||||
|   final String profileImagePath; | ||||
|   final bool isAdmin; | ||||
|   final bool isFirstLogin; | ||||
|   final bool shouldChangePassword; | ||||
|  | ||||
|   LogInReponse({ | ||||
|     required this.accessToken, | ||||
| @@ -18,7 +18,7 @@ class LogInReponse { | ||||
|     required this.lastName, | ||||
|     required this.profileImagePath, | ||||
|     required this.isAdmin, | ||||
|     required this.isFirstLogin, | ||||
|     required this.shouldChangePassword, | ||||
|   }); | ||||
|  | ||||
|   LogInReponse copyWith({ | ||||
| @@ -29,7 +29,7 @@ class LogInReponse { | ||||
|     String? lastName, | ||||
|     String? profileImagePath, | ||||
|     bool? isAdmin, | ||||
|     bool? isFirstLogin, | ||||
|     bool? shouldChangePassword, | ||||
|   }) { | ||||
|     return LogInReponse( | ||||
|       accessToken: accessToken ?? this.accessToken, | ||||
| @@ -39,7 +39,7 @@ class LogInReponse { | ||||
|       lastName: lastName ?? this.lastName, | ||||
|       profileImagePath: profileImagePath ?? this.profileImagePath, | ||||
|       isAdmin: isAdmin ?? this.isAdmin, | ||||
|       isFirstLogin: isFirstLogin ?? this.isFirstLogin, | ||||
|       shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -53,7 +53,7 @@ class LogInReponse { | ||||
|     result.addAll({'lastName': lastName}); | ||||
|     result.addAll({'profileImagePath': profileImagePath}); | ||||
|     result.addAll({'isAdmin': isAdmin}); | ||||
|     result.addAll({'isFirstLogin': isFirstLogin}); | ||||
|     result.addAll({'shouldChangePassword': shouldChangePassword}); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
| @@ -67,7 +67,7 @@ class LogInReponse { | ||||
|       lastName: map['lastName'] ?? '', | ||||
|       profileImagePath: map['profileImagePath'] ?? '', | ||||
|       isAdmin: map['isAdmin'] ?? false, | ||||
|       isFirstLogin: map['isFirstLogin'] ?? false, | ||||
|       shouldChangePassword: map['shouldChangePassword'] ?? false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -78,7 +78,7 @@ class LogInReponse { | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)'; | ||||
|     return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -93,7 +93,7 @@ class LogInReponse { | ||||
|         other.lastName == lastName && | ||||
|         other.profileImagePath == profileImagePath && | ||||
|         other.isAdmin == isAdmin && | ||||
|         other.isFirstLogin == isFirstLogin; | ||||
|         other.shouldChangePassword == shouldChangePassword; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -105,6 +105,6 @@ class LogInReponse { | ||||
|         lastName.hashCode ^ | ||||
|         profileImagePath.hashCode ^ | ||||
|         isAdmin.hashCode ^ | ||||
|         isFirstLogin.hashCode; | ||||
|         shouldChangePassword.hashCode; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -24,7 +24,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|             lastName: '', | ||||
|             profileImagePath: '', | ||||
|             isAdmin: false, | ||||
|             isFirstLogin: false, | ||||
|             shouldChangePassword: false, | ||||
|             isAuthenticated: false, | ||||
|             deviceInfo: DeviceInfoRemote( | ||||
|               id: 0, | ||||
| @@ -87,7 +87,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|         lastName: payload.lastName, | ||||
|         profileImagePath: payload.profileImagePath, | ||||
|         isAdmin: payload.isAdmin, | ||||
|         isFirstLoggedIn: payload.isFirstLogin, | ||||
|         shouldChangePassword: payload.shouldChangePassword, | ||||
|       ); | ||||
|  | ||||
|       if (isSavedLoginInfo) { | ||||
| @@ -112,7 +112,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|     try { | ||||
|       Response res = await _networkService.postRequest( | ||||
|         url: 'device-info', | ||||
|           data: {'deviceId': state.deviceId, 'deviceType': state.deviceType}); | ||||
|         data: { | ||||
|           'deviceId': state.deviceId, | ||||
|           'deviceType': state.deviceType, | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
|       DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString()); | ||||
|       state = state.copyWith(deviceInfo: deviceInfo); | ||||
| @@ -133,7 +137,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|       firstName: '', | ||||
|       lastName: '', | ||||
|       profileImagePath: '', | ||||
|       isFirstLogin: false, | ||||
|       shouldChangePassword: false, | ||||
|       isAuthenticated: false, | ||||
|       isAdmin: false, | ||||
|       deviceInfo: DeviceInfoRemote( | ||||
| @@ -163,6 +167,24 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|   updateUserProfileImagePath(String path) { | ||||
|     state = state.copyWith(profileImagePath: path); | ||||
|   } | ||||
|  | ||||
|   Future<bool> changePassword(String newPassword) async { | ||||
|     Response res = await _networkService.putRequest( | ||||
|       url: 'user', | ||||
|       data: { | ||||
|         'id': state.userId, | ||||
|         'password': newPassword, | ||||
|         'shouldChangePassword': false, | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     if (res.statusCode == 200) { | ||||
|       state = state.copyWith(shouldChangePassword: false); | ||||
|       return true; | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| final authenticationProvider = | ||||
|   | ||||
							
								
								
									
										160
									
								
								mobile/lib/modules/login/ui/change_password_form.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								mobile/lib/modules/login/ui/change_password_form.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | ||||
|  | ||||
| class ChangePasswordForm extends HookConsumerWidget { | ||||
|   const ChangePasswordForm({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final passwordController = | ||||
|         useTextEditingController.fromValue(TextEditingValue.empty); | ||||
|     final confirmPasswordController = | ||||
|         useTextEditingController.fromValue(TextEditingValue.empty); | ||||
|     final authState = ref.watch(authenticationProvider); | ||||
|  | ||||
|     return Center( | ||||
|       child: ConstrainedBox( | ||||
|         constraints: const BoxConstraints(maxWidth: 300), | ||||
|         child: SingleChildScrollView( | ||||
|           child: Wrap( | ||||
|             spacing: 16, | ||||
|             runSpacing: 16, | ||||
|             alignment: WrapAlignment.start, | ||||
|             children: [ | ||||
|               Text( | ||||
|                 'Change Password', | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 24, | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                   color: Theme.of(context).primaryColor, | ||||
|                 ), | ||||
|               ), | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.symmetric(vertical: 24.0), | ||||
|                 child: Text( | ||||
|                   'Hi ${authState.firstName} ${authState.lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.', | ||||
|                   style: TextStyle( | ||||
|                     fontSize: 14, | ||||
|                     color: Colors.grey[700], | ||||
|                     fontWeight: FontWeight.w600, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               PasswordInput(controller: passwordController), | ||||
|               ConfirmPasswordInput( | ||||
|                 originalController: passwordController, | ||||
|                 confirmController: confirmPasswordController, | ||||
|               ), | ||||
|               Align( | ||||
|                 alignment: Alignment.center, | ||||
|                 child: ChangePasswordButton( | ||||
|                     passwordController: passwordController), | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PasswordInput extends StatelessWidget { | ||||
|   final TextEditingController controller; | ||||
|  | ||||
|   const PasswordInput({Key? key, required this.controller}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return TextFormField( | ||||
|       obscureText: true, | ||||
|       controller: controller, | ||||
|       decoration: const InputDecoration( | ||||
|         labelText: 'New Password', | ||||
|         border: OutlineInputBorder(), | ||||
|         hintText: 'New Password', | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ConfirmPasswordInput extends StatelessWidget { | ||||
|   final TextEditingController originalController; | ||||
|   final TextEditingController confirmController; | ||||
|  | ||||
|   const ConfirmPasswordInput({ | ||||
|     Key? key, | ||||
|     required this.originalController, | ||||
|     required this.confirmController, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   String? _validateInput(String? email) { | ||||
|     if (confirmController.value != originalController.value) { | ||||
|       return 'Passwords do not match'; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return TextFormField( | ||||
|       obscureText: true, | ||||
|       controller: confirmController, | ||||
|       decoration: const InputDecoration( | ||||
|         labelText: 'Confirm Password', | ||||
|         hintText: 'Re-enter New Password', | ||||
|         border: OutlineInputBorder(), | ||||
|       ), | ||||
|       validator: _validateInput, | ||||
|       autovalidateMode: AutovalidateMode.always, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ChangePasswordButton extends ConsumerWidget { | ||||
|   final TextEditingController passwordController; | ||||
|  | ||||
|   const ChangePasswordButton({ | ||||
|     Key? key, | ||||
|     required this.passwordController, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return ElevatedButton( | ||||
|         style: ElevatedButton.styleFrom( | ||||
|           visualDensity: VisualDensity.standard, | ||||
|           primary: Theme.of(context).primaryColor, | ||||
|           onPrimary: Colors.grey[50], | ||||
|           elevation: 2, | ||||
|           padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), | ||||
|         ), | ||||
|         onPressed: () async { | ||||
|           var isSuccess = await ref | ||||
|               .watch(authenticationProvider.notifier) | ||||
|               .changePassword(passwordController.value.text); | ||||
|  | ||||
|           if (isSuccess) { | ||||
|             bool res = | ||||
|                 await ref.watch(authenticationProvider.notifier).logout(); | ||||
|  | ||||
|             if (res) { | ||||
|               ref.watch(backupProvider.notifier).cancelBackup(); | ||||
|               ref.watch(assetProvider.notifier).clearAllAsset(); | ||||
|               ref.watch(websocketProvider.notifier).disconnect(); | ||||
|               AutoRouter.of(context).replace(const LoginRoute()); | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         child: const Text( | ||||
|           "Change Password", | ||||
|           style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||
|         )); | ||||
|   } | ||||
| } | ||||
| @@ -5,6 +5,7 @@ import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| @@ -20,7 +21,7 @@ class LoginForm extends HookConsumerWidget { | ||||
|     final passwordController = | ||||
|         useTextEditingController.fromValue(TextEditingValue.empty); | ||||
|     final serverEndpointController = | ||||
|         useTextEditingController(text: 'http://your-server-ip:2283'); | ||||
|         useTextEditingController(text: 'http://your-server-ip:2283/api'); | ||||
|     final isSaveLoginInfo = useState<bool>(false); | ||||
|  | ||||
|     useEffect(() { | ||||
| @@ -106,9 +107,18 @@ class ServerEndpointInput extends StatelessWidget { | ||||
|       : super(key: key); | ||||
|  | ||||
|   String? _validateInput(String? url) { | ||||
|     if (url == null) return null; | ||||
|     if (!url.startsWith(RegExp(r'https?://'))) | ||||
|     if (url == null) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     if (url.isEmpty) { | ||||
|       return 'Server endpoint is required'; | ||||
|     } | ||||
|  | ||||
|     if (!url.startsWith(RegExp(r'https?://'))) { | ||||
|       return 'Please specify http:// or https://'; | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| @@ -119,7 +129,8 @@ class ServerEndpointInput extends StatelessWidget { | ||||
|       decoration: const InputDecoration( | ||||
|         labelText: 'Server Endpoint URL', | ||||
|         border: OutlineInputBorder(), | ||||
|           hintText: 'http://your-server-ip:port'), | ||||
|         hintText: 'http://your-server-ip:port', | ||||
|       ), | ||||
|       validator: _validateInput, | ||||
|       autovalidateMode: AutovalidateMode.always, | ||||
|     ); | ||||
| @@ -146,7 +157,8 @@ class EmailInput extends StatelessWidget { | ||||
|       decoration: const InputDecoration( | ||||
|         labelText: 'Email', | ||||
|         border: OutlineInputBorder(), | ||||
|           hintText: 'youremail@email.com'), | ||||
|         hintText: 'youremail@email.com', | ||||
|       ), | ||||
|       validator: _validateInput, | ||||
|       autovalidateMode: AutovalidateMode.always, | ||||
|     ); | ||||
| @@ -200,14 +212,19 @@ class LoginButton extends ConsumerWidget { | ||||
|           ref.watch(assetProvider.notifier).clearAllAsset(); | ||||
|  | ||||
|           var isAuthenticated = await ref | ||||
|               .read(authenticationProvider.notifier) | ||||
|               .watch(authenticationProvider.notifier) | ||||
|               .login(emailController.text, passwordController.text, | ||||
|                   serverEndpointController.text, isSavedLoginInfo); | ||||
|  | ||||
|           if (isAuthenticated) { | ||||
|             // Resume backup (if enable) then navigate | ||||
|  | ||||
|             if (ref.watch(authenticationProvider).shouldChangePassword) { | ||||
|               AutoRouter.of(context).push(const ChangePasswordRoute()); | ||||
|             } else { | ||||
|               ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|               AutoRouter.of(context).pushNamed("/tab-controller-page"); | ||||
|             } | ||||
|           } else { | ||||
|             ImmichToast.show( | ||||
|               context: context, | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/lib/modules/login/views/change_password_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								mobile/lib/modules/login/views/change_password_page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/login/ui/change_password_form.dart'; | ||||
|  | ||||
| class ChangePasswordPage extends HookConsumerWidget { | ||||
|   const ChangePasswordPage({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return const Scaffold( | ||||
|       body: ChangePasswordForm(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; | ||||
| import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; | ||||
| import 'package:immich_mobile/modules/login/views/change_password_page.dart'; | ||||
| import 'package:immich_mobile/modules/login/views/login_page.dart'; | ||||
| import 'package:immich_mobile/modules/home/views/home_page.dart'; | ||||
| import 'package:immich_mobile/modules/search/views/search_page.dart'; | ||||
| @@ -30,6 +31,7 @@ part 'router.gr.dart'; | ||||
|   routes: <AutoRoute>[ | ||||
|     AutoRoute(page: SplashScreenPage, initial: true), | ||||
|     AutoRoute(page: LoginPage), | ||||
|     AutoRoute(page: ChangePasswordPage), | ||||
|     CustomRoute( | ||||
|       page: TabControllerPage, | ||||
|       guards: [AuthGuard], | ||||
|   | ||||
| @@ -29,6 +29,10 @@ class _$AppRouter extends RootStackRouter { | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, child: const LoginPage()); | ||||
|     }, | ||||
|     ChangePasswordRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, child: const ChangePasswordPage()); | ||||
|     }, | ||||
|     TabControllerRoute.name: (routeData) { | ||||
|       return CustomPage<dynamic>( | ||||
|           routeData: routeData, | ||||
| @@ -131,6 +135,7 @@ class _$AppRouter extends RootStackRouter { | ||||
|   List<RouteConfig> get routes => [ | ||||
|         RouteConfig(SplashScreenRoute.name, path: '/'), | ||||
|         RouteConfig(LoginRoute.name, path: '/login-page'), | ||||
|         RouteConfig(ChangePasswordRoute.name, path: '/change-password-page'), | ||||
|         RouteConfig(TabControllerRoute.name, | ||||
|             path: '/tab-controller-page', | ||||
|             guards: [ | ||||
| @@ -192,6 +197,15 @@ class LoginRoute extends PageRouteInfo<void> { | ||||
|   static const String name = 'LoginRoute'; | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [ChangePasswordPage] | ||||
| class ChangePasswordRoute extends PageRouteInfo<void> { | ||||
|   const ChangePasswordRoute() | ||||
|       : super(ChangePasswordRoute.name, path: '/change-password-page'); | ||||
|  | ||||
|   static const String name = 'ChangePasswordRoute'; | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [TabControllerPage] | ||||
| class TabControllerRoute extends PageRouteInfo<void> { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ name: immich_mobile | ||||
| description: Immich - selfhosted backup media file on mobile phone | ||||
|  | ||||
| publish_to: "none" | ||||
| version: 1.13.0+20 | ||||
| version: 1.14.0+21 | ||||
|  | ||||
| environment: | ||||
|   sdk: ">=2.17.0 <3.0.0" | ||||
|   | ||||
| @@ -30,7 +30,7 @@ export class AuthService { | ||||
|           'lastName', | ||||
|           'isAdmin', | ||||
|           'profileImagePath', | ||||
|           'isFirstLoggedIn', | ||||
|           'shouldChangePassword', | ||||
|         ], | ||||
|       }, | ||||
|     ); | ||||
| @@ -66,7 +66,7 @@ export class AuthService { | ||||
|       lastName: validatedUser.lastName, | ||||
|       isAdmin: validatedUser.isAdmin, | ||||
|       profileImagePath: validatedUser.profileImagePath, | ||||
|       isFirstLogin: validatedUser.isFirstLoggedIn, | ||||
|       shouldChangePassword: validatedUser.shouldChangePassword, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export class CreateUserDto { | ||||
|   isAdmin?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   isFirstLoggedIn?: boolean; | ||||
|   shouldChangePassword?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   id?: string; | ||||
|   | ||||
| @@ -32,6 +32,12 @@ export class UserController { | ||||
|     return await this.userService.getAllUsers(authUser, isAll); | ||||
|   } | ||||
|  | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @Get('me') | ||||
|   async getUserInfo(@GetAuthUser() authUser: AuthUserDto) { | ||||
|     return await this.userService.getUserInfo(authUser); | ||||
|   } | ||||
|  | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @UseGuards(AdminRolesGuard) | ||||
|   @Post() | ||||
|   | ||||
| @@ -37,6 +37,10 @@ export class UserService { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async getUserInfo(authUser: AuthUserDto) { | ||||
|     return this.userRepository.findOne({ id: authUser.id }); | ||||
|   } | ||||
|  | ||||
|   async getUserCount(isAdmin: boolean) { | ||||
|     let users; | ||||
|  | ||||
| @@ -89,7 +93,8 @@ export class UserService { | ||||
|     user.lastName = updateUserDto.lastName || user.lastName; | ||||
|     user.firstName = updateUserDto.firstName || user.firstName; | ||||
|     user.profileImagePath = updateUserDto.profileImagePath || user.profileImagePath; | ||||
|     user.isFirstLoggedIn = updateUserDto.isFirstLoggedIn || user.isFirstLoggedIn; | ||||
|     user.shouldChangePassword = | ||||
|       updateUserDto.shouldChangePassword != undefined ? updateUserDto.shouldChangePassword : user.shouldChangePassword; | ||||
|  | ||||
|     // If payload includes password - Create new password for user | ||||
|     if (updateUserDto.password) { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  | ||||
| export const serverVersion = { | ||||
|   major: 1, | ||||
|   minor: 13, | ||||
|   minor: 14, | ||||
|   patch: 0, | ||||
|   build: 20, | ||||
|   build: 21, | ||||
| }; | ||||
|   | ||||
| @@ -98,7 +98,7 @@ describe('User', () => { | ||||
|               id: expect.anything(), | ||||
|               createdAt: expect.anything(), | ||||
|               isAdmin: false, | ||||
|               isFirstLoggedIn: true, | ||||
|               shouldChangePassword: true, | ||||
|               profileImagePath: '', | ||||
|             }, | ||||
|             { | ||||
| @@ -108,7 +108,7 @@ describe('User', () => { | ||||
|               id: expect.anything(), | ||||
|               createdAt: expect.anything(), | ||||
|               isAdmin: false, | ||||
|               isFirstLoggedIn: true, | ||||
|               shouldChangePassword: true, | ||||
|               profileImagePath: '', | ||||
|             }, | ||||
|           ]), | ||||
|   | ||||
| @@ -27,7 +27,7 @@ export class UserEntity { | ||||
|   profileImagePath!: string; | ||||
|  | ||||
|   @Column() | ||||
|   isFirstLoggedIn!: boolean; | ||||
|   shouldChangePassword!: boolean; | ||||
|  | ||||
|   @CreateDateColumn() | ||||
|   createdAt!: string; | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||
|  | ||||
| export class RenameIsFirstLoggedInColumn1656338626260 implements MigrationInterface { | ||||
|   public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|     await queryRunner.query(` | ||||
|     ALTER TABLE users | ||||
|          RENAME COLUMN "isFirstLoggedIn" to "shouldChangePassword"; | ||||
|     `); | ||||
|   } | ||||
|  | ||||
|   public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|     await queryRunner.query(` | ||||
|       ALTER TABLE users | ||||
|           RENAME COLUMN "shouldChangePassword" to "isFirstLoggedIn"; | ||||
|     `); | ||||
|   } | ||||
| } | ||||
| @@ -1,74 +1,64 @@ | ||||
| type AdminRegistrationResult = Promise<{ | ||||
|   error?: string | ||||
|   success?: string | ||||
| 	error?: string; | ||||
| 	success?: string; | ||||
| 	user?: { | ||||
|     email: string | ||||
|   } | ||||
| }> | ||||
|  | ||||
|  | ||||
| 		email: string; | ||||
| 	}; | ||||
| }>; | ||||
|  | ||||
| type LoginResult = Promise<{ | ||||
|   error?: string | ||||
|   success?: string | ||||
|   needUpdate?: boolean | ||||
|   needSelectAdmin?: boolean | ||||
| 	error?: string; | ||||
| 	success?: string; | ||||
| 	user?: { | ||||
|     accessToken: string | ||||
|     firstName: string | ||||
|     lastName: string | ||||
|     isAdmin: boolean | ||||
|     id: string | ||||
|     email: string | ||||
|   } | ||||
| }> | ||||
| 		accessToken: string; | ||||
| 		firstName: string; | ||||
| 		lastName: string; | ||||
| 		isAdmin: boolean; | ||||
| 		id: string; | ||||
| 		email: string; | ||||
| 		shouldChangePassword: boolean; | ||||
| 	}; | ||||
| }>; | ||||
|  | ||||
| type UpdateResult = Promise<{ | ||||
|   error?: string | ||||
|   success?: string, | ||||
| 	error?: string; | ||||
| 	success?: string; | ||||
| 	user?: { | ||||
|     accessToken: string | ||||
|     firstName: string | ||||
|     lastName: string | ||||
|     isAdmin: boolean | ||||
|     id: string | ||||
|     email: string | ||||
|   } | ||||
| }> | ||||
|  | ||||
| 		accessToken: string; | ||||
| 		firstName: string; | ||||
| 		lastName: string; | ||||
| 		isAdmin: boolean; | ||||
| 		id: string; | ||||
| 		email: string; | ||||
| 	}; | ||||
| }>; | ||||
|  | ||||
| export async function sendRegistrationForm(form: HTMLFormElement): AdminRegistrationResult { | ||||
|  | ||||
| 	const response = await fetch(form.action, { | ||||
| 		method: form.method, | ||||
| 		body: new FormData(form), | ||||
| 		headers: { accept: 'application/json' }, | ||||
|   }) | ||||
|  | ||||
|   return await response.json() | ||||
| 	}); | ||||
|  | ||||
| 	return await response.json(); | ||||
| } | ||||
|  | ||||
|  | ||||
| export async function sendLoginForm(form: HTMLFormElement): LoginResult { | ||||
|  | ||||
| 	const response = await fetch(form.action, { | ||||
| 		method: form.method, | ||||
| 		body: new FormData(form), | ||||
| 		headers: { accept: 'application/json' }, | ||||
|   }) | ||||
| 	}); | ||||
|  | ||||
|   return await response.json() | ||||
| 	return await response.json(); | ||||
| } | ||||
|  | ||||
| export async function sendUpdateForm(form: HTMLFormElement): UpdateResult { | ||||
|  | ||||
| 	const response = await fetch(form.action, { | ||||
| 		method: form.method, | ||||
| 		body: new FormData(form), | ||||
| 		headers: { accept: 'application/json' }, | ||||
|   }) | ||||
| 	}); | ||||
|  | ||||
|   return await response.json() | ||||
| 	return await response.json(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,22 @@ | ||||
| 	let error: string; | ||||
| 	let success: string; | ||||
|  | ||||
| 	let password: string = ''; | ||||
| 	let confirmPassowrd: string = ''; | ||||
|  | ||||
| 	let canRegister = false; | ||||
|  | ||||
| 	$: { | ||||
| 		if (password !== confirmPassowrd && confirmPassowrd.length > 0) { | ||||
| 			error = 'Password does not match'; | ||||
| 			canRegister = false; | ||||
| 		} else { | ||||
| 			error = ''; | ||||
| 			canRegister = true; | ||||
| 		} | ||||
| 	} | ||||
| 	async function registerAdmin(event: SubmitEvent) { | ||||
| 		if (canRegister) { | ||||
| 			error = ''; | ||||
|  | ||||
| 			const formElement = event.target as HTMLFormElement; | ||||
| @@ -21,6 +36,7 @@ | ||||
| 				goto('/auth/login'); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> | ||||
| @@ -41,21 +57,33 @@ | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">Admin Password</label> | ||||
| 			<input class="immich-form-input" id="password" name="password" type="password" required /> | ||||
| 			<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} /> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">First Name</label> | ||||
| 			<label class="immich-form-label" for="confirmPassword">Confirm Admin Password</label> | ||||
| 			<input | ||||
| 				class="immich-form-input" | ||||
| 				id="confirmPassword" | ||||
| 				name="password" | ||||
| 				type="password" | ||||
| 				required | ||||
| 				bind:value={confirmPassowrd} | ||||
| 			/> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="firstName">First Name</label> | ||||
| 			<input class="immich-form-input" id="firstName" name="firstName" type="text" required /> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">Last Name</label> | ||||
| 			<label class="immich-form-label" for="lastName">Last Name</label> | ||||
| 			<input class="immich-form-input" id="lastName" name="lastName" type="text" required /> | ||||
| 		</div> | ||||
|  | ||||
| 		{#if error} | ||||
| 			<p class="text-red-400">{error}</p> | ||||
| 			<p class="text-red-400 ml-4">{error}</p> | ||||
| 		{/if} | ||||
|  | ||||
| 		{#if success} | ||||
|   | ||||
							
								
								
									
										97
									
								
								web/src/lib/components/forms/change-password-form.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/lib/components/forms/change-password-form.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| <script lang="ts"> | ||||
| 	import { session } from '$app/stores'; | ||||
|  | ||||
| 	import { sendRegistrationForm, sendUpdateForm } from '$lib/auth-api'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	import type { ImmichUser } from '../../models/immich-user'; | ||||
|  | ||||
| 	export let user: ImmichUser; | ||||
| 	let error: string; | ||||
| 	let success: string; | ||||
|  | ||||
| 	let password: string = ''; | ||||
| 	let confirmPassowrd: string = ''; | ||||
|  | ||||
| 	let changeChagePassword = false; | ||||
|  | ||||
| 	$: { | ||||
| 		if (password !== confirmPassowrd && confirmPassowrd.length > 0) { | ||||
| 			error = 'Password does not match'; | ||||
| 			changeChagePassword = false; | ||||
| 		} else { | ||||
| 			error = ''; | ||||
| 			changeChagePassword = true; | ||||
| 		} | ||||
| 	} | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | ||||
| 	async function changePassword(event: SubmitEvent) { | ||||
| 		if (changeChagePassword) { | ||||
| 			error = ''; | ||||
|  | ||||
| 			const formElement = event.target as HTMLFormElement; | ||||
|  | ||||
| 			const response = await sendUpdateForm(formElement); | ||||
|  | ||||
| 			if (response.error) { | ||||
| 				error = JSON.stringify(response.error); | ||||
| 			} | ||||
|  | ||||
| 			if (response.success) { | ||||
| 				success = 'Password has been changed'; | ||||
|  | ||||
| 				dispatch('success'); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> | ||||
| 	<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> | ||||
| 		<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> | ||||
| 		<h1 class="text-2xl text-immich-primary font-medium">Chage Password</h1> | ||||
|  | ||||
| 		<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> | ||||
| 			Hi {user.firstName} | ||||
| 			{user.lastName} ({user.email}), | ||||
| 			<br /> | ||||
| 			<br /> | ||||
| 			This is either the first time you are signing into the system or a request has been made to change your password. Please | ||||
| 			enter the new password below. | ||||
| 		</p> | ||||
| 	</div> | ||||
|  | ||||
| 	<form on:submit|preventDefault={changePassword} method="post" autocomplete="off"> | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">New Password</label> | ||||
| 			<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} /> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="confirmPassword">Confirm Password</label> | ||||
| 			<input | ||||
| 				class="immich-form-input" | ||||
| 				id="confirmPassword" | ||||
| 				name="password" | ||||
| 				type="password" | ||||
| 				required | ||||
| 				bind:value={confirmPassowrd} | ||||
| 			/> | ||||
| 		</div> | ||||
|  | ||||
| 		{#if error} | ||||
| 			<p class="text-red-400 ml-4 text-sm">{error}</p> | ||||
| 		{/if} | ||||
|  | ||||
| 		{#if success} | ||||
| 			<p class="text-immich-primary ml-4 text-sm">{success}</p> | ||||
| 		{/if} | ||||
| 		<div class="flex w-full"> | ||||
| 			<button | ||||
| 				type="submit" | ||||
| 				class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full" | ||||
| 				>Change Password</button | ||||
| 			> | ||||
| 		</div> | ||||
| 	</form> | ||||
| </div> | ||||
| @@ -5,9 +5,24 @@ | ||||
| 	let error: string; | ||||
| 	let success: string; | ||||
|  | ||||
| 	let password: string = ''; | ||||
| 	let confirmPassowrd: string = ''; | ||||
|  | ||||
| 	let canCreateUser = false; | ||||
|  | ||||
| 	$: { | ||||
| 		if (password !== confirmPassowrd && confirmPassowrd.length > 0) { | ||||
| 			error = 'Password does not match'; | ||||
| 			canCreateUser = false; | ||||
| 		} else { | ||||
| 			error = ''; | ||||
| 			canCreateUser = true; | ||||
| 		} | ||||
| 	} | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | ||||
| 	async function registerUser(event: SubmitEvent) { | ||||
| 		if (canCreateUser) { | ||||
| 			error = ''; | ||||
|  | ||||
| 			const formElement = event.target as HTMLFormElement; | ||||
| @@ -24,6 +39,7 @@ | ||||
| 				dispatch('user-created'); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> | ||||
| @@ -43,25 +59,37 @@ | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">Password</label> | ||||
| 			<input class="immich-form-input" id="password" name="password" type="password" required /> | ||||
| 			<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} /> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">First Name</label> | ||||
| 			<label class="immich-form-label" for="confirmPassword">Confirm Password</label> | ||||
| 			<input | ||||
| 				class="immich-form-input" | ||||
| 				id="confirmPassword" | ||||
| 				name="password" | ||||
| 				type="password" | ||||
| 				required | ||||
| 				bind:value={confirmPassowrd} | ||||
| 			/> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="firstName">First Name</label> | ||||
| 			<input class="immich-form-input" id="firstName" name="firstName" type="text" required /> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="password">Last Name</label> | ||||
| 			<label class="immich-form-label" for="lastName">Last Name</label> | ||||
| 			<input class="immich-form-input" id="lastName" name="lastName" type="text" required /> | ||||
| 		</div> | ||||
|  | ||||
| 		{#if error} | ||||
| 			<p class="text-red-400">{error}</p> | ||||
| 			<p class="text-red-400 ml-4 text-sm">{error}</p> | ||||
| 		{/if} | ||||
|  | ||||
| 		{#if success} | ||||
| 			<p class="text-immich-primary">{success}</p> | ||||
| 			<p class="text-immich-primary ml-4 text-sm">{success}</p> | ||||
| 		{/if} | ||||
| 		<div class="flex w-full"> | ||||
| 			<button | ||||
|   | ||||
| @@ -18,14 +18,6 @@ | ||||
| 			error = response.error; | ||||
| 		} | ||||
|  | ||||
| 		if (response.needUpdate) { | ||||
| 			return dispatch('need-update'); | ||||
| 		} | ||||
|  | ||||
| 		if (response.needSelectAdmin) { | ||||
| 			return dispatch('need-select-admin'); | ||||
| 		} | ||||
|  | ||||
| 		if (response.success) { | ||||
| 			$session.user = { | ||||
| 				accessToken: response.user!.accessToken, | ||||
| @@ -36,6 +28,10 @@ | ||||
| 				email: response.user!.email, | ||||
| 			}; | ||||
|  | ||||
| 			if (!response.user?.isAdmin && response.user?.shouldChangePassword) { | ||||
| 				return dispatch('first-login'); | ||||
| 			} | ||||
|  | ||||
| 			return dispatch('success'); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,93 +0,0 @@ | ||||
| <script lang="ts"> | ||||
| 	import { session } from '$app/stores'; | ||||
|  | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import type { ImmichUser } from '../../models/immich-user'; | ||||
| 	import Check from 'svelte-material-icons/Check.svelte'; | ||||
|  | ||||
| 	let error: string = ''; | ||||
| 	let allUsers: Array<ImmichUser> = []; | ||||
| 	let selectedUserId: string; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		const res = await fetch('/auth/login/api/get-users', { method: 'GET' }); | ||||
| 		const data = await res.json(); | ||||
| 		allUsers = data.allUsers; | ||||
| 	}); | ||||
|  | ||||
| 	const assignAdmin = async () => { | ||||
| 		const res = await fetch('/auth/login/api/select-admin', { | ||||
| 			method: 'POST', | ||||
| 			body: JSON.stringify({ | ||||
| 				id: selectedUserId, | ||||
| 				isAdmin: true, | ||||
| 			}), | ||||
| 		}); | ||||
|  | ||||
| 		if (res.status === 200) { | ||||
| 			const data = await res.json(); | ||||
|  | ||||
| 			$session.user = { | ||||
| 				accessToken: '', | ||||
| 				firstName: data.userInfo.firstName, | ||||
| 				lastName: data.userInfo.lastName, | ||||
| 				isAdmin: data.userInfo.isAdmin, | ||||
| 				id: data.userInfo.id, | ||||
| 				email: data.userInfo.email, | ||||
| 			}; | ||||
|  | ||||
| 			dispatch('success'); | ||||
| 		} else { | ||||
| 			error = JSON.stringify(await res.json()); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> | ||||
| 	<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> | ||||
| 		<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> | ||||
| 		<h1 class="text-2xl text-immich-primary font-medium">Select Admin</h1> | ||||
| 		<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> | ||||
| 			There are multiple users on the server, and none have been selected to be the admin. Please assign one as the | ||||
| 			admin, who will be responsible for administrative tasks | ||||
| 		</p> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="text-xs m-4">USERS ON SERVER, CLICK TO SELECT ONE</div> | ||||
| 	<div class="overflow-y-auto rounded-md max-h-[300px] block border mx-4 px-4 py-2"> | ||||
| 		{#each allUsers as user, i} | ||||
| 			<div | ||||
| 				class="p-4 flex justify-between place-items-center my-4 rounded-md hover:cursor-pointer shadow-sm bg-gray-50 hover:bg-gray-100" | ||||
| 				on:click={() => (selectedUserId = user.id)} | ||||
| 			> | ||||
| 				<p class="test-sm text-slate-600">{i + 1} | {user.email}</p> | ||||
|  | ||||
| 				<!-- Icon --> | ||||
| 				{#if selectedUserId == user.id} | ||||
| 					<div | ||||
| 						in:fade={{ duration: 100 }} | ||||
| 						class="border rounded-full border-gray-300 bg-immich-primary w-8 h-8 flex place-items-center place-content-center" | ||||
| 					> | ||||
| 						<Check color="white" size="24" /> | ||||
| 					</div> | ||||
| 				{:else} | ||||
| 					<div in:fade={{ duration: 100 }} class="border rounded-full border-gray-300 w-8 h-8" /> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		{/each} | ||||
| 	</div> | ||||
|  | ||||
| 	{#if error} | ||||
| 		<div class="text-xs m-4 text-red-400">Error: {error}</div> | ||||
| 	{/if} | ||||
|  | ||||
| 	<div class="flex w-full"> | ||||
| 		<button | ||||
| 			type="submit" | ||||
| 			class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full font-semibold" | ||||
| 			on:click={assignAdmin}>Assign as Admin</button | ||||
| 		> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -1,68 +0,0 @@ | ||||
| <script lang="ts"> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { session } from '$app/stores'; | ||||
| 	import { sendUpdateForm } from '$lib/auth-api'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
|  | ||||
| 	let error: string; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | ||||
| 	async function updateInfo(event: SubmitEvent) { | ||||
| 		error = ''; | ||||
|  | ||||
| 		const formElement = event.target as HTMLFormElement; | ||||
|  | ||||
| 		const response = await sendUpdateForm(formElement); | ||||
|  | ||||
| 		if (response.error) { | ||||
| 			error = response.error; | ||||
| 		} | ||||
|  | ||||
| 		if (response.success) { | ||||
| 			$session.user = { | ||||
| 				accessToken: response.user!.accessToken, | ||||
| 				firstName: response.user!.firstName, | ||||
| 				lastName: response.user!.lastName, | ||||
| 				isAdmin: response.user!.isAdmin, | ||||
| 				id: response.user!.id, | ||||
| 				email: response.user!.email, | ||||
| 			}; | ||||
|  | ||||
| 			dispatch('success'); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> | ||||
| 	<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> | ||||
| 		<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> | ||||
| 		<h1 class="text-2xl text-immich-primary font-medium">Update User Info</h1> | ||||
| 		<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> | ||||
| 			Your account doesn't have information about your name, please update to continue the login process. | ||||
| 		</p> | ||||
| 	</div> | ||||
|  | ||||
| 	<form on:submit|preventDefault={updateInfo} method="post" action="/auth/login/update" autocomplete="off"> | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="firstName">First name</label> | ||||
| 			<input class="immich-form-input" id="firstName" name="firstName" type="text" required /> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="m-4 flex flex-col gap-2"> | ||||
| 			<label class="immich-form-label" for="lastName">Last name</label> | ||||
| 			<input class="immich-form-input" id="lastName" name="lastName" type="text" required /> | ||||
| 		</div> | ||||
|  | ||||
| 		{#if error} | ||||
| 			<p class="text-red-400 pl-4">{error}</p> | ||||
| 		{/if} | ||||
|  | ||||
| 		<div class="flex w-full"> | ||||
| 			<button | ||||
| 				type="submit" | ||||
| 				class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full font-semibold" | ||||
| 				>Update</button | ||||
| 			> | ||||
| 		</div> | ||||
| 	</form> | ||||
| </div> | ||||
| @@ -1,7 +1,9 @@ | ||||
| export type ImmichUser = { | ||||
|   id: string, | ||||
|   email: string, | ||||
|   firstName: string, | ||||
|   lastName: string, | ||||
|   isAdmin: boolean, | ||||
| } | ||||
| 	id: string; | ||||
| 	email: string; | ||||
| 	firstName: string; | ||||
| 	lastName: string; | ||||
| 	isAdmin: boolean; | ||||
| 	profileImagePath: string; | ||||
| 	shouldChangePassword: boolean; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										75
									
								
								web/src/routes/auth/change-password/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								web/src/routes/auth/change-password/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	export const prerender = false; | ||||
|  | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	import type { ImmichUser } from '$lib/models/immich-user'; | ||||
|  | ||||
| 	export const load: Load = async ({ session }) => { | ||||
| 		if (!session.user) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: '/auth/login', | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			const res = await fetch(serverEndpoint + '/user/me', { | ||||
| 				method: 'GET', | ||||
| 				headers: { | ||||
| 					Authorization: 'Bearer ' + session.user.accessToken, | ||||
| 				}, | ||||
| 			}); | ||||
|  | ||||
| 			const userInfo: ImmichUser = await res.json(); | ||||
|  | ||||
| 			if (userInfo.shouldChangePassword) { | ||||
| 				return { | ||||
| 					status: 200, | ||||
| 					props: { | ||||
| 						user: userInfo, | ||||
| 					}, | ||||
| 				}; | ||||
| 			} else { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: '/photos', | ||||
| 				}; | ||||
| 			} | ||||
| 		} catch (e) { | ||||
| 			console.log('ERROR Getting user info', e); | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: '/photos', | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { session } from '$app/stores'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import ChangePasswordForm from '../../../lib/components/forms/change-password-form.svelte'; | ||||
| 	import { serverEndpoint } from '../../../lib/constants'; | ||||
|  | ||||
| 	export let user: ImmichUser; | ||||
|  | ||||
| 	const onSuccessHandler = async () => { | ||||
| 		const res = await fetch('/auth/logout', { method: 'POST' }); | ||||
|  | ||||
| 		if (res.status == 200 && res.statusText == 'OK') { | ||||
| 			goto('/auth/login'); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <svelte:head> | ||||
| 	<title>Immich - Change Password</title> | ||||
| </svelte:head> | ||||
|  | ||||
| <section class="h-screen w-screen flex place-items-center place-content-center"> | ||||
| 	<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}> | ||||
| 		<ChangePasswordForm {user} on:success={onSuccessHandler} /> | ||||
| 	</div> | ||||
| </section> | ||||
							
								
								
									
										39
									
								
								web/src/routes/auth/change-password/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								web/src/routes/auth/change-password/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import { serverEndpoint } from '$lib/constants'; | ||||
|  | ||||
| export const post: RequestHandler = async ({ request, locals }) => { | ||||
| 	const form = await request.formData(); | ||||
|  | ||||
| 	const password = form.get('password'); | ||||
|  | ||||
| 	const payload = { | ||||
| 		id: locals.user?.id, | ||||
| 		password, | ||||
| 		shouldChangePassword: false, | ||||
| 	}; | ||||
|  | ||||
| 	const res = await fetch(`${serverEndpoint}/user`, { | ||||
| 		method: 'PUT', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${locals.user?.accessToken}`, | ||||
| 		}, | ||||
| 		body: JSON.stringify(payload), | ||||
| 	}); | ||||
|  | ||||
| 	if (res.status === 200) { | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				success: 'Succesfully change password', | ||||
| 			}, | ||||
| 		}; | ||||
| 	} else { | ||||
| 		return { | ||||
| 			status: 400, | ||||
| 			body: { | ||||
| 				error: await res.json(), | ||||
| 			}, | ||||
| 		}; | ||||
| 	} | ||||
| }; | ||||
| @@ -3,25 +3,10 @@ | ||||
| 	import { fade } from 'svelte/transition'; | ||||
|  | ||||
| 	import LoginForm from '$lib/components/forms/login-form.svelte'; | ||||
| 	import UpdateForm from '../../../lib/components/forms/update-form.svelte'; | ||||
| 	import SelectAdminForm from '../../../lib/components/forms/select-admin-form.svelte'; | ||||
|  | ||||
| 	let shouldShowUpdateForm = false; | ||||
| 	let shouldShowSelectAdminForm = false; | ||||
|  | ||||
| 	const onLoginSuccess = async () => { | ||||
| 		goto('/photos'); | ||||
| 	}; | ||||
|  | ||||
| 	const onNeedUpdate = () => { | ||||
| 		shouldShowUpdateForm = true; | ||||
| 		shouldShowSelectAdminForm = false; | ||||
| 	}; | ||||
|  | ||||
| 	const onNeedSelectAdmin = () => { | ||||
| 		shouldShowUpdateForm = false; | ||||
| 		shouldShowSelectAdminForm = true; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <svelte:head> | ||||
| @@ -29,21 +14,7 @@ | ||||
| </svelte:head> | ||||
|  | ||||
| <section class="h-screen w-screen flex place-items-center place-content-center"> | ||||
| 	{#if !shouldShowUpdateForm && !shouldShowSelectAdminForm} | ||||
| 	<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}> | ||||
| 			<LoginForm on:success={onLoginSuccess} on:need-update={onNeedUpdate} on:need-select-admin={onNeedSelectAdmin} /> | ||||
| 		<LoginForm on:success={onLoginSuccess} on:first-login={() => goto('/auth/change-password')} /> | ||||
| 	</div> | ||||
| 	{/if} | ||||
|  | ||||
| 	{#if shouldShowUpdateForm} | ||||
| 		<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}> | ||||
| 			<UpdateForm on:success={onLoginSuccess} /> | ||||
| 		</div> | ||||
| 	{/if} | ||||
|  | ||||
| 	{#if shouldShowSelectAdminForm} | ||||
| 		<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}> | ||||
| 			<SelectAdminForm on:success={onLoginSuccess} /> | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </section> | ||||
|   | ||||
| @@ -1,229 +1,81 @@ | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import { serverEndpoint } from '$lib/constants'; | ||||
| import * as cookie from 'cookie' | ||||
| import * as cookie from 'cookie'; | ||||
| import { getRequest, putRequest } from '$lib/api'; | ||||
|  | ||||
| type LoggedInUser = { | ||||
| type AuthUser = { | ||||
| 	accessToken: string; | ||||
| 	userId: string; | ||||
| 	userEmail: string; | ||||
| 	firstName: string; | ||||
| 	lastName: string; | ||||
| 	isAdmin: boolean; | ||||
| } | ||||
| 	shouldChangePassword: boolean; | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async ({ request }) => { | ||||
| 	const form = await request.formData(); | ||||
|  | ||||
|   const email = form.get('email') | ||||
|   const password = form.get('password') | ||||
| 	const email = form.get('email'); | ||||
| 	const password = form.get('password'); | ||||
|  | ||||
| 	const payload = { | ||||
| 		email, | ||||
| 		password, | ||||
|   } | ||||
| 	}; | ||||
|  | ||||
| 	const res = await fetch(`${serverEndpoint}/auth/login`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
|       'Content-Type': 'application/json' | ||||
| 			'Content-Type': 'application/json', | ||||
| 		}, | ||||
| 		body: JSON.stringify(payload), | ||||
|   }) | ||||
| 	}); | ||||
|  | ||||
| 	if (res.status === 201) { | ||||
| 		// Login success | ||||
|     const loggedInUser = await res.json() as LoggedInUser; | ||||
|  | ||||
|     /** | ||||
|      * Support legacy users with two scenario | ||||
|      *  | ||||
|      * Scenario 1 - If one user exists on the server - make the user admin and ask for name. | ||||
|      * Scenario 2 - After assigned as admin, scenario 1 user not complete update form with names | ||||
|      * Scenario 3 - If two users exists on the server and no admin - ask to choose which one will be made admin | ||||
|      */ | ||||
|  | ||||
|  | ||||
|     // check how many user on the server | ||||
|     const { userCount } = await getRequest('user/count', ''); | ||||
|     const { userCount: adminUserCount } = await getRequest('user/count?isAdmin=true', '') | ||||
|     /** | ||||
|      * Scenario 1 handler | ||||
|      */ | ||||
|     if (userCount == 1 && !loggedInUser.isAdmin) { | ||||
|  | ||||
|       const updatedUser = await putRequest('user', { | ||||
|         id: loggedInUser.userId, | ||||
|         isAdmin: true | ||||
|       }, loggedInUser.accessToken) | ||||
|  | ||||
|  | ||||
|       /** | ||||
|       * Scenario 2 handler for current admin user | ||||
|       */ | ||||
|       let bodyResponse = { success: true, needUpdate: false } | ||||
|  | ||||
|       if (loggedInUser.firstName == "" || loggedInUser.lastName == "") { | ||||
|         bodyResponse = { success: false, needUpdate: true } | ||||
|       } | ||||
|  | ||||
|  | ||||
|       return { | ||||
|         status: 200, | ||||
|         body: { | ||||
|           ...bodyResponse, | ||||
|           user: { | ||||
|             id: updatedUser.userId, | ||||
|             accessToken: loggedInUser.accessToken, | ||||
|             firstName: updatedUser.firstName, | ||||
|             lastName: updatedUser.lastName, | ||||
|             isAdmin: updatedUser.isAdmin, | ||||
|             email: updatedUser.email, | ||||
|           }, | ||||
|         }, | ||||
|         headers: { | ||||
|           'Set-Cookie': cookie.serialize('session', JSON.stringify( | ||||
|             { | ||||
|               id: updatedUser.userId, | ||||
|               accessToken: loggedInUser.accessToken, | ||||
|               firstName: updatedUser.firstName, | ||||
|               lastName: updatedUser.lastName, | ||||
|               isAdmin: updatedUser.isAdmin, | ||||
|               email: updatedUser.email, | ||||
|             }), { | ||||
|             path: '/', | ||||
|             httpOnly: true, | ||||
|             sameSite: 'strict', | ||||
|             maxAge: 60 * 60 * 24 * 30, | ||||
|           }) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|     * Scenario 3 handler | ||||
|     */ | ||||
|     if (userCount >= 2 && adminUserCount == 0) { | ||||
|       return { | ||||
|         status: 200, | ||||
|         body: { | ||||
|           needSelectAdmin: true, | ||||
|           user: { | ||||
|             id: loggedInUser.userId, | ||||
|             accessToken: loggedInUser.accessToken, | ||||
|             firstName: loggedInUser.firstName, | ||||
|             lastName: loggedInUser.lastName, | ||||
|             isAdmin: loggedInUser.isAdmin, | ||||
|             email: loggedInUser.userEmail | ||||
|           }, | ||||
|           success: 'success' | ||||
|         }, | ||||
|         headers: { | ||||
|           'Set-Cookie': cookie.serialize('session', JSON.stringify( | ||||
|             { | ||||
|               id: loggedInUser.userId, | ||||
|               accessToken: loggedInUser.accessToken, | ||||
|               firstName: loggedInUser.firstName, | ||||
|               lastName: loggedInUser.lastName, | ||||
|               isAdmin: loggedInUser.isAdmin, | ||||
|               email: loggedInUser.userEmail | ||||
|             }), { | ||||
|             path: '/', | ||||
|             httpOnly: true, | ||||
|             sameSite: 'strict', | ||||
|             maxAge: 60 * 60 * 24 * 30, | ||||
|           }) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|     * Scenario 2 handler | ||||
|     */ | ||||
|     if (loggedInUser.firstName == "" || loggedInUser.lastName == "") { | ||||
|       return { | ||||
|         status: 200, | ||||
|         body: { | ||||
|           needUpdate: true, | ||||
|           user: { | ||||
|             id: loggedInUser.userId, | ||||
|             accessToken: loggedInUser.accessToken, | ||||
|             firstName: loggedInUser.firstName, | ||||
|             lastName: loggedInUser.lastName, | ||||
|             isAdmin: loggedInUser.isAdmin, | ||||
|             email: loggedInUser.userEmail | ||||
|           }, | ||||
|         }, | ||||
|         headers: { | ||||
|           'Set-Cookie': cookie.serialize('session', JSON.stringify( | ||||
|             { | ||||
|               id: loggedInUser.userId, | ||||
|               accessToken: loggedInUser.accessToken, | ||||
|               firstName: loggedInUser.firstName, | ||||
|               lastName: loggedInUser.lastName, | ||||
|               isAdmin: loggedInUser.isAdmin, | ||||
|               email: loggedInUser.userEmail | ||||
|             }), { | ||||
|             path: '/', | ||||
|             httpOnly: true, | ||||
|             sameSite: 'strict', | ||||
|             maxAge: 60 * 60 * 24 * 30, | ||||
|           }) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|  | ||||
| 		const authUser = (await res.json()) as AuthUser; | ||||
|  | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				user: { | ||||
|           id: loggedInUser.userId, | ||||
|           accessToken: loggedInUser.accessToken, | ||||
|           firstName: loggedInUser.firstName, | ||||
|           lastName: loggedInUser.lastName, | ||||
|           isAdmin: loggedInUser.isAdmin, | ||||
|           email: loggedInUser.userEmail | ||||
| 					id: authUser.userId, | ||||
| 					accessToken: authUser.accessToken, | ||||
| 					firstName: authUser.firstName, | ||||
| 					lastName: authUser.lastName, | ||||
| 					isAdmin: authUser.isAdmin, | ||||
| 					email: authUser.userEmail, | ||||
| 					shouldChangePassword: authUser.shouldChangePassword, | ||||
| 				}, | ||||
|         success: 'success' | ||||
| 				success: 'success', | ||||
| 			}, | ||||
| 			headers: { | ||||
|         'Set-Cookie': cookie.serialize('session', JSON.stringify( | ||||
| 				'Set-Cookie': cookie.serialize( | ||||
| 					'session', | ||||
| 					JSON.stringify({ | ||||
| 						id: authUser.userId, | ||||
| 						accessToken: authUser.accessToken, | ||||
| 						firstName: authUser.firstName, | ||||
| 						lastName: authUser.lastName, | ||||
| 						isAdmin: authUser.isAdmin, | ||||
| 						email: authUser.userEmail, | ||||
| 					}), | ||||
| 					{ | ||||
|             id: loggedInUser.userId, | ||||
|             accessToken: loggedInUser.accessToken, | ||||
|             firstName: loggedInUser.firstName, | ||||
|             lastName: loggedInUser.lastName, | ||||
|             isAdmin: loggedInUser.isAdmin, | ||||
|             email: loggedInUser.userEmail, | ||||
|           }), { | ||||
|           // send cookie for every page | ||||
| 						path: '/', | ||||
|  | ||||
|           // server side only cookie so you can't use `document.cookie` | ||||
| 						httpOnly: true, | ||||
|  | ||||
|           // only requests from same site can send cookies | ||||
|           // and serves to protect from CSRF | ||||
|           // https://developer.mozilla.org/en-US/docs/Glossary/CSRF | ||||
| 						sameSite: 'strict', | ||||
|  | ||||
|           // set cookie to expire after a month | ||||
| 						maxAge: 60 * 60 * 24 * 30, | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|  | ||||
| 					}, | ||||
| 				), | ||||
| 			}, | ||||
| 		}; | ||||
| 	} else { | ||||
| 		return { | ||||
| 			status: 400, | ||||
| 			body: { | ||||
|         error: 'Incorrect email or password' | ||||
| 				error: 'Incorrect email or password', | ||||
| 			}, | ||||
| 		}; | ||||
| 	} | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -35,7 +35,6 @@ | ||||
| <script lang="ts"> | ||||
| 	import { serverEndpoint } from '$lib/constants'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { onMount } from 'svelte'; | ||||
|  | ||||
| 	export let isAdminUserExist: boolean; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user