🆙 Small fix Login

This commit is contained in:
duckietm
2026-04-20 15:19:51 +02:00
parent 2de52235eb
commit 675b864c51
2 changed files with 60 additions and 17 deletions
+10 -10
View File
@@ -1,10 +1,10 @@
{ {
"socket.url": "ws://192.168.1.52:2096", "socket.url": "ws://localhost:2096",
"asset.url": "https://client.slogga.it/nitro/bundled", "asset.url": "https://localhost/nitro/bundled",
"image.library.url": "https://client.slogga.it/c_images/", "image.library.url": "https://localhost/c_images/",
"hof.furni.url": "https://client.slogga.it/c_images/dcr/hof_furni", "hof.furni.url": "https://localhost/c_images/dcr/hof_furni",
"images.url": "https://client.slogga.it/nitro/images", "images.url": "https://localhost/nitro/images",
"gamedata.url": "https://client.slogga.it/nitro/gamedata", "gamedata.url": "https://localhost/nitro/gamedata",
"sounds.url": "${asset.url}/sounds/%sample%.mp3", "sounds.url": "${asset.url}/sounds/%sample%.mp3",
"external.texts.url": [ "external.texts.url": [
"${gamedata.url}/ExternalTexts.json", "${gamedata.url}/ExternalTexts.json",
@@ -40,10 +40,10 @@
"room.landscapes.enabled": true, "room.landscapes.enabled": true,
"room.zoom.enabled": true, "room.zoom.enabled": true,
"login.screen.enabled": false, "login.screen.enabled": false,
"login.endpoint": "https://websocket.yourdomain.com/api/auth/login", "login.endpoint": "${socket.url}/api/auth/login",
"login.register.endpoint": "https://websocket.yourdomain.com/api/auth/register", "login.register.endpoint": "${socket.url}/api/auth/register",
"login.forgot.endpoint": "https://websocket.yourdomain.com/api/auth/forgot-password", "login.forgot.endpoint": "${socket.url}/api/auth/forgot-password",
"login.logout.endpoint": "https://websocket.yourdomain.com/api/auth/logout", "login.logout.endpoint": "${socket.url}/api/auth/logout",
"login.turnstile.enabled": false, "login.turnstile.enabled": false,
"login.turnstile.sitekey": "", "login.turnstile.sitekey": "",
"avatar.mandatory.libraries": [ "avatar.mandatory.libraries": [
+50 -7
View File
@@ -14,8 +14,8 @@ const interpolate = (value: string | null | undefined): string =>
const LOCK_KEY = 'nitro.login.lock'; const LOCK_KEY = 'nitro.login.lock';
const MAX_ATTEMPTS = 5; const MAX_ATTEMPTS = 5;
const LOCK_WINDOW_MS = 60_000; const LOCK_WINDOW_MS = 60_000; // rolling 60s window
const LOCK_DURATION_MS = 2 * 60_000; const LOCK_DURATION_MS = 2 * 60_000; // 2 minute lockout
type AttemptState = { attempts: number; firstAt: number; lockedUntil: number }; type AttemptState = { attempts: number; firstAt: number; lockedUntil: number };
@@ -33,7 +33,7 @@ const readLock = (): AttemptState =>
const writeLock = (state: AttemptState) => const writeLock = (state: AttemptState) =>
{ {
try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); } try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); }
catch { } catch { /* ignore */ }
}; };
export interface LoginViewProps export interface LoginViewProps
@@ -63,6 +63,21 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
const rightRepeat = interpolate(loginImages['right.repeat'] || GetConfigurationValue<string>('login_right.repeat', '')); const rightRepeat = interpolate(loginImages['right.repeat'] || GetConfigurationValue<string>('login_right.repeat', ''));
const right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', '')); const right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', ''));
useEffect(() =>
{
// eslint-disable-next-line no-console
console.info('[LoginView] resolved background assets', {
'asset.url': GetConfigurationValue<string>('asset.url', ''),
login_background: background,
'login_background.colour': backgroundColor,
login_sun: sun,
login_drape: drape,
login_left: left,
login_right: right,
'login_right.repeat': rightRepeat
});
}, [ background, backgroundColor, sun, drape, left, right, rightRepeat ]);
const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login'); const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register'); const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register');
const forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password'); const forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password');
@@ -73,19 +88,40 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|| rawTurnstileEnabled === 1 || rawTurnstileEnabled === 1
|| rawTurnstileEnabled === '1') && !!turnstileSiteKey; || rawTurnstileEnabled === '1') && !!turnstileSiteKey;
useEffect(() =>
{
// eslint-disable-next-line no-console
console.info('[LoginView] turnstile config', {
rawTurnstileEnabled,
turnstileEnabled,
turnstileSiteKey: turnstileSiteKey ? (turnstileSiteKey.slice(0, 6) + '…') : '(empty)'
});
}, [ rawTurnstileEnabled, turnstileEnabled, turnstileSiteKey ]);
const resetLoginTurnstile = useCallback(() => const resetLoginTurnstile = useCallback(() =>
{ {
setLoginTurnstileToken(''); setLoginTurnstileToken('');
setLoginTurnstileResetSignal(prev => prev + 1); setLoginTurnstileResetSignal(prev => prev + 1);
}, []); }, []);
// Clear error on mode change but keep the success notification so users
// returning to the login form can read it (e.g. "Account created").
// Reset the login captcha only when we're actually on the login form.
useEffect(() => useEffect(() =>
{ {
setError(null); setError(null);
setInfo(null);
if(mode === 'login') resetLoginTurnstile(); if(mode === 'login') resetLoginTurnstile();
}, [ mode, resetLoginTurnstile ]); }, [ mode, resetLoginTurnstile ]);
// Auto-dismiss the info notification after a few seconds so it doesn't
// hang around forever once the user has seen it.
useEffect(() =>
{
if(!info) return;
const timeout = window.setTimeout(() => setInfo(null), 8000);
return () => window.clearTimeout(timeout);
}, [ info ]);
const lockState = useMemo(() => readLock(), [ submitting ]); const lockState = useMemo(() => readLock(), [ submitting ]);
const now = Date.now(); const now = Date.now();
const isLocked = lockState.lockedUntil > now; const isLocked = lockState.lockedUntil > now;
@@ -126,7 +162,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
let payload: Record<string, unknown> = {}; let payload: Record<string, unknown> = {};
try { payload = await response.json(); } try { payload = await response.json(); }
catch { } catch { /* ignore non-json responses */ }
return { ok: response.ok, status: response.status, payload }; return { ok: response.ok, status: response.status, payload };
}, []); }, []);
@@ -198,6 +234,11 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
} }
}, [ submitting, username, password, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile ]); }, [ submitting, username, password, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile ]);
// Register + forgot-password submit handlers receive the Turnstile token
// from the dialog (the dialog owns its own widget lifecycle), so the
// login widget underneath can't reset or overwrite it while the user
// is working on the modal.
const handleRegisterSubmit = useCallback(async (body: { username: string; email: string; password: string; turnstileToken: string; }, onDialogReset: () => void) => const handleRegisterSubmit = useCallback(async (body: { username: string; email: string; password: string; turnstileToken: string; }, onDialogReset: () => void) =>
{ {
if(turnstileEnabled && !body.turnstileToken) if(turnstileEnabled && !body.turnstileToken)
@@ -221,7 +262,8 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
if(ok) if(ok)
{ {
setInfo(typeof payload.message === 'string' ? payload.message : 'Account created. You can now log in.'); const friendly = `Welcome aboard, ${ body.username }! Your account is ready — log in below with the password you just chose.`;
setInfo(typeof payload.message === 'string' ? payload.message : friendly);
setMode('login'); setMode('login');
setUsername(body.username); setUsername(body.username);
setPassword(''); setPassword('');
@@ -263,7 +305,8 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
if(ok) if(ok)
{ {
setInfo(typeof payload.message === 'string' ? payload.message : 'If an account exists we just sent a reset link to your email.'); const friendly = 'Email sent! If an account matches that address you\'ll find a reset link in your inbox shortly (check spam if it doesn\'t show up within a minute).';
setInfo(typeof payload.message === 'string' ? payload.message : friendly);
setMode('login'); setMode('login');
return; return;
} }