Erstzugang schuetzen, Kettenstatus richtigstellen, CCs nachtraeglich aendern

Der Zugang aus der Erstinstallation ist ab Werk unantastbar: weder Rolle noch
Zustand lassen sich aendern, sein Passwort nicht von aussen zuruecksetzen, und
loeschen laesst er sich nicht - auch nicht durch sich selbst. So bleibt ein
Weg zurueck, wenn sich jemand aus Versehen die Rechte entzieht. Sein eigenes
Passwort aendert er weiterhin unter "Konto". BOOTSTRAP_ADMIN_EDITABLE=true
hebt den Schutz auf. Welcher Zugang gemeint ist, traegt der Start nach: den
Namen kennt nur er, die Migration nicht.

Ein CC behielt nach dem Verschieben seines Redesigns den Vermerk "Redesignt",
obwohl ueber ihm nichts mehr stand. Der Vermerk stammte von einem Nachfolger,
der inzwischen woanders lag. Jetzt folgt er der Kette: wer einen Nachfolger
hat, wurde ersetzt - wer ans Ende rueckt, nicht mehr. Ein Platz entsteht
dabei nicht aus dem Nichts, die Deckung wird geprueft wie sonst auch.

Neu bearbeitbar: Name, Beschreibung und der verknuepfte Forums-Antrag eines
CCs. Ein Antrag, den der Abgleich noch nicht kennt, wird dabei vorgemerkt -
sonst staende er beim naechsten Lauf als offener Antrag da, obwohl er laengst
eingetragen ist. Ebenso laesst sich nachtraeglich umstellen, ob ein Redesign
bezahlt oder Kulanz war; umgebucht wird ueber eine Gegenbuchung, ueberschrieben
wird nichts.

Zuletzt: Eintraege eines anderen Editors kamen nicht an, solange die Seite
offen stand. Jetzt wird der Stand geholt, sobald der Tab wieder in den
Vordergrund kommt - das trifft den Moment, in dem jemand wieder hinsieht,
ohne im Hintergrund dauernd zu fragen.
This commit is contained in:
2026-09-21 14:36:26 +02:00
parent c82391e73f
commit 2527f6a240
19 changed files with 941 additions and 34 deletions
+109
View File
@@ -219,6 +219,115 @@ describe('Rollen', () => {
});
});
describe('Der Zugang aus der Erstinstallation', () => {
/** Legt einen geschützten Admin an, wie es der erste Start tut. */
async function erstzugang() {
return db.prisma.user.create({
data: {
username: 'erstzugang',
passwordHash: 'x',
role: 'ADMIN',
isProtected: true,
},
});
}
it('lässt sich seine Rolle nicht nehmen', async () => {
const geschuetzt = await erstzugang();
const credentials = await login('chef');
const response = await app.inject({
method: 'PATCH',
url: `/api/admin/users/${geschuetzt.id}`,
headers: authHeaders(credentials),
payload: { role: 'VIEWER' },
});
assert.equal(response.statusCode, 409);
assert.equal(response.json().error, 'PROTECTED_ACCOUNT');
const unveraendert = await db.prisma.user.findUniqueOrThrow({ where: { id: geschuetzt.id } });
assert.equal(unveraendert.role, 'ADMIN');
});
it('lässt sich nicht sperren', async () => {
const geschuetzt = await db.prisma.user.findFirstOrThrow({ where: { isProtected: true } });
const credentials = await login('chef');
const response = await app.inject({
method: 'PATCH',
url: `/api/admin/users/${geschuetzt.id}`,
headers: authHeaders(credentials),
payload: { isActive: false },
});
assert.equal(response.statusCode, 409);
});
it('bekommt sein Passwort nicht von aussen zurückgesetzt', async () => {
const geschuetzt = await db.prisma.user.findFirstOrThrow({ where: { isProtected: true } });
const credentials = await login('chef');
const response = await app.inject({
method: 'POST',
url: `/api/admin/users/${geschuetzt.id}/reset-password`,
headers: authHeaders(credentials),
payload: { newPassword: 'EinLangesPasswort1' },
});
assert.equal(response.statusCode, 409);
});
it('lässt sich nicht löschen', async () => {
const geschuetzt = await db.prisma.user.findFirstOrThrow({ where: { isProtected: true } });
const credentials = await login('chef');
const response = await app.inject({
method: 'DELETE',
url: `/api/admin/users/${geschuetzt.id}`,
headers: authHeaders(credentials),
});
assert.equal(response.statusCode, 409);
assert.ok(await db.prisma.user.findUnique({ where: { id: geschuetzt.id } }));
});
it('ist in der Liste als geschützt ausgewiesen', async () => {
const credentials = await login('chef');
const response = await app.inject({
method: 'GET',
url: '/api/admin/users',
headers: authHeaders(credentials),
});
const eintrag = response
.json()
.items.find((u: { username: string }) => u.username === 'erstzugang');
assert.equal(eintrag.isLocked, true, 'die Oberfläche soll die Knöpfe gar nicht erst zeigen');
});
it('lässt gewöhnliche Konten unangetastet', async () => {
const credentials = await login('chef');
const normal = await db.prisma.user.findFirstOrThrow({ where: { username: 'gast' } });
const response = await app.inject({
method: 'PATCH',
url: `/api/admin/users/${normal.id}`,
headers: authHeaders(credentials),
payload: { role: 'EDITOR' },
});
assert.equal(response.statusCode, 200, 'der Schutz gilt nur für den Erstzugang');
await app.inject({
method: 'PATCH',
url: `/api/admin/users/${normal.id}`,
headers: authHeaders(credentials),
payload: { role: 'VIEWER' },
});
});
});
describe('Erstpasswort', () => {
it('sperrt schreibende Aktionen bis zum Wechsel', async () => {
const credentials = await login('neu');