Add family roles to member management

This commit is contained in:
Rafael Foster
2026-04-27 07:53:43 -03:00
parent 08199495b6
commit b82a86c4b3
22 changed files with 296 additions and 21 deletions
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "الاسم المعروض",
"memberPasswordLabel": "كلمة المرور",
"colorLabel": "اللون",
"familyRoleLabel": "دور العائلة",
"familyRoleDad": "الأب",
"familyRoleMom": "الأم",
"familyRoleParent": "ولي الأمر",
"familyRoleChild": "طفل",
"familyRoleGrandparent": "جد/جدة",
"familyRoleRelative": "قريب",
"familyRoleOther": "فرد من العائلة",
"systemAdminLabel": "مسؤول النظام",
"systemAdminHint": "يمكن لمسؤولي النظام إدارة الإعدادات والتكاملات ورموز API وحسابات العائلة.",
"systemAdminBadge": "مسؤول النظام",
"roleLabel": "الدور",
"roleMember": "عضو",
"roleAdmin": "مسؤول",
+11
View File
@@ -630,6 +630,17 @@
"displayNameLabel": "Anzeigename",
"memberPasswordLabel": "Passwort",
"colorLabel": "Farbe",
"familyRoleLabel": "Familienrolle",
"familyRoleDad": "Vater",
"familyRoleMom": "Mutter",
"familyRoleParent": "Elternteil",
"familyRoleChild": "Kind",
"familyRoleGrandparent": "Großelternteil",
"familyRoleRelative": "Verwandte/r",
"familyRoleOther": "Familienmitglied",
"systemAdminLabel": "Systemadministrator",
"systemAdminHint": "Systemadministratoren können App-Einstellungen, Integrationen, API-Tokens und Familienkonten verwalten.",
"systemAdminBadge": "Systemadministrator",
"roleLabel": "Rolle",
"roleMember": "Mitglied",
"roleAdmin": "Admin",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Εμφανιζόμενο όνομα",
"memberPasswordLabel": "Κωδικός",
"colorLabel": "Χρώμα",
"familyRoleLabel": "Ρόλος στην οικογένεια",
"familyRoleDad": "Μπαμπάς",
"familyRoleMom": "Μαμά",
"familyRoleParent": "Γονέας",
"familyRoleChild": "Παιδί",
"familyRoleGrandparent": "Παππούς/Γιαγιά",
"familyRoleRelative": "Συγγενής",
"familyRoleOther": "Μέλος οικογένειας",
"systemAdminLabel": "Διαχειριστής συστήματος",
"systemAdminHint": "Οι διαχειριστές συστήματος μπορούν να διαχειρίζονται ρυθμίσεις, ενσωματώσεις, API tokens και λογαριασμούς οικογένειας.",
"systemAdminBadge": "Διαχειριστής συστήματος",
"roleLabel": "Ρόλος",
"roleMember": "Μέλος",
"roleAdmin": "Διαχειριστής",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Display name",
"memberPasswordLabel": "Password",
"colorLabel": "Color",
"familyRoleLabel": "Family role",
"familyRoleDad": "Dad",
"familyRoleMom": "Mom",
"familyRoleParent": "Parent",
"familyRoleChild": "Child",
"familyRoleGrandparent": "Grandparent",
"familyRoleRelative": "Relative",
"familyRoleOther": "Family member",
"systemAdminLabel": "System admin",
"systemAdminHint": "System admins can manage application settings, integrations, API tokens and family accounts.",
"systemAdminBadge": "System admin",
"roleLabel": "Role",
"roleMember": "Member",
"roleAdmin": "Admin",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Nombre para mostrar",
"memberPasswordLabel": "Contraseña",
"colorLabel": "Color",
"familyRoleLabel": "Rol familiar",
"familyRoleDad": "Papá",
"familyRoleMom": "Mamá",
"familyRoleParent": "Progenitor",
"familyRoleChild": "Hijo/a",
"familyRoleGrandparent": "Abuelo/a",
"familyRoleRelative": "Familiar",
"familyRoleOther": "Miembro de la familia",
"systemAdminLabel": "Administrador del sistema",
"systemAdminHint": "Los administradores del sistema pueden gestionar ajustes, integraciones, tokens de API y cuentas familiares.",
"systemAdminBadge": "Admin del sistema",
"roleLabel": "Rol",
"roleMember": "Miembro",
"roleAdmin": "Administrador",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Nom affiché",
"memberPasswordLabel": "Mot de passe",
"colorLabel": "Couleur",
"familyRoleLabel": "Rôle familial",
"familyRoleDad": "Papa",
"familyRoleMom": "Maman",
"familyRoleParent": "Parent",
"familyRoleChild": "Enfant",
"familyRoleGrandparent": "Grand-parent",
"familyRoleRelative": "Proche",
"familyRoleOther": "Membre de la famille",
"systemAdminLabel": "Administrateur système",
"systemAdminHint": "Les administrateurs système peuvent gérer les paramètres, intégrations, jetons API et comptes familiaux.",
"systemAdminBadge": "Admin système",
"roleLabel": "Rôle",
"roleMember": "Membre",
"roleAdmin": "Admin",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "प्रदर्शन नाम",
"memberPasswordLabel": "पासवर्ड",
"colorLabel": "रंग",
"familyRoleLabel": "परिवार भूमिका",
"familyRoleDad": "पिता",
"familyRoleMom": "माँ",
"familyRoleParent": "अभिभावक",
"familyRoleChild": "बच्चा",
"familyRoleGrandparent": "दादा-दादी/नाना-नानी",
"familyRoleRelative": "रिश्तेदार",
"familyRoleOther": "परिवार सदस्य",
"systemAdminLabel": "सिस्टम एडमिन",
"systemAdminHint": "सिस्टम एडमिन सेटिंग्स, इंटीग्रेशन, API टोकन और परिवार खातों को प्रबंधित कर सकते हैं।",
"systemAdminBadge": "सिस्टम एडमिन",
"roleLabel": "भूमिका",
"roleMember": "सदस्य",
"roleAdmin": "एडमिन",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Nome visualizzato",
"memberPasswordLabel": "Password",
"colorLabel": "Colore",
"familyRoleLabel": "Ruolo familiare",
"familyRoleDad": "Papà",
"familyRoleMom": "Mamma",
"familyRoleParent": "Genitore",
"familyRoleChild": "Figlio/a",
"familyRoleGrandparent": "Nonno/a",
"familyRoleRelative": "Parente",
"familyRoleOther": "Membro della famiglia",
"systemAdminLabel": "Amministratore di sistema",
"systemAdminHint": "Gli amministratori di sistema possono gestire impostazioni, integrazioni, token API e account familiari.",
"systemAdminBadge": "Admin sistema",
"roleLabel": "Ruolo",
"roleMember": "Membro",
"roleAdmin": "Admin",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "表示名",
"memberPasswordLabel": "パスワード",
"colorLabel": "色",
"familyRoleLabel": "家族内の役割",
"familyRoleDad": "父",
"familyRoleMom": "母",
"familyRoleParent": "保護者",
"familyRoleChild": "子ども",
"familyRoleGrandparent": "祖父母",
"familyRoleRelative": "親族",
"familyRoleOther": "家族メンバー",
"systemAdminLabel": "システム管理者",
"systemAdminHint": "システム管理者は設定、連携、APIトークン、家族アカウントを管理できます。",
"systemAdminBadge": "システム管理者",
"roleLabel": "役割",
"roleMember": "メンバー",
"roleAdmin": "管理者",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Nome de exibição",
"memberPasswordLabel": "Senha",
"colorLabel": "Cor",
"familyRoleLabel": "Papel na família",
"familyRoleDad": "Pai",
"familyRoleMom": "Mãe",
"familyRoleParent": "Responsável",
"familyRoleChild": "Filho(a)",
"familyRoleGrandparent": "Avô/Avó",
"familyRoleRelative": "Parente",
"familyRoleOther": "Membro da família",
"systemAdminLabel": "Administrador do sistema",
"systemAdminHint": "Administradores do sistema podem gerenciar configurações, integrações, tokens de API e contas da família.",
"systemAdminBadge": "Admin do sistema",
"roleLabel": "Função",
"roleMember": "Membro",
"roleAdmin": "Admin",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Отображаемое имя",
"memberPasswordLabel": "Пароль",
"colorLabel": "Цвет",
"familyRoleLabel": "Роль в семье",
"familyRoleDad": "Папа",
"familyRoleMom": "Мама",
"familyRoleParent": "Родитель",
"familyRoleChild": "Ребёнок",
"familyRoleGrandparent": "Дедушка/бабушка",
"familyRoleRelative": "Родственник",
"familyRoleOther": "Член семьи",
"systemAdminLabel": "Системный администратор",
"systemAdminHint": "Системные администраторы могут управлять настройками, интеграциями, API-токенами и семейными аккаунтами.",
"systemAdminBadge": "Системный администратор",
"roleLabel": "Роль",
"roleMember": "Участник",
"roleAdmin": "Администратор",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Visningsnamn",
"memberPasswordLabel": "Lösenord",
"colorLabel": "Färg",
"familyRoleLabel": "Familjeroll",
"familyRoleDad": "Pappa",
"familyRoleMom": "Mamma",
"familyRoleParent": "Förälder",
"familyRoleChild": "Barn",
"familyRoleGrandparent": "Far-/morförälder",
"familyRoleRelative": "Släkting",
"familyRoleOther": "Familjemedlem",
"systemAdminLabel": "Systemadministratör",
"systemAdminHint": "Systemadministratörer kan hantera inställningar, integrationer, API-token och familjekonton.",
"systemAdminBadge": "Systemadmin",
"roleLabel": "Roll",
"roleMember": "Medlem",
"roleAdmin": "Admin",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Görünen ad",
"memberPasswordLabel": "Şifre",
"colorLabel": "Renk",
"familyRoleLabel": "Aile rolü",
"familyRoleDad": "Baba",
"familyRoleMom": "Anne",
"familyRoleParent": "Ebeveyn",
"familyRoleChild": "Çocuk",
"familyRoleGrandparent": "Büyükanne/Büyükbaba",
"familyRoleRelative": "Akraba",
"familyRoleOther": "Aile üyesi",
"systemAdminLabel": "Sistem yöneticisi",
"systemAdminHint": "Sistem yöneticileri ayarları, entegrasyonları, API tokenlarını ve aile hesaplarını yönetebilir.",
"systemAdminBadge": "Sistem yöneticisi",
"roleLabel": "Rol",
"roleMember": "Üye",
"roleAdmin": "Yönetici",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "Відображуване ім'я",
"memberPasswordLabel": "Пароль",
"colorLabel": "Колір",
"familyRoleLabel": "Роль у родині",
"familyRoleDad": "Тато",
"familyRoleMom": "Мама",
"familyRoleParent": "Батько/мати",
"familyRoleChild": "Дитина",
"familyRoleGrandparent": "Дідусь/бабуся",
"familyRoleRelative": "Родич",
"familyRoleOther": "Член родини",
"systemAdminLabel": "Системний адміністратор",
"systemAdminHint": "Системні адміністратори можуть керувати налаштуваннями, інтеграціями, API-токенами та сімейними акаунтами.",
"systemAdminBadge": "Системний адміністратор",
"roleLabel": "Роль",
"roleMember": "Учасник",
"roleAdmin": "Адміністратор",
+11
View File
@@ -624,6 +624,17 @@
"displayNameLabel": "显示名称",
"memberPasswordLabel": "密码",
"colorLabel": "颜色",
"familyRoleLabel": "家庭角色",
"familyRoleDad": "爸爸",
"familyRoleMom": "妈妈",
"familyRoleParent": "父母",
"familyRoleChild": "孩子",
"familyRoleGrandparent": "祖父母",
"familyRoleRelative": "亲属",
"familyRoleOther": "家庭成员",
"systemAdminLabel": "系统管理员",
"systemAdminHint": "系统管理员可以管理应用设置、集成、API 令牌和家庭账户。",
"systemAdminBadge": "系统管理员",
"roleLabel": "角色",
"roleMember": "成员",
"roleAdmin": "管理员",
+24 -6
View File
@@ -14,6 +14,7 @@ const SUPPORTED_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', '
const SETTINGS_TAB_KEY = 'oikos:settings:tab';
const APP_NAME_STORAGE_KEY = 'oikos-app-name';
const DEFAULT_APP_NAME = 'Oikos';
const FAMILY_ROLES = ['dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other'];
const CATEGORY_I18N = {
'Obst & Gemüse': 'shopping.catFruitVeg',
@@ -44,6 +45,16 @@ function buildCurrencyOptions(selected) {
.join('');
}
function familyRoleLabel(role) {
return t(`settings.familyRole${String(role || 'other').replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase())}`);
}
function buildFamilyRoleOptions(selected = 'other') {
return FAMILY_ROLES.map((role) => `
<option value="${role}"${role === selected ? ' selected' : ''}>${familyRoleLabel(role)}</option>
`).join('');
}
/**
* @param {HTMLElement} container
* @param {{ user: object }} context
@@ -476,12 +487,16 @@ export async function render(container, { user }) {
<input class="form-input form-input--color" type="color" id="new-avatar-color" value="#007AFF" />
</div>
<div class="form-group">
<label class="form-label" for="new-role">${t('settings.roleLabel')}</label>
<select class="form-input" id="new-role">
<option value="member">${t('settings.roleMember')}</option>
<option value="admin">${t('settings.roleAdmin')}</option>
<label class="form-label" for="new-family-role">${t('settings.familyRoleLabel')}</label>
<select class="form-input" id="new-family-role">
${buildFamilyRoleOptions()}
</select>
</div>
<label class="toggle-row">
<input type="checkbox" id="new-system-admin" />
<span>${t('settings.systemAdminLabel')}</span>
</label>
<p class="form-hint">${t('settings.systemAdminHint')}</p>
<div id="member-error" class="form-error" hidden></div>
<div class="settings-form-actions">
<button type="submit" class="btn btn--primary">${t('settings.createMember')}</button>
@@ -773,7 +788,8 @@ function bindEvents(container, user, categories, icsSubscriptions, apiTokens) {
display_name: container.querySelector('#new-display-name').value.trim(),
password: container.querySelector('#new-member-password').value,
avatar_color: container.querySelector('#new-avatar-color').value,
role: container.querySelector('#new-role').value,
family_role: container.querySelector('#new-family-role').value,
system_admin: container.querySelector('#new-system-admin')?.checked === true,
};
const btn = addMemberForm.querySelector('[type=submit]');
@@ -1103,12 +1119,14 @@ function bindCategoryEvents(container) {
}
function memberHtml(u) {
const familyRole = familyRoleLabel(u.family_role);
const systemRole = u.role === 'admin' ? ` · ${esc(t('settings.systemAdminBadge'))}` : '';
return `
<li class="settings-member" data-id="${u.id}">
<div class="settings-avatar settings-avatar--sm" style="background:${esc(u.avatar_color)}">${initials(u.display_name)}</div>
<div class="settings-member__info">
<span class="settings-member__name">${esc(u.display_name)}</span>
<span class="settings-member__meta">@${esc(u.username)} · ${u.role === 'admin' ? t('settings.roleAdmin') : t('settings.roleMember')}</span>
<span class="settings-member__meta">@${esc(u.username)} · ${esc(familyRole)}${systemRole}</span>
</div>
<button class="btn btn--icon btn--danger-outline" data-delete-user="${u.id}" data-name="${esc(u.display_name)}" aria-label="${esc(u.display_name)} ${t('settings.deleteMemberLabel')}" title="${t('settings.deleteMemberLabel')}">
<i data-lucide="trash-2" aria-hidden="true"></i>
+24 -13
View File
@@ -16,6 +16,7 @@ import { createLogger } from './logger.js';
const log = createLogger('Auth');
const router = express.Router();
const API_TOKEN_PREFIX = 'oikos_';
const FAMILY_ROLES = ['dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other'];
// --------------------------------------------------------
// Session-Store (better-sqlite3, gleiche DB-Instanz wie App)
@@ -156,7 +157,7 @@ function authenticateApiToken(req) {
const tokenHash = hashApiToken(token);
const row = db.get().prepare(`
SELECT t.*, u.role, u.username, u.display_name, u.avatar_color
SELECT t.*, u.role, u.username, u.display_name, u.avatar_color, u.family_role
FROM api_tokens t
JOIN users u ON u.id = t.created_by
WHERE t.token_hash = ?
@@ -176,6 +177,7 @@ function authenticateApiToken(req) {
display_name: row.display_name,
avatar_color: row.avatar_color,
role: row.role,
family_role: row.family_role,
};
return row;
}
@@ -225,7 +227,7 @@ const avatarColors = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#F
/**
* POST /api/v1/auth/login
* Body: { username: string, password: string }
* Response: { user: { id, username, display_name, avatar_color, role } }
* Response: { user: { id, username, display_name, avatar_color, role, family_role } }
*/
router.post('/login', loginLimiter, async (req, res) => {
try {
@@ -277,6 +279,7 @@ router.post('/login', loginLimiter, async (req, res) => {
display_name: user.display_name,
avatar_color: user.avatar_color,
role: user.role,
family_role: user.family_role,
},
csrfToken: req.session.csrfToken,
});
@@ -344,7 +347,7 @@ router.post('/setup', loginLimiter, async (req, res) => {
.run(username, display_name, hash, avatarColor, 'admin');
res.status(201).json({
user: { id: result.lastInsertRowid, username, display_name, avatar_color: avatarColor, role: 'admin' },
user: { id: result.lastInsertRowid, username, display_name, avatar_color: avatarColor, role: 'admin', family_role: 'other' },
});
} catch (err) {
if (err.message?.includes('UNIQUE constraint')) {
@@ -362,7 +365,7 @@ router.post('/setup', loginLimiter, async (req, res) => {
router.get('/me', requireAuth, (req, res) => {
try {
const user = db.get()
.prepare('SELECT id, username, display_name, avatar_color, role FROM users WHERE id = ?')
.prepare('SELECT id, username, display_name, avatar_color, role, family_role FROM users WHERE id = ?')
.get(req.authUserId);
if (!user) {
@@ -404,7 +407,7 @@ router.get('/me', requireAuth, (req, res) => {
router.get('/users', requireAuth, requireAdmin, (req, res) => {
try {
const users = db.get()
.prepare('SELECT id, username, display_name, avatar_color, role, created_at FROM users ORDER BY display_name')
.prepare('SELECT id, username, display_name, avatar_color, role, family_role, created_at FROM users ORDER BY display_name')
.all();
res.json({ data: users });
} catch (err) {
@@ -488,12 +491,20 @@ router.delete('/api-tokens/:id', requireAuth, requireAdmin, csrfMiddleware, (req
/**
* POST /api/v1/auth/users
* Admin only. Erstellt neues Familienmitglied.
* Body: { username, display_name, password, avatar_color?, role? }
* Body: { username, display_name, password, avatar_color?, family_role?, system_admin? }
* Response: { user: { id, username, display_name, avatar_color, role } }
*/
router.post('/users', requireAuth, requireAdmin, csrfMiddleware, async (req, res) => {
try {
const { username, display_name, password, avatar_color = '#007AFF', role = 'member' } = req.body;
const {
username,
display_name,
password,
avatar_color = '#007AFF',
family_role = 'other',
system_admin = req.body.role === 'admin',
} = req.body;
const role = system_admin === true || system_admin === 'true' ? 'admin' : 'member';
if (!username || !display_name || !password) {
return res.status(400).json({ error: 'Username, display name, and password are required.', code: 400 });
@@ -511,21 +522,21 @@ router.post('/users', requireAuth, requireAdmin, csrfMiddleware, async (req, res
return res.status(400).json({ error: 'Display name may be at most 128 characters long.', code: 400 });
}
if (!['admin', 'member'].includes(role)) {
return res.status(400).json({ error: 'Invalid role.', code: 400 });
if (!FAMILY_ROLES.includes(family_role)) {
return res.status(400).json({ error: 'Invalid family role.', code: 400 });
}
const hash = await bcrypt.hash(password, 12);
const result = db.get()
.prepare(`
INSERT INTO users (username, display_name, password_hash, avatar_color, role)
VALUES (?, ?, ?, ?, ?)
INSERT INTO users (username, display_name, password_hash, avatar_color, role, family_role)
VALUES (?, ?, ?, ?, ?, ?)
`)
.run(username, display_name, hash, avatar_color, role);
.run(username, display_name, hash, avatar_color, role, family_role);
res.status(201).json({
user: { id: result.lastInsertRowid, username, display_name, avatar_color, role },
user: { id: result.lastInsertRowid, username, display_name, avatar_color, role, family_role },
});
} catch (err) {
if (err.message && err.message.includes('UNIQUE constraint')) {
+2
View File
@@ -17,6 +17,8 @@ const MIGRATIONS_SQL = {
avatar_color TEXT NOT NULL DEFAULT '#007AFF',
role TEXT NOT NULL DEFAULT 'member'
CHECK(role IN ('admin', 'member')),
family_role TEXT NOT NULL DEFAULT 'other'
CHECK(family_role IN ('dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other')),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
+10
View File
@@ -717,6 +717,16 @@ const MIGRATIONS = [
CREATE INDEX IF NOT EXISTS idx_birthdays_calendar_ref ON birthdays(calendar_event_id);
`,
},
{
version: 19,
description: 'Separate family member role from system access role',
up: `
ALTER TABLE users ADD COLUMN family_role TEXT NOT NULL DEFAULT 'other'
CHECK(family_role IN ('dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other'));
CREATE INDEX IF NOT EXISTS idx_users_family_role ON users(family_role);
`,
},
];
/**
+2
View File
@@ -31,6 +31,7 @@ import weatherRouter from './routes/weather.js';
import preferencesRouter from './routes/preferences.js';
import remindersRouter from './routes/reminders.js';
import searchRouter from './routes/search.js';
import familyRouter from './routes/family.js';
const log = createLogger('Server');
const logSync = createLogger('Sync');
@@ -203,6 +204,7 @@ app.use('/api/v1/weather', weatherRouter);
app.use('/api/v1/preferences', preferencesRouter);
app.use('/api/v1/reminders', remindersRouter);
app.use('/api/v1/search', searchRouter);
app.use('/api/v1/family', familyRouter);
// --------------------------------------------------------
// Health-Check (für Docker)
+41 -2
View File
@@ -241,6 +241,21 @@ function buildPaths() {
params: [idParam('id', 'API token ID')],
}),
},
'/api/v1/family/members': {
get: op({
summary: 'List family members',
tag: 'Family',
description: 'Read-only endpoint for family-member profiles. It does not expose usernames or system access roles and does not support create/update/delete operations.',
responses: {
200: {
description: 'Family members',
content: { 'application/json': { schema: { $ref: '#/components/schemas/FamilyMembersResponse' } } },
},
401: { $ref: '#/components/responses/Unauthorized' },
500: { $ref: '#/components/responses/InternalServerError' },
},
}),
},
'/api/v1/dashboard': { get: op({ summary: 'Get dashboard data', tag: 'Dashboard' }) },
'/api/v1/tasks': {
get: op({ summary: 'List tasks', tag: 'Tasks' }),
@@ -443,6 +458,7 @@ function buildOpenApiSpec(req, appVersion) {
tags: [
{ name: 'System' },
{ name: 'Auth' },
{ name: 'Family' },
{ name: 'Dashboard' },
{ name: 'Tasks' },
{ name: 'Shopping' },
@@ -528,8 +544,30 @@ function buildOpenApiSpec(req, appVersion) {
display_name: { type: 'string' },
avatar_color: { type: 'string' },
role: { type: 'string', enum: ['admin', 'member'] },
family_role: { type: 'string', enum: ['dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other'] },
},
required: ['id', 'username', 'display_name', 'avatar_color', 'role'],
required: ['id', 'username', 'display_name', 'avatar_color', 'role', 'family_role'],
},
FamilyMember: {
type: 'object',
properties: {
id: { type: 'integer' },
display_name: { type: 'string' },
avatar_color: { type: 'string' },
family_role: { type: 'string', enum: ['dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other'] },
created_at: { type: 'string', format: 'date-time' },
},
required: ['id', 'display_name', 'avatar_color', 'family_role'],
},
FamilyMembersResponse: {
type: 'object',
properties: {
data: {
type: 'array',
items: { $ref: '#/components/schemas/FamilyMember' },
},
},
required: ['data'],
},
LoginRequest: {
type: 'object',
@@ -579,7 +617,8 @@ function buildOpenApiSpec(req, appVersion) {
display_name: { type: 'string' },
password: { type: 'string' },
avatar_color: { type: 'string' },
role: { type: 'string', enum: ['admin', 'member'] },
family_role: { type: 'string', enum: ['dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other'] },
system_admin: { type: 'boolean' },
},
required: ['username', 'display_name', 'password'],
},
+28
View File
@@ -0,0 +1,28 @@
/**
* Module: Family
* Purpose: Read-only family member API.
* Dependencies: express, server/db.js
*/
import express from 'express';
import * as db from '../db.js';
import { createLogger } from '../logger.js';
const log = createLogger('Family');
const router = express.Router();
router.get('/members', (req, res) => {
try {
const members = db.get().prepare(`
SELECT id, display_name, avatar_color, family_role, created_at
FROM users
ORDER BY display_name COLLATE NOCASE ASC
`).all();
res.json({ data: members });
} catch (err) {
log.error('GET /members error:', err);
res.status(500).json({ error: 'Internal server error.', code: 500 });
}
});
export default router;