v0.12.53 Restricted Login type rule changes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-backend",
|
"name": "rosterchirp-backend",
|
||||||
"version": "0.12.52",
|
"version": "0.12.53",
|
||||||
"description": "RosterChirp backend server",
|
"description": "RosterChirp backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -192,6 +192,18 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
|
|||||||
const sgId = await getOrCreateSupportGroup(req.schema);
|
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]);
|
if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, id]);
|
||||||
}
|
}
|
||||||
|
// Auto-unsuspend minor in players group if both guardian and DOB are now set
|
||||||
|
if (isMinor && guardianId && dob && target.status === 'suspended') {
|
||||||
|
const playersRow = await queryOne(req.schema, "SELECT value FROM settings WHERE key='feature_players_group_id'");
|
||||||
|
const playersGroupId = parseInt(playersRow?.value);
|
||||||
|
if (playersGroupId) {
|
||||||
|
const inPlayers = await queryOne(req.schema, 'SELECT 1 FROM user_group_members WHERE user_id=$1 AND user_group_id=$2', [id, playersGroupId]);
|
||||||
|
if (inPlayers) {
|
||||||
|
await exec(req.schema, "UPDATE users SET status='active',updated_at=NOW() WHERE id=$1", [id]);
|
||||||
|
await addUserToPublicGroups(req.schema, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const user = await queryOne(req.schema,
|
const user = await queryOne(req.schema,
|
||||||
'SELECT id,name,first_name,last_name,phone,is_minor,date_of_birth,guardian_user_id,guardian_approval_required,email,role,status,must_change_password,last_online,created_at FROM users WHERE id=$1',
|
'SELECT id,name,first_name,last_name,phone,is_minor,date_of_birth,guardian_user_id,guardian_approval_required,email,role,status,must_change_password,last_online,created_at FROM users WHERE id=$1',
|
||||||
[id]
|
[id]
|
||||||
@@ -713,19 +725,25 @@ router.get('/minor-players', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Claim minor as guardian (Mixed Age — Family Manager direct link, no approval needed)
|
// Claim minor as guardian (Mixed Age — Family Manager direct link, no approval needed)
|
||||||
|
// dateOfBirth is required to activate the minor — without it the guardian is saved but the account stays suspended.
|
||||||
router.post('/me/guardian-children/:minorId', authMiddleware, async (req, res) => {
|
router.post('/me/guardian-children/:minorId', authMiddleware, async (req, res) => {
|
||||||
const minorId = parseInt(req.params.minorId);
|
const minorId = parseInt(req.params.minorId);
|
||||||
|
const { dateOfBirth } = req.body;
|
||||||
try {
|
try {
|
||||||
const minor = await queryOne(req.schema, "SELECT * FROM users WHERE id=$1 AND status!='deleted'", [minorId]);
|
const minor = await queryOne(req.schema, "SELECT * FROM users WHERE id=$1 AND status!='deleted'", [minorId]);
|
||||||
if (!minor) return res.status(404).json({ error: 'User not found' });
|
if (!minor) return res.status(404).json({ error: 'User not found' });
|
||||||
if (!minor.is_minor) return res.status(400).json({ error: 'User is not a minor' });
|
if (!minor.is_minor) return res.status(400).json({ error: 'User is not a minor' });
|
||||||
if (minor.guardian_user_id && minor.guardian_user_id !== req.user.id)
|
if (minor.guardian_user_id && minor.guardian_user_id !== req.user.id)
|
||||||
return res.status(409).json({ error: 'This minor already has a guardian' });
|
return res.status(409).json({ error: 'This minor already has a guardian' });
|
||||||
|
const dob = dateOfBirth || minor.date_of_birth || null;
|
||||||
|
const isMinor = dob ? isMinorFromDOB(dob) : minor.is_minor;
|
||||||
|
const shouldActivate = !!dob;
|
||||||
|
const newStatus = shouldActivate ? 'active' : 'suspended';
|
||||||
await exec(req.schema,
|
await exec(req.schema,
|
||||||
"UPDATE users SET guardian_user_id=$1,guardian_approval_required=FALSE,status='active',updated_at=NOW() WHERE id=$2",
|
'UPDATE users SET guardian_user_id=$1,guardian_approval_required=FALSE,date_of_birth=$2,is_minor=$3,status=$4,updated_at=NOW() WHERE id=$5',
|
||||||
[req.user.id, minorId]
|
[req.user.id, dob, isMinor, newStatus, minorId]
|
||||||
);
|
);
|
||||||
await addUserToPublicGroups(req.schema, minorId);
|
if (shouldActivate) await addUserToPublicGroups(req.schema, minorId);
|
||||||
const user = await queryOne(req.schema,
|
const user = await queryOne(req.schema,
|
||||||
'SELECT id,name,first_name,last_name,date_of_birth,avatar,status,guardian_user_id FROM users WHERE id=$1',
|
'SELECT id,name,first_name,last_name,date_of_birth,avatar,status,guardian_user_id FROM users WHERE id=$1',
|
||||||
[minorId]
|
[minorId]
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.12.52}"
|
VERSION="${1:-0.12.53}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="rosterchirp"
|
IMAGE_NAME="rosterchirp"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-frontend",
|
"name": "rosterchirp-frontend",
|
||||||
"version": "0.12.52",
|
"version": "0.12.53",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function AddChildAliasModal({ features = {}, onClose }) {
|
|||||||
// ── Mixed-age state (real minor users) ────────────────────────────────────
|
// ── Mixed-age state (real minor users) ────────────────────────────────────
|
||||||
const [minorPlayers, setMinorPlayers] = useState([]); // available + already-mine
|
const [minorPlayers, setMinorPlayers] = useState([]); // available + already-mine
|
||||||
const [selectedMinorId, setSelectedMinorId] = useState('');
|
const [selectedMinorId, setSelectedMinorId] = useState('');
|
||||||
|
const [childDob, setChildDob] = useState('');
|
||||||
const [addingMinor, setAddingMinor] = useState(false);
|
const [addingMinor, setAddingMinor] = useState(false);
|
||||||
|
|
||||||
// ── Partner state (shared) ────────────────────────────────────────────────
|
// ── Partner state (shared) ────────────────────────────────────────────────
|
||||||
@@ -49,6 +50,13 @@ export default function AddChildAliasModal({ features = {}, onClose }) {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, [isMixedAge]);
|
}, [isMixedAge]);
|
||||||
|
|
||||||
|
// Pre-populate DOB when a minor is selected from the dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedMinorId) { setChildDob(''); return; }
|
||||||
|
const minor = availableMinors.find(u => u.id === parseInt(selectedMinorId));
|
||||||
|
setChildDob(minor?.date_of_birth ? minor.date_of_birth.slice(0, 10) : '');
|
||||||
|
}, [selectedMinorId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
const set = k => e => setForm(p => ({ ...p, [k]: e.target.value }));
|
const set = k => e => setForm(p => ({ ...p, [k]: e.target.value }));
|
||||||
|
|
||||||
@@ -164,12 +172,14 @@ export default function AddChildAliasModal({ features = {}, onClose }) {
|
|||||||
|
|
||||||
const handleAddMinor = async () => {
|
const handleAddMinor = async () => {
|
||||||
if (!selectedMinorId) return;
|
if (!selectedMinorId) return;
|
||||||
|
if (!childDob.trim()) return toast('Date of Birth is required', 'error');
|
||||||
setAddingMinor(true);
|
setAddingMinor(true);
|
||||||
try {
|
try {
|
||||||
await api.addGuardianChild(parseInt(selectedMinorId));
|
await api.addGuardianChild(parseInt(selectedMinorId), childDob.trim());
|
||||||
const { users: fresh } = await api.getMinorPlayers();
|
const { users: fresh } = await api.getMinorPlayers();
|
||||||
setMinorPlayers(fresh || []);
|
setMinorPlayers(fresh || []);
|
||||||
setSelectedMinorId('');
|
setSelectedMinorId('');
|
||||||
|
setChildDob('');
|
||||||
toast('Child added and account activated', 'success');
|
toast('Child added and account activated', 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e.message, 'error');
|
toast(e.message, 'error');
|
||||||
@@ -282,24 +292,36 @@ export default function AddChildAliasModal({ features = {}, onClose }) {
|
|||||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||||
Add Child
|
Add Child
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
|
||||||
<select
|
<select
|
||||||
className="input"
|
className="input"
|
||||||
style={{ flex: 1 }}
|
style={{ marginBottom: 8 }}
|
||||||
value={selectedMinorId}
|
value={selectedMinorId}
|
||||||
onChange={e => setSelectedMinorId(e.target.value)}
|
onChange={e => setSelectedMinorId(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">— Select a player —</option>
|
<option value="">— Select a player —</option>
|
||||||
{availableMinors.map(u => (
|
{availableMinors.map(u => (
|
||||||
<option key={u.id} value={u.id}>
|
<option key={u.id} value={u.id}>
|
||||||
{u.first_name} {u.last_name}{u.date_of_birth ? ` (${u.date_of_birth.slice(0, 10)})` : ''}
|
{u.first_name} {u.last_name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
{lbl('Date of Birth', true)}
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="YYYY-MM-DD"
|
||||||
|
value={childDob}
|
||||||
|
onChange={e => setChildDob(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
style={childDob === '' && selectedMinorId ? { borderColor: 'var(--error)' } : {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleAddMinor}
|
onClick={handleAddMinor}
|
||||||
disabled={addingMinor || !selectedMinorId}
|
disabled={addingMinor || !selectedMinorId || !childDob.trim()}
|
||||||
style={{ whiteSpace: 'nowrap' }}
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
>
|
>
|
||||||
{addingMinor ? 'Adding…' : 'Add'}
|
{addingMinor ? 'Adding…' : 'Add'}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const api = {
|
|||||||
},
|
},
|
||||||
searchMinorUsers: (q) => req('GET', `/users/search-minors?q=${encodeURIComponent(q || '')}`),
|
searchMinorUsers: (q) => req('GET', `/users/search-minors?q=${encodeURIComponent(q || '')}`),
|
||||||
getMinorPlayers: () => req('GET', '/users/minor-players'),
|
getMinorPlayers: () => req('GET', '/users/minor-players'),
|
||||||
addGuardianChild: (minorId) => req('POST', `/users/me/guardian-children/${minorId}`),
|
addGuardianChild: (minorId, dateOfBirth) => req('POST', `/users/me/guardian-children/${minorId}`, { dateOfBirth: dateOfBirth || null }),
|
||||||
removeGuardianChild: (minorId) => req('DELETE', `/users/me/guardian-children/${minorId}`),
|
removeGuardianChild: (minorId) => req('DELETE', `/users/me/guardian-children/${minorId}`),
|
||||||
approveGuardian: (id) => req('PATCH', `/users/${id}/approve-guardian`),
|
approveGuardian: (id) => req('PATCH', `/users/${id}/approve-guardian`),
|
||||||
denyGuardian: (id) => req('PATCH', `/users/${id}/deny-guardian`),
|
denyGuardian: (id) => req('PATCH', `/users/${id}/deny-guardian`),
|
||||||
|
|||||||
Reference in New Issue
Block a user