feat(admin): editable member email + Danish slug folding
Lets a fenja admin edit a member's email in the People resource (the field was read-only). Email is required, format- and uniqueness-checked, and normalised to lowercase on save; collisions surface as a form error via the new updateUserEmail() helper. Also folds ø/æ/å in slugifyName so Danish names produce clean member slugs (soren-friis, not s-ren-friis) — NFKD leaves those letters intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
59842432bd
commit
4c4df45f0c
2 changed files with 28 additions and 2 deletions
|
|
@ -15,6 +15,7 @@ import {
|
|||
getAllUsersPublic,
|
||||
getUserPublicById,
|
||||
updateUserAdminFields,
|
||||
updateUserEmail,
|
||||
updateUserProfile,
|
||||
updateUserRole,
|
||||
deactivateUser,
|
||||
|
|
@ -217,7 +218,15 @@ export const usersResource: Resource<UserPublic> = {
|
|||
'Changing the role has real access consequences. Setting to Council also allocates a member number.',
|
||||
},
|
||||
{ key: 'name', label: 'Name', kind: 'text', required: true, maxLength: 120 },
|
||||
{ key: 'email', label: 'Email', kind: 'text', readOnly: true },
|
||||
{
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
kind: 'text',
|
||||
required: true,
|
||||
maxLength: 200,
|
||||
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
helperText: 'The member’s login identity. Normalised to lowercase on save; must be unique.',
|
||||
},
|
||||
{ key: 'organisation', label: 'Organisation', kind: 'text', readOnly: true,
|
||||
helperText: 'Set at sign-up; editing is not yet supported.' },
|
||||
{ key: 'bio', label: 'Bio', kind: 'textarea', rows: 4 },
|
||||
|
|
@ -282,6 +291,13 @@ export const usersResource: Resource<UserPublic> = {
|
|||
const current = getUserPublicById(id);
|
||||
if (!current) throw new Error(`User ${id} not found`);
|
||||
|
||||
// Email (login identity) — only written when changed. Throws on a
|
||||
// collision, which the save handler surfaces as a form error.
|
||||
const newEmail = String(data.email ?? '').trim().toLowerCase();
|
||||
if (newEmail && newEmail !== current.email) {
|
||||
updateUserEmail(id, newEmail);
|
||||
}
|
||||
|
||||
// Profile fields (name + bio).
|
||||
const newName = String(data.name ?? current.name);
|
||||
const newBio = String(data.bio ?? current.bio ?? '');
|
||||
|
|
|
|||
|
|
@ -152,6 +152,15 @@ export function updateUserProfile(id: number, name: string, bio: string): void {
|
|||
db.prepare('UPDATE users SET name = ?, bio = ? WHERE id = ?').run(name, bio, id);
|
||||
}
|
||||
|
||||
/** Update a user's email — their login identity. Throws if the address is
|
||||
* already used by another account (the column is UNIQUE). */
|
||||
export function updateUserEmail(id: number, email: string): void {
|
||||
const clash = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?')
|
||||
.get(email, id) as { id: number } | undefined;
|
||||
if (clash) throw new Error('That email is already in use by another account.');
|
||||
db.prepare('UPDATE users SET email = ? WHERE id = ?').run(email, id);
|
||||
}
|
||||
|
||||
/** Returns the newly-allocated member_number when the transition lands on
|
||||
* cab and the user had none; null otherwise. Callers may ignore. */
|
||||
export function updateUserRole(id: number, role: Role): { allocated: number | null } {
|
||||
|
|
@ -213,7 +222,8 @@ export function updateUserAdminFields(id: number, data: {
|
|||
export function slugifyName(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.normalize('NFKD').replace(/[̀-ͯ]/g, '') // strip diacritics
|
||||
.replace(/ø/g, 'o').replace(/æ/g, 'ae').replace(/å/g, 'a') // Danish letters NFKD leaves intact
|
||||
.normalize('NFKD').replace(/[̀-ͯ]/g, '') // strip remaining diacritics
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue