Add auth system with invite-only registration and admin roles
JWT-based auth (hono/jwt + bcrypt), anonymous-first flow preserved. Registration requires invite code when REQUIRE_INVITE=true. Admin user seeded on startup (admin/admin, forced password reset). Login accepts email or username. Admin invitations management page in sidebar. Regular users get invite-a-friend button when USER_INVITATIONS > 0. Frontend gate screen blocks game access for unregistered users with invite code entry, registration, login, and password reset flows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { api, getTokenPayload, isRegistered as checkRegistered, needsPasswordReset as checkNeedsReset, setAuthToken } from '@/lib/api';
|
||||
import { ensureAuth } from './useCloudSave';
|
||||
|
||||
interface AuthGateState {
|
||||
loading: boolean;
|
||||
needsInvite: boolean;
|
||||
needsPasswordReset: boolean;
|
||||
registered: boolean;
|
||||
isAdmin: boolean;
|
||||
config: { requireInvite: boolean; userInvitations: number } | null;
|
||||
setRegistered: (value: boolean) => void;
|
||||
setNeedsPasswordReset: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function useAuthGate(): AuthGateState {
|
||||
const [config, setConfig] = useState<{ requireInvite: boolean; userInvitations: number } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [registered, setRegistered] = useState(false);
|
||||
const [passwordReset, setPasswordReset] = useState(false);
|
||||
const [admin, setAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const [cfg] = await Promise.all([
|
||||
api.config.get(),
|
||||
ensureAuth(),
|
||||
]);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setConfig(cfg);
|
||||
|
||||
const payload = getTokenPayload();
|
||||
const reg = checkRegistered();
|
||||
setRegistered(reg);
|
||||
setPasswordReset(checkNeedsReset());
|
||||
setAdmin(payload?.role === 'admin');
|
||||
} catch {
|
||||
// Config fetch failed — allow game to load (fail open)
|
||||
setConfig({ requireInvite: false, userInvitations: 0 });
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const handleSetRegistered = useCallback((value: boolean) => {
|
||||
setRegistered(value);
|
||||
const payload = getTokenPayload();
|
||||
if (payload) {
|
||||
setAdmin(payload.role === 'admin');
|
||||
setPasswordReset(payload.mustResetPassword);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSetPasswordReset = useCallback((value: boolean) => {
|
||||
setPasswordReset(value);
|
||||
}, []);
|
||||
|
||||
const needsInvite = !!(config?.requireInvite && !registered);
|
||||
|
||||
return {
|
||||
loading,
|
||||
needsInvite,
|
||||
needsPasswordReset: passwordReset,
|
||||
registered,
|
||||
isAdmin: admin,
|
||||
config,
|
||||
setRegistered: handleSetRegistered,
|
||||
setNeedsPasswordReset: handleSetPasswordReset,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user