🐛 fix(auth): enable automatic token refresh across all web apps

Users were getting signed out after ~15 minutes because API clients were
reading tokens directly from localStorage without triggering the refresh
logic. The tokenManager exists with proper refresh capability but was
never being used.

Changes:
- Add getValidToken() method to auth stores in 9 web apps
- Update API clients to use authStore.getValidToken() instead of localStorage
- Token refresh now happens proactively before requests fail

Apps updated: chat, calendar, picture, zitare, contacts, todo, clock, manacore, manadeck

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Wuesteon 2025-12-10 20:52:29 +01:00
parent 6f74e1d9a6
commit 39b04e4b34
12 changed files with 209 additions and 33 deletions

View file

@ -1,9 +1,12 @@
/**
* API Client for Calendar Backend
*
* Token handling: Uses authStore.getValidToken() which automatically
* refreshes expired tokens before making requests.
*/
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { authStore } from '$lib/stores/auth.svelte';
const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3014';
@ -20,10 +23,8 @@ export async function fetchApi<T>(
): Promise<{ data: T | null; error: Error | null }> {
const { method = 'GET', body, token, isFormData = false } = options;
let authToken = token;
if (!authToken && browser) {
authToken = localStorage.getItem('@auth/appToken') || undefined;
}
// Get a valid token (auto-refreshes if expired)
const authToken = token || (await authStore.getValidToken());
try {
const headers: Record<string, string> = {};

View file

@ -34,6 +34,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -184,7 +191,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -193,4 +201,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -3,10 +3,13 @@
*
* This replaces direct Supabase calls with backend API calls.
* All database operations now go through the NestJS backend.
*
* Token handling: Uses authStore.getValidToken() which automatically
* refreshes expired tokens before making requests.
*/
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { authStore } from '$lib/stores/auth.svelte';
import type {
Conversation,
Message,
@ -46,12 +49,8 @@ async function fetchApi<T>(
): Promise<{ data: T | null; error: Error | null }> {
const { method = 'GET', body, token } = options;
// Get token from localStorage if not provided
// Token is stored by @manacore/shared-auth under '@auth/appToken'
let authToken = token;
if (!authToken && browser) {
authToken = localStorage.getItem('@auth/appToken') || undefined;
}
// Get a valid token (auto-refreshes if expired)
const authToken = token || (await authStore.getValidToken());
if (!authToken) {
return { data: null, error: new Error('No authentication token') };

View file

@ -34,6 +34,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -202,7 +209,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -211,4 +219,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -33,6 +33,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -183,7 +190,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -192,4 +200,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -25,6 +25,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -175,7 +182,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -184,4 +192,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -34,6 +34,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -240,7 +247,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -249,4 +257,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -1,5 +1,5 @@
import type { ManaUser } from '$lib/types/auth';
import { authService } from '$lib/auth';
import { authService, tokenManager } from '$lib/auth';
import type { UserData } from '$lib/auth';
// Svelte 5 runes-based auth store
@ -109,4 +109,20 @@ export const authStore = {
async forgotPassword(email: string) {
return authService.forgotPassword(email);
},
/**
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken(): Promise<string | null> {
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
return await tokenManager.getValidToken();
},
};

View file

@ -1,10 +1,13 @@
/**
* API Client for Picture Backend
* Replaces direct Supabase calls with backend API calls.
*
* Token handling: Uses authStore.getValidToken() which automatically
* refreshes expired tokens before making requests.
*/
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { authStore } from '$lib/stores/auth.svelte';
const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3003';
@ -21,10 +24,8 @@ export async function fetchApi<T>(
): Promise<{ data: T | null; error: Error | null }> {
const { method = 'GET', body, token, isFormData = false } = options;
let authToken = token;
if (!authToken && browser) {
authToken = localStorage.getItem('@auth/appToken') || undefined;
}
// Get a valid token (auto-refreshes if expired)
const authToken = token || (await authStore.getValidToken());
try {
const headers: Record<string, string> = {};
@ -75,10 +76,8 @@ export async function uploadFile(
file: File,
token?: string
): Promise<{ data: any; error: Error | null }> {
let authToken = token;
if (!authToken && browser) {
authToken = localStorage.getItem('@auth/appToken') || undefined;
}
// Get a valid token (auto-refreshes if expired)
const authToken = token || (await authStore.getValidToken());
try {
const formData = new FormData();
@ -121,10 +120,8 @@ export async function uploadFiles(
files: File[],
token?: string
): Promise<{ data: any; error: Error | null }> {
let authToken = token;
if (!authToken && browser) {
authToken = localStorage.getItem('@auth/appToken') || undefined;
}
// Get a valid token (auto-refreshes if expired)
const authToken = token || (await authStore.getValidToken());
try {
const formData = new FormData();

View file

@ -39,6 +39,13 @@ async function getAuthService() {
return _authService;
}
async function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
await getAuthService();
return _tokenManager;
}
// State using Svelte 5 runes
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -164,12 +171,28 @@ export const authStore = {
}
},
/**
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken(): Promise<string | null> {
const authService = await getAuthService();
if (!authService) return null;
return authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = await getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
// For compatibility with old code that reads user store directly
setUser(userData: UserData | null) {
user = userData;

View file

@ -34,6 +34,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -199,7 +206,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -208,4 +216,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -25,6 +25,13 @@ function getAuthService() {
return _authService;
}
function getTokenManager() {
if (!browser) return null;
// Ensure auth service is initialized first
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
@ -175,7 +182,8 @@ export const authStore = {
},
/**
* Get access token for API calls
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh
*/
async getAccessToken() {
const authService = getAuthService();
@ -184,4 +192,16 @@ export const authStore = {
}
return await authService.getAppToken();
},
/**
* Get a valid access token for API calls
* Automatically refreshes if the token is expired or about to expire
*/
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};