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 tickersStartedRef = useRef(false);
const heartbeatIntervalRef = useRef<number>(null);
const rememberRotateIntervalRef = useRef<number>(null);
const showSessionExpired = useCallback(() =>
{
const baseUrl = window.location.origin + '/';
@@ -135,6 +136,45 @@ export const App: FC<{}> = props =>
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)
useNitroEvent(NitroEventType.SOCKET_CLOSED, showSessionExpired);
@@ -305,6 +345,11 @@ export const App: FC<{}> = props =>
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
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)
{
tickersStartedRef.current = true;
@@ -330,8 +375,9 @@ export const App: FC<{}> = props =>
return () =>
{
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 (
<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 LEGACY_REMEMBER_LOGIN_KEY = 'nitro.remember.token';
const DEFAULT_REMEMBER_SECONDS = 30 * 24 * 60 * 60;
export const GetRememberLogin = (): RememberLoginData | null =>
@@ -26,7 +27,25 @@ export const GetRememberLogin = (): RememberLoginData | null =>
}
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 =>
{
try { window.localStorage.removeItem(REMEMBER_LOGIN_KEY); }
try
{
window.localStorage.removeItem(REMEMBER_LOGIN_KEY);
window.localStorage.removeItem(LEGACY_REMEMBER_LOGIN_KEY);
}
catch {}
};
export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void =>
{
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 expiresAt = (Number.isFinite(parsedExpiresAt) && parsedExpiresAt > 0)
? parsedExpiresAt
@@ -39,6 +39,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
const [ groupName, setGroupName ] = useState<string>(null);
const [ isJukeBox, setIsJukeBox ] = useState<boolean>(false);
const [ isSongDisk, setIsSongDisk ] = useState<boolean>(false);
const [ isBranded, setIsBranded ] = useState<boolean>(false);
const [ songId, setSongId ] = useState<number>(-1);
const [ songName, setSongName ] = useState<string>('');
const [ songCreator, setSongCreator ] = useState<string>('');
@@ -315,6 +316,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
setIsJukeBox(furniIsJukebox);
setIsSongDisk(furniIsSongDisk);
setSongId(furniSongId);
setIsBranded(!!avatarInfo.extraParam && avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS) === 0);
if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
}, [ roomSession, avatarInfo ]);
@@ -474,22 +476,24 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
</Flex>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<Flex gap={ 1 } position="relative">
{ avatarInfo.stuffData.isUnique &&
<div className="absolute inset-e-0">
<LayoutLimitedEditionCompactPlateView uniqueNumber={ avatarInfo.stuffData.uniqueNumber } uniqueSeries={ avatarInfo.stuffData.uniqueSeries } />
</div> }
{ (avatarInfo.stuffData.rarityLevel > -1) &&
<div className="absolute inset-e-0">
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
</div> }
<Flex center fullWidth>
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
{ !isBranded &&
<div className="flex flex-col gap-1">
<Flex gap={ 1 } position="relative">
{ avatarInfo.stuffData.isUnique &&
<div className="absolute inset-e-0">
<LayoutLimitedEditionCompactPlateView uniqueNumber={ avatarInfo.stuffData.uniqueNumber } uniqueSeries={ avatarInfo.stuffData.uniqueSeries } />
</div> }
{ (avatarInfo.stuffData.rarityLevel > -1) &&
<div className="absolute inset-e-0">
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
</div> }
<Flex center fullWidth>
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
</Flex>
</Flex>
</Flex>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</div>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</div>
}
<div className="flex flex-col gap-1">
<Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
@@ -681,7 +685,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
return (
<Flex key={ index } alignItems="center" gap={ 1 }>
<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>);
}) }
</div>
@@ -696,7 +700,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
return (
<Flex key={ index } alignItems="center" gap={ 1 }>
<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>);
}) }
</div>