Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Member } from '@wellformed/shared';
import { badRequest, conflict, notFound, unauthorized } from '../errors.js';
import {
countActiveAdmins,
listMembers,
removeMembership,
setMembership,
} from '../control/workspaces.js';
import { recordAudit } from '../engine/audit.js';
import { updateMemberSchema } from './schemas.js';
const idParams = z.object({ id: z.string().min(1).max(64) });
/**
* Who is in this workspace, and what they may do here.
*
* Note what is *not* here. There is no create-a-person route any more: an
* account is global, so a workspace admin cannot mint one, only invite one.
* And there is no password reset or disable: those belong to the account, not
* to any single workspace, and letting one customer's admin disable an account
* shared with another customer would be a cross-tenant hole.
*/
export async function memberRoutes(app: FastifyInstance): Promise<void> {
app.get('/members', async (request): Promise<{ members: Member[] }> => {
// Reading the workspace is not reading the people in it. A visitor to the
// published demo holds `viewer` without holding an account, and this list
// is names, email addresses and when each of them was last here.
if (!request.ws.require('viewer')) {
throw unauthorized('Sign in to see who is in this workspace.');
}
return { members: listMembers(app.control, request.ws.workspace.id) };
});
app.patch('/members/:id', async (request) => {
const { workspace } = request.ws;
const actor = request.ws.require('admin');
const { id } = idParams.parse(request.params);
const body = updateMemberSchema.parse(request.body);
const target = listMembers(app.control, workspace.id).find((m) => m.id === id);
if (!target) throw notFound('That person is not in this workspace.');
if (!body.role || body.role === target.role) return { member: target };
// An admin must not be able to leave the workspace with no way back in.
if (target.role === 'admin' && countActiveAdmins(app.control, workspace.id) <= 1) {
throw conflict('This is the last active admin. Promote someone else first.');
}
setMembership(app.control, workspace.id, id, body.role);
recordAudit(request.ws.db, request.ws.actor(), {
kind: 'auth',
old: target.email,
nw: `${target.email}: role ${target.role} → ${body.role}`,
});
return {
member: listMembers(app.control, workspace.id).find((m) => m.id === id),
/** A caller who just demoted themselves needs to know before their next request 403s. */
you: id === actor.id ? body.role : undefined,
};
});
app.delete('/members/:id', async (request) => {
const { workspace } = request.ws;
const actor = request.ws.require('admin');
const { id } = idParams.parse(request.params);
if (id === actor.id) {
throw badRequest('Use "leave workspace" to remove yourself.');
}
const target = listMembers(app.control, workspace.id).find((m) => m.id === id);
if (!target) throw notFound('That person is not in this workspace.');
if (target.role === 'admin' && countActiveAdmins(app.control, workspace.id) <= 1) {
throw conflict('This is the last active admin.');
}
// The membership goes; the account does not. They may well be the sole
// admin of a workspace of their own.
removeMembership(app.control, workspace.id, id);
recordAudit(request.ws.db, request.ws.actor(), {
kind: 'auth',
old: target.email,
nw: `removed ${target.email} from the workspace`,
});
return { ok: true };
});
}