diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 316ddcb53..e96a577d9 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), - verifier: z.string().trim() + verifier: z.string().trim(), + password: z.string().trim() }), response: { 200: z.object({ @@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { userId: token.userId }); - return { message: "Successfully updated backup private key" }; + return { message: "Successfully reset password" }; + } + }); + + server.route({ + method: "POST", + url: "/email/password-setup", + config: { + rateLimit: authRateLimit + }, + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.password.sendPasswordSetupEmail(req.permission); + + return { + message: "A password setup link has been sent" + }; + } + }); + + server.route({ + method: "POST", + url: "/password-setup", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim(), + password: z.string().trim(), + token: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req, res) => { + await server.services.password.setupPassword(req.body, req.permission); + + const appCfg = getConfig(); + void res.cookie("jid", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { message: "Successfully setup password" }; } }); }; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index c0bb7dc17..d15fa4543 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => { const expiresAt = new Date(new Date().getTime() + 86400000); return { token, expiresAt }; } + case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: { + // generate random hex + const token = crypto.randomBytes(16).toString("hex"); + const expiresAt = new Date(new Date().getTime() + 86400000); + return { token, expiresAt }; + } case TokenType.TOKEN_USER_UNLOCK: { const token = crypto.randomBytes(16).toString("hex"); const expiresAt = new Date(new Date().getTime() + 259200000); diff --git a/backend/src/services/auth-token/auth-token-types.ts b/backend/src/services/auth-token/auth-token-types.ts index 65d16850a..5f5843bc6 100644 --- a/backend/src/services/auth-token/auth-token-types.ts +++ b/backend/src/services/auth-token/auth-token-types.ts @@ -6,6 +6,7 @@ export enum TokenType { TOKEN_EMAIL_MFA = "emailMfa", TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", TOKEN_EMAIL_PASSWORD_RESET = "passwordReset", + TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup", TOKEN_USER_UNLOCK = "userUnlock" } diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 9ed9951fe..aef8eafb9 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -4,6 +4,8 @@ import jwt from "jsonwebtoken"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; +import { BadRequestError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; @@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TAuthDALFactory } from "./auth-dal"; -import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type"; -import { AuthTokenType } from "./auth-type"; +import { + TChangePasswordDTO, + TCreateBackupPrivateKeyDTO, + TResetPasswordViaBackupKeyDTO, + TSetupPasswordViaBackupKeyDTO +} from "./auth-password-type"; +import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; @@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({ verifier, encryptedPrivateKeyIV, encryptedPrivateKeyTag, - userId + userId, + password }: TResetPasswordViaBackupKeyDTO) => { + const cfg = getConfig(); + + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + await userDAL.updateUserEncryptionByUserId(userId, { encryptionVersion: 2, protectedKey, @@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, salt, - verifier + verifier, + hashedPassword }); await userDAL.updateById(userId, { @@ -267,6 +280,106 @@ export const authPaswordServiceFactory = ({ return backupKey; }; + const sendPasswordSetupEmail = async (actor: OrgServiceActor) => { + if (actor.type !== ActorType.USER) + throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` }); + + const user = await userDAL.findById(actor.id); + + if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` }); + + if (!user.isAccepted || !user.authMethods) + throw new BadRequestError({ message: `You must complete signup to set a password` }); + + const cfg = getConfig(); + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, + userId: user.id + }); + + const email = user.email ?? user.username; + + await smtpService.sendMail({ + template: SmtpTemplates.SetupPassword, + recipients: [email], + subjectLine: "Infisical Password Setup", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : "" + } + }); + }; + + const setupPassword = async ( + { + encryptedPrivateKey, + protectedKeyTag, + protectedKey, + protectedKeyIV, + salt, + verifier, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + password, + token + }: TSetupPasswordViaBackupKeyDTO, + actor: OrgServiceActor + ) => { + try { + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, + userId: actor.id, + code: token + }); + } catch (e) { + throw new BadRequestError({ message: "Expired or invalid token. Please try again." }); + } + + await userDAL.transaction(async (tx) => { + const user = await userDAL.findById(actor.id, tx); + + if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` }); + + if (!user.isAccepted || !user.authMethods) + throw new BadRequestError({ message: `You must complete signup to set a password` }); + + await userDAL.updateById( + actor.id, + { + authMethods: [...user.authMethods, AuthMethod.EMAIL] + }, + tx + ); + + const cfg = getConfig(); + + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + + await userDAL.updateUserEncryptionByUserId( + actor.id, + { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier, + hashedPassword, + serverPrivateKey: null, + clientPublicKey: null + }, + tx + ); + }); + + await tokenService.revokeAllMySessions(actor.id); + }; + return { generateServerPubKey, changePassword, @@ -274,6 +387,8 @@ export const authPaswordServiceFactory = ({ sendPasswordResetEmail, verifyPasswordResetEmail, createBackupPrivateKey, - getBackupPrivateKeyOfUser + getBackupPrivateKeyOfUser, + sendPasswordSetupEmail, + setupPassword }; }; diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index a52374506..7c67c0934 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = { encryptedPrivateKeyTag: string; salt: string; verifier: string; + password: string; +}; + +export type TSetupPasswordViaBackupKeyDTO = { + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + password: string; + token: string; }; export type TCreateBackupPrivateKeyDTO = { diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index d997d52f4..67168f1dd 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -30,6 +30,7 @@ export enum SmtpTemplates { NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", ResetPassword = "passwordReset.handlebars", + SetupPassword = "passwordSetup.handlebars", SecretLeakIncident = "secretLeakIncident.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars", diff --git a/backend/src/services/smtp/templates/passwordSetup.handlebars b/backend/src/services/smtp/templates/passwordSetup.handlebars new file mode 100644 index 000000000..1d3a5f72d --- /dev/null +++ b/backend/src/services/smtp/templates/passwordSetup.handlebars @@ -0,0 +1,16 @@ + + + + + Password Setup + + +

Setup your password

+

Someone requested to set up a password for your account. Make sure you are already logged in to Infisical in the current browser before clicking the link below.

+ Setup password +

If you didn't initiate this request, please contact + {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

+ + {{emailFooter}} + + \ No newline at end of file diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 03e077a8d..42823af4d 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({ "/_restrict-login-signup/login/provider/success" ), SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"), - PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset") + PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"), + PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") }, Organization: { SecretScanning: setRoute( diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 8d8ee0c3f..d7703e3b3 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -23,6 +23,7 @@ import { MfaMethod, ResetPasswordDTO, SendMfaTokenDTO, + SetupPasswordDTO, SRP1DTO, SRPR1Res, TOauthTokenExchangeDTO, @@ -286,7 +287,8 @@ export const useResetPassword = () => { encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, salt: details.salt, - verifier: details.verifier + verifier: details.verifier, + password: details.password }, { headers: { @@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => { return data.isVerified; }; + +export const useSendPasswordSetupEmail = () => { + return useMutation({ + mutationFn: async () => { + const { data } = await apiRequest.post("/api/v1/password/email/password-setup"); + + return data; + } + }); +}; + +export const useSetupPassword = () => { + return useMutation({ + mutationFn: async ({ verificationToken, ...payload }: SetupPasswordDTO) => { + const { data } = await apiRequest.post("/api/v1/password/password-setup", payload); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index d0c718e48..036897fed 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -133,6 +133,20 @@ export type ResetPasswordDTO = { salt: string; verifier: string; verificationToken: string; + password: string; +}; + +export type SetupPasswordDTO = { + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + token: string; + password: string; }; export type IssueBackupPrivateKeyDTO = { diff --git a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx index ba28c3871..3361dd961 100644 --- a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx +++ b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx @@ -136,7 +136,8 @@ export const PasswordResetPage = () => { encryptedPrivateKeyTag, salt: result.salt, verifier: result.verifier, - verificationToken + verificationToken, + password: newPassword }); navigate({ to: "/login" }); diff --git a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx new file mode 100644 index 000000000..311d613ef --- /dev/null +++ b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx @@ -0,0 +1,349 @@ +import crypto from "crypto"; + +import { FormEvent, useState } from "react"; +import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useSearch } from "@tanstack/react-router"; +import jsrp from "jsrp"; + +import { createNotification } from "@app/components/notifications"; +import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; +import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; +import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; +import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { useSetupPassword } from "@app/hooks/api/auth/queries"; + +// eslint-disable-next-line new-cap +const client = new jsrp.client(); + +export const PasswordSetupPage = () => { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwordsMatch, setPasswordsMatch] = useState(true); + const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(true); + const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); + const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(true); + const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(true); + const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false); + const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false); + const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false); + const [passwordErrorBreached, setPasswordErrorBreached] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); + + const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordSetupPage.id }); + + const navigate = useNavigate(); + + const setupPassword = useSetupPassword(); + + const parsedUrl = search; + const token = parsedUrl.token as string; + const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); + + const handleSetPassword = async (e: FormEvent) => { + e.preventDefault(); + const errorCheck = await passwordCheck({ + password, + setPasswordErrorTooShort, + setPasswordErrorTooLong, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached + }); + + if (password !== confirmPassword) { + setPasswordsMatch(false); + return; + } + + setPasswordsMatch(true); + + if (!errorCheck) { + client.init( + { + username: email, + password + }, + async () => { + client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { + const derivedKey = await deriveArgonKey({ + password, + salt: result.salt, + mem: 65536, + time: 3, + parallelism: 1, + hashLen: 32 + }); + + if (!derivedKey) throw new Error("Failed to derive key from password"); + + const key = crypto.randomBytes(32); + + // create encrypted private key by encrypting the private + // key with the symmetric key [key] + const { + ciphertext: encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + } = Aes256Gcm.encrypt({ + text: localStorage.getItem("PRIVATE_KEY") as string, + secret: key + }); + + // create the protected key by encrypting the symmetric key + // [key] with the derived key + const { + ciphertext: protectedKey, + iv: protectedKeyIV, + tag: protectedKeyTag + } = Aes256Gcm.encrypt({ + text: key.toString("hex"), + secret: Buffer.from(derivedKey.hash) + }); + + try { + await setupPassword.mutateAsync({ + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt: result.salt, + verifier: result.verifier, + token, + password + }); + + setIsRedirecting(true); + + createNotification({ + type: "success", + title: "Password successfully set", + text: "Redirecting to login..." + }); + + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + } catch (error) { + createNotification({ + type: "error", + text: (error as Error).message ?? "Error setting password" + }); + navigate({ to: "/personal-settings" }); + } + }); + } + ); + } + }; + + const isInvalidPassword = + passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNoLetterChar || + passwordErrorNoNumOrSpecialChar || + passwordErrorRepeatedChar || + passwordErrorEscapeChar || + passwordErrorLowEntropy || + passwordErrorBreached; + + return ( +
+
+ + +
+
+ +
+ Set Password +
+
+ + { + setPassword(e.target.value); + passwordCheck({ + password: e.target.value, + setPasswordErrorTooShort, + setPasswordErrorTooLong, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached + }); + }} + rightIcon={ + + } + /> + + + setConfirmPassword(e.target.value)} + rightIcon={ + + } + /> + +
+
Password must contain:
+
+ {passwordErrorTooShort ? ( + + ) : ( + + )} +
+ at least 14 characters +
+
+
+ {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters +
+
+
+ {passwordErrorNoLetterChar ? ( + + ) : ( + + )} +
+ at least 1 letter character +
+
+
+ {passwordErrorNoNumOrSpecialChar ? ( + + ) : ( + + )} +
+ at least 1 number or special character +
+
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 3 repeated, consecutive characters +
+
+
+ {passwordErrorEscapeChar ? ( + + ) : ( + + )} +
+ no escape characters +
+
+
+ {passwordErrorLowEntropy ? ( + + ) : ( + + )} +
+ no personal information +
+
+
+ {passwordErrorBreached ? ( + + ) : ( + + )} +
+ password not found in a data breach. +
+
+
+ +
+
+
+ ); +}; diff --git a/frontend/src/pages/auth/PasswordSetupPage/route.tsx b/frontend/src/pages/auth/PasswordSetupPage/route.tsx new file mode 100644 index 000000000..5224feedb --- /dev/null +++ b/frontend/src/pages/auth/PasswordSetupPage/route.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { PasswordSetupPage } from "./PasswordSetupPage"; + +const PasswordSetupPageQueryParamsSchema = z.object({ + token: z.string(), + to: z.string() +}); + +export const Route = createFileRoute("/_authenticate/password-setup")({ + component: PasswordSetupPage, + validateSearch: zodValidator(PasswordSetupPageQueryParamsSchema) +}); diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 7810690e5..cb9690837 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -1,12 +1,13 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; +import { ROUTE_PATHS } from "@app/const/routes"; import { userKeys } from "@app/hooks/api"; import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; import { fetchUserDetails } from "@app/hooks/api/users/queries"; export const Route = createFileRoute("/_authenticate")({ - beforeLoad: async ({ context }) => { + beforeLoad: async ({ context, location }) => { if (!context.serverConfig.initialized) { throw redirect({ to: "/admin/signup" }); } @@ -26,7 +27,7 @@ export const Route = createFileRoute("/_authenticate")({ }); }); - if (!data.organizationId) { + if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) { throw redirect({ to: "/login/select-organization" }); } diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx index efe07961e..9c34693df 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx @@ -11,6 +11,7 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; +import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries"; type Errors = { tooShort?: string; @@ -45,6 +46,7 @@ export const ChangePasswordSection = () => { }); const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); + const sendSetupPasswordEmail = useSendPasswordSetupEmail(); const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { try { @@ -80,6 +82,24 @@ export const ChangePasswordSection = () => { } }; + const onSetupPassword = async () => { + try { + await sendSetupPasswordEmail.mutateAsync(); + + createNotification({ + title: "Password setup verification email sent", + text: "Check your email to confirm password setup", + type: "info" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to send password setup email", + type: "error" + }); + } + }; + return (
{ +

+ Need to setup a password?{" "} + +

); }; diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 7c5a749a4..cc89058c3 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -24,6 +24,7 @@ import { Route as authSignUpInvitePageRouteImport } from './pages/auth/SignUpInv import { Route as authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route' import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route' import { Route as authEmailNotVerifiedPageRouteImport } from './pages/auth/EmailNotVerifiedPage/route' +import { Route as authPasswordSetupPageRouteImport } from './pages/auth/PasswordSetupPage/route' import { Route as userLayoutImport } from './pages/user/layout' import { Route as organizationLayoutImport } from './pages/organization/layout' import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route' @@ -310,6 +311,14 @@ const authEmailNotVerifiedPageRouteRoute = getParentRoute: () => middlewaresRestrictLoginSignupRoute, } as any) +const authPasswordSetupPageRouteRoute = authPasswordSetupPageRouteImport.update( + { + id: '/password-setup', + path: '/password-setup', + getParentRoute: () => middlewaresAuthenticateRoute, + } as any, +) + const userLayoutRoute = userLayoutImport.update({ id: '/_layout', getParentRoute: () => AuthenticatePersonalSettingsRoute, @@ -1577,6 +1586,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof middlewaresRestrictLoginSignupImport parentRoute: typeof rootRoute } + '/_authenticate/password-setup': { + id: '/_authenticate/password-setup' + path: '/password-setup' + fullPath: '/password-setup' + preLoaderRoute: typeof authPasswordSetupPageRouteImport + parentRoute: typeof middlewaresAuthenticateImport + } '/_restrict-login-signup/email-not-verified': { id: '/_restrict-login-signup/email-not-verified' path: '/email-not-verified' @@ -3397,12 +3413,14 @@ const AuthenticatePersonalSettingsRouteWithChildren = ) interface middlewaresAuthenticateRouteChildren { + authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren } const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren = { + authPasswordSetupPageRouteRoute: authPasswordSetupPageRouteRoute, middlewaresInjectOrgDetailsRoute: middlewaresInjectOrgDetailsRouteWithChildren, AuthenticatePersonalSettingsRoute: @@ -3487,6 +3505,7 @@ export interface FileRoutesByFullPath { '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute '': typeof organizationLayoutRouteWithChildren + '/password-setup': typeof authPasswordSetupPageRouteRoute '/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute '/password-reset': typeof authPasswordResetPageRouteRoute '/requestnewinvite': typeof authRequestNewInvitePageRouteRoute @@ -3657,6 +3676,7 @@ export interface FileRoutesByTo { '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute '': typeof organizationLayoutRouteWithChildren + '/password-setup': typeof authPasswordSetupPageRouteRoute '/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute '/password-reset': typeof authPasswordResetPageRouteRoute '/requestnewinvite': typeof authRequestNewInvitePageRouteRoute @@ -3824,6 +3844,7 @@ export interface FileRoutesById { '/share-secret': typeof publicShareSecretPageRouteRoute '/_authenticate': typeof middlewaresAuthenticateRouteWithChildren '/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren + '/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute '/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute '/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute '/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute @@ -4004,6 +4025,7 @@ export interface FileRouteTypes { | '/cli-redirect' | '/share-secret' | '' + | '/password-setup' | '/email-not-verified' | '/password-reset' | '/requestnewinvite' @@ -4173,6 +4195,7 @@ export interface FileRouteTypes { | '/cli-redirect' | '/share-secret' | '' + | '/password-setup' | '/email-not-verified' | '/password-reset' | '/requestnewinvite' @@ -4338,6 +4361,7 @@ export interface FileRouteTypes { | '/share-secret' | '/_authenticate' | '/_restrict-login-signup' + | '/_authenticate/password-setup' | '/_restrict-login-signup/email-not-verified' | '/_restrict-login-signup/password-reset' | '/_restrict-login-signup/requestnewinvite' @@ -4562,6 +4586,7 @@ export const routeTree = rootRoute "/_authenticate": { "filePath": "middlewares/authenticate.tsx", "children": [ + "/_authenticate/password-setup", "/_authenticate/_inject-org-details", "/_authenticate/personal-settings" ] @@ -4579,6 +4604,10 @@ export const routeTree = rootRoute "/_restrict-login-signup/admin/signup" ] }, + "/_authenticate/password-setup": { + "filePath": "auth/PasswordSetupPage/route.tsx", + "parent": "/_authenticate" + }, "/_restrict-login-signup/email-not-verified": { "filePath": "auth/EmailNotVerifiedPage/route.tsx", "parent": "/_restrict-login-signup" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index b695f0c3a..5a7f3645b 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -335,6 +335,7 @@ export const routes = rootRoute("root.tsx", [ route("/verify-email", "auth/VerifyEmailPage/route.tsx") ]), middleware("authenticate.tsx", [ + route("/password-setup", "auth/PasswordSetupPage/route.tsx"), route("/personal-settings", [ layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")]) ]),