mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
🆙 Added ban to the login UI
This commit is contained in:
@@ -3,6 +3,12 @@ import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from
|
|||||||
import { GetConfigurationValue, LocalizeText } from '../../api';
|
import { GetConfigurationValue, LocalizeText } from '../../api';
|
||||||
import { TurnstileWidget } from './TurnstileWidget';
|
import { TurnstileWidget } from './TurnstileWidget';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a localized string. Falls back to `fallback` when the key is
|
||||||
|
* missing (LocalizeText returns the key itself) or when the localization
|
||||||
|
* manager isn't ready yet (login runs very early). Parameters are
|
||||||
|
* %name%-substituted into the fallback so the UI stays correct pre-init.
|
||||||
|
*/
|
||||||
const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string =>
|
const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -10,7 +16,7 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri
|
|||||||
const value = LocalizeText(key, params ?? null, replacements ?? null);
|
const value = LocalizeText(key, params ?? null, replacements ?? null);
|
||||||
if(value && value !== key) return value;
|
if(value && value !== key) return value;
|
||||||
}
|
}
|
||||||
catch {}
|
catch { /* localization manager not initialised yet */ }
|
||||||
|
|
||||||
if(!params || !replacements) return fallback;
|
if(!params || !replacements) return fallback;
|
||||||
let out = fallback;
|
let out = fallback;
|
||||||
@@ -23,6 +29,39 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri
|
|||||||
|
|
||||||
type DialogMode = 'login' | 'register' | 'forgot';
|
type DialogMode = 'login' | 'register' | 'forgot';
|
||||||
|
|
||||||
|
interface BanInfo
|
||||||
|
{
|
||||||
|
type: 'account' | 'ip' | 'machine' | 'super' | string;
|
||||||
|
reason: string;
|
||||||
|
permanent: boolean;
|
||||||
|
expiresAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseBan = (payload: Record<string, unknown>): BanInfo | null =>
|
||||||
|
{
|
||||||
|
const raw = payload?.ban;
|
||||||
|
if(!raw || typeof raw !== 'object') return null;
|
||||||
|
const ban = raw as Record<string, unknown>;
|
||||||
|
const type = typeof ban.type === 'string' ? ban.type : 'account';
|
||||||
|
const reason = typeof ban.reason === 'string' ? ban.reason : '';
|
||||||
|
const permanent = ban.permanent === true || ban.permanent === 'true';
|
||||||
|
const expiresAt = typeof ban.expiresAt === 'number' ? ban.expiresAt : undefined;
|
||||||
|
return { type, reason, permanent, expiresAt };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRemaining = (epochSeconds: number): string =>
|
||||||
|
{
|
||||||
|
const totalSeconds = Math.max(0, epochSeconds - Math.floor(Date.now() / 1000));
|
||||||
|
const days = Math.floor(totalSeconds / 86400);
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
if(days > 0) return `${ days }d ${ hours }h ${ minutes }m`;
|
||||||
|
if(hours > 0) return `${ hours }h ${ minutes }m`;
|
||||||
|
if(minutes > 0) return `${ minutes }m ${ seconds }s`;
|
||||||
|
return `${ seconds }s`;
|
||||||
|
};
|
||||||
|
|
||||||
const interpolate = (value: string | null | undefined): string =>
|
const interpolate = (value: string | null | undefined): string =>
|
||||||
{
|
{
|
||||||
if(!value) return '';
|
if(!value) return '';
|
||||||
@@ -66,6 +105,8 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
const [ password, setPassword ] = useState('');
|
const [ password, setPassword ] = useState('');
|
||||||
const [ rememberMe, setRememberMe ] = useState(false);
|
const [ rememberMe, setRememberMe ] = useState(false);
|
||||||
const [ error, setError ] = useState<string | null>(null);
|
const [ error, setError ] = useState<string | null>(null);
|
||||||
|
const [ banInfo, setBanInfo ] = useState<BanInfo | null>(null);
|
||||||
|
const [ , setBanTick ] = useState(0);
|
||||||
const [ info, setInfo ] = useState<string | null>(null);
|
const [ info, setInfo ] = useState<string | null>(null);
|
||||||
const [ submitting, setSubmitting ] = useState(false);
|
const [ submitting, setSubmitting ] = useState(false);
|
||||||
const [ loginTurnstileToken, setLoginTurnstileToken ] = useState('');
|
const [ loginTurnstileToken, setLoginTurnstileToken ] = useState('');
|
||||||
@@ -103,9 +144,25 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setBanInfo(null);
|
||||||
if(mode === 'login') resetLoginTurnstile();
|
if(mode === 'login') resetLoginTurnstile();
|
||||||
}, [ mode, resetLoginTurnstile ]);
|
}, [ mode, resetLoginTurnstile ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if(!banInfo || banInfo.permanent || !banInfo.expiresAt) return;
|
||||||
|
const interval = window.setInterval(() =>
|
||||||
|
{
|
||||||
|
if(banInfo.expiresAt && banInfo.expiresAt <= Math.floor(Date.now() / 1000))
|
||||||
|
{
|
||||||
|
setBanInfo(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBanTick(t => t + 1);
|
||||||
|
}, 1000);
|
||||||
|
return () => window.clearInterval(interval);
|
||||||
|
}, [ banInfo ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(!info) return;
|
if(!info) return;
|
||||||
@@ -243,6 +300,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setBanInfo(null);
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -277,6 +335,14 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ban = parseBan(payload);
|
||||||
|
if(ban)
|
||||||
|
{
|
||||||
|
setBanInfo(ban);
|
||||||
|
resetLoginTurnstile();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
recordFailure();
|
recordFailure();
|
||||||
const message = typeof payload.error === 'string' ? payload.error : t('nitro.login.error.invalid_credentials', 'Invalid Habbo name or password.');
|
const message = typeof payload.error === 'string' ? payload.error : t('nitro.login.error.invalid_credentials', 'Invalid Habbo name or password.');
|
||||||
setError(message);
|
setError(message);
|
||||||
@@ -503,13 +569,29 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{ banInfo &&
|
||||||
|
<div className="error-line ban-message">
|
||||||
|
<div className="ban-title">
|
||||||
|
{ banInfo.type === 'ip'
|
||||||
|
? t('nitro.login.error.banned.ip.title', 'This connection is banned')
|
||||||
|
: t('nitro.login.error.banned.account.title', 'Your account is banned') }
|
||||||
|
</div>
|
||||||
|
{ banInfo.permanent
|
||||||
|
? <div className="ban-status">{ t('nitro.login.error.banned.permanent', 'This is a permanent ban.') }</div>
|
||||||
|
: (banInfo.expiresAt
|
||||||
|
? <div className="ban-status">{ t('nitro.login.error.banned.temporary', 'You can log in again in %time%.', [ 'time' ], [ formatRemaining(banInfo.expiresAt) ]) }</div>
|
||||||
|
: null) }
|
||||||
|
{ banInfo.reason &&
|
||||||
|
<div className="ban-reason">{ t('nitro.login.error.banned.reason', 'Reason: %reason%', [ 'reason' ], [ banInfo.reason ]) }</div> }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{ error && <div className="error-line">{ error }</div> }
|
{ error && <div className="error-line">{ error }</div> }
|
||||||
{ info && <div className="info-line">{ info }</div> }
|
{ info && <div className="info-line">{ info }</div> }
|
||||||
<div className="submit-row">
|
<div className="submit-row">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ok-button"
|
className="ok-button"
|
||||||
disabled={ submitting || isLocked || loginServerReachable === false || loginPingingServer }
|
disabled={ submitting || isLocked || loginServerReachable === false || loginPingingServer || !!banInfo }
|
||||||
>{ loginPingingServer ? t('nitro.login.server.checking', 'Checking…') : t('login.title', 'Log in') }</button>
|
>{ loginPingingServer ? t('nitro.login.server.checking', 'Checking…') : t('login.title', 'Log in') }</button>
|
||||||
</div>
|
</div>
|
||||||
<a className="forgot" onClick={ () => setMode('forgot') }>{ t('login.forgot_password', 'Forgotten your password?') }</a>
|
<a className="forgot" onClick={ () => setMode('forgot') }>{ t('login.forgot_password', 'Forgotten your password?') }</a>
|
||||||
|
|||||||
@@ -225,6 +225,33 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nitro-login-card .error-line.ban-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nitro-login-card .error-line.ban-message .ban-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nitro-login-card .error-line.ban-message .ban-status {
|
||||||
|
font-size: 11px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nitro-login-card .error-line.ban-message .ban-reason {
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.nitro-login-card .register-card-body a {
|
.nitro-login-card .register-card-body a {
|
||||||
color: #134b6e;
|
color: #134b6e;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
Reference in New Issue
Block a user