Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/[locale]/auth/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { redirect } from '@/lib/locale/navigation';

export default async function AccountPage({
params,
searchParams,
}: {
params: Promise<{ locale: Locale }>;
searchParams: Promise<{ r?: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const { r: redirectTo } = await searchParams;

const { user } = await api.auth.state();

Expand All @@ -21,5 +24,5 @@ export default async function AccountPage({
return redirect({ href: '/', locale });
}

return <AccountSignInForm />;
return <AccountSignInForm redirectTo={redirectTo} />;
}
15 changes: 10 additions & 5 deletions src/app/[locale]/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import { ErrorToast } from '@/components/layout/ErrorToast';
import { Link } from '@/components/ui/Link';
import { Separator } from '@/components/ui/Separator';
import { api } from '@/lib/api/server';
import type { routing } from '@/lib/locale';
import { redirect } from '@/lib/locale/navigation';

export default async function SignInPage({
params,
searchParams,
}: {
params: Promise<{ locale: Locale }>;
searchParams: Promise<{ error?: string }>;
searchParams: Promise<{ r?: string; error?: string }>;
}) {
const { locale } = await params;
let { error } = await searchParams;
let { r: redirectTo, error } = await searchParams;

setRequestLocale(locale);
const t = await getTranslations('auth');

Expand All @@ -41,18 +43,21 @@ export default async function SignInPage({
<Separator />
<div className='absolute bottom-0 left-0 w-full space-y-4'>
<p className='text-center font-montserrat'>{t('signInWith')}</p>
<FeideButton />
<FeideButton redirectTo={redirectTo} />
<Link
className='flex w-full gap-1 bg-primary/80 font-montserrat font-semibold text-black text-md dark:bg-primary/50 dark:text-white hover:dark:bg-primary/40'
variant='default'
size='default'
href='/auth/account'
href={{ pathname: '/auth/account', query: { r: redirectTo } }}
>
<FingerprintIcon className='text-accent dark:text-primary' />
{t('hackerspaceAccount')}
</Link>
</div>
<ErrorToast error={error} cleanPath='/auth' />
<ErrorToast
error={error}
cleanPath={redirectTo ? `/auth?r=${redirectTo}` : '/auth'}
/>
</div>
);
}
6 changes: 5 additions & 1 deletion src/app/api/auth/feide/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const storedState = cookieStore.get('feide-state')?.value;
const codeVerifier = cookieStore.get('feide-code-verifier')?.value;
const redirectTo = cookieStore.get('feide-redirect-to')?.value;
cookieStore.delete('feide-state');
cookieStore.delete('feide-code-verifier');
cookieStore.delete('feide-redirect-to');

if (
!(
Expand Down Expand Up @@ -184,5 +186,7 @@ export async function GET(request: NextRequest) {
const session = await createSession(sessionToken, user.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);

return NextResponse.redirect(new URL('/', env.NEXT_PUBLIC_SITE_URL));
return NextResponse.redirect(
new URL(redirectTo ?? '/', env.NEXT_PUBLIC_SITE_URL),
);
}
5 changes: 3 additions & 2 deletions src/components/auth/AccountSignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { TRPCClientError } from '@/lib/api/types';
import { useRouter } from '@/lib/locale/navigation';
import { accountSignInSchema } from '@/validations/auth/accountSignInSchema';

function AccountSignInForm() {
function AccountSignInForm({ redirectTo }: { redirectTo?: string }) {
const router = useRouter();
const t = useTranslations('auth');
const formSchema = accountSignInSchema(useTranslations());
Expand Down Expand Up @@ -40,7 +40,8 @@ function AccountSignInForm() {
password: '',
},
onSubmit: () => {
router.push('/');
// Honestly, this is somewhat cursed, but it works to let TS know we assume the string is a valid path
router.push((redirectTo as Parameters<typeof router.push>[0]) ?? '/');
router.refresh();
},
});
Expand Down
4 changes: 2 additions & 2 deletions src/components/auth/FeideButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/Button';
import { Spinner } from '@/components/ui/Spinner';
import { api } from '@/lib/api/client';

function FeideButton() {
function FeideButton({ redirectTo }: { redirectTo?: string }) {
const router = useRouter();
const signInMutation = api.auth.signInFeide.useMutation({
onSuccess: (data) => {
Expand All @@ -17,7 +17,7 @@ function FeideButton() {
return (
<Button
className='w-full bg-[#3FACC2]/90 hover:bg-[#3FACC2] dark:bg-[#222832] hover:dark:bg-[#222832]/40'
onClick={() => signInMutation.mutate()}
onClick={() => signInMutation.mutate(redirectTo)}
aria-label='Feide'
title='Feide'
>
Expand Down
14 changes: 13 additions & 1 deletion src/components/layout/header/ProfileMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { UserIcon } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import {
DropdownMenu,
Expand All @@ -25,11 +26,18 @@ type ProfileMenuProps = {

function ProfileMenu({ hasUser, t }: ProfileMenuProps) {
const router = useRouter();
// We use the pathname from next/navigation instead of next-intl.
// If we used pathname from next-intl, the dynamic sections wouldn't be filled out,
// so we would get /events/[eventId], /users/[userId] and so on.
// We want the actual pathname, language doesn't matter as next-intl will handle it.
const pathname = usePathname();

const signOutMutation = api.auth.signOut.useMutation({
onSuccess: () => {
router.refresh();
},
});

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -68,7 +76,11 @@ function ProfileMenu({ hasUser, t }: ProfileMenuProps) {
) : (
<DropdownMenuItem asChild>
<Link
href='/auth'
href={{
pathname: '/auth',
...(pathname !== '/' &&
pathname !== '/en' && { query: { r: pathname } }),
}}
className='w-full justify-start focus-visible:hover:ring-0 focus-visible:hover:ring-offset-0'
>
{t.signIn}
Expand Down
47 changes: 25 additions & 22 deletions src/server/api/routers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,33 +64,36 @@ const authRouter = createRouter({
session: sanitized.session,
};
}),
signInFeide: publicProcedure.mutation(async ({ ctx }) => {
const headerStore = await headers();
const clientIP = headerStore.get('X-Forwarded-For');
signInFeide: publicProcedure
.input((input) => z.string().optional().parse(input))
.mutation(async ({ ctx, input: redirectTo }) => {
const headerStore = await headers();
const clientIP = headerStore.get('X-Forwarded-For');

if (clientIP !== null && !ipBucket.check(clientIP, 1)) {
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: ctx.t('api.tooManyRequests'),
});
}
if (clientIP !== null && !ipBucket.check(clientIP, 1)) {
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: ctx.t('api.tooManyRequests'),
});
}

const feideAuthorization = await createFeideAuthorization();
const feideAuthorization = await createFeideAuthorization();

if (!feideAuthorization) {
throw new TRPCError({
code: 'SERVICE_UNAVAILABLE',
message: ctx.t('auth.feideNotConfigured'),
});
}
if (!feideAuthorization) {
throw new TRPCError({
code: 'SERVICE_UNAVAILABLE',
message: ctx.t('auth.feideNotConfigured'),
});
}

await setFeideAuthorizationCookies(
feideAuthorization.state,
feideAuthorization.codeVerifier,
);
await setFeideAuthorizationCookies(
feideAuthorization.state,
feideAuthorization.codeVerifier,
redirectTo,
);

return feideAuthorization.url.href;
}),
return feideAuthorization.url.href;
}),
signIn: publicProcedure
.input((input) =>
accountSignInSchema(useTranslationsFromContext()).parse(input),
Expand Down
10 changes: 10 additions & 0 deletions src/server/services/feide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ async function validateFeideAuthorization(code: string, codeVerifier: string) {
async function setFeideAuthorizationCookies(
state: string,
codeVerifier: string,
redirectTo?: string,
) {
if (!isFeideServiceConfigured()) {
return console.log(
Expand All @@ -130,6 +131,15 @@ async function setFeideAuthorizationCookies(
maxAge: 60 * 10,
secure: env.NODE_ENV === 'production',
});
if (redirectTo) {
cookieStore.set('feide-redirect-to', redirectTo, {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 10,
secure: env.NODE_ENV === 'production',
});
}
}

export {
Expand Down
Loading