diff --git a/messages/en-US.json b/messages/en-US.json index 75760164..c2e1a6f1 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -975,7 +975,6 @@ "actionUpdateOrg": "Update Organization", "actionUpdateUser": "Update User", "actionGetUser": "Get User", - "actionResetUserPassword": "Reset User Password", "actionGetOrgUser": "Get Organization User", "actionListOrgDomains": "List Organization Domains", "actionCreateSite": "Create Site", @@ -1155,25 +1154,9 @@ "initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.", "createAdminAccount": "Create Admin Account", "setupErrorCreateAdmin": "An error occurred while creating the server admin account.", -"passwordReset": "Password Reset", - "passwordResetAdminInstructions": "Clicking the button below will send a password reset email to the user. They will be able to set a new password using the link provided.", - "passwordResetSent": "Password Reset Sent", - "passwordResetSentDescription": "A password reset email has been sent to {email}.", - "passwordResetError": "Failed to Send Reset", - "passwordResetErrorDescription": "Unable to send password reset email. Please try again.", - "passwordResetSending": "Sending...", - "passwordResetSendEmail": "Send Reset Email", - "passwordResetUnavailable": "Password Reset Unavailable", - "passwordResetExternalUserDescription": "Password reset is only available for internal users. External users authenticate through their identity provider.", + "adminSettings": "Admin Settings", "adminSettingsDescription": "Configure server-wide administration settings", - "security": "Security", - "tokenExpiration": "Token Expiration", - "tokenExpirationDescription": "Configure how long password reset tokens remain valid before expiring", - "passwordResetExpireLimit": "Password Reset Token Expiry", - "passwordResetExpireLimitDescription": "Set how many hours password reset tokens remain valid. Default is 1 hour.", - "hours": "hours", - "securitySettingsSaved": "Security Settings Saved", "securitySettingsSavedDescription": "Your security settings have been updated successfully", "securitySettingsUpdated": "Security Settings Updated", "securitySettingsUpdatedDescription": "Your security settings have been updated successfully", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 7fd92f74..ee2c5dac 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -93,7 +93,6 @@ export enum ActionsEnum { listApiKeyActions = "listApiKeyActions", listApiKeys = "listApiKeys", getApiKey = "getApiKey", - resetUserPassword = "resetUserPassword", createOrgDomain = "createOrgDomain", deleteOrgDomain = "deleteOrgDomain", restartOrgDomain = "restartOrgDomain" diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index aabebbbe..37a9aa60 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -23,7 +23,6 @@ import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; export const signupBodySchema = z.object({ - name: z.string().optional(), email: z .string() .toLowerCase() @@ -55,7 +54,7 @@ export async function signup( ); } - const { name, email, password, inviteToken, inviteId } = parsedBody.data; + const { email, password, inviteToken, inviteId } = parsedBody.data; const passwordHash = await hashPassword(password); const userId = generateId(15); @@ -165,7 +164,6 @@ export async function signup( userId: userId, type: UserType.Internal, username: email, - name: name, email: email, passwordHash, dateCreated: moment().toISOString() diff --git a/server/routers/external.ts b/server/routers/external.ts index 11ce07b2..8c90170a 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -552,11 +552,6 @@ authenticated.delete( verifyUserIsServerAdmin, user.adminRemoveUser ); -authenticated.post( - "/admin/user/:userId/password", - verifyUserIsServerAdmin, - user.adminResetUserPassword -); authenticated.post( "/admin/user/:userId", diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 48e5bfef..51604a11 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -388,13 +388,6 @@ authenticated.post( user.updateUser2FA ); -authenticated.post( - "/user/:userId/password", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.resetUserPassword), - user.adminResetUserPassword -); - authenticated.get( "/user/:userId", verifyApiKeyIsRoot, diff --git a/server/routers/user/adminGetUser.ts b/server/routers/user/adminGetUser.ts index 0a961bec..0a260a08 100644 --- a/server/routers/user/adminGetUser.ts +++ b/server/routers/user/adminGetUser.ts @@ -32,7 +32,6 @@ async function queryUser(userId: string) { userId: users.userId, email: users.email, username: users.username, - name: users.name, type: users.type, twoFactorEnabled: users.twoFactorEnabled, twoFactorSetupRequested: users.twoFactorSetupRequested, diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts index 308b9def..dae6405f 100644 --- a/server/routers/user/adminListUsers.ts +++ b/server/routers/user/adminListUsers.ts @@ -32,7 +32,6 @@ async function queryUsers(limit: number, offset: number) { id: users.userId, email: users.email, username: users.username, - name: users.name, dateCreated: users.dateCreated, serverAdmin: users.serverAdmin, type: users.type, diff --git a/server/routers/user/adminResetUserPassword.ts b/server/routers/user/adminResetUserPassword.ts deleted file mode 100644 index a6784156..00000000 --- a/server/routers/user/adminResetUserPassword.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import createHttpError from "http-errors"; -import { db } from "@server/db"; -import { passwordResetTokens, users } from "@server/db"; -import { eq } from "drizzle-orm"; -import { alphabet, generateRandomString } from "oslo/crypto"; -import { createDate, TimeSpan } from "oslo"; -import { hashPassword } from "@server/auth/password"; -import { sendEmail } from "@server/emails"; -import ResetPasswordCode from "@server/emails/templates/ResetPasswordCode"; -import config from "@server/lib/config"; -import logger from "@server/logger"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import { OpenAPITags, registry } from "@server/openApi"; -import { UserType } from "@server/types/UserTypes"; - -const adminResetUserPasswordParamsSchema = z - .object({ - userId: z.string() - }) - .strict(); - -const adminResetUserPasswordBodySchema = z - .object({ - sendEmail: z.boolean().optional().default(true), - expirationHours: z.number().int().positive().optional().default(24) - }) - .strict(); - -export type AdminResetUserPasswordBody = z.infer; -export type AdminResetUserPasswordResponse = { - resetLink?: string; - emailSent: boolean; -}; - -registry.registerPath({ - method: "post", - path: "/admin/user/{userId}/password", - description: "Generate a password reset link for a user (server admin only).", - tags: [OpenAPITags.User], - request: { - params: adminResetUserPasswordParamsSchema, - body: { - content: { - "application/json": { - schema: adminResetUserPasswordBodySchema - } - } - } - }, - responses: {} -}); - -export async function adminResetUserPassword( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedParams = adminResetUserPasswordParamsSchema.safeParse(req.params); - - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = adminResetUserPasswordBodySchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { userId } = parsedParams.data; - const { sendEmail: shouldSendEmail, expirationHours } = parsedBody.data; - - try { - // Get the target user - const targetUser = await db - .select() - .from(users) - .where(eq(users.userId, userId)); - - if (!targetUser || !targetUser.length) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "User not found" - ) - ); - } - - const user = targetUser[0]; - - // Only allow resetting passwords for internal users - if (user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Password reset is only available for internal users" - ) - ); - } - - if (!user.email) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "User does not have an email address" - ) - ); - } - - // Generate reset token - const token = generateRandomString(16, alphabet("0-9", "A-Z", "a-z")); - const tokenHash = await hashPassword(token); - - // Store reset token in database - await db.transaction(async (trx) => { - // Delete any existing reset tokens for this user - await trx - .delete(passwordResetTokens) - .where(eq(passwordResetTokens.userId, userId)); - - // Insert new reset token - await trx.insert(passwordResetTokens).values({ - userId: userId, - email: user.email!, - tokenHash, - expiresAt: createDate(new TimeSpan(expirationHours, "h")).getTime() - }); - }); - - const resetUrl = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(user.email!)}&token=${token}`; - - let emailSent = false; - - // Get the admin identifier (either user ID or API key ID) - const adminId = req.user?.userId || req.apiKey?.apiKeyId || 'unknown'; - - // Send email if requested - if (shouldSendEmail) { - // Check if email is configured - if (!config.getRawConfig().email) { - logger.info( - `Server admin ${adminId} generated password reset link for user ${userId}. Email not configured, no email sent. Token expires in ${expirationHours} hours.` - ); - emailSent = false; - } else { - try { - await sendEmail( - ResetPasswordCode({ - email: user.email!, - code: token, - link: resetUrl - }), - { - from: config.getNoReplyEmail(), - to: user.email!, - subject: "Password Reset - Initiated by Server Administrator" - } - ); - emailSent = true; - - logger.info( - `Server admin ${adminId} initiated password reset for user ${userId}. Email sent to ${user.email}. Token expires in ${expirationHours} hours.` - ); - } catch (e) { - logger.error("Failed to send server admin-initiated password reset email", e); - // Don't fail the request if email fails, just log it - emailSent = false; - } - } - } else { - logger.info( - `Server admin ${adminId} generated password reset link for user ${userId}. No email sent. Token expires in ${expirationHours} hours.` - ); - } - - return response(res, { - data: { - resetLink: resetUrl, - emailSent - }, - success: true, - error: false, - message: emailSent - ? `Password reset email sent to ${user.email}` - : "Password reset link generated successfully", - status: HttpCode.OK - }); - - } catch (e) { - logger.error("Failed to generate server admin password reset", e); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to generate password reset" - ) - ); - } -} \ No newline at end of file diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 05e231c9..fbc9740d 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -18,7 +18,6 @@ async function queryUser(orgId: string, userId: string) { userId: users.userId, email: users.email, username: users.username, - name: users.name, type: users.type, roleId: userOrgs.roleId, roleName: roles.name, diff --git a/server/routers/user/getUser.ts b/server/routers/user/getUser.ts index e33daab6..98c586cd 100644 --- a/server/routers/user/getUser.ts +++ b/server/routers/user/getUser.ts @@ -14,7 +14,6 @@ async function queryUser(userId: string) { userId: users.userId, email: users.email, username: users.username, - name: users.name, type: users.type, twoFactorEnabled: users.twoFactorEnabled, emailVerified: users.emailVerified, diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 2abcde07..85b2474a 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -13,6 +13,4 @@ export * from "./removeInvitation"; export * from "./createOrgUser"; export { updateUser } from "./updateUser"; export { adminUpdateUser } from "./adminUpdateUser"; -export { adminResetUserPassword } from "./adminResetUserPassword"; -export type { AdminResetUserPasswordBody, AdminResetUserPasswordResponse } from "./adminResetUserPassword"; export * from "./adminUpdateUser2FA"; \ No newline at end of file diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 83c1e492..c02eeae9 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -43,7 +43,6 @@ async function queryUsers(orgId: string, limit: number, offset: number) { dateCreated: users.dateCreated, orgId: userOrgs.orgId, username: users.username, - name: users.name, type: users.type, roleId: userOrgs.roleId, roleName: roles.name, diff --git a/server/routers/user/updateUser.ts b/server/routers/user/updateUser.ts index 09a98c33..4e979766 100644 --- a/server/routers/user/updateUser.ts +++ b/server/routers/user/updateUser.ts @@ -20,7 +20,6 @@ const updateUserParamsSchema = z const updateUserBodySchema = z .object({ - name: z.string().min(1).max(255).optional(), email: z.string().email().optional() }) .strict() @@ -30,7 +29,6 @@ const updateUserBodySchema = z export type UpdateUserResponse = { userId: string; - name: string | null; email: string | null; username: string; }; @@ -129,7 +127,6 @@ export async function updateUser( .where(eq(users.userId, userId)) .returning({ userId: users.userId, - name: users.name, email: users.email, username: users.username }); diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/app/[orgId]/settings/access/users/UsersTable.tsx index 44e606f1..5721e59c 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersTable.tsx @@ -27,7 +27,6 @@ export type UserRow = { email: string | null; displayUsername: string | null; username: string; - name: string | null; idpId: number | null; idpName: string; type: string; @@ -248,7 +247,6 @@ export default function UsersTable({ users: u }: UsersTableProps) { {t("userQuestionOrgRemove", { email: selectedUser?.email || - selectedUser?.name || selectedUser?.username || "" })} @@ -263,7 +261,6 @@ export default function UsersTable({ users: u }: UsersTableProps) { onConfirm={removeUser} string={ selectedUser?.email || - selectedUser?.name || selectedUser?.username || "" } diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 665b3436..f34eb2bb 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -74,7 +74,6 @@ export default async function UsersPage(props: UsersPageProps) { id: user.id, username: user.username, displayUsername: user.email || user.username, - name: user.name, email: user.email, type: user.type, idpId: user.idpId, diff --git a/src/app/admin/users/AdminUserManagement.tsx b/src/app/admin/users/AdminUserManagement.tsx index ec3e12d0..b0473a84 100644 --- a/src/app/admin/users/AdminUserManagement.tsx +++ b/src/app/admin/users/AdminUserManagement.tsx @@ -60,13 +60,13 @@ export default function AdminUserManagement({ // Form schema for user details const formSchema = z.object({ - name: z.string().min(1, { message: t('nameRequired') }).max(255) + // Name field removed - no longer needed }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - name: userName || "" + // No default values needed } }); @@ -74,16 +74,11 @@ export default function AdminUserManagement({ setLoading(true); try { - const res = await api.post(`/admin/user/${userId}`, { - name: values.name + // No update needed since name field is removed + toast({ + title: t('userUpdated'), + description: t('userUpdatedDescription'), }); - - if (res.status === 200) { - toast({ - title: t('userUpdated'), - description: t('userUpdatedDescription'), - }); - } } catch (e) { toast({ variant: "destructive", @@ -124,24 +119,8 @@ export default function AdminUserManagement({

- Update user information. Email addresses cannot be changed via the UI. + User information display. Email addresses cannot be changed via the UI.

- ( - - {t('name')} - - - - - - )} - />

Email: {userEmail}

diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index 556a708c..a2cc1edd 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -22,7 +22,6 @@ import { export type GlobalUserRow = { id: string; - name: string | null; username: string; email: string | null; type: string; @@ -242,7 +241,6 @@ export default function UsersTable({ users }: Props) { {t("userQuestionRemove", { selectedUser: selected?.email || - selected?.name || selected?.username })}

@@ -257,7 +255,7 @@ export default function UsersTable({ users }: Props) { buttonText={t("userDeleteConfirm")} onConfirm={async () => deleteUser(selected!.id)} string={ - selected.email || selected.name || selected.username + selected.email || selected.username } title={t("userDeleteServer")} /> diff --git a/src/app/admin/users/[userId]/general/page.tsx b/src/app/admin/users/[userId]/general/page.tsx index 6117a540..99aac1fb 100644 --- a/src/app/admin/users/[userId]/general/page.tsx +++ b/src/app/admin/users/[userId]/general/page.tsx @@ -19,7 +19,7 @@ import { SettingsSectionForm } from "@app/components/Settings"; import { UserType } from "@server/types/UserTypes"; -import AdminPasswordReset from "@app/components/AdminPasswordReset"; + export default function GeneralPage() { const { userId } = useParams(); @@ -31,7 +31,6 @@ export default function GeneralPage() { const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); const [userType, setUserType] = useState(null); const [userEmail, setUserEmail] = useState(""); - const [userName, setUserName] = useState(""); useEffect(() => { // Fetch current user 2FA status @@ -47,7 +46,6 @@ export default function GeneralPage() { ); setUserType(userData.type); setUserEmail(userData.email || ""); - setUserName(userData.name || userData.username || ""); } } catch (error) { console.error("Failed to fetch user data:", error); @@ -123,29 +121,7 @@ export default function GeneralPage() { - - - - {t("passwordReset")} - - - {t("passwordResetAdminInstructions")} - - - - -
- -
-
-
-
diff --git a/src/app/admin/users/[userId]/layout.tsx b/src/app/admin/users/[userId]/layout.tsx index 062b40d8..78ea59c5 100644 --- a/src/app/admin/users/[userId]/layout.tsx +++ b/src/app/admin/users/[userId]/layout.tsx @@ -44,7 +44,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) { return ( <> diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index e9673374..21282ab7 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -32,7 +32,6 @@ export default async function UsersPage(props: PageProps) { return { id: row.id, email: row.email, - name: row.name, username: row.username, type: row.type, idpId: row.idpId, diff --git a/src/components/AdminPasswordReset.tsx b/src/components/AdminPasswordReset.tsx deleted file mode 100644 index 2b0acdf4..00000000 --- a/src/components/AdminPasswordReset.tsx +++ /dev/null @@ -1,232 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "@app/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@app/components/ui/dialog"; -import { Input } from "@app/components/ui/input"; -import { Checkbox } from "@app/components/ui/checkbox"; -import { Label } from "@app/components/ui/label"; -import { toast } from "@app/hooks/useToast"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; -import { Key, Mail, Copy, Link } from "lucide-react"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; - -type AdminPasswordResetProps = { - userId: string; - userEmail: string; - userName: string; - userType: string; -}; - -export default function AdminPasswordReset({ - userId, - userEmail, - userName, - userType, -}: AdminPasswordResetProps) { - const api = createApiClient(useEnvContext()); - const { env } = useEnvContext(); - const t = useTranslations(); - - const [open, setOpen] = useState(false); - const [passwordLoading, setPasswordLoading] = useState(false); - const [sendEmail, setSendEmail] = useState(env.email.emailEnabled); - const [resetLink, setResetLink] = useState(); - - const isExternalUser = userType !== "internal"; - - // Don't render the button for external users - if (isExternalUser) { - return null; - } - - const handleResetPassword = async () => { - setPasswordLoading(true); - - try { - const response = await api.post(`/admin/user/${userId}/password`, { - sendEmail, - expirationHours: 24 - }); - - const data = response.data.data; - - if (data.resetLink) { - setResetLink(data.resetLink); - } - - toast({ - title: t('passwordResetSuccess'), - description: data.emailSent - ? `Password reset email sent to ${userEmail}` - : data.message, - }); - - if (env.email.emailEnabled && sendEmail && data.emailSent) { - setOpen(false); - } - } catch (error) { - toast({ - variant: "destructive", - title: t('passwordResetError'), - description: formatAxiosError(error, t('passwordResetErrorDescription')), - }); - } finally { - setPasswordLoading(false); - } - }; - - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - toast({ - title: t('linkCopied'), - description: t('linkCopiedDescription'), - }); - } catch (e) { - toast({ - variant: "destructive", - title: "Copy failed", - description: "Failed to copy to clipboard. Please copy manually.", - }); - } - }; - - const handleClose = () => { - setOpen(false); - setResetLink(undefined); - setSendEmail(true); - }; - - return ( - <> - - - - - - - - - {t('passwordReset')} - - - Reset password for {userName || userEmail} - - - -
- {!env.email.emailEnabled && ( - - - {t('otpEmailSmtpRequired')} - - - Email is not configured. A reset link will be generated for you to share manually. - - - )} - - {env.email.emailEnabled && ( -
-
- -

Email Notification

-
-

- Send a password reset email to the user with a secure reset link. -

-
- setSendEmail(checked as boolean)} - /> - -
-
- )} - - {resetLink && (!sendEmail || !env.email.emailEnabled) && ( - - - Reset Link Generated - -
-

Share this link with the user:

-
- - -
-

- This link expires in 24 hours. -

-
-
-
- )} -
- - - - {!resetLink && ( - - )} - -
-
- - ); -} \ No newline at end of file diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 17bc8f3d..6f11d98e 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -113,8 +113,7 @@ function getActionsCategories(root: boolean) { actionsByCategory["User"] = { [t('actionUpdateUser')]: "updateUser", - [t('actionGetUser')]: "getUser", - [t('actionResetUserPassword')]: "resetUserPassword" + [t('actionGetUser')]: "getUser" }; } diff --git a/src/components/ProfileIcon.tsx b/src/components/ProfileIcon.tsx index 0348058f..031086fe 100644 --- a/src/components/ProfileIcon.tsx +++ b/src/components/ProfileIcon.tsx @@ -45,7 +45,7 @@ export default function ProfileIcon() { const t = useTranslations(); function getInitials() { - return (user.name || user.email || user.username) + return (user.email || user.username) .substring(0, 1) .toUpperCase(); } @@ -96,7 +96,7 @@ export default function ProfileIcon() { {t("signingAs")}

- {user.email || user.name || user.username} + {user.email || user.username}

{user.serverAdmin ? (