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:
@@ -17,10 +17,33 @@ const userSelect = {
|
||||
isActive: true,
|
||||
totpEnabled: true,
|
||||
mustChangePassword: true,
|
||||
isProtected: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
/**
|
||||
* Hält den Zugang aus der Erstinstallation aus dem Spiel.
|
||||
*
|
||||
* An ihm lässt sich nichts ändern, was ihn aussperren könnte: keine Rolle,
|
||||
* kein Zustand, kein Passwort von aussen, keine Löschung - und zwar auch
|
||||
* dann nicht, wenn er es selbst versucht. Damit bleibt ein Weg zurück offen,
|
||||
* wenn sich jemand die Rechte entzieht oder ein Konto abhanden kommt. Sein
|
||||
* eigenes Passwort ändert er weiterhin selbst, über den Bereich "Konto".
|
||||
*
|
||||
* Wer das nicht will, setzt BOOTSTRAP_ADMIN_EDITABLE auf true; dann gilt er
|
||||
* wie jeder andere Zugang.
|
||||
*/
|
||||
function assertNichtGeschuetzt(target: { isProtected: boolean; username: string }): void {
|
||||
if (!target.isProtected || env.BOOTSTRAP_ADMIN_EDITABLE) return;
|
||||
|
||||
throw conflict(
|
||||
'PROTECTED_ACCOUNT',
|
||||
`"${target.username}" ist der Zugang aus der Erstinstallation und bleibt unangetastet. ` +
|
||||
'Wer das ändern will, setzt BOOTSTRAP_ADMIN_EDITABLE=true in der Umgebung.',
|
||||
);
|
||||
}
|
||||
|
||||
// Verhindert, dass sich die Installation selbst den letzten Zugang entzieht.
|
||||
async function assertNotLastAdmin(userId: number): Promise<void> {
|
||||
const target = await prisma.user.findUnique({ where: { id: userId } });
|
||||
@@ -42,7 +65,16 @@ export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
orderBy: [{ isActive: 'desc' }, { username: 'asc' }],
|
||||
select: userSelect,
|
||||
});
|
||||
return { items };
|
||||
|
||||
// Ob der Schutz gerade greift, entscheidet die Umgebung. Die Oberfläche
|
||||
// bekommt das Ergebnis, nicht die Regel - sie soll nicht nachbauen
|
||||
// müssen, was hier schon entschieden ist.
|
||||
return {
|
||||
items: items.map((user) => ({
|
||||
...user,
|
||||
isLocked: user.isProtected && !env.BOOTSTRAP_ADMIN_EDITABLE,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/admin/users', async (request, reply) => {
|
||||
@@ -102,6 +134,7 @@ export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) throw notFound('Nutzer');
|
||||
assertNichtGeschuetzt(target);
|
||||
|
||||
const losesAdmin =
|
||||
(body.role !== undefined && body.role !== Role.ADMIN) || body.isActive === false;
|
||||
@@ -138,6 +171,7 @@ export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) throw notFound('Nutzer');
|
||||
assertNichtGeschuetzt(target);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
@@ -166,6 +200,7 @@ export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) throw notFound('Nutzer');
|
||||
assertNichtGeschuetzt(target);
|
||||
await assertNotLastAdmin(id);
|
||||
|
||||
await prisma.user.delete({ where: { id } });
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
removeLink,
|
||||
moveInChain,
|
||||
detachFromChain,
|
||||
changeRedesignKind,
|
||||
} from '../services/characters.js';
|
||||
import { parseThreadId } from '../lib/forum.js';
|
||||
import { linkThreadToCharacter } from '../services/forum.js';
|
||||
@@ -74,6 +75,8 @@ export async function characterRoutes(app: FastifyInstance): Promise<void> {
|
||||
z.object({
|
||||
name: z.string().trim().min(1).max(160).optional(),
|
||||
loreText: z.string().trim().max(5000).nullable().optional(),
|
||||
/// Thread-Nummer oder volle Adresse; leer löst die Verknüpfung.
|
||||
threadRef: z.string().trim().max(500).nullable().optional(),
|
||||
}),
|
||||
request.body,
|
||||
);
|
||||
@@ -178,6 +181,21 @@ export async function characterRoutes(app: FastifyInstance): Promise<void> {
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
/**
|
||||
* Nachträglich umstellen, woraus ein Redesign bezahlt wurde. Umgebucht
|
||||
* wird im Kontobuch, nicht überschrieben.
|
||||
*/
|
||||
app.post('/api/characters/:id/redesign-kind', { preHandler: requireEditor }, async (request) => {
|
||||
const { id } = parseWith(idParam, request.params);
|
||||
const body = parseWith(
|
||||
z.object({
|
||||
useKind: z.enum([CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]),
|
||||
}),
|
||||
request.body,
|
||||
);
|
||||
return changeRedesignKind(id, body.useKind, contextOf(request));
|
||||
});
|
||||
|
||||
// --- Ketten ---
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user