v0.12.12 user manager update
This commit is contained in:
17
backend/src/models/migrations/009_user_profile_fields.sql
Normal file
17
backend/src/models/migrations/009_user_profile_fields.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Migration 009: Extended user profile fields
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_minor BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Back-fill first_name / last_name from existing combined name for non-deleted users
|
||||
UPDATE users
|
||||
SET
|
||||
first_name = SPLIT_PART(TRIM(name), ' ', 1),
|
||||
last_name = CASE
|
||||
WHEN POSITION(' ' IN TRIM(name)) > 0
|
||||
THEN NULLIF(TRIM(SUBSTR(TRIM(name), POSITION(' ' IN TRIM(name)) + 1)), '')
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE first_name IS NULL
|
||||
AND TRIM(name) NOT IN ('Deleted User', '');
|
||||
@@ -33,7 +33,7 @@ function isValidEmail(e) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); }
|
||||
router.get('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||
try {
|
||||
const users = await query(req.schema,
|
||||
"SELECT id,name,email,role,status,is_default_admin,must_change_password,avatar,about_me,display_name,allow_dm,created_at,last_online FROM users WHERE status != 'deleted' ORDER BY created_at ASC"
|
||||
"SELECT id,name,first_name,last_name,phone,is_minor,email,role,status,is_default_admin,must_change_password,avatar,about_me,display_name,allow_dm,created_at,last_online FROM users WHERE status != 'deleted' ORDER BY name ASC"
|
||||
);
|
||||
res.json({ users });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
@@ -82,26 +82,66 @@ router.get('/check-display-name', authMiddleware, async (req, res) => {
|
||||
|
||||
// Create user
|
||||
router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||
const { name, email, password, role } = req.body;
|
||||
if (!name || !email) return res.status(400).json({ error: 'Name and email required' });
|
||||
if (!isValidEmail(email)) return res.status(400).json({ error: 'Invalid email address' });
|
||||
const { firstName, lastName, email, password, role, phone, isMinor } = req.body;
|
||||
if (!firstName?.trim() || !lastName?.trim() || !email)
|
||||
return res.status(400).json({ error: 'First name, last name and email required' });
|
||||
if (!isValidEmail(email.trim())) return res.status(400).json({ error: 'Invalid email address' });
|
||||
const validRoles = ['member', 'admin', 'manager'];
|
||||
const assignedRole = validRoles.includes(role) ? role : 'member';
|
||||
const name = `${firstName.trim()} ${lastName.trim()}`;
|
||||
try {
|
||||
const exists = await queryOne(req.schema, "SELECT id FROM users WHERE email = $1 AND status != 'deleted'", [email]);
|
||||
const exists = await queryOne(req.schema, "SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND status != 'deleted'", [email.trim()]);
|
||||
if (exists) return res.status(400).json({ error: 'Email already in use' });
|
||||
const resolvedName = await resolveUniqueName(req.schema, name.trim());
|
||||
const resolvedName = await resolveUniqueName(req.schema, name);
|
||||
const pw = (password || '').trim() || process.env.USER_PASS || 'user@1234';
|
||||
const hash = bcrypt.hashSync(pw, 10);
|
||||
const r = await queryResult(req.schema,
|
||||
"INSERT INTO users (name,email,password,role,status,must_change_password) VALUES ($1,$2,$3,$4,'active',TRUE) RETURNING id",
|
||||
[resolvedName, email, hash, role === 'admin' ? 'admin' : 'member']
|
||||
"INSERT INTO users (name,first_name,last_name,email,password,role,phone,is_minor,status,must_change_password) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'active',TRUE) RETURNING id",
|
||||
[resolvedName, firstName.trim(), lastName.trim(), email.trim().toLowerCase(), hash, assignedRole, phone?.trim() || null, !!isMinor]
|
||||
);
|
||||
const userId = r.rows[0].id;
|
||||
await addUserToPublicGroups(req.schema, userId);
|
||||
if (role === 'admin') {
|
||||
if (assignedRole === 'admin') {
|
||||
const sgId = await getOrCreateSupportGroup(req.schema);
|
||||
if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, userId]);
|
||||
}
|
||||
const user = await queryOne(req.schema, 'SELECT id,name,email,role,status,must_change_password,created_at FROM users WHERE id=$1', [userId]);
|
||||
const user = await queryOne(req.schema, 'SELECT id,name,first_name,last_name,phone,is_minor,email,role,status,must_change_password,created_at FROM users WHERE id=$1', [userId]);
|
||||
res.json({ user });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Update user (general — name components, phone, is_minor, role, optional password reset)
|
||||
router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).json({ error: 'Invalid user ID' });
|
||||
const { firstName, lastName, phone, isMinor, role, password } = req.body;
|
||||
if (!firstName?.trim() || !lastName?.trim())
|
||||
return res.status(400).json({ error: 'First and last name required' });
|
||||
const validRoles = ['member', 'admin', 'manager'];
|
||||
if (!validRoles.includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
||||
try {
|
||||
const target = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [id]);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
if (target.is_default_admin && role !== 'admin')
|
||||
return res.status(403).json({ error: 'Cannot change default admin role' });
|
||||
const name = `${firstName.trim()} ${lastName.trim()}`;
|
||||
const resolvedName = await resolveUniqueName(req.schema, name, id);
|
||||
await exec(req.schema,
|
||||
'UPDATE users SET name=$1,first_name=$2,last_name=$3,phone=$4,is_minor=$5,role=$6,updated_at=NOW() WHERE id=$7',
|
||||
[resolvedName, firstName.trim(), lastName.trim(), phone?.trim() || null, !!isMinor, role, id]
|
||||
);
|
||||
if (password && password.length >= 6) {
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
await exec(req.schema, 'UPDATE users SET password=$1,must_change_password=TRUE,updated_at=NOW() WHERE id=$2', [hash, id]);
|
||||
}
|
||||
if (role === 'admin' && target.role !== 'admin') {
|
||||
const sgId = await getOrCreateSupportGroup(req.schema);
|
||||
if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, id]);
|
||||
}
|
||||
const user = await queryOne(req.schema,
|
||||
'SELECT id,name,first_name,last_name,phone,is_minor,email,role,status,must_change_password,last_online,created_at FROM users WHERE id=$1',
|
||||
[id]
|
||||
);
|
||||
res.json({ user });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
@@ -159,7 +199,7 @@ router.patch('/:id/name', authMiddleware, teamManagerMiddleware, async (req, res
|
||||
// Patch role
|
||||
router.patch('/:id/role', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||
const { role } = req.body;
|
||||
if (!['member','admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
||||
if (!['member','admin','manager'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
||||
try {
|
||||
const target = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
@@ -216,6 +256,10 @@ router.delete('/:id', authMiddleware, teamManagerMiddleware, async (req,
|
||||
status = 'deleted',
|
||||
email = $1,
|
||||
name = 'Deleted User',
|
||||
first_name = NULL,
|
||||
last_name = NULL,
|
||||
phone = NULL,
|
||||
is_minor = FALSE,
|
||||
display_name = NULL,
|
||||
avatar = NULL,
|
||||
about_me = NULL,
|
||||
|
||||
Reference in New Issue
Block a user