checkpoint: secure assets and login flow baseline

This commit is contained in:
Lorenzune
2026-04-23 07:01:09 +02:00
parent f6096371be
commit 237c523f9a
17 changed files with 3573 additions and 694 deletions
+3 -8
View File
@@ -11,11 +11,9 @@ export const LoadingView: FC<LoadingViewProps> = props => {
const { isError = false, message = '', homeUrl = '' } = props;
return (
<Column fullHeight position="relative" className="relative z-[100] bg-[radial-gradient(#1d1a24,#003a6b)]">
<Column fullHeight position="fixed" className="fixed inset-0 z-[2147483000] bg-[radial-gradient(#1d1a24,#003a6b)]">
<Base fullHeight className="container h-100">
<Column fullHeight alignItems="center" justifyContent="center">
{ !isError &&
<Base className="absolute inset-0 m-auto w-[84px] h-[84px] [zoom:1.5] [image-rendering:pixelated] bg-[url('@/assets/images/loading/loading.gif')] bg-no-repeat bg-left-top" /> }
<Base className="absolute top-[20px] left-[20px] z-[2] w-[150px] h-[100px] bg-[url('@/assets/images/notifications/nitro_v3.png')] bg-no-repeat bg-left-top" />
{ isError && (message && message.length) ?
<Column alignItems="center" className="absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 max-w-[80%]" gap={ 2 }>
@@ -31,13 +29,10 @@ export const LoadingView: FC<LoadingViewProps> = props => {
</a>
}
</Column>
:
<Text fontSizeCustom={32} variant="white" className="absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">
The hotel is loading ...
</Text>
: null
}
</Column>
</Base>
</Column>
);
};
};
+50 -12
View File
@@ -16,6 +16,13 @@ const LOCK_KEY = 'nitro.login.lock';
const MAX_ATTEMPTS = 5;
const LOCK_WINDOW_MS = 60_000;
const LOCK_DURATION_MS = 2 * 60_000;
const DEFAULT_LOGIN_IMAGES: Record<string, string> = {
background: 'https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png',
'background.colour': '#6eadc8',
drape: 'https://hotel.slogga.it/client/nitro/images/reception/drape.png',
left: 'https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png',
right: 'https://hotel.slogga.it/client/nitro/images/reception/background_right.png'
};
type AttemptState = { attempts: number; firstAt: number; lockedUntil: number };
@@ -39,9 +46,10 @@ const writeLock = (state: AttemptState) =>
export interface LoginViewProps
{
onAuthenticated: (ssoTicket: string) => void;
isEntering?: boolean;
}
export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = false }) =>
{
const [ mode, setMode ] = useState<DialogMode>('login');
const [ username, setUsername ] = useState('');
@@ -55,7 +63,8 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
const [ loginPingingServer, setLoginPingingServer ] = useState(false);
const submitTimeRef = useRef(0);
const loginImages: Record<string, string> = ((GetConfigurationValue<Record<string, unknown>>('loginview', {})?.['images']) as Record<string, string>) ?? {};
const configuredLoginImages: Record<string, string> = ((GetConfigurationValue<Record<string, unknown>>('loginview', {})?.['images']) as Record<string, string>) ?? {};
const loginImages: Record<string, string> = { ...DEFAULT_LOGIN_IMAGES, ...configuredLoginImages };
const backgroundColor = (loginImages['background.colour'] || GetConfigurationValue<string>('login_background.colour', '#6eadc8'));
const background = interpolate(loginImages['background'] || GetConfigurationValue<string>('login_background', ''));
@@ -64,6 +73,8 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
const left = interpolate(loginImages['left'] || GetConfigurationValue<string>('login_left', ''));
const rightRepeat = interpolate(loginImages['right.repeat'] || GetConfigurationValue<string>('login_right.repeat', ''));
const right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', ''));
const loginImageUrls = useMemo(() => [ background, sun, drape, left, rightRepeat, right ].filter(Boolean), [ background, sun, drape, left, rightRepeat, right ]);
const [ loginImagesVersion, setLoginImagesVersion ] = useState(0);
const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register');
const forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password');
@@ -86,6 +97,30 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
if(mode === 'login') resetLoginTurnstile();
}, [ mode, resetLoginTurnstile ]);
useEffect(() =>
{
if(!loginImageUrls.length) return;
let cancelled = false;
loginImageUrls.forEach(url =>
{
const image = new Image();
image.onload = image.onerror = () =>
{
if(!cancelled) setLoginImagesVersion(version => version + 1);
};
image.src = url;
});
return () =>
{
cancelled = true;
};
}, [ loginImageUrls ]);
useEffect(() =>
{
if(!info) return;
@@ -138,7 +173,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
return { ok: response.ok, status: response.status, payload };
}, []);
const healthUrl = GetConfigurationValue<string>('login.health.endpoint', '/api/health');
const healthUrl = GetConfigurationValue<string>('login.health.endpoint', '');
const healthMethodRaw = GetConfigurationValue<string>('login.health.method', 'GET');
const healthMethod = (healthMethodRaw || 'GET').toUpperCase();
const checkServerReachable = useCallback(async (): Promise<boolean> =>
@@ -196,7 +231,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
{
event.preventDefault();
if(submitting) return;
if(submitting || isEntering) return;
const nowTs = Date.now();
if(nowTs - submitTimeRef.current < 1000) return;
@@ -263,7 +298,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
{
setSubmitting(false);
}
}, [ submitting, username, password, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile, pingLoginServer ]);
}, [ submitting, isEntering, username, password, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile, pingLoginServer ]);
const checkEmailUrl = GetConfigurationValue<string>('login.check-email.endpoint', '/api/auth/check-email');
const checkUsernameUrl = GetConfigurationValue<string>('login.check-username.endpoint', '/api/auth/check-username');
@@ -409,12 +444,15 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
className="nitro-login-view"
style={ backgroundColor ? { background: backgroundColor } : undefined }
>
{ background ? <div className="login-background login-layer" style={ { backgroundImage: `url(${ background })` } } /> : null }
{ sun ? <div className="login-sun login-layer" style={ { backgroundImage: `url(${ sun })` } } /> : null }
{ drape ? <div className="login-drape login-layer" style={ { backgroundImage: `url(${ drape })` } } /> : null }
{ left ? <div className="login-left login-layer" style={ { backgroundImage: `url(${ left })` } } /> : null }
{ background ? <img className="login-background login-layer login-layer-img" src={ background } alt="" draggable={ false } /> : null }
{ sun ? <img className="login-sun login-layer login-layer-img" src={ sun } alt="" draggable={ false } /> : null }
{ drape ? <img className="login-drape login-layer login-layer-img" src={ drape } alt="" draggable={ false } /> : null }
{ left ? <img className="login-left login-layer login-layer-img" src={ left } alt="" draggable={ false } /> : null }
{ rightRepeat ? <div className="login-right-repeat login-layer" style={ { backgroundImage: `url(${ rightRepeat })` } } /> : null }
{ right ? <div className="login-right login-layer" style={ { backgroundImage: `url(${ right })` } } /> : null }
{ right ? <img className="login-right login-layer login-layer-img" src={ right } alt="" draggable={ false } /> : null }
<div className="login-image-preloader" aria-hidden="true" data-version={ loginImagesVersion }>
{ loginImageUrls.map(url => <img key={ url } src={ url } decoding="async" loading="eager" alt="" />) }
</div>
<div className="login-stack">
<div className="nitro-login-card">
@@ -475,8 +513,8 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
<button
type="submit"
className="ok-button"
disabled={ submitting || isLocked || loginServerReachable === false || loginPingingServer }
>{ loginPingingServer ? 'Checking' : 'OK' }</button>
disabled={ submitting || isEntering || isLocked || loginServerReachable === false || loginPingingServer }
>{ isEntering ? 'Entrando' : loginPingingServer ? 'Checking' : 'OK' }</button>
</div>
<a className="forgot" onClick={ () => setMode('forgot') }>Forgotten your password?</a>
</form>