mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge duckie main into live merge branch
This commit is contained in:
+47
-1
@@ -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' }>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user