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")}
+
+
+
+
+
+
+ )
+}
+
+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())
}