diff --git a/Dockerfile b/Dockerfile index 2251e120..bfe04bad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:22.20.0-alpine3.22 AS workspace-base -RUN apk add --no-cache bash curl jq +RUN apk add --no-cache bash curl jq openssl RUN export COREPACK_INTEGRITY_KEYS="$(curl https://registry.npmjs.org/-/npm/v1/keys | jq -c '{npm: .keys}')" diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index d6b7558d..4fdff314 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -47,7 +47,7 @@ export const configSchema = z.strictObject({ } return trimmed }), - allowInsecure: z.boolean(), + allowInsecure: z.coerce.boolean(), }), }), diff --git a/apps/api/src/routes/exports/index.ts b/apps/api/src/routes/exports/index.ts index 5ae64bed..54b550f9 100644 --- a/apps/api/src/routes/exports/index.ts +++ b/apps/api/src/routes/exports/index.ts @@ -1,12 +1,13 @@ import { makeApp } from '../../util/make-app.js' +import { authorize } from './middleware/authorize.js' import { veranstaltungPhotoArchive } from './photos.archive.js' import { veranstaltungTeilnehmendenliste } from './teilnehmendenliste.sheet.js' import { veranstaltungVerpflegung } from './verpflegung.sheet.js' const exportRouter = makeApp() - -exportRouter.get('/sheet/teilnehmendenliste', veranstaltungTeilnehmendenliste) -exportRouter.get('/sheet/verpflegung', veranstaltungVerpflegung) -exportRouter.get('/archive/photos', veranstaltungPhotoArchive) + .use(authorize) + .get('/sheet/teilnehmendenliste', veranstaltungTeilnehmendenliste) + .get('/sheet/verpflegung', veranstaltungVerpflegung) + .get('/archive/photos', veranstaltungPhotoArchive) export { exportRouter } diff --git a/apps/api/src/routes/exports/middleware/authorize.ts b/apps/api/src/routes/exports/middleware/authorize.ts new file mode 100644 index 00000000..d411eb61 --- /dev/null +++ b/apps/api/src/routes/exports/middleware/authorize.ts @@ -0,0 +1,74 @@ +import type { Context } from 'hono' +import { createMiddleware } from 'hono/factory' +import { zodSafe } from '../../../util/zod.js' +import { sheetQuerySchema } from '../sheets.schema.js' +import { getEntityIdFromHeader } from '../../../authentication.js' +import prisma from '../../../prisma.js' +import { Role, type Gliederung } from '@prisma/client' +import { getGliederungRequireAdmin } from '../../../util/getGliederungRequireAdmin.js' + +export type AuthorizeResults = Exclude>, false> + +export const authorize = createMiddleware<{ + Variables: AuthorizeResults +}>(async (ctx, next) => { + const authorization = await sheetAuthorize(ctx) + if (!authorization) { + return ctx.text('Unauthorized', 401) + } + + const { query, account, gliederung } = authorization + + ctx.set('query', query) + ctx.set('account', account) + ctx.set('gliederung', gliederung) + + await next() +}) + +async function sheetAuthorize(ctx: Context) { + const [success, query] = await zodSafe(sheetQuerySchema, ctx.req.query()) + if (!success) { + ctx.status(400) + return false + } + + const accountId = getEntityIdFromHeader(`Bearer ${query.jwt}`) + if (typeof accountId !== 'string') { + ctx.status(401) + return false + } + + const account = await prisma.account.findUnique({ + where: { + id: accountId, + }, + select: { + role: true, + person: { + select: { + firstname: true, + lastname: true, + }, + }, + }, + }) + + if (account == null) { + ctx.status(401) + return false + } + + let gliederung: Gliederung | undefined = undefined + if (account.role == Role.GLIEDERUNG_ADMIN) { + try { + gliederung = await getGliederungRequireAdmin(accountId) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + ctx.status(401) + return false + } + } + + return { query, account, gliederung } +} diff --git a/apps/api/src/routes/exports/photos.archive.ts b/apps/api/src/routes/exports/photos.archive.ts index a0a7bc99..e5d22e82 100644 --- a/apps/api/src/routes/exports/photos.archive.ts +++ b/apps/api/src/routes/exports/photos.archive.ts @@ -3,15 +3,16 @@ import XLSX from '@e965/xlsx' import type { Gliederung } from '@prisma/client' import { TRPCError } from '@trpc/server' import archiver from 'archiver' +import type { Context } from 'hono' import { stream } from 'hono/streaming' import mime from 'mime' import { Readable } from 'node:stream' import { z } from 'zod' import prisma from '../../prisma.js' import { openFileStream } from '../../services/file/helpers/getFileUrl.js' -import type { AppContext } from '../../util/make-app.js' import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js' -import { sheetAuthorize, type SheetQuery } from './sheets.schema.js' +import type { AuthorizeResults } from './middleware/authorize.js' +import { type SheetQuery } from './sheets.schema.js' const querySchema = z.object({ mode: z.enum(['group', 'flat']), @@ -104,13 +105,8 @@ function buildSheet( const baseDirectory = 'Fotos' -export async function veranstaltungPhotoArchive(ctx: AppContext) { - const authorization = await sheetAuthorize(ctx) - if (!authorization) { - return - } - - const { query, gliederung, account } = authorization +export async function veranstaltungPhotoArchive(ctx: Context<{ Variables: AuthorizeResults }>) { + const { query, account, gliederung } = ctx.var const { mode } = querySchema.parse(ctx.req.query()) if (mode === 'flat' && account.role !== 'ADMIN') { diff --git a/apps/api/src/routes/exports/sheets.schema.ts b/apps/api/src/routes/exports/sheets.schema.ts index c3b4ff3d..6b46ed08 100644 --- a/apps/api/src/routes/exports/sheets.schema.ts +++ b/apps/api/src/routes/exports/sheets.schema.ts @@ -1,10 +1,4 @@ -import { Role, type Gliederung } from '@prisma/client' -import type { Context } from 'hono' import { z } from 'zod' -import { getEntityIdFromHeader } from '../../authentication.js' -import prisma from '../../prisma.js' -import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' -import { zodSafe } from '../../util/zod.js' export const sheetQuerySchema = z .object({ @@ -17,50 +11,3 @@ export const sheetQuerySchema = z }) export type SheetQuery = z.infer - -export async function sheetAuthorize(ctx: Context) { - const [success, query] = await zodSafe(sheetQuerySchema, ctx.req.query()) - if (!success) { - ctx.status(400) - return false - } - - const accountId = getEntityIdFromHeader(`Bearer ${query.jwt}`) - if (typeof accountId !== 'string') { - ctx.status(401) - return false - } - - const account = await prisma.account.findUnique({ - where: { - id: accountId, - }, - select: { - role: true, - person: { - select: { - firstname: true, - lastname: true, - }, - }, - }, - }) - - if (account == null) { - ctx.status(401) - return false - } - - let gliederung: Gliederung | undefined = undefined - if (account.role == Role.GLIEDERUNG_ADMIN) { - try { - gliederung = await getGliederungRequireAdmin(accountId) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - ctx.status(401) - return false - } - } - - return { query, account, gliederung } -} diff --git a/apps/api/src/routes/exports/teilnehmendenliste.sheet.ts b/apps/api/src/routes/exports/teilnehmendenliste.sheet.ts index 517b5e6f..f7465cea 100644 --- a/apps/api/src/routes/exports/teilnehmendenliste.sheet.ts +++ b/apps/api/src/routes/exports/teilnehmendenliste.sheet.ts @@ -1,18 +1,13 @@ import XLSX from '@e965/xlsx' import dayjs from 'dayjs' +import type { Context } from 'hono' import { AnmeldungStatusMapping, GenderMapping } from '../../client.js' import prisma from '../../prisma.js' import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js' -import { sheetAuthorize } from './sheets.schema.js' -import type { Context } from 'hono' - -export async function veranstaltungTeilnehmendenliste(ctx: Context) { - const authorization = await sheetAuthorize(ctx) - if (!authorization) { - return - } +import type { AuthorizeResults } from './middleware/authorize.js' - const { query, account, gliederung } = authorization +export async function veranstaltungTeilnehmendenliste(ctx: Context<{ Variables: AuthorizeResults }>) { + const { query, account, gliederung } = ctx.var const anmeldungenList = await prisma.anmeldung.findMany({ where: { diff --git a/apps/api/src/routes/exports/verpflegung.sheet.ts b/apps/api/src/routes/exports/verpflegung.sheet.ts index ed8a2589..22eb063b 100644 --- a/apps/api/src/routes/exports/verpflegung.sheet.ts +++ b/apps/api/src/routes/exports/verpflegung.sheet.ts @@ -1,19 +1,14 @@ +import XLSX from '@e965/xlsx' import { AnmeldungStatus, Essgewohnheit, NahrungsmittelIntoleranz } from '@prisma/client' import dayjs from 'dayjs' import type { Context } from 'hono' -import XLSX from '@e965/xlsx' import prisma from '../../prisma.js' import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js' import { getWorkbookDefaultProps } from './helpers/getWorkbookDefaultProps.js' -import { sheetAuthorize } from './sheets.schema.js' - -export async function veranstaltungVerpflegung(ctx: Context) { - const authorization = await sheetAuthorize(ctx) - if (!authorization) { - return - } +import type { AuthorizeResults } from './middleware/authorize.js' - const { query, account, gliederung } = authorization +export async function veranstaltungVerpflegung(ctx: Context<{ Variables: AuthorizeResults }>) { + const { query, account, gliederung } = ctx.var const unterveranstaltungen = await prisma.unterveranstaltung.findMany({ where: { diff --git a/apps/api/src/routes/imports/index.ts b/apps/api/src/routes/imports/index.ts index e1c25234..d0eedfeb 100644 --- a/apps/api/src/routes/imports/index.ts +++ b/apps/api/src/routes/imports/index.ts @@ -11,14 +11,12 @@ const importRouter = makeApp() importRouter.post('/anmeldungen/:unterveranstaltungId', async (ctx) => { const authorization = ctx.req.header('Authorization') if (!authorization) { - ctx.status(401) - return + return ctx.status(401) } const accountId = getEntityIdFromHeader(authorization) if (!accountId) { - ctx.status(401) - return + return ctx.status(401) } const account = await prisma.account.findUnique({ @@ -36,8 +34,7 @@ importRouter.post('/anmeldungen/:unterveranstaltungId', async (ctx) => { }, }) if (!account || account.role !== Role.ADMIN) { - ctx.status(401) - return + return ctx.status(401) } const body = await ctx.req.parseBody({ diff --git a/apps/api/src/routes/oidc/index.ts b/apps/api/src/routes/oidc/index.ts index eed98eaa..ceeb1765 100644 --- a/apps/api/src/routes/oidc/index.ts +++ b/apps/api/src/routes/oidc/index.ts @@ -31,7 +31,7 @@ oidcRouter.get('/dlrg/callback', async (c) => { config.authentication.dlrg.clientSecret === undefined ? oauth.None() : oauth.ClientSecretPost(config.authentication.dlrg.clientSecret) - const redirect_uri = `${config.clientUrl}/api/oidc/dlrg/callback` + const redirect_uri = `${config.clientUrl}/api/connect/dlrg/callback` const currentUrl: URL = new URL(c.req.url, config.clientUrl) const params = oauth.validateAuthResponse(as, client, currentUrl) @@ -144,7 +144,7 @@ oidcRouter.get('/dlrg/login', async (c) => { const as = await oauth.processDiscoveryResponse(issuer, discoveryRequestResponse) const authorizationUrl = new URL(as.authorization_endpoint!) - const redirectUri = new URL('/api/oidc/dlrg/callback', config.clientUrl) + const redirectUri = new URL('/api/connect/dlrg/callback', config.clientUrl) const registerAs = c.req.query('as')?.trim() if (registerAs !== undefined && registerAs?.length > 0) { redirectUri.searchParams.set('as', registerAs) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 3cce184b..d7319e59 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -13,6 +13,8 @@ import { logger as appLogger } from './logger.js' import * as routes from './routes/index.js' import { makeApp } from './util/make-app.js' +const staticRoot = resolve('./static') + const app = makeApp() .use(async (c, next) => { // generic error handler @@ -46,16 +48,23 @@ const app = makeApp() createContext, }) ) + .route('/api/export', routes.exportRouter) + .route('/api/import', routes.importRouter) + .route('/api/file', routes.fileRouter) + .route('/api/connect', routes.oidcRouter) .use( serveStatic({ - root: resolve('./static'), + root: staticRoot, rewriteRequestPath: (p) => p.replace(/^\/static/, '/'), }) ) - .route('/export', routes.exportRouter) - .route('/import', routes.importRouter) - .route('/file', routes.fileRouter) - .route('/oidc', routes.oidcRouter) + // fallback to index.html for client-side routing + .use( + serveStatic({ + root: staticRoot, + rewriteRequestPath: () => '/index.html', + }) + ) const server = serve({ fetch: app.fetch, diff --git a/apps/frontend/src/views/Login/Login.vue b/apps/frontend/src/views/Login/Login.vue index ba523aeb..f632577e 100644 --- a/apps/frontend/src/views/Login/Login.vue +++ b/apps/frontend/src/views/Login/Login.vue @@ -54,7 +54,7 @@ const formatLoginError = computed(() => { const version = `${import.meta.env.VITE_APP_VERSION || 'unknown'}-${import.meta.env.VITE_APP_COMMIT_HASH || 'unknown'}` -const oauthHref = `/api/oidc/dlrg/login` +const oauthHref = `/api/connect/dlrg/login`