diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index ca9d6f875614b..8ef2a024e037b 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -738,7 +738,7 @@ "invalidInvite": "The invite is invalid or has expired.", "successTitle": "Your account has been created", "successHint": "Get started with Medusa Admin right away.", - "successAction": "Start using Medusa", + "successAction": "Sign in to start using Medusa", "invalidTokenTitle": "Your invite token is invalid", "invalidTokenHint": "Try requesting a new invite link." }, diff --git a/packages/admin-next/dashboard/src/lib/api-v2/auth.ts b/packages/admin-next/dashboard/src/lib/api-v2/auth.ts index c16e0657bacff..172bafabef1e0 100644 --- a/packages/admin-next/dashboard/src/lib/api-v2/auth.ts +++ b/packages/admin-next/dashboard/src/lib/api-v2/auth.ts @@ -1,6 +1,7 @@ import { useMutation } from "@tanstack/react-query" import { adminAuthKeys, useAdminCustomQuery } from "medusa-react" import { medusa } from "../medusa" +import { AcceptInviteInput, CreateAuthUserInput } from "./types/auth" export const useV2Session = (options: any = {}) => { const { data, isLoading, isError, error } = useAdminCustomQuery( @@ -15,7 +16,7 @@ export const useV2Session = (options: any = {}) => { return { user, isLoading, isError, error } } -export const useV2LoginWithSession = () => { +export const useV2LoginAndSetSession = () => { return useMutation( (payload: { email: string; password: string }) => medusa.client.request("POST", "/auth/admin/emailpass", { @@ -41,3 +42,24 @@ export const useV2LoginWithSession = () => { } ) } + +export const useV2CreateAuthUser = (provider = "emailpass") => { + // TODO: Migrate type to work for other providers, e.g. Google + return useMutation((args: CreateAuthUserInput) => + medusa.client.request("POST", `/auth/admin/${provider}`, args) + ) +} + +export const useV2AcceptInvite = (inviteToken: string) => { + return useMutation((input: AcceptInviteInput) => + medusa.client.request( + "POST", + `/admin/invites/accept?token=${inviteToken}`, + input.payload, + {}, + { + Authorization: `Bearer ${input.token}`, + } + ) + ) +} diff --git a/packages/admin-next/dashboard/src/lib/api-v2/types/auth.ts b/packages/admin-next/dashboard/src/lib/api-v2/types/auth.ts new file mode 100644 index 0000000000000..d794acf1f947f --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/api-v2/types/auth.ts @@ -0,0 +1,13 @@ +export type AcceptInviteInput = { + payload: { + first_name: string + last_name: string + } + // Token for the created auth user + token: string +} + +export type CreateAuthUserInput = { + email: string + password: string +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index 44146ec6b7d63..394bc5a733db2 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -53,6 +53,10 @@ export const v2Routes: RouteObject[] = [ path: "*", lazy: () => import("../../routes/no-match"), }, + { + path: "/invite", + lazy: () => import("../../v2-routes/invite"), + }, { element: , errorElement: , diff --git a/packages/admin-next/dashboard/src/v2-routes/invite/index.ts b/packages/admin-next/dashboard/src/v2-routes/invite/index.ts new file mode 100644 index 0000000000000..b49eed9731248 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/invite/index.ts @@ -0,0 +1 @@ +export { Invite as Component } from "./invite" diff --git a/packages/admin-next/dashboard/src/v2-routes/invite/invite.tsx b/packages/admin-next/dashboard/src/v2-routes/invite/invite.tsx new file mode 100644 index 0000000000000..a2b29784f7086 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/invite/invite.tsx @@ -0,0 +1,393 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { UserRoles } from "@medusajs/medusa" +import { Alert, Button, Heading, Input, Text } from "@medusajs/ui" +import { AnimatePresence, motion } from "framer-motion" +import { Trans, useTranslation } from "react-i18next" +import { Link, useSearchParams } from "react-router-dom" +import * as z from "zod" + +import { useState } from "react" +import { useForm } from "react-hook-form" +import { decodeToken } from "react-jwt" +import { Form } from "../../components/common/form" +import { LogoBox } from "../../components/common/logo-box" +import { isAxiosError } from "../../lib/is-axios-error" +import { useV2AcceptInvite, useV2CreateAuthUser } from "../../lib/api-v2" + +const CreateAccountSchema = z + .object({ + email: z.string().email(), + first_name: z.string().min(1), + last_name: z.string().min(1), + password: z.string().min(1), + repeat_password: z.string().min(1), + }) + .superRefine(({ password, repeat_password }, ctx) => { + if (password !== repeat_password) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Passwords do not match", + path: ["repeat_password"], + }) + } + }) + +type DecodedInvite = { + id: string + jti: UserRoles + exp: string + iat: number +} + +export const Invite = () => { + const [searchParams] = useSearchParams() + const [success, setSuccess] = useState(false) + + const token = searchParams.get("token") + const invite: DecodedInvite | null = token ? decodeToken(token) : null + const isValidInvite = invite && validateDecodedInvite(invite) + + return ( +
+
+ +
+ {isValidInvite ? ( + + {!success ? ( + + + setSuccess(true)} + token={token!} + invite={invite} + /> + + + ) : ( + + + + )} + + ) : ( + + )} +
+
+
+ ) +} + +const LoginLink = () => { + const { t } = useTranslation() + + return ( +
+
+ + , + ]} + /> + +
+ ) +} + +const InvalidView = () => { + const { t } = useTranslation() + + return ( +
+
+ {t("invite.invalidTokenTitle")} + + {t("invite.invalidTokenHint")} + +
+ +
+ ) +} + +const CreateView = ({ + onSuccess, + token, +}: { + onSuccess: () => void + token: string + invite: DecodedInvite +}) => { + const { t } = useTranslation() + const [invalid, setInvalid] = useState(false) + + const form = useForm>({ + resolver: zodResolver(CreateAccountSchema), + defaultValues: { + email: "", + first_name: "", + last_name: "", + password: "", + repeat_password: "", + }, + }) + + const { mutateAsync: createAuthUser, isLoading: isCreatingAuthUser } = + useV2CreateAuthUser() + + const { mutateAsync: acceptInvite, isLoading: isAcceptingInvite } = + useV2AcceptInvite(token) + + const handleSubmit = form.handleSubmit(async (data) => { + await createAuthUser( + { + email: data.email, + password: data.password, + }, + { + onSuccess: async ({ token: authToken }) => { + await acceptInvite({ + payload: { + first_name: data.first_name, + last_name: data.last_name, + }, + token: authToken, + }) + + onSuccess() + }, + onError: (error) => { + if (isAxiosError(error) && error.response?.status === 400) { + form.setError("root", { + type: "manual", + message: t("invite.invalidInvite"), + }) + setInvalid(true) + return + } + + form.setError("root", { + type: "manual", + message: t("errors.serverError"), + }) + }, + } + ) + }) + + return ( +
+
+ {t("invite.title")} + + {t("invite.hint")} + +
+
+ +
+ { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.firstName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.lastName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.password")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.repeatPassword")} + + + + + + ) + }} + /> + {form.formState.errors.root && ( + + {form.formState.errors.root.message} + + )} +
+ +
+ + +
+ ) +} + +const SuccessView = () => { + const { t } = useTranslation() + + return ( +
+
+ {t("invite.successTitle")} + + {t("invite.successHint")} + +
+ +
+ ) +} + +const InviteSchema = z.object({ + id: z.string(), + jti: z.string(), + exp: z.number(), + iat: z.number(), +}) + +const validateDecodedInvite = (decoded: any): decoded is DecodedInvite => { + return InviteSchema.safeParse(decoded).success +} diff --git a/packages/admin-next/dashboard/src/v2-routes/login/login.tsx b/packages/admin-next/dashboard/src/v2-routes/login/login.tsx index bbee7dc4640ac..7364d91ec4311 100644 --- a/packages/admin-next/dashboard/src/v2-routes/login/login.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/login/login.tsx @@ -7,7 +7,7 @@ import * as z from "zod" import { Form } from "../../components/common/form" import { LogoBox } from "../../components/common/logo-box" -import { useV2LoginWithSession } from "../../lib/api-v2" +import { useV2LoginAndSetSession } from "../../lib/api-v2" import { isAxiosError } from "../../lib/is-axios-error" const LoginSchema = z.object({ @@ -31,7 +31,7 @@ export const Login = () => { }) // TODO: Update when more than emailpass is supported - const { mutateAsync, isLoading } = useV2LoginWithSession() + const { mutateAsync, isLoading } = useV2LoginAndSetSession() const handleSubmit = form.handleSubmit(async ({ email, password }) => { await mutateAsync( diff --git a/packages/auth/src/loaders/providers.ts b/packages/auth/src/loaders/providers.ts index 9cb09d9788ddd..af8772efe980b 100644 --- a/packages/auth/src/loaders/providers.ts +++ b/packages/auth/src/loaders/providers.ts @@ -1,18 +1,18 @@ import * as defaultProviders from "@providers" -import { - asClass, - AwilixContainer, - ClassOrFunctionReturning, - Constructor, - Resolver, -} from "awilix" import { AuthModuleProviderConfig, AuthProviderScope, LoaderOptions, ModulesSdkTypes, } from "@medusajs/types" +import { + AwilixContainer, + ClassOrFunctionReturning, + Constructor, + Resolver, + asClass, +} from "awilix" type AuthModuleProviders = { providers: AuthModuleProviderConfig[] diff --git a/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts b/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts index 406b078a17aed..0199940531205 100644 --- a/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts +++ b/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts @@ -1,7 +1,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk" -import { IAuthModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IAuthModuleService } from "@medusajs/types" import { isDefined } from "@medusajs/utils" type StepInput = { diff --git a/packages/link-modules/src/repositories/link.ts b/packages/link-modules/src/repositories/link.ts index 04ff247fcce00..272e6294c3e4f 100644 --- a/packages/link-modules/src/repositories/link.ts +++ b/packages/link-modules/src/repositories/link.ts @@ -38,7 +38,6 @@ export function getLinkRepository(model: EntitySchema) { this.joinerConfig_.databaseConfig?.idPrefix ?? "link" ) link.deleted_at = null - return manager.create(model, link) }) diff --git a/packages/medusa/src/api-v2/admin/invites/accept/route.ts b/packages/medusa/src/api-v2/admin/invites/accept/route.ts index 4cc9c8843f79f..aae2a040c36d2 100644 --- a/packages/medusa/src/api-v2/admin/invites/accept/route.ts +++ b/packages/medusa/src/api-v2/admin/invites/accept/route.ts @@ -5,6 +5,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../types/routing" + import { AdminPostInvitesInviteAcceptReq } from "../validators" export const POST = async ( @@ -27,6 +28,7 @@ export const POST = async ( } as InviteWorkflow.AcceptInviteWorkflowInputDTO let users + try { const { result } = await acceptInviteWorkflow(req.scope).run({ input }) users = result diff --git a/packages/medusa/src/api-v2/admin/invites/validators.ts b/packages/medusa/src/api-v2/admin/invites/validators.ts index 094dd4a4d0d7b..1ea4ee94eb967 100644 --- a/packages/medusa/src/api-v2/admin/invites/validators.ts +++ b/packages/medusa/src/api-v2/admin/invites/validators.ts @@ -109,7 +109,6 @@ export class AdminCreateInviteRequest { */ export class AdminPostInvitesInviteAcceptReq { /** - * The invite's first name. * If email is not passed, we default to using the email of the invite. */ @IsString() diff --git a/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts index e1cade11a0e59..d3d635c6317eb 100644 --- a/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts @@ -31,7 +31,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { if (successRedirectUrl) { const url = new URL(successRedirectUrl!) - url.searchParams.append("auth_token", token) + url.searchParams.append("access_token", token) return res.redirect(url.toString()) }