diff --git a/app/api/auth/agent-connect/agent-connect-types.ts b/app/api/auth/agent-connect/agent-connect-types.ts new file mode 100644 index 000000000..d4ed4a1f2 --- /dev/null +++ b/app/api/auth/agent-connect/agent-connect-types.ts @@ -0,0 +1,19 @@ +import { Exception } from '#models/exceptions'; + +export class AgentConnectLogoutFailedException extends Exception { + constructor(args: { cause?: any }) { + super({ + ...args, + name: 'LogoutFailedException', + }); + } +} + +export class AgentConnectFailedException extends Exception { + constructor(args: { cause?: any }) { + super({ + name: 'AgentConnectionFailedException', + ...args, + }); + } +} diff --git a/pages/api/auth/agent-connect/callback.ts b/app/api/auth/agent-connect/callback/route.ts similarity index 59% rename from pages/api/auth/agent-connect/callback.ts rename to app/api/auth/agent-connect/callback/route.ts index 30f1b17f1..4d6fe0871 100644 --- a/pages/api/auth/agent-connect/callback.ts +++ b/app/api/auth/agent-connect/callback/route.ts @@ -1,12 +1,13 @@ import { agentConnectAuthenticate } from '#clients/authentication/agent-connect/strategy'; import { HttpForbiddenError } from '#clients/exceptions'; -import { Exception } from '#models/exceptions'; import { getAgent } from '#models/user/agent'; import { logFatalErrorInSentry } from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; import { cleanPathFrom, getPathFrom, setAgentSession } from '#utils/session'; import withSession from '#utils/session/with-session'; +import { AgentConnectFailedException } from '../agent-connect-types'; -export default withSession(async function callbackRoute(req, res) { +export const GET = withSession(async function callbackRoute(req) { try { const userInfo = await agentConnectAuthenticate(req); const agent = await getAgent(userInfo); @@ -17,25 +18,16 @@ export default withSession(async function callbackRoute(req, res) { if (pathFrom) { await cleanPathFrom(session); - res.redirect(pathFrom); + return redirectTo(req, pathFrom); } else { - res.redirect('/'); + return redirectTo(req, '/'); } } catch (e: any) { - logFatalErrorInSentry(new AgentConnectionFailedException({ cause: e })); + logFatalErrorInSentry(new AgentConnectFailedException({ cause: e })); if (e instanceof HttpForbiddenError) { - res.redirect('/connexion/echec-authorisation-requise'); + return redirectTo(req, '/connexion/echec-authorisation-requise'); } else { - res.redirect('/connexion/echec-connexion'); + return redirectTo(req, '/connexion/echec-connexion'); } } }); - -export class AgentConnectionFailedException extends Exception { - constructor(args: { cause?: any }) { - super({ - name: 'AgentConnectionFailedException', - ...args, - }); - } -} diff --git a/app/api/auth/agent-connect/login/route.ts b/app/api/auth/agent-connect/login/route.ts new file mode 100644 index 000000000..00a787f2e --- /dev/null +++ b/app/api/auth/agent-connect/login/route.ts @@ -0,0 +1,23 @@ +import { agentConnectAuthorizeUrl } from '#clients/authentication/agent-connect/strategy'; +import { logFatalErrorInSentry } from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; +import { setPathFrom } from '#utils/session'; +import withSession from '#utils/session/with-session'; +import { NextResponse } from 'next/server'; +import { AgentConnectFailedException } from '../agent-connect-types'; + +export const GET = withSession(async function loginRoute(req) { + try { + const pathFrom = + req.nextUrl.searchParams.get('pathFrom') || + req.headers.get('referer') || + ''; + + await setPathFrom(req.session, pathFrom); + const url = await agentConnectAuthorizeUrl(req); + return NextResponse.redirect(url); + } catch (e: any) { + logFatalErrorInSentry(new AgentConnectFailedException({ cause: e })); + return redirectTo(req, '/connexion/echec-connexion'); + } +}); diff --git a/app/api/auth/agent-connect/logout-callback/route.ts b/app/api/auth/agent-connect/logout-callback/route.ts new file mode 100644 index 000000000..57c41a4e4 --- /dev/null +++ b/app/api/auth/agent-connect/logout-callback/route.ts @@ -0,0 +1,22 @@ +import logErrorInSentry from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; +import { cleanAgentSession, cleanPathFrom, getPathFrom } from '#utils/session'; +import withSession from '#utils/session/with-session'; +import { AgentConnectLogoutFailedException } from '../agent-connect-types'; + +export const GET = withSession(async function logoutCallbackRoute(req) { + try { + const session = req.session; + await cleanAgentSession(session); + const pathFrom = getPathFrom(session); + if (pathFrom) { + await cleanPathFrom(session); + return redirectTo(req, pathFrom); + } else { + return redirectTo(req, '/connexion/au-revoir'); + } + } catch (e: any) { + logErrorInSentry(new AgentConnectLogoutFailedException({ cause: e })); + return redirectTo(req, '/connexion/au-revoir'); + } +}); diff --git a/app/api/auth/agent-connect/logout/route.ts b/app/api/auth/agent-connect/logout/route.ts new file mode 100644 index 000000000..7d3815720 --- /dev/null +++ b/app/api/auth/agent-connect/logout/route.ts @@ -0,0 +1,18 @@ +import { agentConnectLogoutUrl } from '#clients/authentication/agent-connect/strategy'; +import logErrorInSentry from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; +import { setPathFrom } from '#utils/session'; +import withSession from '#utils/session/with-session'; +import { NextResponse } from 'next/server'; +import { AgentConnectLogoutFailedException } from '../agent-connect-types'; + +export const GET = withSession(async function logoutRoute(req) { + try { + await setPathFrom(req.session, req.headers.get('referer') || ''); + const url = await agentConnectLogoutUrl(req); + return NextResponse.redirect(url); + } catch (e: any) { + logErrorInSentry(new AgentConnectLogoutFailedException({ cause: e })); + return redirectTo(req, '/connexion/au-revoir'); + } +}); diff --git a/pages/api/auth/france-connect/callback.ts b/app/api/auth/france-connect/callback/route.ts similarity index 60% rename from pages/api/auth/france-connect/callback.ts rename to app/api/auth/france-connect/callback/route.ts index 7906df629..239583ee7 100644 --- a/pages/api/auth/france-connect/callback.ts +++ b/app/api/auth/france-connect/callback/route.ts @@ -1,10 +1,11 @@ import { franceConnectAuthenticate } from '#clients/authentication/france-connect/strategy'; -import { Exception } from '#models/exceptions'; import logErrorInSentry from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; import { setHidePersonalDataRequestFCSession } from '#utils/session'; import withSession from '#utils/session/with-session'; +import { FranceConnectFailedException } from '../france-connect-types'; -export default withSession(async function (req, res) { +export const GET = withSession(async function callbackRoute(req) { try { const userInfo = await franceConnectAuthenticate(req); await setHidePersonalDataRequestFCSession( @@ -15,18 +16,12 @@ export default withSession(async function (req, res) { userInfo.sub, req.session ); - res.redirect('/formulaire/supprimer-donnees-personnelles-entreprise'); + return redirectTo( + req, + '/formulaire/supprimer-donnees-personnelles-entreprise' + ); } catch (e: any) { logErrorInSentry(new FranceConnectFailedException({ cause: e })); - res.redirect('/connexion/echec-connexion'); + return redirectTo(req, '/connexion/echec-connexion'); } }); - -export class FranceConnectFailedException extends Exception { - constructor(args: { cause?: any }) { - super({ - name: 'FranceConnectFailedException', - ...args, - }); - } -} diff --git a/app/api/auth/france-connect/france-connect-types.ts b/app/api/auth/france-connect/france-connect-types.ts new file mode 100644 index 000000000..0e9966392 --- /dev/null +++ b/app/api/auth/france-connect/france-connect-types.ts @@ -0,0 +1,19 @@ +import { Exception } from '#models/exceptions'; + +export class FranceConnectFailedException extends Exception { + constructor(args: { cause?: any }) { + super({ + name: 'FranceConnectFailedException', + ...args, + }); + } +} + +export class FranceConnectLogoutFailedException extends Exception { + constructor(args: { cause?: any }) { + super({ + ...args, + name: 'LogoutFailedException', + }); + } +} diff --git a/pages/api/auth/france-connect/login.ts b/app/api/auth/france-connect/login/route.ts similarity index 51% rename from pages/api/auth/france-connect/login.ts rename to app/api/auth/france-connect/login/route.ts index be0264231..28be7dbb8 100644 --- a/pages/api/auth/france-connect/login.ts +++ b/app/api/auth/france-connect/login/route.ts @@ -1,14 +1,16 @@ import { FranceConnectAuthorizeUrl } from '#clients/authentication/france-connect/strategy'; import { logFatalErrorInSentry } from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; import withSession from '#utils/session/with-session'; -import { FranceConnectFailedException } from './callback'; +import { NextResponse } from 'next/server'; +import { FranceConnectFailedException } from '../france-connect-types'; -export default withSession(async function loginRoute(req, res) { +export const GET = withSession(async function loginRoute(req) { try { const url = await FranceConnectAuthorizeUrl(req); - res.redirect(url); + return NextResponse.redirect(url); } catch (e: any) { logFatalErrorInSentry(new FranceConnectFailedException({ cause: e })); - res.redirect('/connexion/echec-connexion'); + return redirectTo(req, '/connexion/echec-connexion'); } }); diff --git a/app/api/auth/france-connect/logout-callback/route.ts b/app/api/auth/france-connect/logout-callback/route.ts new file mode 100644 index 000000000..39bc6a38d --- /dev/null +++ b/app/api/auth/france-connect/logout-callback/route.ts @@ -0,0 +1,23 @@ +import logErrorInSentry from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; +import { getPathFrom } from '#utils/session'; +import withSession from '#utils/session/with-session'; +import { FranceConnectLogoutFailedException } from '../france-connect-types'; + +export const GET = withSession(async function logoutCallbackRoute(req) { + try { + const pathFrom = getPathFrom(req.session); + + req.session.destroy(); + await req.session.save(); + + if (pathFrom) { + return redirectTo(req, pathFrom); + } else { + return redirectTo(req, '/connexion/au-revoir'); + } + } catch (e: any) { + logErrorInSentry(new FranceConnectLogoutFailedException({ cause: e })); + return redirectTo(req, '/connexion/au-revoir'); + } +}); diff --git a/app/api/auth/france-connect/logout/route.ts b/app/api/auth/france-connect/logout/route.ts new file mode 100644 index 000000000..143e8718b --- /dev/null +++ b/app/api/auth/france-connect/logout/route.ts @@ -0,0 +1,21 @@ +import { franceConnectLogoutUrl } from '#clients/authentication/france-connect/strategy'; +import logErrorInSentry from '#utils/sentry'; +import { redirectTo } from '#utils/server-side-helper/app/redirect-to'; +import { setPathFrom } from '#utils/session'; +import withSession from '#utils/session/with-session'; +import { NextResponse } from 'next/server'; +import { FranceConnectLogoutFailedException } from '../france-connect-types'; + +export const GET = withSession(async function logoutRoute(req) { + try { + await setPathFrom( + req.session, + (req.nextUrl.searchParams.get('pathFrom') || '') as string + ); + const url = await franceConnectLogoutUrl(req); + return NextResponse.redirect(url); + } catch (e: any) { + logErrorInSentry(new FranceConnectLogoutFailedException({ cause: e })); + return redirectTo(req, '/connexion/au-revoir'); + } +}); diff --git a/app/api/data-fetching/README.md b/app/api/data-fetching/README.md index 1c71f1cc5..f79b2a04a 100644 --- a/app/api/data-fetching/README.md +++ b/app/api/data-fetching/README.md @@ -1,8 +1,8 @@ # data-fetching folder -This foder contains the data fetching routes called from the frontend app. +This folder contains the data fetching routes called from the frontend app. Link between routes and model is done automatically with the `routes-handlers` module. Route protection is done with the `routes-scopes` module. -The two are in different files to tree-shake the unused code for client (only routes-scopes is used in the front). \ No newline at end of file +The two are in different files to tree-shake the unused code for client (only routes-scopes is used in the front). diff --git a/app/api/data-fetching/utils.ts b/app/api/data-fetching/utils.ts index 31655742b..d3e3ea11a 100644 --- a/app/api/data-fetching/utils.ts +++ b/app/api/data-fetching/utils.ts @@ -1,17 +1,17 @@ -import { userAgent } from 'next/server'; import { Exception } from '#models/exceptions'; import { ISession } from '#models/user/session'; import logErrorInSentry, { logInfoInSentry } from '#utils/sentry'; +import { userAgent } from 'next/server'; import getSession from '../../../utils/server-side-helper/app/get-session'; type RouteHandler = ( request: Request, - params: { params: { slug: Array } } + context: { params: { slug: Array } } ) => Promise; type RouteHandlerWithSession = ( request: Request, - params: { params: { slug: Array } }, + context: { params: { slug: Array } }, session: ISession ) => Promise; @@ -25,9 +25,9 @@ type RouteHandlerWithSession = ( * @returns */ export function withIgnoreBot(handler: RouteHandlerWithSession): RouteHandler { - return async function (request, params) { + return async function (request, context) { const { isBot } = userAgent(request); - const routeAndSlug = getRouteAndSlug(params); + const routeAndSlug = getRouteAndSlug(context); if (isBot) { throw new APIRouteError( @@ -46,7 +46,7 @@ export function withIgnoreBot(handler: RouteHandlerWithSession): RouteHandler { ); } - return handler(request, params, session); + return handler(request, context, session); }; } @@ -79,10 +79,10 @@ export class APIRouteError extends Exception { } } -export function getRouteAndSlug(params: { params: { slug: Array } }) { +export function getRouteAndSlug(context: { params: { slug: Array } }) { try { - const slug = params.params.slug.at(-1) as string; - const route = params.params.slug.slice(0, -1).join('/'); + const slug = context.params.slug.at(-1) as string; + const route = context.params.slug.slice(0, -1).join('/'); return { route, slug }; } catch (e) { throw new APIRouteError('Invalid route', { route: '', slug: '' }, 404, e); @@ -90,10 +90,10 @@ export function getRouteAndSlug(params: { params: { slug: Array } }) { } export function withHandleError(handler: RouteHandler): RouteHandler { - return async function (request, params) { + return async function (request, context) { try { - return await handler(request, params); - } catch (e: any) { + return await handler(request, context); + } catch (e) { if (e instanceof APIRouteError) { logInfoInSentry(e); return new Response(e.message, { status: e.status }); @@ -101,7 +101,7 @@ export function withHandleError(handler: RouteHandler): RouteHandler { let routeAndSlug; try { - routeAndSlug = getRouteAndSlug(params); + routeAndSlug = getRouteAndSlug(context); } catch (e) { routeAndSlug = { route: '', slug: '' }; } diff --git a/clients/authentication/agent-connect/strategy.ts b/clients/authentication/agent-connect/strategy.ts index 29270ed7b..c684ed0d2 100644 --- a/clients/authentication/agent-connect/strategy.ts +++ b/clients/authentication/agent-connect/strategy.ts @@ -1,6 +1,6 @@ -import { BaseClient, Issuer, generators } from 'openid-client'; import { HttpForbiddenError } from '#clients/exceptions'; import { IReqWithSession } from '#utils/session/with-session'; +import { BaseClient, Issuer, generators } from 'openid-client'; let _client = undefined as BaseClient | undefined; @@ -80,7 +80,7 @@ export type IAgentConnectUserInfo = { export const agentConnectAuthenticate = async (req: IReqWithSession) => { const client = await getClient(); - const params = client.callbackParams(req); + const params = client.callbackParams(req.nextUrl.toString()); const tokenSet = await client.grant({ grant_type: 'authorization_code', diff --git a/pages/api/auth/agent-connect/login.ts b/pages/api/auth/agent-connect/login.ts deleted file mode 100644 index b6659265a..000000000 --- a/pages/api/auth/agent-connect/login.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { agentConnectAuthorizeUrl } from '#clients/authentication/agent-connect/strategy'; -import { logFatalErrorInSentry } from '#utils/sentry'; -import { setPathFrom } from '#utils/session'; -import withSession from '#utils/session/with-session'; -import { AgentConnectionFailedException } from './callback'; - -export default withSession(async function loginRoute(req, res) { - try { - const pathFrom = - (req?.query?.pathFrom as string) || req.headers.referer || ''; - await setPathFrom(req.session, pathFrom); - const url = await agentConnectAuthorizeUrl(req); - res.redirect(url); - } catch (e: any) { - logFatalErrorInSentry(new AgentConnectionFailedException({ cause: e })); - res.redirect('/connexion/echec-connexion'); - } -}); diff --git a/pages/api/auth/agent-connect/logout-callback.ts b/pages/api/auth/agent-connect/logout-callback.ts deleted file mode 100644 index 71f0e0c91..000000000 --- a/pages/api/auth/agent-connect/logout-callback.ts +++ /dev/null @@ -1,21 +0,0 @@ -import logErrorInSentry from '#utils/sentry'; -import { cleanAgentSession, cleanPathFrom, getPathFrom } from '#utils/session'; -import withSession from '#utils/session/with-session'; -import { LogoutFailedException } from './logout'; - -export default withSession(async function loginCallback(req, res) { - try { - const session = req.session; - await cleanAgentSession(session); - const pathFrom = getPathFrom(session); - if (pathFrom) { - await cleanPathFrom(session); - res.redirect(pathFrom); - } else { - res.redirect('/connexion/au-revoir'); - } - } catch (e: any) { - logErrorInSentry(new LogoutFailedException({ cause: e })); - res.redirect('/connexion/au-revoir'); - } -}); diff --git a/pages/api/auth/agent-connect/logout.ts b/pages/api/auth/agent-connect/logout.ts deleted file mode 100644 index ef377c3a2..000000000 --- a/pages/api/auth/agent-connect/logout.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { agentConnectLogoutUrl } from '#clients/authentication/agent-connect/strategy'; -import { Exception } from '#models/exceptions'; -import logErrorInSentry from '#utils/sentry'; -import { setPathFrom } from '#utils/session'; -import withSession from '#utils/session/with-session'; - -export default withSession(async function logoutRoute(req, res) { - try { - await setPathFrom(req.session, req.headers.referer || ''); - const url = await agentConnectLogoutUrl(req); - res.redirect(url); - } catch (e: any) { - logErrorInSentry(new LogoutFailedException({ cause: e })); - res.redirect('/connexion/au-revoir'); - } -}); - -export class LogoutFailedException extends Exception { - constructor(args: { cause?: any }) { - super({ - ...args, - name: 'LogoutFailedException', - }); - } -} diff --git a/pages/api/auth/france-connect/logout-callback.ts b/pages/api/auth/france-connect/logout-callback.ts deleted file mode 100644 index 9ff868c79..000000000 --- a/pages/api/auth/france-connect/logout-callback.ts +++ /dev/null @@ -1,22 +0,0 @@ -import logErrorInSentry from '#utils/sentry'; -import { getPathFrom } from '#utils/session'; -import withSession from '#utils/session/with-session'; -import { LogoutFailedException } from './logout'; - -export default withSession(async function loginCallback(req, res) { - try { - const pathFrom = getPathFrom(req.session); - - req.session.destroy(); - await req.session.save(); - - if (pathFrom) { - res.redirect(pathFrom); - } else { - res.redirect('/connexion/au-revoir'); - } - } catch (e: any) { - logErrorInSentry(new LogoutFailedException({ cause: e })); - res.redirect('/connexion/au-revoir'); - } -}); diff --git a/pages/api/auth/france-connect/logout.ts b/pages/api/auth/france-connect/logout.ts deleted file mode 100644 index 6adf1505c..000000000 --- a/pages/api/auth/france-connect/logout.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { franceConnectLogoutUrl } from '#clients/authentication/france-connect/strategy'; -import { Exception } from '#models/exceptions'; -import logErrorInSentry from '#utils/sentry'; -import { setPathFrom } from '#utils/session'; -import withSession from '#utils/session/with-session'; - -export default withSession(async function logoutRoute(req, res) { - try { - await setPathFrom(req.session, (req?.query?.pathFrom || '') as string); - const url = await franceConnectLogoutUrl(req); - res.redirect(url); - } catch (e: any) { - logErrorInSentry(new LogoutFailedException({ cause: e })); - res.redirect('/connexion/au-revoir'); - } -}); - -export class LogoutFailedException extends Exception { - constructor(args: { cause?: any }) { - super({ - ...args, - name: 'LogoutFailedException', - }); - } -} diff --git a/pages/api/download/espace-agent/documents/[slug].ts b/pages/api/download/espace-agent/documents/[slug].ts index acf17621f..a64db9037 100644 --- a/pages/api/download/espace-agent/documents/[slug].ts +++ b/pages/api/download/espace-agent/documents/[slug].ts @@ -7,9 +7,16 @@ import { EAdministration } from '#models/administrations/EAdministration'; import { FetchRessourceException } from '#models/exceptions'; import { AppScope, hasRights } from '#models/user/rights'; import logErrorInSentry from '#utils/sentry'; -import withSession from '#utils/session/with-session'; +import withSessionPagesRouter from '#utils/session/with-session-pages-router'; -export default withSession(async function download(req, res) { +// This can't be migrated to App Router +// because responseLimit: false is not supported +// +// https://github.com/vercel/next.js/issues/57501 +// https://github.com/vercel/next.js/issues/55589 +// +// +export default withSessionPagesRouter(async function download(req, res) { const { query: { slug, type }, session, diff --git a/utils/server-side-helper/app/redirect-to.ts b/utils/server-side-helper/app/redirect-to.ts new file mode 100644 index 000000000..daf78e5c7 --- /dev/null +++ b/utils/server-side-helper/app/redirect-to.ts @@ -0,0 +1,8 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export function redirectTo(req: NextRequest, path: string) { + const requestUrl = new URL(req.url); + const targetUrl = new URL(path, requestUrl.origin); + + return NextResponse.redirect(targetUrl.toString()); +} diff --git a/utils/session/with-session-pages-router.ts b/utils/session/with-session-pages-router.ts new file mode 100644 index 000000000..fde6aa145 --- /dev/null +++ b/utils/session/with-session-pages-router.ts @@ -0,0 +1,22 @@ +import { ISession } from '#models/user/session'; +import { IronSession, getIronSession } from 'iron-session'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { sessionOptions } from '.'; + +type IReqWithSession = NextApiRequest & { + session: IronSession; +}; + +export default function withSessionPagesRouter( + handler: (req: IReqWithSession, res: NextApiResponse) => Promise +) { + return async (req: NextApiRequest, res: NextApiResponse) => { + const reqWithSession = req as IReqWithSession; + reqWithSession.session = await getIronSession( + req, + res, + sessionOptions + ); + return handler(reqWithSession, res); + }; +} diff --git a/utils/session/with-session.ts b/utils/session/with-session.ts index df27ed150..d7f864a8e 100644 --- a/utils/session/with-session.ts +++ b/utils/session/with-session.ts @@ -1,19 +1,19 @@ -import { IronSession, getIronSession } from 'iron-session'; -import { NextApiRequest, NextApiResponse } from 'next'; import { ISession } from '#models/user/session'; +import { IronSession, getIronSession } from 'iron-session'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; import { sessionOptions } from '.'; -export type IReqWithSession = NextApiRequest & { +export type IReqWithSession = NextRequest & { session: IronSession; }; export default function withSession( - handler: (req: IReqWithSession, res: NextApiResponse) => Promise + handler: (req: IReqWithSession, res: NextResponse) => Promise ) { - return async (req: NextApiRequest, res: NextApiResponse) => { + return async (req: NextRequest, res: NextResponse) => { const reqWithSession = req as IReqWithSession; reqWithSession.session = await getIronSession( - req, - res, + cookies(), sessionOptions ); return handler(reqWithSession, res);