Files
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

184 lines
6.0 KiB
TypeScript

import { db } from '@/lib/db'
import { writeAudit } from '@/lib/audit/write'
import { visibleCaseWhere } from '@/lib/access/caseFilter'
import { visiblePersonnelWhere } from '@/lib/access/personnelFilter'
import { canEditCase } from '@/lib/access/caseWrite'
import { AccessError, must } from '@/lib/errors'
import { loadVisibleCase } from '@/lib/cases/service'
import type { ParticipantRoleName } from '@/lib/cases/terms'
import type { EffectivePermissions } from '@/lib/auth/permissions'
/**
* Beteiligte einer Akte.
*
* Zwei Dinge zugleich: eine fachliche Angabe („wer ist Zeuge") und der Weg zum
* **ausnahmsweisen Zugang**. Wer als Beteiligter eingetragen ist, liest die
* Akte unabhängig von seiner Freigabestufe. Das ist der vorgesehene Weg,
* jemanden gezielt hinzuzuziehen, ohne ihm allgemein eine höhere Stufe zu
* geben. Es setzt `case.edit` voraus und wird protokolliert.
* Siehe docs/03-auth-und-rechte.md.
*/
export async function listParticipants(p: EffectivePermissions, caseId: string) {
const rows = await db.caseParticipant.findMany({
where: { AND: [{ caseId }, { case: { is: visibleCaseWhere(p) } }] },
orderBy: { createdAt: 'asc' },
select: {
id: true,
role: true,
createdAt: true,
addedById: true,
user: { select: { id: true, displayName: true } },
// Die Personalakte kommt nur mit, wenn der Leser sie sehen darf. Sonst
// steht der Name da, aber der Verweis führt ins Leere - das ist richtig
// so: der Beteiligte ist Teil der Akte, seine Personalakte nicht.
personnelFile: { select: { id: true, displayName: true } },
},
})
const sichtbareAkten = new Set(
(
await db.personnelFile.findMany({
where: {
AND: [
visiblePersonnelWhere(p),
{
id: {
in: rows.flatMap((row) => (row.personnelFile ? [row.personnelFile.id] : [])),
},
},
],
},
select: { id: true },
})
).map((row) => row.id),
)
return rows.map((row) => ({
id: row.id,
role: row.role,
createdAt: row.createdAt,
userId: row.user?.id ?? null,
displayName: row.user?.displayName ?? row.personnelFile?.displayName ?? null,
personnelFileId:
row.personnelFile && sichtbareAkten.has(row.personnelFile.id) ? row.personnelFile.id : null,
}))
}
/**
* Trägt einen Beteiligten ein. Entweder ein Discord-Konto oder eine
* Personalakte, nicht beides und nicht keins.
*/
export async function addParticipant(
p: EffectivePermissions,
input: {
caseNumber: string
role: ParticipantRoleName
userId?: string | null
personnelFileId?: string | null
},
): Promise<{ id: string }> {
const target = await loadVisibleCase(p, input.caseNumber)
if (!target) throw new AccessError('not-found')
must(canEditCase(p, target))
const userId = input.userId || null
const personnelFileId = input.personnelFileId || null
if ((userId === null) === (personnelFileId === null)) throw new AccessError('participant-target')
if (userId) {
const vorhanden = await db.user.findUnique({ where: { id: userId }, select: { id: true } })
if (!vorhanden) throw new AccessError('not-found')
}
if (personnelFileId) {
// Eine Personalakte, die der Eintragende nicht sehen darf, kann er auch
// nicht als Beteiligte setzen - sonst verrät ihm die Fehlermeldung ihre
// Existenz.
const akte = await db.personnelFile.findFirst({
where: { AND: [visiblePersonnelWhere(p), { id: personnelFileId }] },
select: { id: true },
})
if (!akte) throw new AccessError('not-found')
}
const bereitsDa = await db.caseParticipant.findFirst({
where: { caseId: target.id, role: input.role, userId, personnelFileId },
select: { id: true },
})
if (bereitsDa) return bereitsDa
return db.$transaction(async (tx) => {
const created = await tx.caseParticipant.create({
data: { caseId: target.id, role: input.role, userId, personnelFileId, addedById: p.userId },
select: { id: true },
})
// Sichtbar im Verlauf, nicht nur im Protokoll: dass jemand Zugang zu einer
// Akte bekommt, geht alle an, die die Akte lesen.
await tx.caseEntry.create({
data: {
id: crypto.randomUUID(),
caseId: target.id,
type: 'SYSTEM',
body: `Beteiligte eingetragen: ${input.role}`,
authorId: p.userId,
minClearance: 0,
},
})
await writeAudit(tx, {
actorId: p.userId,
action: 'case.participant.add',
targetType: 'CaseParticipant',
targetId: created.id,
clearanceAtTime: p.clearanceLevel,
meta: {
caseNumber: target.caseNumber,
role: input.role,
userId: userId ?? undefined,
personnelFileId: personnelFileId ?? undefined,
},
})
return created
})
}
/**
* Entfernt einen Beteiligten. Anders als ein Verlaufseintrag ist das keine
* Aussage über den Sachverhalt, sondern eine Zuständigkeit - sie darf
* verschwinden. Das Protokoll behält sie.
*/
export async function removeParticipant(
p: EffectivePermissions,
input: { caseNumber: string; participantId: string },
): Promise<void> {
const target = await loadVisibleCase(p, input.caseNumber)
if (!target) throw new AccessError('not-found')
must(canEditCase(p, target))
const teilnehmer = await db.caseParticipant.findFirst({
where: { id: input.participantId, caseId: target.id },
select: { id: true, role: true, userId: true, personnelFileId: true },
})
if (!teilnehmer) throw new AccessError('not-found')
await db.$transaction(async (tx) => {
await tx.caseParticipant.delete({ where: { id: teilnehmer.id } })
await writeAudit(tx, {
actorId: p.userId,
action: 'case.participant.remove',
targetType: 'CaseParticipant',
targetId: teilnehmer.id,
clearanceAtTime: p.clearanceLevel,
meta: {
caseNumber: target.caseNumber,
role: teilnehmer.role,
userId: teilnehmer.userId ?? undefined,
personnelFileId: teilnehmer.personnelFileId ?? undefined,
},
})
})
}