diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 3dc6331..5abf4be 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -123,9 +123,6 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { const name = `${firstName.trim()} ${lastName.trim()}`; try { const loginType = await getLoginType(req.schema); - if (loginType === 'mixed_age' && !dateOfBirth) - return res.status(400).json({ error: 'Date of birth is required in Mixed Age mode' }); - const dob = dateOfBirth || null; const isMinor = isMinorFromDOB(dob); // In mixed_age mode, minors start suspended and need guardian approval @@ -164,10 +161,6 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => const validRoles = ['member', 'admin', 'manager']; if (!validRoles.includes(role)) return res.status(400).json({ error: 'Invalid role' }); try { - const loginType = await getLoginType(req.schema); - if (loginType === 'mixed_age' && !dateOfBirth) - return res.status(400).json({ error: 'Date of birth is required in Mixed Age mode' }); - 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') @@ -235,12 +228,16 @@ router.post('/bulk', authMiddleware, teamManagerMiddleware, async (req, res) => const newRole = validRoles.includes(u.role) ? u.role : 'member'; const fn = firstName || name.split(' ')[0] || ''; const ln = lastName || name.split(' ').slice(1).join(' ') || ''; + const dob = (u.dateOfBirth || u.dob || '').trim() || null; + const isMinor = isMinorFromDOB(dob); + const loginType = await getLoginType(req.schema); + const initStatus = (loginType === 'mixed_age' && isMinor) ? 'suspended' : 'active'; const r = await queryResult(req.schema, - "INSERT INTO users (name,first_name,last_name,email,password,role,status,must_change_password) VALUES ($1,$2,$3,$4,$5,$6,'active',TRUE) RETURNING id", - [resolvedName, fn, ln, email, hash, newRole] + "INSERT INTO users (name,first_name,last_name,email,password,role,date_of_birth,is_minor,status,must_change_password) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,TRUE) RETURNING id", + [resolvedName, fn, ln, email, hash, newRole, dob, isMinor, initStatus] ); const userId = r.rows[0].id; - await addUserToPublicGroups(req.schema, userId); + if (initStatus === 'active') await addUserToPublicGroups(req.schema, userId); if (newRole === '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]); diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx index 8d83dd6..a92a6f8 100644 --- a/frontend/src/components/ProfileModal.jsx +++ b/frontend/src/components/ProfileModal.jsx @@ -36,8 +36,10 @@ export default function ProfileModal({ onClose }) { const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag); const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0); - // Minor age protection — DOB/phone display only + // Minor age protection — DOB/phone display + mixed_age forced-DOB gate const [loginType, setLoginType] = useState('all_ages'); + // True when mixed_age mode and the user still has no DOB on record + const needsDob = loginType === 'mixed_age' && !user?.date_of_birth; const savedScale = parseFloat(localStorage.getItem(LS_FONT_KEY)); const [fontScale, setFontScale] = useState( @@ -105,6 +107,55 @@ export default function ProfileModal({ onClose }) { } }; + // ── Forced DOB gate for mixed_age users ─────────────────────────────────── + if (needsDob) { + return ( +
+ Your organisation requires a date of birth on file. Please enter yours to continue. +
+CSV Format
-{'FULL: email,firstname,lastname,password,role,usergroup'}
- {'MINIMUM: email,firstname,lastname,,,'}
+ {'FULL: email,firstname,lastname,dob,password,role,usergroup'}
+ {'MINIMUM: email,firstname,lastname,,,,'}
Examples:
-{'example@rosterchirp.com,Barney,Rubble,,member,parents'}
- {'example@rosterchirp.com,Barney,Rubble,Ori0n2026!,member,players'}
+ {'example@rosterchirp.com,Barney,Rubble,1970-11-21,,member,parents'}
+ {'example@rosterchirp.com,Barney,Rubble,2013-06-11,Ori0n2026!,member,players'}
Blank password defaults to {userPass}. Blank role defaults to member. We recommend using a spreadsheet editor and saving as CSV.
@@ -455,8 +457,8 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {CSV Requirements
email, firstname, lastname are requiredemail, firstname, lastname are required fields{loginType === 'mixed_age' ? <> (DOB field required for Restricted login type)> : ''}.