Skip to content

Commit 98a7ce2

Browse files
committed
Masked Session, access token display and usage for login status.
1 parent 6ea129e commit 98a7ce2

File tree

3 files changed

+152
-3
lines changed

3 files changed

+152
-3
lines changed

src/routes/(protected)/user/+layout.server.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ const logger = createLogger('UserPageServer');
33
import type { RequestEvent } from '@sveltejs/kit';
44
import { obpIntegrationService } from '$lib/opey/services/OBPIntegrationService';
55
import { env } from '$env/dynamic/private';
6+
import { jwtDecode } from 'jwt-decode';
7+
import type { OAuth2AccessTokenPayload } from '$lib/oauth/types';
8+
9+
function maskString(value: string, visibleChars = 4): string {
10+
if (value.length <= visibleChars * 2) return '****';
11+
return `${value.slice(0, visibleChars)}...${value.slice(-visibleChars)}`;
12+
}
13+
14+
function decodeTokenTimes(token: string): { issuedAt: string | null; expiresAt: string | null; expiresInSeconds: number | null } {
15+
try {
16+
const payload = jwtDecode<OAuth2AccessTokenPayload>(token);
17+
const now = Date.now();
18+
return {
19+
issuedAt: payload.iat ? new Date(payload.iat * 1000).toISOString() : null,
20+
expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : null,
21+
expiresInSeconds: payload.exp ? Math.round((payload.exp * 1000 - now) / 1000) : null,
22+
};
23+
} catch {
24+
return { issuedAt: null, expiresAt: null, expiresInSeconds: null };
25+
}
26+
}
627

728
export async function load(event: RequestEvent) {
829
const session = event.locals.session;
@@ -43,8 +64,31 @@ export async function load(event: RequestEvent) {
4364
}
4465
}
4566

67+
// Build masked session info for the profile page
68+
const oauth = session?.data?.oauth;
69+
const accessToken = oauth?.access_token;
70+
const refreshToken = oauth?.refresh_token;
71+
72+
const accessTokenTimes = accessToken ? decodeTokenTimes(accessToken) : null;
73+
const refreshTokenTimes = refreshToken ? decodeTokenTimes(refreshToken) : null;
74+
75+
const sessionInfo = {
76+
sessionId: session?.id ? maskString(session.id) : null,
77+
oauthProvider: oauth?.provider || null,
78+
hasAccessToken: !!accessToken,
79+
accessTokenPreview: accessToken ? maskString(accessToken) : null,
80+
accessTokenIssuedAt: accessTokenTimes?.issuedAt || null,
81+
accessTokenExpiresAt: accessTokenTimes?.expiresAt || null,
82+
accessTokenExpiresInSeconds: accessTokenTimes?.expiresInSeconds ?? null,
83+
hasRefreshToken: !!refreshToken,
84+
refreshTokenPreview: refreshToken ? maskString(refreshToken) : null,
85+
refreshTokenExpiresAt: refreshTokenTimes?.expiresAt || null,
86+
refreshTokenExpiresInSeconds: refreshTokenTimes?.expiresInSeconds ?? null,
87+
};
88+
4689
return {
4790
userData: userData || null,
48-
opeyConsentInfo
91+
opeyConsentInfo,
92+
sessionInfo
4993
};
5094
}

