Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
820a77b
feat: implement access request for Gliederung
axelrindle Feb 12, 2026
2a230e9
update
axelrindle Feb 12, 2026
b4eaa8a
update
axelrindle Feb 12, 2026
14d53d6
fix type error
axelrindle Feb 12, 2026
a98d833
fix: update account create data
axelrindle Feb 12, 2026
e5391c3
fix missing type
axelrindle Feb 12, 2026
d30c069
feat: seed gliederung email addresses
axelrindle Feb 15, 2026
5a5c599
feat: manual access grants
axelrindle Feb 15, 2026
489ba63
Merge branch 'development' of https://github.com/codeanker/brahmsee.d…
axelrindle Feb 15, 2026
04e020f
fix: add public endpoint
axelrindle Feb 15, 2026
313ee4e
fix: update account role
axelrindle Feb 15, 2026
c948a74
refactor: add request modal
axelrindle Feb 15, 2026
d0c56db
refactor: delete old view
axelrindle Feb 15, 2026
a5f534a
fix: remove route
axelrindle Feb 15, 2026
670863b
feat: allow view access to own gliederung
axelrindle Feb 15, 2026
a4d928b
increase modal size
axelrindle Feb 20, 2026
1f1de25
show note for access control
axelrindle Feb 20, 2026
38b8d1c
changes
silasjak Feb 20, 2026
2d7c94e
Merge branch 'development' into feature/request-gliederung-access
danielswiatek Feb 20, 2026
998ec2e
Merge branch 'development' into feature/request-gliederung-access
danielswiatek Feb 20, 2026
b4545c8
display access permission on other detail pages
axelrindle Feb 20, 2026
d8dc223
Merge branch 'development' into feature/request-gliederung-access
danielswiatek Feb 22, 2026
7231ce2
Refactor Gliederung email handling to domain; remove unused Keycloak …
danielswiatek Feb 22, 2026
63e6862
Add Keycloak service configuration to docker-compose
danielswiatek Feb 22, 2026
d1d0774
upgrade to eslint 10
danielswiatek Feb 22, 2026
b000398
Remove gliederungEmails utility file and its associated email enrichm…
danielswiatek Feb 22, 2026
d20efa8
Refactor gliederung procedures to replace email with domain in input …
danielswiatek Feb 22, 2026
ecc6fc3
Replace email with domain in gliederung selection for anmeldungGet pr…
danielswiatek Feb 22, 2026
df232af
Replace 'email' column with 'domain' in GliederungList component
danielswiatek Feb 22, 2026
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
11 changes: 11 additions & 0 deletions apps/api/email/gliederung-access-request-decision.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{#> layout }}

<mj-text
font-size="16px"
line-height="1.5"
>
{{#if decision}} Deinem Zugriffsantrag auf die Gliederung {{ gliederung }} wurde zugestimmt. {{else}} Dein
Zugriffsantrag auf die Gliederung {{ gliederung }} wurde abgelehnt. {{/if}}
</mj-text>

{{/layout}}
18 changes: 18 additions & 0 deletions apps/api/email/gliederung-access-request-info.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{#> layout }}

<mj-text
font-size="16px"
line-height="1.5"
>
{{ name }} hat soeben einen Antrag auf Zugriff auf die Gliederung {{ gliederung }} gestellt. Bitte prüft den Antrag
und bestätigt diesen, damit {{ name }} Zugriff auf die Ausschreibungen eurer Gliederung erhält.
</mj-text>

<mj-text
font-size="16px"
line-height="1.5"
>
Nutzt für die Bestätigung oder Ablehnung des Antrags bitte folgenden Link: {{ confirmLink }}
</mj-text>

{{/layout}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Gliederung" ADD COLUMN "email" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- Added the required column `createdAt` to the `GliederungToAccount` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "GliederungToAccount" ADD COLUMN "confirmByGliederungToken" TEXT,
ADD COLUMN "confirmedAt" TIMESTAMP(3),
ADD COLUMN "confirmedByGliederung" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- You are about to drop the column `email` on the `Gliederung` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "Gliederung" DROP COLUMN "email",
ADD COLUMN "domain" TEXT;
1 change: 1 addition & 0 deletions apps/api/prisma/schema/Gliederung.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ model Gliederung {
id String @id @default(uuid(7))
name String
edv String @unique
domain String?
unterveranstaltungen Unterveranstaltung[]
personen Person[]
GliederungToAccount GliederungToAccount[]
Expand Down
5 changes: 5 additions & 0 deletions apps/api/prisma/schema/GliederungToAccount.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ enum GliederungAccountRole {

model GliederungToAccount {
id Int @id @default(autoincrement())
createdAt DateTime
gliederungId String
gliederung Gliederung @relation(fields: [gliederungId], references: [id], onDelete: Cascade)
accountId String
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
role GliederungAccountRole

confirmedAt DateTime?
confirmByGliederungToken String?
confirmedByGliederung Boolean @default(false)

@@unique([gliederungId, accountId])
}
13 changes: 9 additions & 4 deletions apps/api/prisma/seeders/anmeldungen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,24 @@ async function create(prisma: PrismaClient, unterveranstaltung: Unterveranstaltu
description: 'address created via db seeder',
})

// Make sure the email isn't NOT based on the person's name
const firstName = faker.person.firstName()
const lastName = faker.person.lastName()
const mail = faker.internet.email({ firstName, lastName })

const account = await prisma.account.create({
data: {
email: faker.internet.email(),
email: mail,
role: 'USER',
activatedAt: new Date(),
status: 'AKTIV',
person: {
create: {
firstname: faker.person.firstName(),
lastname: faker.person.lastName(),
firstname: firstName,
lastname: lastName,
birthday: faker.date.birthdate({ min: 12, max: 30, mode: 'age' }),
gender: faker.helpers.enumValue(Gender),
email: faker.internet.email(),
email: mail,
telefon: faker.string.numeric('+49151########'),
essgewohnheit: faker.helpers.enumValue(Essgewohnheit),
nahrungsmittelIntoleranzen: faker.helpers.arrayElements(Object.values(NahrungsmittelIntoleranz)),
Expand Down
9,916 changes: 7,955 additions & 1,961 deletions apps/api/prisma/seeders/gliederungen.ts

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions apps/api/src/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ export const { getEntityIdFromHeader, authenticationLogin, sign } = createAuthen
activationToken: true,
},
})
if (account === null) throw new Error('Es konnte kein Account gefunden werden.')
if (account.activationToken !== null)
throw new Error('Account noch nicht bestätigt, bitte bestätige deine E-Mail-Adresse.')
if (account.status !== 'AKTIV')
throw new Error('Dein Account ist noch nicht von einem Administrator freigeschaltet worden.')
if (account.password === null) throw new Error('Account has no password')

if (account === null) {
throw new Error('Es konnte kein Account gefunden werden.')
}
if (account.status !== 'AKTIV') {
throw new Error('Dein Account ist nicht aktiviert. Bitte prüfe dein E-Mail Postfach, ob du den Account noch aktivieren musst. Wende dich ansonsten an deine Gliederung.')
}
if (account.password === null) {
throw new Error('Der Account hat kein Passwort, wahrscheinlich hast du dich über das ISC angemeldet.')
}

return {
id: account.id,
password: account.password,
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/cli/inquireGenerateService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import inquirer from 'inquirer'

Expand Down
12 changes: 2 additions & 10 deletions apps/api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { StringValue } from '@codeanker/authentication'
import { FileProvider } from '@prisma/client'
import config from 'config'
import { z } from 'zod'
import { zEmptyStringAsUndefined } from './util/zod.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
Expand Down Expand Up @@ -37,16 +38,7 @@ export const configSchema = z.strictObject({
dlrg: z.strictObject({
issuer: z.string().url(),
clientId: z.string(),
clientSecret: z
.string()
.optional()
.transform((v) => {
const trimmed = v?.trim()
if (trimmed === undefined || trimmed.length === 0) {
return undefined
}
return trimmed
}),
clientSecret: z.string().optional().transform(zEmptyStringAsUndefined),
allowInsecure: z.coerce.boolean(),
}),
}),
Expand Down
36 changes: 29 additions & 7 deletions apps/api/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Account } from '@prisma/client'
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'
import { getEntityIdFromHeader } from './authentication.js'
import { logger } from './logger.js'
Expand All @@ -12,6 +11,33 @@ function getAuthorizationHeader(headers: FetchCreateContextFnOptions['req']['hea
}
}

function getAccountById(accountId: string) {
return prisma.account.findFirst({
where: {
id: accountId,
},
select: {
id: true,
activatedAt: true,
email: true,
role: true,
status: true,
GliederungToAccount: {
select: {
confirmedByGliederung: true,
role: true,
},
},
person: {
select: {
firstname: true,
lastname: true,
},
},
},
})
}

export async function createContext({ req }: FetchCreateContextFnOptions): Promise<Context> {
try {
const authorization = getAuthorizationHeader(req.headers)
Expand All @@ -26,11 +52,7 @@ export async function createContext({ req }: FetchCreateContextFnOptions): Promi
}
}

const account = await prisma.account.findFirst({
where: {
id: accountId,
},
})
const account = await getAccountById(accountId)

if (account === null) {
return {
Expand Down Expand Up @@ -64,7 +86,7 @@ type AuthContext =
| {
authenticated: true
accountId: string
account: Account
account: NonNullable<Awaited<ReturnType<typeof getAccountById>>>
}

export type Context = AuthContext
68 changes: 68 additions & 0 deletions apps/api/src/services/access/access.createForGliederung.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { z } from 'zod'
import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js'
import prisma from '../../prisma.js'

export const createAccessForGliederungProcedure = defineProtectedMutateProcedure({
key: 'createForGliederung',
roleIds: ['ADMIN'],
inputSchema: z.strictObject({
accountId: z.string().uuid(),
gliederungId: z.string().uuid(),
}),
handler: async ({ ctx, input }) => {
await prisma.$transaction(async (tx) => {
const record = await tx.gliederungToAccount.create({
data: {
createdAt: new Date(),
confirmedAt: new Date(),
confirmedByGliederung: true,
role: 'DELEGATIONSLEITER',
gliederungId: input.gliederungId,
accountId: input.accountId,
},
select: {
id: true,
account: {
select: {
email: true,
person: {
select: {
firstname: true,
lastname: true,
},
},
},
},
gliederung: {
select: {
name: true,
},
},
},
})
await tx.account.update({
where: {
id: input.accountId,
},
data: {
role: 'GLIEDERUNG_ADMIN',
},
})

const description = `
Der Account von ${record.account.person.firstname} ${record.account.person.lastname} (${record.account.email}) hat
jetzt Zugriff auf die Gliederung ${record.gliederung.name}
`
await tx.activity.create({
data: {
type: 'CREATE',
subjectType: 'gliederungtoaccount',
subjectId: `${record.id}`,
causerId: ctx.accountId,
createdAt: new Date(),
description,
},
})
})
},
})
Loading