Skip to content
Merged

2.7.0 #399

Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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}')"

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const configSchema = z.strictObject({
}
return trimmed
}),
allowInsecure: z.boolean(),
allowInsecure: z.coerce.boolean(),
}),
}),

Expand Down
9 changes: 5 additions & 4 deletions apps/api/src/routes/exports/index.ts
Original file line number Diff line number Diff line change
@@ -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 }
74 changes: 74 additions & 0 deletions apps/api/src/routes/exports/middleware/authorize.ts
Original file line number Diff line number Diff line change
@@ -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<Awaited<ReturnType<typeof sheetAuthorize>>, 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 }
}
14 changes: 5 additions & 9 deletions apps/api/src/routes/exports/photos.archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
Expand Down Expand Up @@ -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') {
Expand Down
53 changes: 0 additions & 53 deletions apps/api/src/routes/exports/sheets.schema.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -17,50 +11,3 @@ export const sheetQuerySchema = z
})

export type SheetQuery = z.infer<typeof sheetQuerySchema>

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 }
}
13 changes: 4 additions & 9 deletions apps/api/src/routes/exports/teilnehmendenliste.sheet.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
13 changes: 4 additions & 9 deletions apps/api/src/routes/exports/verpflegung.sheet.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
9 changes: 3 additions & 6 deletions apps/api/src/routes/imports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/routes/oidc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
19 changes: 14 additions & 5 deletions apps/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/views/Login/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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`
</script>

<template>
Expand Down
1 change: 0 additions & 1 deletion apps/frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export default defineConfig({
proxy: {
'/api': {
target: 'http://127.0.0.1:3030',
rewrite: (path) => path.replace(/^\/api/, ''),
ws: true,
},
},
Expand Down
33 changes: 1 addition & 32 deletions chart/brahmsee-digital/templates/ingress.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: stripapiprefix
labels:
{{- include "codeanker.label" . | indent 4 }}
spec:
stripPrefix:
prefixes:
- /api
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutestripprefix
name: {{ .Chart.Name }}-ingress
labels:
{{- include "codeanker.label" . | indent 4 }}
spec:
Expand All @@ -28,25 +18,4 @@ spec:
services:
- name: {{ $.Chart.Name }}-app-svc
port: 80
- match: Host(`{{ . }}`) && PathPrefix(`/api/connect`)
kind: Rule
services:
- name: {{ $.Chart.Name }}-app-svc
port: 80
middlewares:
- name: stripapiprefix
- match: Host(`{{ . }}`) && PathPrefix(`/api/export`)
kind: Rule
services:
- name: {{ $.Chart.Name }}-app-svc
port: 80
middlewares:
- name: stripapiprefix
- match: Host(`{{ . }}`) && PathPrefix(`/api/upload`)
kind: Rule
services:
- name: {{ $.Chart.Name }}-app-svc
port: 80
middlewares:
- name: stripapiprefix
{{- end }}
Loading
Loading