update keycloak signup / login

This commit is contained in:
2025-09-08 01:02:10 +03:00
parent 7815a1c011
commit f01829de96
25 changed files with 501 additions and 239 deletions

View File

@@ -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() {
</Link>
</Button>
<Button asChild className="text-xs md:text-sm" variant={'default'}>
<Link href={pathsConfig.auth.signUp}>
<Trans i18nKey={'auth:signUp'} />
</Link>
</Button>
{authConfig.providers.password && (
<Button asChild className="text-xs md:text-sm" variant={'default'}>
<Link href={pathsConfig.auth.signUp}>
<Trans i18nKey={'auth:signUp'} />
</Link>
</Button>
)}
</div>
</div>
);

View File

@@ -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);
}

View File

@@ -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 (
<>
<div className={'flex flex-col items-center gap-1'}>
<Heading level={4} className={'tracking-tight'}>
<Trans i18nKey={'auth:signInHeading'} />
</Heading>
<p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'auth:signInSubheading'} />
</p>
</div>
<SignInMethodsContainer
inviteToken={inviteToken}
paths={paths}
providers={authConfig.providers}
/>
<div className={'flex justify-center'}>
<Button asChild variant={'link'} size={'sm'}>
<Link href={signUpPath} prefetch={true}>
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
</Link>
</Button>
</div>
</>
);
}

View File

@@ -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 <Loading />;
}

View File

@@ -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 <PasswordOption inviteToken={inviteToken} returnPath={returnPath} />;
}
const paths = {
callback: pathsConfig.auth.callback,
returnPath: next ?? pathsConfig.app.home,
joinTeam: pathsConfig.app.joinTeam,
updateAccount: pathsConfig.auth.updateAccount,
};
return (
<>
<div className={'flex flex-col items-center gap-1'}>
<Heading level={4} className={'tracking-tight'}>
<Trans i18nKey={'auth:signInHeading'} />
</Heading>
<p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'auth:signInSubheading'} />
</p>
</div>
<SignInMethodsContainer
inviteToken={inviteToken}
paths={paths}
providers={authConfig.providers}
/>
<div className={'flex justify-center'}>
<Button asChild variant={'link'} size={'sm'}>
<Link href={signUpPath} prefetch={true}>
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
</Link>
</Button>
</div>
</>
);
return <SignInPageClientRedirect />;
}
export default withI18n(SignInPage);

View File

@@ -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 (
<>
<div className={'flex flex-col items-center gap-1'}>

View File

@@ -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();

View File

@@ -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);
}