浏览代码

Set password for users

Jamie Curnow 3 月之前
父节点
当前提交
8ad95c5695

+ 4 - 0
frontend/src/locale/lang/en.json

@@ -58,6 +58,9 @@
   "offline": "Offline",
   "online": "Online",
   "password": "Password",
+  "password.generate": "Generate random password",
+  "password.hide": "Hide Password",
+  "password.show": "Show Password",
   "permissions.hidden": "Hidden",
   "permissions.manage": "Manage",
   "permissions.view": "View Only",
@@ -101,6 +104,7 @@
   "user.new": "New User",
   "user.new-password": "New Password",
   "user.nickname": "Nickname",
+  "user.set-password": "Set Password",
   "user.set-permissions": "Set Permissions for {name}",
   "user.switch-dark": "Switch to Dark mode",
   "user.switch-light": "Switch to Light mode",

+ 12 - 0
frontend/src/locale/src/en.json

@@ -176,6 +176,15 @@
 	"password": {
 		"defaultMessage": "Password"
 	},
+	"password.generate": {
+		"defaultMessage": "Generate random password"
+	},
+	"password.hide": {
+		"defaultMessage": "Hide Password"
+	},
+	"password.show": {
+		"defaultMessage": "Show Password"
+	},
 	"permissions.hidden": {
 		"defaultMessage": "Hidden"
 	},
@@ -305,6 +314,9 @@
 	"user.nickname": {
 		"defaultMessage": "Nickname"
 	},
+	"user.set-password": {
+		"defaultMessage": "Set Password"
+	},
 	"user.set-permissions": {
 		"defaultMessage": "Set Permissions for {name}"
 	},

+ 132 - 0
frontend/src/modals/SetPasswordModal.tsx

@@ -0,0 +1,132 @@
+import { Field, Form, Formik } from "formik";
+import { generate } from "generate-password-browser";
+import { useState } from "react";
+import { Alert } from "react-bootstrap";
+import Modal from "react-bootstrap/Modal";
+import { updateAuth } from "src/api/backend";
+import { Button } from "src/components";
+import { intl } from "src/locale";
+import { validateString } from "src/modules/Validations";
+
+interface Props {
+	userId: number;
+	onClose: () => void;
+}
+export function SetPasswordModal({ userId, onClose }: Props) {
+	const [error, setError] = useState<string | null>(null);
+	const [showPassword, setShowPassword] = useState(false);
+
+	const onSubmit = async (values: any, { setSubmitting }: any) => {
+		setError(null);
+		try {
+			await updateAuth(userId, values.new);
+			onClose();
+		} catch (err: any) {
+			setError(intl.formatMessage({ id: err.message }));
+		}
+		setSubmitting(false);
+	};
+
+	return (
+		<Modal show onHide={onClose} animation={false}>
+			<Formik
+				initialValues={
+					{
+						new: "",
+					} as any
+				}
+				onSubmit={onSubmit}
+			>
+				{({ isSubmitting }) => (
+					<Form>
+						<Modal.Header closeButton>
+							<Modal.Title>{intl.formatMessage({ id: "user.set-password" })}</Modal.Title>
+						</Modal.Header>
+						<Modal.Body>
+							<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
+								{error}
+							</Alert>
+							<div className="mb-3">
+								<Field name="new" validate={validateString(8, 100)}>
+									{({ field, form }: any) => (
+										<>
+											<p className="text-end">
+												<small>
+													<a
+														href="#"
+														onClick={(e) => {
+															e.preventDefault();
+															form.setFieldValue(
+																field.name,
+																generate({
+																	length: 12,
+																	numbers: true,
+																}),
+															);
+															setShowPassword(true);
+														}}
+													>
+														{intl.formatMessage({
+															id: "password.generate",
+														})}
+													</a>{" "}
+													&mdash;{" "}
+													<a
+														href="#"
+														className="text-xs"
+														onClick={(e) => {
+															e.preventDefault();
+															setShowPassword(!showPassword);
+														}}
+													>
+														{intl.formatMessage({
+															id: showPassword ? "password.hide" : "password.show",
+														})}
+													</a>
+												</small>
+											</p>
+											<div className="form-floating mb-3">
+												<input
+													id="new"
+													type={showPassword ? "text" : "password"}
+													required
+													className={`form-control ${form.errors.new && form.touched.new ? "is-invalid" : ""}`}
+													placeholder={intl.formatMessage({ id: "user.new-password" })}
+													{...field}
+												/>
+												<label htmlFor="new">
+													{intl.formatMessage({ id: "user.new-password" })}
+												</label>
+
+												{form.errors.new ? (
+													<div className="invalid-feedback">
+														{form.errors.new && form.touched.new ? form.errors.new : null}
+													</div>
+												) : null}
+											</div>
+										</>
+									)}
+								</Field>
+							</div>
+						</Modal.Body>
+						<Modal.Footer>
+							<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
+								{intl.formatMessage({ id: "cancel" })}
+							</Button>
+							<Button
+								type="submit"
+								actionType="primary"
+								className="ms-auto"
+								data-bs-dismiss="modal"
+								isLoading={isSubmitting}
+								disabled={isSubmitting}
+							>
+								{intl.formatMessage({ id: "save" })}
+							</Button>
+						</Modal.Footer>
+					</Form>
+				)}
+			</Formik>
+		</Modal>
+	);
+}

