diff --git a/app/(marketing)/_components/site-header-account-section.tsx b/app/(marketing)/_components/site-header-account-section.tsx index c4c388f..ff7ad03 100644 --- a/app/(marketing)/_components/site-header-account-section.tsx +++ b/app/(marketing)/_components/site-header-account-section.tsx @@ -13,10 +13,7 @@ import { Button } from '@kit/ui/button'; import { If } from '@kit/ui/if'; import { Trans } from '@kit/ui/trans'; -import { featureFlagsConfig } from '@kit/shared/config'; - -import { pathsConfig } from '@kit/shared/config'; - +import { authConfig, featureFlagsConfig, pathsConfig } from '@kit/shared/config'; const ModeToggle = dynamic(() => import('@kit/ui/mode-toggle').then((mod) => ({ @@ -75,11 +72,13 @@ function AuthButtons() { - + {authConfig.providers.password && ( + + )} ); diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index 0786a08..b5ce0c0 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -1,19 +1,62 @@ import { redirect } from 'next/navigation'; import type { NextRequest } from 'next/server'; -import { createAuthCallbackService } from '@kit/supabase/auth'; +import { createAuthCallbackService, getErrorURLParameters } from '@kit/supabase/auth'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { pathsConfig } from '@kit/shared/config'; +import { createAccountsApi } from '@/packages/features/accounts/src/server/api'; +const ERROR_PATH = '/auth/callback/error'; + +const redirectOnError = (searchParams?: string) => { + return redirect(`${ERROR_PATH}${searchParams ? `?${searchParams}` : ''}`); +} export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const error = searchParams.get('error'); + if (error) { + const { searchParams } = getErrorURLParameters({ error }); + return redirectOnError(searchParams); + } + + const authCode = searchParams.get('code'); + if (!authCode) { + return redirectOnError(); + } + + let redirectPath = searchParams.get('next') || pathsConfig.app.home; + // if we have an invite token, we redirect to the join team page + // instead of the default next url. This is because the user is trying + // to join a team and we want to make sure they are redirected to the + // correct page. + const inviteToken = searchParams.get('invite_token'); + if (inviteToken) { + const urlParams = new URLSearchParams({ + invite_token: inviteToken, + email: searchParams.get('email') ?? '', + }); + + redirectPath = `${pathsConfig.app.joinTeam}?${urlParams.toString()}`; + } + const service = createAuthCallbackService(getSupabaseServerClient()); + const oauthResult = await service.exchangeCodeForSession(authCode); + if (!("isSuccess" in oauthResult)) { + return redirectOnError(oauthResult.searchParams); + } - const { nextPath } = await service.exchangeCodeForSession(request, { - joinTeamPath: pathsConfig.app.joinTeam, - redirectPath: pathsConfig.app.home, - }); + const api = createAccountsApi(getSupabaseServerClient()); - return redirect(nextPath); + const account = await api.getPersonalAccountByUserId( + oauthResult.user.id, + ); + + if (!account.email || !account.name || !account.last_name) { + return redirect(pathsConfig.auth.updateAccount); + } + + return redirect(redirectPath); } diff --git a/app/auth/sign-in/components/PasswordOption.tsx b/app/auth/sign-in/components/PasswordOption.tsx new file mode 100644 index 0000000..5ef56a4 --- /dev/null +++ b/app/auth/sign-in/components/PasswordOption.tsx @@ -0,0 +1,54 @@ +import Link from 'next/link'; + +import { SignInMethodsContainer } from '@kit/auth/sign-in'; +import { authConfig, pathsConfig } from '@kit/shared/config'; +import { Button } from '@kit/ui/button'; +import { Heading } from '@kit/ui/heading'; +import { Trans } from '@kit/ui/trans'; + +export default function PasswordOption({ + inviteToken, + returnPath, +}: { + inviteToken?: string; + returnPath?: string; +}) { + const signUpPath = + pathsConfig.auth.signUp + + (inviteToken ? `?invite_token=${inviteToken}` : ''); + + const paths = { + callback: pathsConfig.auth.callback, + returnPath: returnPath ?? pathsConfig.app.home, + joinTeam: pathsConfig.app.joinTeam, + updateAccount: pathsConfig.auth.updateAccount, + }; + + return ( + <> +
+ + + + +

+ +

+
+ + + +
+ +
+ + ); +} diff --git a/app/auth/sign-in/components/SignInPageClientRedirect.tsx b/app/auth/sign-in/components/SignInPageClientRedirect.tsx new file mode 100644 index 0000000..2e79df4 --- /dev/null +++ b/app/auth/sign-in/components/SignInPageClientRedirect.tsx @@ -0,0 +1,37 @@ +'use client'; + +import Loading from '@/app/home/loading'; +import { useEffect } from 'react'; +import { getSupabaseBrowserClient } from '@/packages/supabase/src/clients/browser-client'; +import { useRouter } from 'next/navigation'; + +export function SignInPageClientRedirect() { + const router = useRouter(); + + useEffect(() => { + async function signIn() { + const { data, error } = await getSupabaseBrowserClient() + .auth + .signInWithOAuth({ + provider: 'keycloak', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + queryParams: { + prompt: 'login', + }, + } + }); + + if (error) { + console.error('OAuth error', error); + router.push('/'); + } else if (data.url) { + router.push(data.url); + } + } + + signIn(); + }, [router]); + + return ; +} diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index 3728b38..a82d4a7 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -1,14 +1,9 @@ -import Link from 'next/link'; - - -import { SignInMethodsContainer } from '@kit/auth/sign-in'; -import { authConfig, pathsConfig } from '@kit/shared/config'; -import { Button } from '@kit/ui/button'; -import { Heading } from '@kit/ui/heading'; -import { Trans } from '@kit/ui/trans'; +import { pathsConfig, authConfig } from '@kit/shared/config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import { SignInPageClientRedirect } from './components/SignInPageClientRedirect'; +import PasswordOption from './components/PasswordOption'; interface SignInPageProps { searchParams: Promise<{ @@ -26,47 +21,14 @@ export const generateMetadata = async () => { }; async function SignInPage({ searchParams }: SignInPageProps) { - const { invite_token: inviteToken, next = pathsConfig.app.home } = + const { invite_token: inviteToken, next: returnPath = pathsConfig.app.home } = await searchParams; - const signUpPath = - pathsConfig.auth.signUp + - (inviteToken ? `?invite_token=${inviteToken}` : ''); + if (authConfig.providers.password) { + return ; + } - const paths = { - callback: pathsConfig.auth.callback, - returnPath: next ?? pathsConfig.app.home, - joinTeam: pathsConfig.app.joinTeam, - updateAccount: pathsConfig.auth.updateAccount, - }; - - return ( - <> -
- - - - -

- -

-
- - - -
- -
- - ); + return ; } export default withI18n(SignInPage); diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 5c0a4e2..0394078 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import { redirect } from 'next/navigation'; import { SignUpMethodsContainer } from '@kit/auth/sign-up'; import { authConfig, pathsConfig } from '@kit/shared/config'; @@ -37,6 +38,10 @@ async function SignUpPage({ searchParams }: Props) { pathsConfig.auth.signIn + (inviteToken ? `?invite_token=${inviteToken}` : ''); + if (!authConfig.providers.password) { + return redirect('/'); + } + return ( <>
diff --git a/app/auth/update-account/_lib/server/update-account.ts b/app/auth/update-account/_lib/server/update-account.ts index e2fcd0f..ff9a80d 100644 --- a/app/auth/update-account/_lib/server/update-account.ts +++ b/app/auth/update-account/_lib/server/update-account.ts @@ -28,11 +28,15 @@ export const onUpdateAccount = enhanceAction( console.warn('On update account error: ', err); } - await updateCustomer({ - first_name: params.firstName, - last_name: params.lastName, - phone: params.phone, - }); + try { + await updateCustomer({ + first_name: params.firstName, + last_name: params.lastName, + phone: params.phone, + }); + } catch (e) { + console.error("Failed to update Medusa customer", e); + } const hasUnseenMembershipConfirmation = await api.hasUnseenMembershipConfirmation(); diff --git a/app/home/(user)/_lib/server/load-user-account.ts b/app/home/(user)/_lib/server/load-user-account.ts index 471def2..1b324ea 100644 --- a/app/home/(user)/_lib/server/load-user-account.ts +++ b/app/home/(user)/_lib/server/load-user-account.ts @@ -16,14 +16,14 @@ export const loadUserAccount = cache(accountLoader); export async function loadCurrentUserAccount() { const user = await requireUserInServerComponent(); - return user?.identities?.[0]?.id - ? await loadUserAccount(user?.identities?.[0]?.id) + return user?.id + ? await loadUserAccount(user.id) : null; } -async function accountLoader(accountId: string) { +async function accountLoader(userId: string) { const client = getSupabaseServerClient(); const api = createAccountsApi(client); - return api.getAccount(accountId); + return api.getPersonalAccountByUserId(userId); } diff --git a/lib/actions/sign-out.tsx b/lib/actions/sign-out.tsx index a168684..21bf5c0 100644 --- a/lib/actions/sign-out.tsx +++ b/lib/actions/sign-out.tsx @@ -3,9 +3,26 @@ import { redirect } from 'next/navigation'; import { createClient } from '@/utils/supabase/server'; +import { medusaLogout } from '@lib/data/customer'; export const signOutAction = async () => { - const supabase = await createClient(); - await supabase.auth.signOut(); + const client = await createClient(); + + try { + try { + await medusaLogout(); + } catch (medusaError) { + console.warn('Medusa logout failed or not available:', medusaError); + } + + const { error } = await client.auth.signOut(); + if (error) { + throw error; + } + } catch (error) { + console.error('Logout error:', error); + throw error; + } + return redirect('/'); }; diff --git a/packages/features/accounts/src/components/personal-account-dropdown.tsx b/packages/features/accounts/src/components/personal-account-dropdown.tsx index 2a77099..b88eea9 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -79,15 +79,9 @@ export function PersonalAccountDropdown({ }) { const { data: personalAccountData } = usePersonalAccountData(user.id); - const signedInAsLabel = useMemo(() => { - const email = user?.email ?? undefined; - const phone = user?.phone ?? undefined; - - return email ?? phone; - }, [user]); - - const displayName = - personalAccountData?.name ?? account?.name ?? user?.email ?? ''; + const { name, last_name } = personalAccountData ?? {}; + const firstNameLabel = toTitleCase(name) ?? '-'; + const fullNameLabel = name && last_name ? toTitleCase(`${name} ${last_name}`) : '-'; const hasTotpFactor = useMemo(() => { const factors = user?.factors ?? []; @@ -128,7 +122,7 @@ export function PersonalAccountDropdown({ @@ -142,7 +136,7 @@ export function PersonalAccountDropdown({ data-test={'account-dropdown-display-name'} className={'truncate text-sm'} > - {toTitleCase(displayName)} + {firstNameLabel}
@@ -164,7 +158,7 @@ export function PersonalAccountDropdown({
- {signedInAsLabel} + {fullNameLabel}
diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index 4c9e467..f28a490 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -48,6 +48,41 @@ class AccountsApi { return data; } + /** + * @name getPersonalAccountByUserId + * @description Get the personal account data for the given user ID. + * @param userId + */ + async getPersonalAccountByUserId(userId: string): Promise { + const { data, error } = await this.client + .schema('medreport') + .from('accounts') + .select( + '*, accountParams: account_params (weight, height, isSmoker:is_smoker)', + ) + .eq('primary_owner_user_id', userId) + .eq('is_personal_account', true) + .single(); + + if (error) { + throw error; + } + + const { personal_code, ...rest } = data; + return { + ...rest, + personal_code: (() => { + if (!personal_code) { + return null; + } + if (personal_code.toLowerCase().startsWith('ee')) { + return personal_code.substring(2); + } + return personal_code; + })(), + }; + } + /** * @name getAccountWorkspace * @description Get the account workspace data. diff --git a/packages/features/auth/src/components/oauth-providers.tsx b/packages/features/auth/src/components/oauth-providers.tsx index 454b552..11dcc94 100644 --- a/packages/features/auth/src/components/oauth-providers.tsx +++ b/packages/features/auth/src/components/oauth-providers.tsx @@ -25,8 +25,8 @@ import { AuthProviderButton } from './auth-provider-button'; * @see https://supabase.com/docs/guides/auth/social-login */ const OAUTH_SCOPES: Partial> = { - azure: 'email', - keycloak: 'openid', + // azure: 'email', + // keycloak: 'openid', // add your OAuth providers here }; @@ -88,10 +88,12 @@ export const OauthProviders: React.FC<{ queryParams.set('invite_token', props.inviteToken); } - const redirectPath = [ - props.paths.callback, - queryParams.toString(), - ].join('?'); + // signicat/keycloak will not allow redirect-uri with changing query params + const INCLUDE_QUERY_PARAMS = false as boolean; + + const redirectPath = INCLUDE_QUERY_PARAMS + ? [props.paths.callback, queryParams.toString()].join('?') + : props.paths.callback; const redirectTo = [origin, redirectPath].join(''); const scopes = OAUTH_SCOPES[provider] ?? undefined; @@ -102,6 +104,7 @@ export const OauthProviders: React.FC<{ redirectTo, queryParams: props.queryParams, scopes, + // skipBrowserRedirect: false, }, } satisfies SignInWithOAuthCredentials; diff --git a/packages/features/auth/src/components/sign-in-methods-container.tsx b/packages/features/auth/src/components/sign-in-methods-container.tsx index 83bda02..344c040 100644 --- a/packages/features/auth/src/components/sign-in-methods-container.tsx +++ b/packages/features/auth/src/components/sign-in-methods-container.tsx @@ -108,6 +108,9 @@ export function SignInMethodsContainer(props: { callback: props.paths.callback, returnPath: props.paths.returnPath, }} + queryParams={{ + prompt: 'login', + }} /> diff --git a/packages/features/auth/src/components/sign-up-methods-container.tsx b/packages/features/auth/src/components/sign-up-methods-container.tsx index 8b9661c..aadbfb5 100644 --- a/packages/features/auth/src/components/sign-up-methods-container.tsx +++ b/packages/features/auth/src/components/sign-up-methods-container.tsx @@ -79,6 +79,9 @@ export function SignUpMethodsContainer(props: { callback: props.paths.callback, returnPath: props.paths.appHome, }} + queryParams={{ + prompt: 'login', + }} /> diff --git a/packages/features/medusa-storefront/src/lib/data/customer.ts b/packages/features/medusa-storefront/src/lib/data/customer.ts index bf56d6e..a33a33a 100644 --- a/packages/features/medusa-storefront/src/lib/data/customer.ts +++ b/packages/features/medusa-storefront/src/lib/data/customer.ts @@ -4,7 +4,6 @@ import { sdk } from "@lib/config" import medusaError from "@lib/util/medusa-error" import { HttpTypes } from "@medusajs/types" import { revalidateTag } from "next/cache" -import { redirect } from "next/navigation" import { getAuthHeaders, getCacheOptions, @@ -127,7 +126,7 @@ export async function login(_currentState: unknown, formData: FormData) { } } -export async function signout(countryCode?: string, shouldRedirect = true) { +export async function medusaLogout(countryCode = 'ee') { await sdk.auth.logout() await removeAuthToken() @@ -139,10 +138,6 @@ export async function signout(countryCode?: string, shouldRedirect = true) { const cartCacheTag = await getCacheTag("carts") revalidateTag(cartCacheTag) - - if (shouldRedirect) { - redirect(`/${countryCode!}/account`) - } } export async function transferCart() { @@ -262,72 +257,110 @@ export const updateCustomerAddress = async ( }) } -export async function medusaLoginOrRegister(credentials: { - email: string - password?: string -}) { - const { email, password } = credentials; +async function medusaLogin(email: string, password: string) { + const token = await sdk.auth.login("customer", "emailpass", { email, password }); + await setAuthToken(token as string); try { - const token = await sdk.auth.login("customer", "emailpass", { - email, - password, + await transferCart(); + } catch (e) { + console.error("Failed to transfer cart", e); + } + + const customer = await retrieveCustomer(); + if (!customer) { + throw new Error("Customer not found for active session"); + } + + return customer.id; +} + +async function medusaRegister({ + email, + password, + name, + lastName, +}: { + email: string; + password: string; + name: string | undefined; + lastName: string | undefined; +}) { + console.info(`Creating new Medusa account for Keycloak user with email=${email}`); + + const registerToken = await sdk.auth.register("customer", "emailpass", { email, password }); + await setAuthToken(registerToken); + + console.info(`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`); + await sdk.store.customer.create( + { email, first_name: name, last_name: lastName }, + {}, + { + ...(await getAuthHeaders()), }); - await setAuthToken(token as string); +} - try { - await transferCart(); - } catch (e) { - console.error("Failed to transfer cart", e); +export async function medusaLoginOrRegister(credentials: { + email: string + supabaseUserId?: string + name?: string, + lastName?: string, +} & ({ isDevPasswordLogin: true; password: string } | { isDevPasswordLogin?: false; password?: undefined })) { + const { email, supabaseUserId, name, lastName } = credentials; + + + const password = await (async () => { + if (credentials.isDevPasswordLogin) { + return credentials.password; } - const customerCacheTag = await getCacheTag("customers"); - revalidateTag(customerCacheTag); + return generateDeterministicPassword(email, supabaseUserId); + })(); + + try { + return await medusaLogin(email, password); + } catch (loginError) { + console.error("Failed to login customer, attempting to register", loginError); - const customer = await retrieveCustomer(); - if (!customer) { - throw new Error("Customer not found"); - } - return customer.id; - } catch (error) { - console.error("Failed to login customer, attempting to register", error); try { - const registerToken = await sdk.auth.register("customer", "emailpass", { - email: email, - password: password, - }) - - await setAuthToken(registerToken as string); - - const headers = { - ...(await getAuthHeaders()), - }; - - await sdk.store.customer.create({ email }, {}, headers); - - const loginToken = await sdk.auth.login("customer", "emailpass", { - email, - password, - }); - - await setAuthToken(loginToken as string); - - const customerCacheTag = await getCacheTag("customers"); - revalidateTag(customerCacheTag); - - try { - await transferCart(); - } catch (e) { - console.error("Failed to transfer cart", e); - } - - const customer = await retrieveCustomer(); - if (!customer) { - throw new Error("Customer not found"); - } - return customer.id; + await medusaRegister({ email, password, name, lastName }); + return await medusaLogin(email, password); } catch (registerError) { + console.error("Failed to create Medusa account for user with email=${email}", registerError); throw medusaError(registerError); } } } + +/** + * Generate a deterministic password based on user identifier + * This ensures the same user always gets the same password for Medusa + */ +async function generateDeterministicPassword(email: string, userId?: string): Promise { + // Use the user ID or email as the base for deterministic generation + const baseString = userId || email; + const secret = process.env.MEDUSA_PASSWORD_SECRET!; + + // Create a deterministic password using HMAC + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const messageData = encoder.encode(baseString); + + // Import key for HMAC + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + // Generate HMAC + const signature = await crypto.subtle.sign('HMAC', key, messageData); + // Convert to base64 and make it a valid password + const hashArray = Array.from(new Uint8Array(signature)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + // Take first 24 characters and add some complexity + const basePassword = hashHex.substring(0, 24); + // Add some required complexity for Medusa (uppercase, lowercase, numbers, symbols) + return `Mk${basePassword}9!`; +} diff --git a/packages/features/medusa-storefront/src/modules/account/components/account-nav/index.tsx b/packages/features/medusa-storefront/src/modules/account/components/account-nav/index.tsx index 61dd0c2..338dd22 100644 --- a/packages/features/medusa-storefront/src/modules/account/components/account-nav/index.tsx +++ b/packages/features/medusa-storefront/src/modules/account/components/account-nav/index.tsx @@ -10,7 +10,7 @@ import MapPin from "@modules/common/icons/map-pin" import Package from "@modules/common/icons/package" import LocalizedClientLink from "@modules/common/components/localized-client-link" import { HttpTypes } from "@medusajs/types" -import { signout } from "@lib/data/customer" +import { medusaLogout } from "@lib/data/customer" const AccountNav = ({ customer, @@ -21,7 +21,7 @@ const AccountNav = ({ const { countryCode } = useParams() as { countryCode: string } const handleLogout = async () => { - await signout(countryCode) + await medusaLogout(countryCode) } return ( diff --git a/packages/shared/src/config/auth.config.ts b/packages/shared/src/config/auth.config.ts index 9e73291..ab460ee 100644 --- a/packages/shared/src/config/auth.config.ts +++ b/packages/shared/src/config/auth.config.ts @@ -32,7 +32,7 @@ const authConfig = AuthConfigSchema.parse({ providers: { password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true', magicLink: process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true', - oAuth: ['google'], + oAuth: ['keycloak'], }, } satisfies z.infer); diff --git a/packages/supabase/src/auth-callback.service.ts b/packages/supabase/src/auth-callback.service.ts index 1190bc2..c1d8126 100644 --- a/packages/supabase/src/auth-callback.service.ts +++ b/packages/supabase/src/auth-callback.service.ts @@ -4,6 +4,7 @@ import { AuthError, type EmailOtpType, SupabaseClient, + User, } from '@supabase/supabase-js'; /** @@ -20,7 +21,7 @@ export function createAuthCallbackService(client: SupabaseClient) { * @description Service for handling auth callbacks in Supabase */ class AuthCallbackService { - constructor(private readonly client: SupabaseClient) {} + constructor(private readonly client: SupabaseClient) { } /** * @name verifyTokenHash @@ -128,89 +129,117 @@ class AuthCallbackService { /** * @name exchangeCodeForSession * @description Exchanges the auth code for a session and redirects the user to the next page or an error page - * @param request - * @param params + * @param authCode */ - async exchangeCodeForSession( - request: Request, - params: { - joinTeamPath: string; - redirectPath: string; - errorPath?: string; - }, - ): Promise<{ - nextPath: string; - }> { - const requestUrl = new URL(request.url); - const searchParams = requestUrl.searchParams; + async exchangeCodeForSession(authCode: string): Promise<{ + isSuccess: boolean; + user: User; + } | ErrorURLParameters> { + let user: User; + try { + const { data, error } = + await this.client.auth.exchangeCodeForSession(authCode); - const authCode = searchParams.get('code'); - const error = searchParams.get('error'); - const nextUrlPathFromParams = searchParams.get('next'); - const inviteToken = searchParams.get('invite_token'); - const errorPath = params.errorPath ?? '/auth/callback/error'; - - let nextUrl = nextUrlPathFromParams ?? params.redirectPath; - - // if we have an invite token, we redirect to the join team page - // instead of the default next url. This is because the user is trying - // to join a team and we want to make sure they are redirected to the - // correct page. - if (inviteToken) { - const emailParam = searchParams.get('email'); - - const urlParams = new URLSearchParams({ - invite_token: inviteToken, - email: emailParam ?? '', - }); - - nextUrl = `${params.joinTeamPath}?${urlParams.toString()}`; - } - - if (authCode) { - try { - const { error } = - await this.client.auth.exchangeCodeForSession(authCode); - - // if we have an error, we redirect to the error page - if (error) { - return onError({ - code: error.code, - error: error.message, - path: errorPath, - }); - } - } catch (error) { - console.error( - { - error, - name: `auth.callback`, - }, - `An error occurred while exchanging code for session`, - ); - - const message = error instanceof Error ? error.message : error; - - return onError({ - code: (error as AuthError)?.code, - error: message as string, - path: errorPath, + // if we have an error, we redirect to the error page + if (error) { + return getErrorURLParameters({ + code: error.code, + error: error.message, }); } - } - if (error) { - return onError({ - error, - path: errorPath, + // Handle Keycloak users - set up Medusa integration + if (data?.user && this.isKeycloakUser(data.user)) { + await this.setupMedusaUserForKeycloak(data.user); + } + + user = data.user; + } catch (error) { + console.error( + { + error, + name: `auth.callback`, + }, + `An error occurred while exchanging code for session`, + ); + + const message = error instanceof Error ? error.message : error; + + return getErrorURLParameters({ + code: (error as AuthError)?.code, + error: message as string, }); } return { - nextPath: nextUrl, + isSuccess: true, + user, }; } + /** + * Check if user is from Keycloak provider + */ + private isKeycloakUser(user: any): boolean { + return user?.app_metadata?.provider === 'keycloak' || + user?.app_metadata?.providers?.includes('keycloak'); + } + + private async setupMedusaUserForKeycloak(user: any): Promise { + if (!user.email) { + console.warn('Keycloak user has no email, skipping Medusa setup'); + return; + } + + try { + // Check if user already has medusa_account_id + const { data: accountData, error: fetchError } = await this.client + .schema('medreport') + .from('accounts') + .select('medusa_account_id, name, last_name') + .eq('primary_owner_user_id', user.id) + .eq('is_personal_account', true) + .single(); + + if (fetchError && fetchError.code !== 'PGRST116') { + console.error('Error fetching account data for Keycloak user:', fetchError); + return; + } + + // If user already has Medusa account, we're done + if (accountData?.medusa_account_id) { + console.log('Keycloak user already has Medusa account:', accountData.medusa_account_id); + return; + } + + const { medusaLoginOrRegister } = await import('../../features/medusa-storefront/src/lib/data/customer'); + + const medusaAccountId = await medusaLoginOrRegister({ + email: user.email, + supabaseUserId: user.id, + name: accountData?.name ?? '-', + lastName: accountData?.last_name ?? '-', + }); + + // Update the account with the Medusa account ID + const { error: updateError } = await this.client + .schema('medreport') + .from('accounts') + .update({ medusa_account_id: medusaAccountId }) + .eq('primary_owner_user_id', user.id) + .eq('is_personal_account', true); + + if (updateError) { + console.error('Error updating account with Medusa ID:', updateError); + return; + } + + console.log('Successfully set up Medusa account for Keycloak user:', medusaAccountId); + } catch (error) { + console.error('Error setting up Medusa account for Keycloak user:', error); + } + } + private adjustUrlHostForLocalDevelopment(url: URL, host: string | null) { if (this.isLocalhost(url.host) && !this.isLocalhost(host)) { url.host = host as string; @@ -231,15 +260,19 @@ class AuthCallbackService { } } -function onError({ +interface ErrorURLParameters { + error: string; + code?: string; + searchParams: string; +} + +export function getErrorURLParameters({ error, - path, code, }: { error: string; - path: string; code?: string; -}) { +}): ErrorURLParameters { const errorMessage = getAuthErrorMessage({ error, code }); console.error( @@ -255,10 +288,10 @@ function onError({ code: code ?? '', }); - const nextPath = `${path}?${searchParams.toString()}`; - return { - nextPath, + error: errorMessage, + code: code ?? '', + searchParams: searchParams.toString(), }; } diff --git a/packages/supabase/src/clients/browser-client.ts b/packages/supabase/src/clients/browser-client.ts index 747945e..69bb463 100644 --- a/packages/supabase/src/clients/browser-client.ts +++ b/packages/supabase/src/clients/browser-client.ts @@ -10,5 +10,11 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; export function getSupabaseBrowserClient() { const keys = getSupabaseClientKeys(); - return createBrowserClient(keys.url, keys.anonKey); + return createBrowserClient(keys.url, keys.anonKey, { + auth: { + flowType: 'pkce', + autoRefreshToken: true, + persistSession: true, + }, + }); } diff --git a/packages/supabase/src/clients/middleware-client.ts b/packages/supabase/src/clients/middleware-client.ts index 608dc3b..e9c0a31 100644 --- a/packages/supabase/src/clients/middleware-client.ts +++ b/packages/supabase/src/clients/middleware-client.ts @@ -20,6 +20,11 @@ export function createMiddlewareClient( const keys = getSupabaseClientKeys(); return createServerClient(keys.url, keys.anonKey, { + auth: { + flowType: 'pkce', + autoRefreshToken: true, + persistSession: true, + }, cookies: { getAll() { return request.cookies.getAll(); diff --git a/packages/supabase/src/clients/server-client.ts b/packages/supabase/src/clients/server-client.ts index cd4c82c..c9b8d7e 100644 --- a/packages/supabase/src/clients/server-client.ts +++ b/packages/supabase/src/clients/server-client.ts @@ -15,6 +15,11 @@ export function getSupabaseServerClient() { const keys = getSupabaseClientKeys(); return createServerClient(keys.url, keys.anonKey, { + auth: { + flowType: 'pkce', + autoRefreshToken: true, + persistSession: true, + }, cookies: { async getAll() { const cookieStore = await cookies(); diff --git a/packages/supabase/src/hooks/use-sign-in-with-email-password.ts b/packages/supabase/src/hooks/use-sign-in-with-email-password.ts index 6ed91c9..549bc1b 100644 --- a/packages/supabase/src/hooks/use-sign-in-with-email-password.ts +++ b/packages/supabase/src/hooks/use-sign-in-with-email-password.ts @@ -28,6 +28,7 @@ export function useSignInWithEmailPassword() { const medusaAccountId = await medusaLoginOrRegister({ email: credentials.email, password: credentials.password, + isDevPasswordLogin: true, }); await client .schema('medreport').from('accounts') diff --git a/packages/supabase/src/hooks/use-sign-in-with-provider.ts b/packages/supabase/src/hooks/use-sign-in-with-provider.ts index d68700b..7361549 100644 --- a/packages/supabase/src/hooks/use-sign-in-with-provider.ts +++ b/packages/supabase/src/hooks/use-sign-in-with-provider.ts @@ -9,7 +9,13 @@ export function useSignInWithProvider() { const mutationKey = ['auth', 'sign-in-with-provider']; const mutationFn = async (credentials: SignInWithOAuthCredentials) => { - const response = await client.auth.signInWithOAuth(credentials); + const response = await client.auth.signInWithOAuth({ + ...credentials, + options: { + ...credentials.options, + redirectTo: `${window.location.origin}/auth/callback`, + }, + }); if (response.error) { throw response.error.message; diff --git a/packages/supabase/src/hooks/use-sign-out.ts b/packages/supabase/src/hooks/use-sign-out.ts index fbe65ee..c354cee 100644 --- a/packages/supabase/src/hooks/use-sign-out.ts +++ b/packages/supabase/src/hooks/use-sign-out.ts @@ -1,15 +1,28 @@ import { useMutation } from '@tanstack/react-query'; import { useSupabase } from './use-supabase'; -import { signout } from '../../../features/medusa-storefront/src/lib/data/customer'; export function useSignOut() { const client = useSupabase(); return useMutation({ mutationFn: async () => { - await signout(undefined, false); - return client.auth.signOut(); + try { + try { + const { medusaLogout } = await import('../../../features/medusa-storefront/src/lib/data/customer'); + await medusaLogout(); + } catch (medusaError) { + console.warn('Medusa logout failed or not available:', medusaError); + } + + const { error } = await client.auth.signOut(); + if (error) { + throw error; + } + } catch (error) { + console.error('Logout error:', error); + throw error; + } }, }); } diff --git a/packages/supabase/src/hooks/use-sign-up-with-email-password.ts b/packages/supabase/src/hooks/use-sign-up-with-email-password.ts index f6dc21f..59a864c 100644 --- a/packages/supabase/src/hooks/use-sign-up-with-email-password.ts +++ b/packages/supabase/src/hooks/use-sign-up-with-email-password.ts @@ -43,6 +43,7 @@ export function useSignUpWithEmailAndPassword() { const medusaAccountId = await medusaLoginOrRegister({ email: credentials.email, password: credentials.password, + isDevPasswordLogin: true, }); await client .schema('medreport').from('accounts')