Merge duckie main into live merge branch

This commit is contained in:
Lorenzune
2026-04-25 13:52:11 +02:00
3 changed files with 94 additions and 21 deletions
+47 -1
View File
@@ -57,6 +57,7 @@ export const App: FC<{}> = props =>
const rendererPromiseRef = useRef<Promise<any>>(null); const rendererPromiseRef = useRef<Promise<any>>(null);
const tickersStartedRef = useRef(false); const tickersStartedRef = useRef(false);
const heartbeatIntervalRef = useRef<number>(null); const heartbeatIntervalRef = useRef<number>(null);
const rememberRotateIntervalRef = useRef<number>(null);
const showSessionExpired = useCallback(() => const showSessionExpired = useCallback(() =>
{ {
const baseUrl = window.location.origin + '/'; const baseUrl = window.location.origin + '/';
@@ -135,6 +136,45 @@ export const App: FC<{}> = props =>
return ''; return '';
}, []); }, []);
const rotateRememberLogin = useCallback(async (): Promise<void> =>
{
const remembered = GetRememberLogin();
if(!remembered?.token?.length) return;
try
{
const rawEndpoint = GetConfiguration().getValue<string>('login.refresh.endpoint', '${api.url}/api/auth/refresh');
const endpoint = GetConfiguration().interpolate(rawEndpoint);
const response = await fetch(endpoint, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'NitroRememberRotate'
},
body: JSON.stringify({ rememberToken: remembered.token })
});
let payload: Record<string, unknown> = {};
try { payload = await response.json(); }
catch {}
if(response.ok)
{
StoreRememberLoginFromPayload(payload, remembered.username, remembered.ssoTicket);
return;
}
if(response.status === 400 || response.status === 401 || response.status === 403) ClearRememberLogin();
}
catch(error)
{
NitroLogger.error('[LoginScreen] Remember rotation failed', error);
}
}, []);
// Listen for socket closed events (code 1000 "Bye" - server rejected SSO) // Listen for socket closed events (code 1000 "Bye" - server rejected SSO)
useNitroEvent(NitroEventType.SOCKET_CLOSED, showSessionExpired); useNitroEvent(NitroEventType.SOCKET_CLOSED, showSessionExpired);
@@ -305,6 +345,11 @@ export const App: FC<{}> = props =>
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current); if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000); heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000);
if(rememberRotateIntervalRef.current !== null) window.clearInterval(rememberRotateIntervalRef.current);
const rotateMinutes = Math.max(1, Number(GetConfiguration().getValue<unknown>('login.remember.rotate.interval.minutes', 15)) || 15);
if(GetRememberLogin()?.token?.length) rememberRotateIntervalRef.current = window.setInterval(() => rotateRememberLogin(), rotateMinutes * 60 * 1000);
if(!tickersStartedRef.current) if(!tickersStartedRef.current)
{ {
tickersStartedRef.current = true; tickersStartedRef.current = true;
@@ -330,8 +375,9 @@ export const App: FC<{}> = props =>
return () => return () =>
{ {
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current); if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
if(rememberRotateIntervalRef.current !== null) window.clearInterval(rememberRotateIntervalRef.current);
}; };
}, [ prepareTrigger, startWarmup, startRenderer, tryRememberLogin, applySsoTicket ]); }, [ prepareTrigger, startWarmup, startRenderer, tryRememberLogin, applySsoTicket, rotateRememberLogin ]);
return ( return (
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }> <Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
+26 -3
View File
@@ -7,6 +7,7 @@ export interface RememberLoginData
} }
const REMEMBER_LOGIN_KEY = 'nitro.auth.remember'; const REMEMBER_LOGIN_KEY = 'nitro.auth.remember';
const LEGACY_REMEMBER_LOGIN_KEY = 'nitro.remember.token';
const DEFAULT_REMEMBER_SECONDS = 30 * 24 * 60 * 60; const DEFAULT_REMEMBER_SECONDS = 30 * 24 * 60 * 60;
export const GetRememberLogin = (): RememberLoginData | null => export const GetRememberLogin = (): RememberLoginData | null =>
@@ -26,7 +27,25 @@ export const GetRememberLogin = (): RememberLoginData | null =>
} }
catch catch
{ {
return null; try
{
const legacyToken = window.localStorage.getItem(LEGACY_REMEMBER_LOGIN_KEY) || '';
if(!legacyToken.length) return null;
const data: RememberLoginData = {
token: legacyToken,
expiresAt: Math.floor(Date.now() / 1000) + DEFAULT_REMEMBER_SECONDS
};
SetRememberLogin(data);
return data;
}
catch
{
return null;
}
} }
}; };
@@ -40,14 +59,18 @@ export const SetRememberLogin = (data: RememberLoginData): void =>
export const ClearRememberLogin = (): void => export const ClearRememberLogin = (): void =>
{ {
try { window.localStorage.removeItem(REMEMBER_LOGIN_KEY); } try
{
window.localStorage.removeItem(REMEMBER_LOGIN_KEY);
window.localStorage.removeItem(LEGACY_REMEMBER_LOGIN_KEY);
}
catch {} catch {}
}; };
export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void => export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void =>
{ {
const token = typeof payload.rememberToken === 'string' ? payload.rememberToken : ''; const token = typeof payload.rememberToken === 'string' ? payload.rememberToken : '';
const rawExpiresAt = payload.rememberExpiresAt; const rawExpiresAt = (payload.rememberExpiresAt ?? payload.expiresAt);
const parsedExpiresAt = typeof rawExpiresAt === 'number' ? rawExpiresAt : Number(rawExpiresAt || 0); const parsedExpiresAt = typeof rawExpiresAt === 'number' ? rawExpiresAt : Number(rawExpiresAt || 0);
const expiresAt = (Number.isFinite(parsedExpiresAt) && parsedExpiresAt > 0) const expiresAt = (Number.isFinite(parsedExpiresAt) && parsedExpiresAt > 0)
? parsedExpiresAt ? parsedExpiresAt
@@ -39,6 +39,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
const [ groupName, setGroupName ] = useState<string>(null); const [ groupName, setGroupName ] = useState<string>(null);
const [ isJukeBox, setIsJukeBox ] = useState<boolean>(false); const [ isJukeBox, setIsJukeBox ] = useState<boolean>(false);
const [ isSongDisk, setIsSongDisk ] = useState<boolean>(false); const [ isSongDisk, setIsSongDisk ] = useState<boolean>(false);
const [ isBranded, setIsBranded ] = useState<boolean>(false);
const [ songId, setSongId ] = useState<number>(-1); const [ songId, setSongId ] = useState<number>(-1);
const [ songName, setSongName ] = useState<string>(''); const [ songName, setSongName ] = useState<string>('');
const [ songCreator, setSongCreator ] = useState<string>(''); const [ songCreator, setSongCreator ] = useState<string>('');
@@ -315,6 +316,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
setIsJukeBox(furniIsJukebox); setIsJukeBox(furniIsJukebox);
setIsSongDisk(furniIsSongDisk); setIsSongDisk(furniIsSongDisk);
setSongId(furniSongId); setSongId(furniSongId);
setIsBranded(!!avatarInfo.extraParam && avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS) === 0);
if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false)); if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
}, [ roomSession, avatarInfo ]); }, [ roomSession, avatarInfo ]);
@@ -474,22 +476,24 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
</Flex> </Flex>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" /> <hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</div> </div>
<div className="flex flex-col gap-1"> { !isBranded &&
<Flex gap={ 1 } position="relative"> <div className="flex flex-col gap-1">
{ avatarInfo.stuffData.isUnique && <Flex gap={ 1 } position="relative">
<div className="absolute inset-e-0"> { avatarInfo.stuffData.isUnique &&
<LayoutLimitedEditionCompactPlateView uniqueNumber={ avatarInfo.stuffData.uniqueNumber } uniqueSeries={ avatarInfo.stuffData.uniqueSeries } /> <div className="absolute inset-e-0">
</div> } <LayoutLimitedEditionCompactPlateView uniqueNumber={ avatarInfo.stuffData.uniqueNumber } uniqueSeries={ avatarInfo.stuffData.uniqueSeries } />
{ (avatarInfo.stuffData.rarityLevel > -1) && </div> }
<div className="absolute inset-e-0"> { (avatarInfo.stuffData.rarityLevel > -1) &&
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } /> <div className="absolute inset-e-0">
</div> } <LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
<Flex center fullWidth> </div> }
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } /> <Flex center fullWidth>
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
</Flex>
</Flex> </Flex>
</Flex> <hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" /> </div>
</div> }
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text> <Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" /> <hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
@@ -681,7 +685,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
return ( return (
<Flex key={ index } alignItems="center" gap={ 1 }> <Flex key={ index } alignItems="center" gap={ 1 }>
<Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text> <Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text>
<NitroInput type="text" value={ furniValues[index] } onChange={ event => onFurniSettingChange(index, event.target.value) } /> <NitroInput type="text" className="text-black" style={ { color: '#000' } } value={ furniValues[index] } onChange={ event => onFurniSettingChange(index, event.target.value) } />
</Flex>); </Flex>);
}) } }) }
</div> </div>
@@ -696,7 +700,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
return ( return (
<Flex key={ index } alignItems="center" gap={ 1 }> <Flex key={ index } alignItems="center" gap={ 1 }>
<Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text> <Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text>
<NitroInput type="text" value={ customValues[index] } onChange={ event => onCustomVariableChange(index, event.target.value) } /> <NitroInput type="text" className="text-black" style={ { color: '#000' } } value={ customValues[index] } onChange={ event => onCustomVariableChange(index, event.target.value) } />
</Flex>); </Flex>);
}) } }) }
</div> </div>