Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
♻️ Recupération token depuis composant client
Browse files Browse the repository at this point in the history
spontoreau committed Jan 22, 2025
1 parent 6a970f4 commit 3953e06
Showing 9 changed files with 99 additions and 77 deletions.
3 changes: 3 additions & 0 deletions packages/applications/ssr/src/app/csrf/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function CSRF() {
return <></>;
}
19 changes: 16 additions & 3 deletions packages/applications/ssr/src/components/atoms/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { FC, FormHTMLAttributes, ReactNode } from 'react';
import { FC, FormHTMLAttributes, ReactNode, useEffect, useState } from 'react';
import { useFormState } from 'react-dom';

import { formAction, ValidationErrors } from '@/utils/formAction';
@@ -23,7 +23,6 @@ export type FormProps = {
onError?: FormHTMLAttributes<HTMLFormElement>['onError'];
onInvalid?: FormHTMLAttributes<HTMLFormElement>['onInvalid'];
successMessage?: string;
csrfToken?: string;
};

export const Form: FC<FormProps> = ({
@@ -38,8 +37,22 @@ export const Form: FC<FormProps> = ({
actions,
onError,
onInvalid,
csrfToken,
}) => {
const [csrfToken, setCsrfToken] = useState('');

useEffect(() => {
const fetchCSRFToken = async () => {
const response = await fetch('/csrf', {
method: 'HEAD',
});

const tokenFromHeader = response.headers.get('csrf_token');
setCsrfToken(tokenFromHeader ?? 'empty_token');
};

fetchCSRFToken();
}, []);

const [state, formAction] = useFormState(action, {
status: undefined,
});
Original file line number Diff line number Diff line change
@@ -12,13 +12,9 @@ import { demanderAbandonAction, DemanderAbandonFormKeys } from './demanderAbando

export type DemanderAbandonFormProps = {
identifiantProjet: string;
csrfToken: string;
};

export const DemanderAbandonForm: FC<DemanderAbandonFormProps> = ({
identifiantProjet,
csrfToken,
}) => {
export const DemanderAbandonForm: FC<DemanderAbandonFormProps> = ({ identifiantProjet }) => {
const [validationErrors, setValidationErrors] = useState<
ValidationErrors<DemanderAbandonFormKeys>
>({});
@@ -27,7 +23,6 @@ export const DemanderAbandonForm: FC<DemanderAbandonFormProps> = ({
action={demanderAbandonAction}
onValidationError={(validationErrors) => setValidationErrors(validationErrors)}
actions={<SubmitButton>Demander l'abandon</SubmitButton>}
csrfToken={csrfToken}
>
<input type={'hidden'} value={identifiantProjet} name="identifiantProjet" />

Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import Alert from '@codegouvfr/react-dsfr/Alert';
import { FC } from 'react';
import { headers } from 'next/headers';

import { ProjetBanner } from '@/components/molecules/projet/ProjetBanner';
import { ColumnPageTemplate } from '@/components/templates/ColumnPage.template';
import { Heading1 } from '@/components/atoms/headings';

import { DemanderAbandonForm, DemanderAbandonFormProps } from './DemanderAbandon.form';

export type DemanderAbandonPageProps = Omit<DemanderAbandonFormProps, 'csrfToken'>;
export type DemanderAbandonPageProps = DemanderAbandonFormProps;

export const DemanderAbandonPage: FC<DemanderAbandonPageProps> = ({ identifiantProjet }) => {
const csrfToken = headers().get('X-CSRF-Token') || 'no_token';

return (
<ColumnPageTemplate
banner={<ProjetBanner identifiantProjet={identifiantProjet} />}
heading={<Heading1>Demander l'abandon du projet</Heading1>}
leftColumn={{
children: (
<DemanderAbandonForm identifiantProjet={identifiantProjet} csrfToken={csrfToken} />
),
children: <DemanderAbandonForm identifiantProjet={identifiantProjet} />,
}}
rightColumn={{
children: (
65 changes: 4 additions & 61 deletions packages/applications/ssr/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,12 @@
import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
import { NextMiddlewareResult } from 'next/dist/server/web/types';
import { getToken } from 'next-auth/jwt';

export type CustomMiddleware = (
request: NextRequest,
event: NextFetchEvent,
response: NextResponse,
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;

type MiddlewareFactory = (middleware: CustomMiddleware) => CustomMiddleware;

export const chain = (functions: MiddlewareFactory[], index = 0): CustomMiddleware => {
const current = functions[index];

if (current) {
const next = chain(functions, index + 1);
return current(next);
}

return (request: NextRequest, event: NextFetchEvent, response: NextResponse) => {
return response;
};
};

const csrfProtect = createCsrfProtect({
cookie: {
sameSite: true,
secure: true,
},
});

function withCSRF(middleware: CustomMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const response = NextResponse.next();

try {
await csrfProtect(request, response);
} catch (err) {
if (err instanceof CsrfError) {
return NextResponse.redirect(new URL('/error', request.url));
}
throw err;
}

return middleware(request, event, response);
};
}

function withNextAuth(middleware: CustomMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
if (!token) {
return NextResponse.redirect(new URL('/auth/signIn', request.url));
}

return middleware(request, event, NextResponse.next());
};
}
import { chain } from './middlewares/chain';
import { withNextAuth } from './middlewares/withNextAuth';
import { withCSRF } from './middlewares/withCSRF';

export default chain([withNextAuth, withCSRF]);

export const config = {
// do not run middleware for paths matching one of following
matcher: [
'/((?!api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|illustrations|$).*)',
'/((?!api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|illustrations|error|$).*)',
],
};
16 changes: 16 additions & 0 deletions packages/applications/ssr/src/middlewares/chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';

import { CustomMiddleware, MiddlewareFactory } from './middleware';

export const chain = (functions: MiddlewareFactory[], index = 0): CustomMiddleware => {
const current = functions[index];

if (current) {
const next = chain(functions, index + 1);
return current(next);
}

return (_request: NextRequest, _event: NextFetchEvent, response: NextResponse) => {
return response;
};
};
10 changes: 10 additions & 0 deletions packages/applications/ssr/src/middlewares/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextMiddlewareResult } from 'next/dist/server/web/types';
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';

export type CustomMiddleware = (
request: NextRequest,
event: NextFetchEvent,
response: NextResponse,
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;

export type MiddlewareFactory = (middleware: CustomMiddleware) => CustomMiddleware;
32 changes: 32 additions & 0 deletions packages/applications/ssr/src/middlewares/withCSRF.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createCsrfProtect, CsrfError } from '@edge-csrf/nextjs';
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';

import { CustomMiddleware } from './middleware';

const csrfProtect = createCsrfProtect({
cookie: {
sameSite: true,
secure: true,
httpOnly: true,
},
token: {
responseHeader: 'csrf_token',
},
});

export function withCSRF(middleware: CustomMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const response = NextResponse.next();

try {
await csrfProtect(request, response);
} catch (err) {
if (err instanceof CsrfError) {
return NextResponse.redirect(new URL('/error', request.url));
}
throw err;
}

return middleware(request, event, response);
};
}
15 changes: 15 additions & 0 deletions packages/applications/ssr/src/middlewares/withNextAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

import { CustomMiddleware } from './middleware';

export function withNextAuth(middleware: CustomMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
if (!token) {
return NextResponse.redirect(new URL('/auth/signIn', request.url));
}

return middleware(request, event, NextResponse.next());
};
}

0 comments on commit 3953e06

Please sign in to comment.