Update
This commit is contained in:
@@ -3,6 +3,36 @@ import { db } from '../db/index.js';
|
||||
|
||||
export const SUPERUSER_GROUP_NAME = 'superuser';
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'bg-arcticBlue-600',
|
||||
'bg-copperRust-500',
|
||||
'bg-sunsetCoral-600',
|
||||
'bg-mintTea-400',
|
||||
'bg-lavenderSmoke-500',
|
||||
'bg-emeraldMist-500',
|
||||
'bg-roseQuartz-500',
|
||||
'bg-auroraTeal-500',
|
||||
'bg-citrusPunch-500',
|
||||
'bg-mossGreen-400'
|
||||
];
|
||||
|
||||
const DEFAULT_AVATAR_COLOR = 'bg-mossGreen-500';
|
||||
const AVATAR_COLOR_SET = new Set([...AVATAR_COLORS, DEFAULT_AVATAR_COLOR]);
|
||||
|
||||
const pickRandomAvatarColor = () => {
|
||||
if (!Array.isArray(AVATAR_COLORS) || AVATAR_COLORS.length === 0) {
|
||||
return DEFAULT_AVATAR_COLOR;
|
||||
}
|
||||
const index = Math.floor(Math.random() * AVATAR_COLORS.length);
|
||||
return AVATAR_COLORS[index] ?? DEFAULT_AVATAR_COLOR;
|
||||
};
|
||||
|
||||
const normalizeAvatarColor = (value) => {
|
||||
if (!value) return null;
|
||||
const candidate = String(value).trim();
|
||||
return AVATAR_COLOR_SET.has(candidate) ? candidate : null;
|
||||
};
|
||||
|
||||
const selectGroupByName = db.prepare('SELECT * FROM user_groups WHERE name = ?');
|
||||
const insertGroup = db.prepare('INSERT INTO user_groups (name, description) VALUES (?, ?)');
|
||||
|
||||
@@ -26,13 +56,13 @@ const updateLastLogin = db.prepare(`
|
||||
`);
|
||||
|
||||
const insertUser = db.prepare(`
|
||||
INSERT INTO users (username, email, password_hash, password_salt, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
INSERT INTO users (username, email, password_hash, password_salt, avatar_color, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`);
|
||||
|
||||
const updateUser = db.prepare(`
|
||||
UPDATE users
|
||||
SET username = ?, email = ?, password_hash = ?, password_salt = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP
|
||||
SET username = ?, email = ?, password_hash = ?, password_salt = ?, is_active = 1, avatar_color = COALESCE(?, avatar_color), updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
@@ -63,15 +93,19 @@ const countSuperusers = db.prepare(`
|
||||
WHERE g.name = ?
|
||||
`);
|
||||
|
||||
const creationTransaction = db.transaction(({ username, email, passwordHash, passwordSalt, groupId }) => {
|
||||
const creationTransaction = db.transaction(({ username, email, passwordHash, passwordSalt, groupId, avatarColor }) => {
|
||||
const existingUser = selectUserByUsername.get(username) || selectUserByEmail.get(email);
|
||||
|
||||
let userId;
|
||||
if (existingUser) {
|
||||
updateUser.run(username, email, passwordHash, passwordSalt, existingUser.id);
|
||||
const existingColor = normalizeAvatarColor(existingUser.avatar_color);
|
||||
const normalizedNewColor = normalizeAvatarColor(avatarColor);
|
||||
const colorToPersist = existingColor || normalizedNewColor || DEFAULT_AVATAR_COLOR;
|
||||
updateUser.run(username, email, passwordHash, passwordSalt, colorToPersist, existingUser.id);
|
||||
userId = existingUser.id;
|
||||
} else {
|
||||
const result = insertUser.run(username, email, passwordHash, passwordSalt);
|
||||
const colorToPersist = normalizeAvatarColor(avatarColor) || DEFAULT_AVATAR_COLOR;
|
||||
const result = insertUser.run(username, email, passwordHash, passwordSalt, colorToPersist);
|
||||
userId = result.lastInsertRowid;
|
||||
}
|
||||
|
||||
@@ -164,19 +198,24 @@ function createOrUpdateSuperuser({ username, email, password }) {
|
||||
|
||||
const { hash, salt } = hashPassword(password);
|
||||
const group = ensureGroup(SUPERUSER_GROUP_NAME, 'System Superuser');
|
||||
const avatarColor = pickRandomAvatarColor();
|
||||
|
||||
const user = creationTransaction({
|
||||
username: normalizedUsername,
|
||||
email: normalizedEmail,
|
||||
passwordHash: hash,
|
||||
passwordSalt: salt,
|
||||
groupId: group.id
|
||||
groupId: group.id,
|
||||
avatarColor
|
||||
});
|
||||
|
||||
const persistedColor = normalizeAvatarColor(user.avatar_color) || avatarColor || DEFAULT_AVATAR_COLOR;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
avatarColor: persistedColor
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
password_salt TEXT,
|
||||
avatar_color TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
last_login DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -130,6 +131,17 @@ console.log('✅ settings table ready');
|
||||
db.exec(createUsersTable);
|
||||
console.log('✅ users table ready');
|
||||
|
||||
try {
|
||||
const userColumns = db.prepare('PRAGMA table_info(users)').all();
|
||||
const hasAvatarColor = userColumns.some((column) => column.name === 'avatar_color');
|
||||
if (!hasAvatarColor) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN avatar_color TEXT');
|
||||
console.log('ℹ️ avatar_color column hinzugefügt');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte avatar_color Spalte nicht prüfen/erstellen:', err.message);
|
||||
}
|
||||
|
||||
db.exec(createUserGroupsTable);
|
||||
console.log('✅ user_groups table ready');
|
||||
|
||||
|
||||
+50
-2
@@ -45,7 +45,7 @@ import {
|
||||
removeServer,
|
||||
setServerApiKey
|
||||
} from './setup/index.js';
|
||||
import { listUsers } from './users/index.js';
|
||||
import { listUsers, getUserById, updateUserGroups } from './users/index.js';
|
||||
import { listGroups } from './groups/index.js';
|
||||
|
||||
dotenv.config();
|
||||
@@ -145,7 +145,8 @@ const PUBLIC_API_ROUTES = [
|
||||
const sanitizeUser = (user) => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
avatarColor: user.avatar_color || null
|
||||
});
|
||||
|
||||
const cleanupExpiredSessions = () => {
|
||||
@@ -1683,6 +1684,53 @@ app.get('/api/users', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users/:userId', (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const numericId = Number(userId);
|
||||
|
||||
if (!Number.isFinite(numericId) || numericId <= 0) {
|
||||
return res.status(400).json({ error: 'INVALID_USER_ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = getUserById(numericId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'USER_NOT_FOUND' });
|
||||
}
|
||||
res.json({ item: user });
|
||||
} catch (error) {
|
||||
console.error(`⚠️ [Users] Abruf der Benutzerdetails fehlgeschlagen (${userId}):`, error);
|
||||
res.status(500).json({ error: 'USER_FETCH_FAILED' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/users/:userId/groups', (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const numericId = Number(userId);
|
||||
const { groupIds } = req.body ?? {};
|
||||
|
||||
if (!Number.isFinite(numericId) || numericId <= 0) {
|
||||
return res.status(400).json({ error: 'INVALID_USER_ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = updateUserGroups(numericId, Array.isArray(groupIds) ? groupIds : []);
|
||||
res.json({ item: updatedUser });
|
||||
} catch (error) {
|
||||
if (error.code === 'INVALID_USER_ID') {
|
||||
return res.status(400).json({ error: 'INVALID_USER_ID' });
|
||||
}
|
||||
if (error.code === 'USER_NOT_FOUND') {
|
||||
return res.status(404).json({ error: 'USER_NOT_FOUND' });
|
||||
}
|
||||
if (error.code === 'GROUP_NOT_FOUND') {
|
||||
return res.status(400).json({ error: 'GROUP_NOT_FOUND', details: error.missingGroupIds || [] });
|
||||
}
|
||||
console.error(`⚠️ [Users] Aktualisierung der Gruppenzuordnung fehlgeschlagen (${userId}):`, error);
|
||||
res.status(500).json({ error: 'USER_GROUPS_UPDATE_FAILED' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/groups', (req, res) => {
|
||||
try {
|
||||
const groups = listGroups();
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -32,8 +32,10 @@
|
||||
<!-- Nepcha Analytics (nepcha.com) -->
|
||||
<!-- Nepcha is a easy-to-use web analytics. No cookies and fully compliant with GDPR, CCPA and PECR. -->
|
||||
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-af8f32f4.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-58708f0f.css">
|
||||
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-ec92bae1.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-ecd5fb94.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+105
-6
@@ -6,10 +6,11 @@ const selectUsersWithGroups = db.prepare(`
|
||||
u.username,
|
||||
u.email,
|
||||
u.is_active,
|
||||
u.avatar_color,
|
||||
u.last_login,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
GROUP_CONCAT(g.name, '|||') AS group_names
|
||||
GROUP_CONCAT(g.id || '::' || g.name, '|||') AS group_pairs
|
||||
FROM users u
|
||||
LEFT JOIN user_group_memberships m ON m.user_id = u.id
|
||||
LEFT JOIN user_groups g ON g.id = m.group_id
|
||||
@@ -17,13 +18,54 @@ const selectUsersWithGroups = db.prepare(`
|
||||
ORDER BY u.username COLLATE NOCASE
|
||||
`);
|
||||
|
||||
const normalizeGroupNames = (rawNames) => {
|
||||
if (!rawNames) {
|
||||
const selectUserWithGroupsById = db.prepare(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.email,
|
||||
u.is_active,
|
||||
u.avatar_color,
|
||||
u.last_login,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
GROUP_CONCAT(g.id || '::' || g.name, '|||') AS group_pairs
|
||||
FROM users u
|
||||
LEFT JOIN user_group_memberships m ON m.user_id = u.id
|
||||
LEFT JOIN user_groups g ON g.id = m.group_id
|
||||
WHERE u.id = ?
|
||||
GROUP BY u.id
|
||||
`);
|
||||
|
||||
const deleteMembershipsByUser = db.prepare(`
|
||||
DELETE FROM user_group_memberships
|
||||
WHERE user_id = ?
|
||||
`);
|
||||
|
||||
const insertMembershipForUser = db.prepare(`
|
||||
INSERT OR IGNORE INTO user_group_memberships (user_id, group_id)
|
||||
VALUES (?, ?)
|
||||
`);
|
||||
|
||||
const selectGroupById = db.prepare('SELECT id, name FROM user_groups WHERE id = ?');
|
||||
|
||||
const parseGroupPairs = (rawPairs) => {
|
||||
if (!rawPairs) {
|
||||
return [];
|
||||
}
|
||||
return rawNames
|
||||
return rawPairs
|
||||
.split('|||')
|
||||
.map((name) => String(name || '').trim())
|
||||
.map((entry) => {
|
||||
const [idPart, namePart] = entry.split('::');
|
||||
const groupName = String(namePart || '').trim();
|
||||
if (!groupName) {
|
||||
return null;
|
||||
}
|
||||
const numericId = Number(idPart);
|
||||
return {
|
||||
id: Number.isFinite(numericId) ? numericId : null,
|
||||
name: groupName
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
@@ -32,13 +74,70 @@ const sanitizeUserRecord = (row) => ({
|
||||
username: row.username,
|
||||
email: row.email,
|
||||
isActive: Boolean(row.is_active),
|
||||
avatarColor: row.avatar_color || null,
|
||||
lastLogin: row.last_login || null,
|
||||
createdAt: row.created_at || null,
|
||||
updatedAt: row.updated_at || null,
|
||||
groups: normalizeGroupNames(row.group_names)
|
||||
groups: parseGroupPairs(row.group_pairs)
|
||||
});
|
||||
|
||||
export function listUsers() {
|
||||
const rows = selectUsersWithGroups.all();
|
||||
return rows.map(sanitizeUserRecord);
|
||||
}
|
||||
|
||||
export function getUserById(userId) {
|
||||
const row = selectUserWithGroupsById.get(userId);
|
||||
return row ? sanitizeUserRecord(row) : null;
|
||||
}
|
||||
|
||||
const applyUserGroupAssignments = db.transaction((userId, groupIds) => {
|
||||
deleteMembershipsByUser.run(userId);
|
||||
groupIds.forEach((groupId) => {
|
||||
insertMembershipForUser.run(userId, groupId);
|
||||
});
|
||||
});
|
||||
|
||||
export function updateUserGroups(userId, groupIds) {
|
||||
const numericUserId = Number(userId);
|
||||
if (!Number.isFinite(numericUserId) || numericUserId <= 0) {
|
||||
const error = new Error('INVALID_USER_ID');
|
||||
error.code = 'INVALID_USER_ID';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingUser = getUserById(numericUserId);
|
||||
if (!existingUser) {
|
||||
const error = new Error('USER_NOT_FOUND');
|
||||
error.code = 'USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const normalizedGroupIds = Array.isArray(groupIds)
|
||||
? Array.from(
|
||||
new Set(
|
||||
groupIds
|
||||
.map((value) => Number(value))
|
||||
.filter((value) => Number.isFinite(value) && value > 0)
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const missingGroupIds = [];
|
||||
normalizedGroupIds.forEach((groupId) => {
|
||||
const groupRow = selectGroupById.get(groupId);
|
||||
if (!groupRow) {
|
||||
missingGroupIds.push(groupId);
|
||||
}
|
||||
});
|
||||
|
||||
if (missingGroupIds.length > 0) {
|
||||
const error = new Error('GROUP_NOT_FOUND');
|
||||
error.code = 'GROUP_NOT_FOUND';
|
||||
error.missingGroupIds = missingGroupIds;
|
||||
throw error;
|
||||
}
|
||||
|
||||
applyUserGroupAssignments(numericUserId, normalizedGroupIds);
|
||||
return getUserById(numericUserId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user