+ 1 - 0
frontend/src/modals/index.ts

@@ -1,4 +1,5 @@
 export * from "./ChangePasswordModal";
 export * from "./DeleteConfirmModal";
 export * from "./PermissionsModal";
+export * from "./SetPasswordModal";
 export * from "./UserModal";

+ 2 - 0
frontend/src/pages/Dashboard/index.tsx

@@ -134,6 +134,8 @@ More for api, then implement here:
 - Properly implement refresh tokens
 - Add error message_18n for all backend errors
 - minor: certificates expand with hosts needs to omit 'is_deleted'
+- properly wrap all logger.debug called in isDebug check
+
 `}</code>
 			</pre>
 		</div>

+ 1 - 1
frontend/src/pages/Users/Table.tsx

@@ -125,7 +125,7 @@ export default function Table({
 											}}
 										>
 											<IconLock size={16} />
-											{intl.formatMessage({ id: "user.change-password" })}
+											{intl.formatMessage({ id: "user.set-password" })}
 										</a>
 										<div className="dropdown-divider" />
 										<a

+ 6 - 1
frontend/src/pages/Users/TableWrapper.tsx

@@ -5,13 +5,14 @@ import { deleteUser } from "src/api/backend";
 import { Button, LoadingPage } from "src/components";
 import { useUser, useUsers } from "src/hooks";
 import { intl } from "src/locale";
-import { DeleteConfirmModal, PermissionsModal, UserModal } from "src/modals";
+import { DeleteConfirmModal, PermissionsModal, SetPasswordModal, UserModal } from "src/modals";
 import { showSuccess } from "src/notifications";
 import Table from "./Table";
 
 export default function TableWrapper() {
 	const [editUserId, setEditUserId] = useState(0 as number | "new");
 	const [editUserPermissionsId, setEditUserPermissionsId] = useState(0);
+	const [editUserPasswordId, setEditUserPasswordId] = useState(0);
 	const [deleteUserId, setDeleteUserId] = useState(0);
 	const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
 	const { data: currentUser } = useUser("me");
@@ -64,6 +65,7 @@ export default function TableWrapper() {
 					currentUserId={currentUser?.id}
 					onEditUser={(id: number) => setEditUserId(id)}
 					onEditPermissions={(id: number) => setEditUserPermissionsId(id)}
+					onSetPassword={(id: number) => setEditUserPasswordId(id)}
 					onDeleteUser={(id: number) => setDeleteUserId(id)}
 					onNewUser={() => setEditUserId("new")}
 				/>
@@ -81,6 +83,9 @@ export default function TableWrapper() {
 						{intl.formatMessage({ id: "user.delete.content" })}
 					</DeleteConfirmModal>
 				) : null}
+				{editUserPasswordId ? (
+					<SetPasswordModal userId={editUserPasswordId} onClose={() => setEditUserPasswordId(0)} />
+				) : null}
 			</div>
 		</div>
 	);