diff --git a/install/config/config.yml b/install/config/config.yml index db6b2b87..cd4bfa58 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -2,37 +2,35 @@ # https://docs.fossorial.io/Pangolin/Configuration/config app: - dashboard_url: "https://{{.DashboardDomain}}" + dashboard_url: "https://localhost:3002" log_level: "info" domains: domain1: - base_domain: "{{.BaseDomain}}" + base_domain: "localhost" cert_resolver: "letsencrypt" server: - secret: "{{.Secret}}" + secret: "your_secret_key_here" cors: - origins: ["https://{{.DashboardDomain}}"] + origins: ["https://localhost:3002"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] allowed_headers: ["X-CSRF-Token", "Content-Type"] credentials: false gerbil: start_port: 51820 - base_endpoint: "{{.DashboardDomain}}" + base_endpoint: "localhost" -{{if .EnableEmail}} email: - smtp_host: "{{.EmailSMTPHost}}" - smtp_port: {{.EmailSMTPPort}} - smtp_user: "{{.EmailSMTPUser}}" - smtp_pass: "{{.EmailSMTPPass}}" - no_reply: "{{.EmailNoReply}}" -{{end}} + smtp_host: "sandbox.smtp.mailtrap.io" + smtp_port: 587 + smtp_user: "4eeaed7a73e4cd" + smtp_pass: "c214cff4be2633" + no_reply: "noreply@example.com" flags: - require_email_verification: {{.EnableEmail}} + require_email_verification: true disable_signup_without_invite: true disable_user_create_org: false allow_raw_resources: true diff --git a/messages/en-US.json b/messages/en-US.json index 236778ca..2c81f4fd 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -973,6 +973,7 @@ "actionUpdateOrg": "Update Organization", "actionUpdateUser": "Update User", "actionGetUser": "Get User", + "actionResetUserPassword": "Reset User Password", "actionGetOrgUser": "Get Organization User", "actionListOrgDomains": "List Organization Domains", "actionCreateSite": "Create Site", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index f68202da..d483c33f 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -87,7 +87,8 @@ export enum ActionsEnum { setApiKeyOrgs = "setApiKeyOrgs", listApiKeyActions = "listApiKeyActions", listApiKeys = "listApiKeys", - getApiKey = "getApiKey" + getApiKey = "getApiKey", + resetUserPassword = "resetUserPassword" } export async function checkUserActionPermission( diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 51604a11..48e5bfef 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -388,6 +388,13 @@ 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/adminResetUserPassword.ts b/server/routers/user/adminResetUserPassword.ts index e4859f1c..a6784156 100644 --- a/server/routers/user/adminResetUserPassword.ts +++ b/server/routers/user/adminResetUserPassword.ts @@ -145,12 +145,15 @@ export async function adminResetUserPassword( 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 ${req.user!.userId} generated password reset link for user ${userId}. Email not configured, no email sent. Token expires in ${expirationHours} hours.` + `Server admin ${adminId} generated password reset link for user ${userId}. Email not configured, no email sent. Token expires in ${expirationHours} hours.` ); emailSent = false; } else { @@ -170,7 +173,7 @@ export async function adminResetUserPassword( emailSent = true; logger.info( - `Server admin ${req.user!.userId} initiated password reset for user ${userId}. Email sent to ${user.email}. Token expires in ${expirationHours} hours.` + `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); @@ -180,7 +183,7 @@ export async function adminResetUserPassword( } } else { logger.info( - `Server admin ${req.user!.userId} generated password reset link for user ${userId}. No email sent. Token expires in ${expirationHours} hours.` + `Server admin ${adminId} generated password reset link for user ${userId}. No email sent. Token expires in ${expirationHours} hours.` ); } diff --git a/src/app/admin/users/AdminUserManagement.tsx b/src/app/admin/users/AdminUserManagement.tsx index 6ab87bc9..ec3e12d0 100644 --- a/src/app/admin/users/AdminUserManagement.tsx +++ b/src/app/admin/users/AdminUserManagement.tsx @@ -20,16 +20,12 @@ import { FormMessage } from "@app/components/ui/form"; 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, Settings, User } from "lucide-react"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@app/components/ui/tabs"; +import { Settings, User } from "lucide-react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -51,13 +47,8 @@ export default function AdminUserManagement({ }: AdminUserManagementProps) { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const [passwordLoading, setPasswordLoading] = useState(false); - const [sendEmail, setSendEmail] = useState(true); - const [resetLink, setResetLink] = useState(); - const [activeTab, setActiveTab] = useState("details"); const api = createApiClient(useEnvContext()); - const { env } = useEnvContext(); const t = useTranslations(); const isExternalUser = userType !== "internal"; @@ -104,62 +95,8 @@ export default function AdminUserManagement({ } }; - 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); form.reset(); }; @@ -179,164 +116,54 @@ export default function AdminUserManagement({ {t('manageUser')} - Manage user details and settings for {userName || userEmail} + Manage user details for {userName || userEmail} - - - - - {t('userDetails')} - - - - {t('passwordReset')} - - - - -
- -

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

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

Email: {userEmail}

-
-
-

Username: {userUsername}

-

Type: {userType}

-
- - - - -
- - - {!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 && ( - - - Reset Link Generated - -
-

Share this link with the user:

-
+
+
+ +

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

+ ( + + {t('name')} + - -
-

- This link expires in 24 hours. -

-
- - - )} - - - - + + + + )} + /> +
+

Email: {userEmail}

+
+
+

Username: {userUsername}

+

Type: {userType}

+
+ + +
+ - {activeTab === "details" && ( - - )} - {activeTab === "password" && !resetLink && ( - - )} diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index d4eac4aa..556a708c 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -12,7 +12,7 @@ import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; -import AdminUserManagement from "./AdminUserManagement"; + import { DropdownMenu, DropdownMenuItem, @@ -187,13 +187,7 @@ export default function UsersTable({ users }: Props) { const r = row.original; 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 6f11d98e..17bc8f3d 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -113,7 +113,8 @@ function getActionsCategories(root: boolean) { actionsByCategory["User"] = { [t('actionUpdateUser')]: "updateUser", - [t('actionGetUser')]: "getUser" + [t('actionGetUser')]: "getUser", + [t('actionResetUserPassword')]: "resetUserPassword" }; }