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:
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;
|
||||
}
|
||||
|
||||
@@ -117,9 +127,10 @@ class ServerEndpointInput extends StatelessWidget {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Server Endpoint URL',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'http://your-server-ip:port'),
|
||||
labelText: 'Server Endpoint URL',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'http://your-server-ip:port',
|
||||
),
|
||||
validator: _validateInput,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
);
|
||||
@@ -144,9 +155,10 @@ class EmailInput extends StatelessWidget {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'youremail@email.com'),
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
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
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user