src/routes/(protected)/user/+page.svelte

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
console.debug("USER DATA:", JSON.stringify(userData));
1515
1616
const opeyConsentInfo = data.opeyConsentInfo || null;
17+
const sessionInfo = data.sessionInfo || null;
18+
19+
function formatTimeRemaining(seconds: number | null): string {
20+
if (seconds === null) return "Unknown";
21+
if (seconds <= 0) return "Expired";
22+
const hours = Math.floor(seconds / 3600);
23+
const minutes = Math.floor((seconds % 3600) / 60);
24+
if (hours > 0) return `${hours}h ${minutes}m`;
25+
if (minutes > 0) return `${minutes}m`;
26+
return `${seconds}s`;
27+
}
28+
29+
function tokenStatusColor(seconds: number | null): string {
30+
if (seconds === null) return "bg-surface-300 dark:bg-surface-600";
31+
if (seconds <= 0) return "bg-error-500";
32+
if (seconds < 300) return "bg-warning-500";
33+
return "bg-success-500";
34+
}
1735
1836
function formatDateFromUnix(epochDateMilliseconds: number | string): string {
1937
console.log("Formatting date:", epochDateMilliseconds);
@@ -361,6 +379,92 @@
361379
<div class="flex flex-col space-y-6">
362380
{@render userInfo(userData)}
363381

382+
<!-- Session Info Section -->
383+
<div class="mx-10 pr-5">
384+
<header class="py-4">
385+
<h1 class="h4 text-center align-middle">Session Info</h1>
386+
</header>
387+
{#if sessionInfo && sessionInfo.hasAccessToken}
388+
<article class="border-primary-500 space-y-1 border-b-[1px] p-4">
389+
{#each [
390+
{ label: "Session ID", value: sessionInfo.sessionId },
391+
{ label: "OAuth Provider", value: sessionInfo.oauthProvider },
392+
] as item}
393+
<div class="hover:bg-primary-500/5 flex items-center justify-between rounded-md p-2" data-testid={`session-${item.label.toLowerCase().replace(/\s+/g, "-")}`}>
394+
<strong>{item.label}</strong>
395+
<span class="rounded-sm bg-gray-800/20 p-2 font-mono text-sm backdrop-blur-2xl">
396+
{item.value || "N/A"}
397+
</span>
398+
</div>
399+
<hr class="hr !my-0 opacity-20" />
400+
{/each}
401+
402+
<!-- Access Token -->
403+
<div class="hover:bg-primary-500/5 flex items-center justify-between rounded-md p-2" data-testid="session-access-token">
404+
<div class="flex items-center gap-2">
405+
<strong>Access Token</strong>
406+
<span class="h-2.5 w-2.5 rounded-full {tokenStatusColor(sessionInfo.accessTokenExpiresInSeconds)}" title={sessionInfo.accessTokenExpiresInSeconds !== null && sessionInfo.accessTokenExpiresInSeconds <= 0 ? "Expired" : "Active"}></span>
407+
</div>
408+
<div class="flex items-center gap-3">
409+
<span class="rounded-sm bg-gray-800/20 p-2 font-mono text-sm backdrop-blur-2xl">
410+
{sessionInfo.accessTokenPreview || "N/A"}
411+
</span>
412+
<span class="text-xs text-surface-600 dark:text-surface-400">
413+
{#if sessionInfo.accessTokenExpiresInSeconds !== null && sessionInfo.accessTokenExpiresInSeconds <= 0}
414+
Expired
415+
{:else}
416+
{formatTimeRemaining(sessionInfo.accessTokenExpiresInSeconds)} remaining
417+
{/if}
418+
</span>
419+
</div>
420+
</div>
421+
<hr class="hr !my-0 opacity-20" />
422+
423+
<!-- Refresh Token -->
424+
<div class="hover:bg-primary-500/5 flex items-center justify-between rounded-md p-2" data-testid="session-refresh-token">
425+
<div class="flex items-center gap-2">
426+
<strong>Refresh Token</strong>
427+
{#if sessionInfo.hasRefreshToken}
428+
<span class="h-2.5 w-2.5 rounded-full {tokenStatusColor(sessionInfo.refreshTokenExpiresInSeconds)}" title={sessionInfo.refreshTokenExpiresInSeconds !== null && sessionInfo.refreshTokenExpiresInSeconds <= 0 ? "Expired" : "Active"}></span>
429+
{:else}
430+
<span class="h-2.5 w-2.5 rounded-full bg-error-500" title="Missing"></span>
431+
{/if}
432+
</div>
433+
<div class="flex items-center gap-3">
434+
{#if sessionInfo.hasRefreshToken}
435+
<span class="rounded-sm bg-gray-800/20 p-2 font-mono text-sm backdrop-blur-2xl">
436+
{sessionInfo.refreshTokenPreview}
437+
</span>
438+
<span class="text-xs text-surface-600 dark:text-surface-400">
439+
{#if sessionInfo.refreshTokenExpiresAt === null}
440+
Expiry unknown (not a JWT)
441+
{:else if sessionInfo.refreshTokenExpiresInSeconds !== null && sessionInfo.refreshTokenExpiresInSeconds <= 0}
442+
Expired
443+
{:else}
444+
{formatTimeRemaining(sessionInfo.refreshTokenExpiresInSeconds)} remaining
445+
{/if}
446+
</span>
447+
{:else}
448+
<span class="text-xs text-error-600 dark:text-error-400">
449+
None — session cannot auto-renew
450+
</span>
451+
{/if}
452+
</div>
453+
</div>
454+
<hr class="hr !my-0 opacity-20" />
455+
</article>
456+
{:else}
457+
<article class="border-primary-500 border-b-[1px] p-4">
458+
<div class="rounded-lg bg-warning-50 p-3 dark:bg-warning-900/20" data-testid="session-warning">
459+
<p class="text-sm text-warning-700 dark:text-warning-300">
460+
No active session found. Your session may have expired.
461+
<a href="/login" class="font-medium underline hover:text-warning-900 dark:hover:text-warning-100">Log in again</a> to restore full functionality.
462+
</p>
463+
</div>
464+
</article>
465+
{/if}
466+
</div>
467+
364468
<!-- Preferences Section -->
365469
<div class="mx-10 pr-5">
366470
<header class="flex items-center justify-between py-4">

src/routes/+layout.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ export async function load(event: RequestEvent) {
5555
);
5656

5757
// Get information about the user from the session if they are logged in
58+
// User is only considered logged in if they have both user data AND a valid access token
5859
logger.info("👤 Checking user session");
59-
if (session?.data?.user) {
60+
if (session?.data?.user && session?.data?.oauth?.access_token) {
6061
data.userId = session.data.user.user_id;
6162
data.email = session.data.user.email;
6263
data.username = session.data.user.username;
6364
logger.info(`✅ User session found: ${data.email}`);
6465
} else {
65-
logger.info("ℹ️ No user session found (user not logged in)");
66+
logger.info("ℹ️ No user session found (user not logged in or no access token)");
6667
}
6768

6869
// Get Opey consent info if we have Opey consumer ID configured

0 commit comments

Comments
 (0)