mirror of
https://github.com/immich-app/immich.git
synced 2025-05-14 22:36:48 +02: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:
parent
2e85e18020
commit
5f00d8b9c6
@ -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"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.13.0"
|
version_number: "1.14.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -191,7 +191,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool res =
|
bool res =
|
||||||
await ref.read(authenticationProvider.notifier).logout();
|
await ref.watch(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
|
@ -11,7 +11,7 @@ class AuthenticationState {
|
|||||||
final String firstName;
|
final String firstName;
|
||||||
final String lastName;
|
final String lastName;
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
final bool isFirstLogin;
|
final bool shouldChangePassword;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
final DeviceInfoRemote deviceInfo;
|
final DeviceInfoRemote deviceInfo;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class AuthenticationState {
|
|||||||
required this.firstName,
|
required this.firstName,
|
||||||
required this.lastName,
|
required this.lastName,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isFirstLogin,
|
required this.shouldChangePassword,
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.deviceInfo,
|
required this.deviceInfo,
|
||||||
});
|
});
|
||||||
@ -38,7 +38,7 @@ class AuthenticationState {
|
|||||||
String? firstName,
|
String? firstName,
|
||||||
String? lastName,
|
String? lastName,
|
||||||
bool? isAdmin,
|
bool? isAdmin,
|
||||||
bool? isFirstLoggedIn,
|
bool? shouldChangePassword,
|
||||||
String? profileImagePath,
|
String? profileImagePath,
|
||||||
DeviceInfoRemote? deviceInfo,
|
DeviceInfoRemote? deviceInfo,
|
||||||
}) {
|
}) {
|
||||||
@ -51,17 +51,12 @@ class AuthenticationState {
|
|||||||
firstName: firstName ?? this.firstName,
|
firstName: firstName ?? this.firstName,
|
||||||
lastName: lastName ?? this.lastName,
|
lastName: lastName ?? this.lastName,
|
||||||
isAdmin: isAdmin ?? this.isAdmin,
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
isFirstLogin: isFirstLoggedIn ?? isFirstLogin,
|
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
|
||||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
deviceInfo: deviceInfo ?? this.deviceInfo,
|
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() {
|
Map<String, dynamic> toMap() {
|
||||||
final result = <String, dynamic>{};
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
@ -73,7 +68,7 @@ class AuthenticationState {
|
|||||||
result.addAll({'firstName': firstName});
|
result.addAll({'firstName': firstName});
|
||||||
result.addAll({'lastName': lastName});
|
result.addAll({'lastName': lastName});
|
||||||
result.addAll({'isAdmin': isAdmin});
|
result.addAll({'isAdmin': isAdmin});
|
||||||
result.addAll({'isFirstLogin': isFirstLogin});
|
result.addAll({'shouldChangePassword': shouldChangePassword});
|
||||||
result.addAll({'profileImagePath': profileImagePath});
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
result.addAll({'deviceInfo': deviceInfo.toMap()});
|
result.addAll({'deviceInfo': deviceInfo.toMap()});
|
||||||
|
|
||||||
@ -90,7 +85,7 @@ class AuthenticationState {
|
|||||||
firstName: map['firstName'] ?? '',
|
firstName: map['firstName'] ?? '',
|
||||||
lastName: map['lastName'] ?? '',
|
lastName: map['lastName'] ?? '',
|
||||||
isAdmin: map['isAdmin'] ?? false,
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
isFirstLogin: map['isFirstLogin'] ?? false,
|
shouldChangePassword: map['shouldChangePassword'] ?? false,
|
||||||
profileImagePath: map['profileImagePath'] ?? '',
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
||||||
);
|
);
|
||||||
@ -101,6 +96,11 @@ class AuthenticationState {
|
|||||||
factory AuthenticationState.fromJson(String source) =>
|
factory AuthenticationState.fromJson(String source) =>
|
||||||
AuthenticationState.fromMap(json.decode(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
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
@ -114,7 +114,7 @@ class AuthenticationState {
|
|||||||
other.firstName == firstName &&
|
other.firstName == firstName &&
|
||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.isFirstLogin == isFirstLogin &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.deviceInfo == deviceInfo;
|
other.deviceInfo == deviceInfo;
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ class AuthenticationState {
|
|||||||
firstName.hashCode ^
|
firstName.hashCode ^
|
||||||
lastName.hashCode ^
|
lastName.hashCode ^
|
||||||
isAdmin.hashCode ^
|
isAdmin.hashCode ^
|
||||||
isFirstLogin.hashCode ^
|
shouldChangePassword.hashCode ^
|
||||||
profileImagePath.hashCode ^
|
profileImagePath.hashCode ^
|
||||||
deviceInfo.hashCode;
|
deviceInfo.hashCode;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ class LogInReponse {
|
|||||||
final String lastName;
|
final String lastName;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
final bool isFirstLogin;
|
final bool shouldChangePassword;
|
||||||
|
|
||||||
LogInReponse({
|
LogInReponse({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
@ -18,7 +18,7 @@ class LogInReponse {
|
|||||||
required this.lastName,
|
required this.lastName,
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isFirstLogin,
|
required this.shouldChangePassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
LogInReponse copyWith({
|
LogInReponse copyWith({
|
||||||
@ -29,7 +29,7 @@ class LogInReponse {
|
|||||||
String? lastName,
|
String? lastName,
|
||||||
String? profileImagePath,
|
String? profileImagePath,
|
||||||
bool? isAdmin,
|
bool? isAdmin,
|
||||||
bool? isFirstLogin,
|
bool? shouldChangePassword,
|
||||||
}) {
|
}) {
|
||||||
return LogInReponse(
|
return LogInReponse(
|
||||||
accessToken: accessToken ?? this.accessToken,
|
accessToken: accessToken ?? this.accessToken,
|
||||||
@ -39,7 +39,7 @@ class LogInReponse {
|
|||||||
lastName: lastName ?? this.lastName,
|
lastName: lastName ?? this.lastName,
|
||||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
isAdmin: isAdmin ?? this.isAdmin,
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
isFirstLogin: isFirstLogin ?? this.isFirstLogin,
|
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class LogInReponse {
|
|||||||
result.addAll({'lastName': lastName});
|
result.addAll({'lastName': lastName});
|
||||||
result.addAll({'profileImagePath': profileImagePath});
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
result.addAll({'isAdmin': isAdmin});
|
result.addAll({'isAdmin': isAdmin});
|
||||||
result.addAll({'isFirstLogin': isFirstLogin});
|
result.addAll({'shouldChangePassword': shouldChangePassword});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ class LogInReponse {
|
|||||||
lastName: map['lastName'] ?? '',
|
lastName: map['lastName'] ?? '',
|
||||||
profileImagePath: map['profileImagePath'] ?? '',
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
isAdmin: map['isAdmin'] ?? false,
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
isFirstLogin: map['isFirstLogin'] ?? false,
|
shouldChangePassword: map['shouldChangePassword'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class LogInReponse {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
@ -93,7 +93,7 @@ class LogInReponse {
|
|||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.isFirstLogin == isFirstLogin;
|
other.shouldChangePassword == shouldChangePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -105,6 +105,6 @@ class LogInReponse {
|
|||||||
lastName.hashCode ^
|
lastName.hashCode ^
|
||||||
profileImagePath.hashCode ^
|
profileImagePath.hashCode ^
|
||||||
isAdmin.hashCode ^
|
isAdmin.hashCode ^
|
||||||
isFirstLogin.hashCode;
|
shouldChangePassword.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isFirstLogin: false,
|
shouldChangePassword: false,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoRemote(
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -87,7 +87,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
lastName: payload.lastName,
|
lastName: payload.lastName,
|
||||||
profileImagePath: payload.profileImagePath,
|
profileImagePath: payload.profileImagePath,
|
||||||
isAdmin: payload.isAdmin,
|
isAdmin: payload.isAdmin,
|
||||||
isFirstLoggedIn: payload.isFirstLogin,
|
shouldChangePassword: payload.shouldChangePassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSavedLoginInfo) {
|
if (isSavedLoginInfo) {
|
||||||
@ -112,7 +112,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
try {
|
try {
|
||||||
Response res = await _networkService.postRequest(
|
Response res = await _networkService.postRequest(
|
||||||
url: 'device-info',
|
url: 'device-info',
|
||||||
data: {'deviceId': state.deviceId, 'deviceType': state.deviceType});
|
data: {
|
||||||
|
'deviceId': state.deviceId,
|
||||||
|
'deviceType': state.deviceType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
|
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
|
||||||
state = state.copyWith(deviceInfo: deviceInfo);
|
state = state.copyWith(deviceInfo: deviceInfo);
|
||||||
@ -133,7 +137,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
isFirstLogin: false,
|
shouldChangePassword: false,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoRemote(
|
||||||
@ -163,6 +167,24 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
updateUserProfileImagePath(String path) {
|
updateUserProfileImagePath(String path) {
|
||||||
state = state.copyWith(profileImagePath: 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 =
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.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/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/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
@ -20,7 +21,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
final passwordController =
|
final passwordController =
|
||||||
useTextEditingController.fromValue(TextEditingValue.empty);
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final serverEndpointController =
|
final serverEndpointController =
|
||||||
useTextEditingController(text: 'http://your-server-ip:2283');
|
useTextEditingController(text: 'http://your-server-ip:2283/api');
|
||||||
final isSaveLoginInfo = useState<bool>(false);
|
final isSaveLoginInfo = useState<bool>(false);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@ -106,9 +107,18 @@ class ServerEndpointInput extends StatelessWidget {
|
|||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
String? _validateInput(String? url) {
|
String? _validateInput(String? url) {
|
||||||
if (url == null) return null;
|
if (url == null) {
|
||||||
if (!url.startsWith(RegExp(r'https?://')))
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.isEmpty) {
|
||||||
|
return 'Server endpoint is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.startsWith(RegExp(r'https?://'))) {
|
||||||
return 'Please specify http:// or https://';
|
return 'Please specify http:// or https://';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +129,8 @@ class ServerEndpointInput extends StatelessWidget {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Server Endpoint URL',
|
labelText: 'Server Endpoint URL',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
hintText: 'http://your-server-ip:port'),
|
hintText: 'http://your-server-ip:port',
|
||||||
|
),
|
||||||
validator: _validateInput,
|
validator: _validateInput,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
);
|
);
|
||||||
@ -146,7 +157,8 @@ class EmailInput extends StatelessWidget {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
hintText: 'youremail@email.com'),
|
hintText: 'youremail@email.com',
|
||||||
|
),
|
||||||
validator: _validateInput,
|
validator: _validateInput,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
);
|
);
|
||||||
@ -200,14 +212,19 @@ class LoginButton extends ConsumerWidget {
|
|||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
|
|
||||||
var isAuthenticated = await ref
|
var isAuthenticated = await ref
|
||||||
.read(authenticationProvider.notifier)
|
.watch(authenticationProvider.notifier)
|
||||||
.login(emailController.text, passwordController.text,
|
.login(emailController.text, passwordController.text,
|
||||||
serverEndpointController.text, isSavedLoginInfo);
|
serverEndpointController.text, isSavedLoginInfo);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Resume backup (if enable) then navigate
|
// Resume backup (if enable) then navigate
|
||||||
|
|
||||||
|
if (ref.watch(authenticationProvider).shouldChangePassword) {
|
||||||
|
AutoRouter.of(context).push(const ChangePasswordRoute());
|
||||||
|
} else {
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
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:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.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/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/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||||
@ -30,6 +31,7 @@ part 'router.gr.dart';
|
|||||||
routes: <AutoRoute>[
|
routes: <AutoRoute>[
|
||||||
AutoRoute(page: SplashScreenPage, initial: true),
|
AutoRoute(page: SplashScreenPage, initial: true),
|
||||||
AutoRoute(page: LoginPage),
|
AutoRoute(page: LoginPage),
|
||||||
|
AutoRoute(page: ChangePasswordPage),
|
||||||
CustomRoute(
|
CustomRoute(
|
||||||
page: TabControllerPage,
|
page: TabControllerPage,
|
||||||
guards: [AuthGuard],
|
guards: [AuthGuard],
|
||||||
|
@ -29,6 +29,10 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const LoginPage());
|
routeData: routeData, child: const LoginPage());
|
||||||
},
|
},
|
||||||
|
ChangePasswordRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData, child: const ChangePasswordPage());
|
||||||
|
},
|
||||||
TabControllerRoute.name: (routeData) {
|
TabControllerRoute.name: (routeData) {
|
||||||
return CustomPage<dynamic>(
|
return CustomPage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
@ -131,6 +135,7 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
List<RouteConfig> get routes => [
|
List<RouteConfig> get routes => [
|
||||||
RouteConfig(SplashScreenRoute.name, path: '/'),
|
RouteConfig(SplashScreenRoute.name, path: '/'),
|
||||||
RouteConfig(LoginRoute.name, path: '/login-page'),
|
RouteConfig(LoginRoute.name, path: '/login-page'),
|
||||||
|
RouteConfig(ChangePasswordRoute.name, path: '/change-password-page'),
|
||||||
RouteConfig(TabControllerRoute.name,
|
RouteConfig(TabControllerRoute.name,
|
||||||
path: '/tab-controller-page',
|
path: '/tab-controller-page',
|
||||||
guards: [
|
guards: [
|
||||||
@ -192,6 +197,15 @@ class LoginRoute extends PageRouteInfo<void> {
|
|||||||
static const String name = 'LoginRoute';
|
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
|
/// generated route for
|
||||||
/// [TabControllerPage]
|
/// [TabControllerPage]
|
||||||
class TabControllerRoute extends PageRouteInfo<void> {
|
class TabControllerRoute extends PageRouteInfo<void> {
|
||||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.13.0+20
|
version: 1.14.0+21
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
@ -30,7 +30,7 @@ export class AuthService {
|
|||||||
'lastName',
|
'lastName',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
'profileImagePath',
|
'profileImagePath',
|
||||||
'isFirstLoggedIn',
|
'shouldChangePassword',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -66,7 +66,7 @@ export class AuthService {
|
|||||||
lastName: validatedUser.lastName,
|
lastName: validatedUser.lastName,
|
||||||
isAdmin: validatedUser.isAdmin,
|
isAdmin: validatedUser.isAdmin,
|
||||||
profileImagePath: validatedUser.profileImagePath,
|
profileImagePath: validatedUser.profileImagePath,
|
||||||
isFirstLogin: validatedUser.isFirstLoggedIn,
|
shouldChangePassword: validatedUser.shouldChangePassword,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export class CreateUserDto {
|
|||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isFirstLoggedIn?: boolean;
|
shouldChangePassword?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -32,6 +32,12 @@ export class UserController {
|
|||||||
return await this.userService.getAllUsers(authUser, isAll);
|
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(JwtAuthGuard)
|
||||||
@UseGuards(AdminRolesGuard)
|
@UseGuards(AdminRolesGuard)
|
||||||
@Post()
|
@Post()
|
||||||
|
@ -37,6 +37,10 @@ export class UserService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserInfo(authUser: AuthUserDto) {
|
||||||
|
return this.userRepository.findOne({ id: authUser.id });
|
||||||
|
}
|
||||||
|
|
||||||
async getUserCount(isAdmin: boolean) {
|
async getUserCount(isAdmin: boolean) {
|
||||||
let users;
|
let users;
|
||||||
|
|
||||||
@ -89,7 +93,8 @@ export class UserService {
|
|||||||
user.lastName = updateUserDto.lastName || user.lastName;
|
user.lastName = updateUserDto.lastName || user.lastName;
|
||||||
user.firstName = updateUserDto.firstName || user.firstName;
|
user.firstName = updateUserDto.firstName || user.firstName;
|
||||||
user.profileImagePath = updateUserDto.profileImagePath || user.profileImagePath;
|
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 payload includes password - Create new password for user
|
||||||
if (updateUserDto.password) {
|
if (updateUserDto.password) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export const serverVersion = {
|
export const serverVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 13,
|
minor: 14,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 20,
|
build: 21,
|
||||||
};
|
};
|
||||||
|
@ -98,7 +98,7 @@ describe('User', () => {
|
|||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
createdAt: expect.anything(),
|
createdAt: expect.anything(),
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isFirstLoggedIn: true,
|
shouldChangePassword: true,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -108,7 +108,7 @@ describe('User', () => {
|
|||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
createdAt: expect.anything(),
|
createdAt: expect.anything(),
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isFirstLoggedIn: true,
|
shouldChangePassword: true,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
@ -27,7 +27,7 @@ export class UserEntity {
|
|||||||
profileImagePath!: string;
|
profileImagePath!: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
isFirstLoggedIn!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: string;
|
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<{
|
type AdminRegistrationResult = Promise<{
|
||||||
error?: string
|
error?: string;
|
||||||
success?: string
|
success?: string;
|
||||||
user?: {
|
user?: {
|
||||||
email: string
|
email: string;
|
||||||
}
|
};
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type LoginResult = Promise<{
|
type LoginResult = Promise<{
|
||||||
error?: string
|
error?: string;
|
||||||
success?: string
|
success?: string;
|
||||||
needUpdate?: boolean
|
|
||||||
needSelectAdmin?: boolean
|
|
||||||
user?: {
|
user?: {
|
||||||
accessToken: string
|
accessToken: string;
|
||||||
firstName: string
|
firstName: string;
|
||||||
lastName: string
|
lastName: string;
|
||||||
isAdmin: boolean
|
isAdmin: boolean;
|
||||||
id: string
|
id: string;
|
||||||
email: string
|
email: string;
|
||||||
}
|
shouldChangePassword: boolean;
|
||||||
}>
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
type UpdateResult = Promise<{
|
type UpdateResult = Promise<{
|
||||||
error?: string
|
error?: string;
|
||||||
success?: string,
|
success?: string;
|
||||||
user?: {
|
user?: {
|
||||||
accessToken: string
|
accessToken: string;
|
||||||
firstName: string
|
firstName: string;
|
||||||
lastName: string
|
lastName: string;
|
||||||
isAdmin: boolean
|
isAdmin: boolean;
|
||||||
id: string
|
id: string;
|
||||||
email: string
|
email: string;
|
||||||
}
|
};
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export async function sendRegistrationForm(form: HTMLFormElement): AdminRegistrationResult {
|
export async function sendRegistrationForm(form: HTMLFormElement): AdminRegistrationResult {
|
||||||
|
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(form.action, {
|
||||||
method: form.method,
|
method: form.method,
|
||||||
body: new FormData(form),
|
body: new FormData(form),
|
||||||
headers: { accept: 'application/json' },
|
headers: { accept: 'application/json' },
|
||||||
})
|
});
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function sendLoginForm(form: HTMLFormElement): LoginResult {
|
export async function sendLoginForm(form: HTMLFormElement): LoginResult {
|
||||||
|
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(form.action, {
|
||||||
method: form.method,
|
method: form.method,
|
||||||
body: new FormData(form),
|
body: new FormData(form),
|
||||||
headers: { accept: 'application/json' },
|
headers: { accept: 'application/json' },
|
||||||
})
|
});
|
||||||
|
|
||||||
return await response.json()
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendUpdateForm(form: HTMLFormElement): UpdateResult {
|
export async function sendUpdateForm(form: HTMLFormElement): UpdateResult {
|
||||||
|
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(form.action, {
|
||||||
method: form.method,
|
method: form.method,
|
||||||
body: new FormData(form),
|
body: new FormData(form),
|
||||||
headers: { accept: 'application/json' },
|
headers: { accept: 'application/json' },
|
||||||
})
|
});
|
||||||
|
|
||||||
return await response.json()
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,22 @@
|
|||||||
let error: string;
|
let error: string;
|
||||||
let success: 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) {
|
async function registerAdmin(event: SubmitEvent) {
|
||||||
|
if (canRegister) {
|
||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
const formElement = event.target as HTMLFormElement;
|
const formElement = event.target as HTMLFormElement;
|
||||||
@ -21,6 +36,7 @@
|
|||||||
goto('/auth/login');
|
goto('/auth/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
|
<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">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Admin Password</label>
|
<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>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<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 />
|
<input class="immich-form-input" id="firstName" name="firstName" type="text" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<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 />
|
<input class="immich-form-input" id="lastName" name="lastName" type="text" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="text-red-400">{error}</p>
|
<p class="text-red-400 ml-4">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if success}
|
{#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 error: string;
|
||||||
let success: 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();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
async function registerUser(event: SubmitEvent) {
|
async function registerUser(event: SubmitEvent) {
|
||||||
|
if (canCreateUser) {
|
||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
const formElement = event.target as HTMLFormElement;
|
const formElement = event.target as HTMLFormElement;
|
||||||
@ -24,6 +39,7 @@
|
|||||||
dispatch('user-created');
|
dispatch('user-created');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
|
<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">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<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>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<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 />
|
<input class="immich-form-input" id="firstName" name="firstName" type="text" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<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 />
|
<input class="immich-form-input" id="lastName" name="lastName" type="text" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="text-red-400">{error}</p>
|
<p class="text-red-400 ml-4 text-sm">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if success}
|
{#if success}
|
||||||
<p class="text-immich-primary">{success}</p>
|
<p class="text-immich-primary ml-4 text-sm">{success}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<button
|
<button
|
||||||
|
@ -18,14 +18,6 @@
|
|||||||
error = response.error;
|
error = response.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.needUpdate) {
|
|
||||||
return dispatch('need-update');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.needSelectAdmin) {
|
|
||||||
return dispatch('need-select-admin');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$session.user = {
|
$session.user = {
|
||||||
accessToken: response.user!.accessToken,
|
accessToken: response.user!.accessToken,
|
||||||
@ -36,6 +28,10 @@
|
|||||||
email: response.user!.email,
|
email: response.user!.email,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!response.user?.isAdmin && response.user?.shouldChangePassword) {
|
||||||
|
return dispatch('first-login');
|
||||||
|
}
|
||||||
|
|
||||||
return dispatch('success');
|
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 = {
|
export type ImmichUser = {
|
||||||
id: string,
|
id: string;
|
||||||
email: string,
|
email: string;
|
||||||
firstName: string,
|
firstName: string;
|
||||||
lastName: string,
|
lastName: string;
|
||||||
isAdmin: boolean,
|
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 { fade } from 'svelte/transition';
|
||||||
|
|
||||||
import LoginForm from '$lib/components/forms/login-form.svelte';
|
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 () => {
|
const onLoginSuccess = async () => {
|
||||||
goto('/photos');
|
goto('/photos');
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNeedUpdate = () => {
|
|
||||||
shouldShowUpdateForm = true;
|
|
||||||
shouldShowSelectAdminForm = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNeedSelectAdmin = () => {
|
|
||||||
shouldShowUpdateForm = false;
|
|
||||||
shouldShowSelectAdminForm = true;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -29,21 +14,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
<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 }}>
|
<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>
|
</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>
|
</section>
|
||||||
|
@ -1,229 +1,81 @@
|
|||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import { serverEndpoint } from '$lib/constants';
|
import { serverEndpoint } from '$lib/constants';
|
||||||
import * as cookie from 'cookie'
|
import * as cookie from 'cookie';
|
||||||
import { getRequest, putRequest } from '$lib/api';
|
import { getRequest, putRequest } from '$lib/api';
|
||||||
|
|
||||||
type LoggedInUser = {
|
type AuthUser = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}
|
shouldChangePassword: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const post: RequestHandler = async ({ request }) => {
|
export const post: RequestHandler = async ({ request }) => {
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
|
|
||||||
const email = form.get('email')
|
const email = form.get('email');
|
||||||
const password = form.get('password')
|
const password = form.get('password');
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
}
|
};
|
||||||
|
|
||||||
const res = await fetch(`${serverEndpoint}/auth/login`, {
|
const res = await fetch(`${serverEndpoint}/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
// Login success
|
// Login success
|
||||||
const loggedInUser = await res.json() as LoggedInUser;
|
const authUser = (await res.json()) as AuthUser;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
user: {
|
user: {
|
||||||
id: loggedInUser.userId,
|
id: authUser.userId,
|
||||||
accessToken: loggedInUser.accessToken,
|
accessToken: authUser.accessToken,
|
||||||
firstName: loggedInUser.firstName,
|
firstName: authUser.firstName,
|
||||||
lastName: loggedInUser.lastName,
|
lastName: authUser.lastName,
|
||||||
isAdmin: loggedInUser.isAdmin,
|
isAdmin: authUser.isAdmin,
|
||||||
email: loggedInUser.userEmail
|
email: authUser.userEmail,
|
||||||
|
shouldChangePassword: authUser.shouldChangePassword,
|
||||||
},
|
},
|
||||||
success: 'success'
|
success: 'success',
|
||||||
},
|
},
|
||||||
headers: {
|
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: '/',
|
path: '/',
|
||||||
|
|
||||||
// server side only cookie so you can't use `document.cookie`
|
|
||||||
httpOnly: true,
|
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',
|
sameSite: 'strict',
|
||||||
|
|
||||||
// set cookie to expire after a month
|
|
||||||
maxAge: 60 * 60 * 24 * 30,
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
})
|
},
|
||||||
}
|
),
|
||||||
}
|
},
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
error: 'Incorrect email or password'
|
error: 'Incorrect email or password',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
@ -35,7 +35,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { serverEndpoint } from '$lib/constants';
|
import { serverEndpoint } from '$lib/constants';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
export let isAdminUserExist: boolean;
|
export let isAdminUserExist: boolean;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user