Merge pull request '[FA-misc] Fixes refresh token usage hopefully' (#42) from feature/FA-misc_RefreshTokens into master
All checks were successful
CI / build-backend (push) Successful in 1m3s
CI / build-frontend (push) Successful in 39s
Build Gateway / build-subgraphs (map[name:novel-service project:FictionArchive.Service.NovelService subgraph:Novel]) (push) Successful in 59s
Build Gateway / build-subgraphs (map[name:scheduler-service project:FictionArchive.Service.SchedulerService subgraph:Scheduler]) (push) Successful in 44s
Build Gateway / build-subgraphs (map[name:translation-service project:FictionArchive.Service.TranslationService subgraph:Translation]) (push) Successful in 46s
Build Gateway / build-subgraphs (map[name:user-service project:FictionArchive.Service.UserService subgraph:User]) (push) Successful in 41s
Release / build-and-push (map[dockerfile:FictionArchive.Service.AuthenticationService/Dockerfile name:authentication-service]) (push) Successful in 2m1s
Release / build-and-push (map[dockerfile:FictionArchive.Service.FileService/Dockerfile name:file-service]) (push) Successful in 1m53s
Release / build-and-push (map[dockerfile:FictionArchive.Service.NovelService/Dockerfile name:novel-service]) (push) Successful in 1m46s
Release / build-and-push (map[dockerfile:FictionArchive.Service.SchedulerService/Dockerfile name:scheduler-service]) (push) Successful in 1m34s
Release / build-and-push (map[dockerfile:FictionArchive.Service.TranslationService/Dockerfile name:translation-service]) (push) Successful in 1m39s
Release / build-and-push (map[dockerfile:FictionArchive.Service.UserService/Dockerfile name:user-service]) (push) Successful in 1m35s
Release / build-frontend (push) Successful in 1m38s
Build Gateway / build-gateway (push) Successful in 3m3s

Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
2025-12-10 00:43:21 +00:00
5 changed files with 55 additions and 12 deletions

View File

@@ -12,6 +12,7 @@
"@astrojs/svelte": "^7.2.2", "@astrojs/svelte": "^7.2.2",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@urql/core": "^6.0.1", "@urql/core": "^6.0.1",
"@urql/exchange-auth": "^3.0.0",
"@urql/svelte": "^5.0.0", "@urql/svelte": "^5.0.0",
"astro": "^5.16.2", "astro": "^5.16.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -4304,6 +4305,19 @@
"wonka": "^6.3.2" "wonka": "^6.3.2"
} }
}, },
"node_modules/@urql/exchange-auth": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@urql/exchange-auth/-/exchange-auth-3.0.0.tgz",
"integrity": "sha512-tj09xiOR2f1J2h8TE9uZWjRZipCdmDoTewEytOacDQ+0Teo+yIZxm3ppHxolQtiA51OHrGYiNTkMte8HtfvaBw==",
"license": "MIT",
"dependencies": {
"@urql/core": "^6.0.0",
"wonka": "^6.3.2"
},
"peerDependencies": {
"@urql/core": "^6.0.0"
}
},
"node_modules/@urql/svelte": { "node_modules/@urql/svelte": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-5.0.0.tgz",

View File

@@ -16,6 +16,7 @@
"@astrojs/svelte": "^7.2.2", "@astrojs/svelte": "^7.2.2",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@urql/core": "^6.0.1", "@urql/core": "^6.0.1",
"@urql/exchange-auth": "^3.0.0",
"@urql/svelte": "^5.0.0", "@urql/svelte": "^5.0.0",
"astro": "^5.16.2", "astro": "^5.16.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@@ -117,3 +117,14 @@ export async function logout() {
user.set(null); user.set(null);
} }
} }
export async function refreshToken(): Promise<User | null> {
if (!userManager) return null;
try {
const newUser = await userManager.signinSilent();
return newUser;
} catch (e) {
console.error('Token refresh failed:', e);
return null;
}
}

View File

@@ -4,7 +4,7 @@ const authority = import.meta.env.PUBLIC_OIDC_AUTHORITY;
const clientId = import.meta.env.PUBLIC_OIDC_CLIENT_ID; const clientId = import.meta.env.PUBLIC_OIDC_CLIENT_ID;
const redirectUri = import.meta.env.PUBLIC_OIDC_REDIRECT_URI; const redirectUri = import.meta.env.PUBLIC_OIDC_REDIRECT_URI;
const postLogoutRedirectUri = import.meta.env.PUBLIC_OIDC_POST_LOGOUT_REDIRECT_URI ?? redirectUri; const postLogoutRedirectUri = import.meta.env.PUBLIC_OIDC_POST_LOGOUT_REDIRECT_URI ?? redirectUri;
const scope = import.meta.env.PUBLIC_OIDC_SCOPE ?? 'openid profile email'; const scope = import.meta.env.PUBLIC_OIDC_SCOPE ?? 'openid profile email offline_access';
export const isOidcConfigured = export const isOidcConfigured =
Boolean(authority) && Boolean(clientId) && Boolean(redirectUri); Boolean(authority) && Boolean(clientId) && Boolean(redirectUri);
@@ -20,7 +20,7 @@ function buildSettings(): UserManagerSettings | null {
response_type: 'code', response_type: 'code',
scope, scope,
loadUserInfo: true, loadUserInfo: true,
automaticSilentRenew: true, automaticSilentRenew: false, // We handle refresh reactively via authExchange
userStore: userStore:
typeof window !== 'undefined' typeof window !== 'undefined'
? new WebStorageStateStore({ store: window.localStorage }) ? new WebStorageStateStore({ store: window.localStorage })

View File

@@ -1,19 +1,36 @@
import { Client, cacheExchange, fetchExchange } from '@urql/core'; import { Client, cacheExchange, fetchExchange } from '@urql/core';
import { authExchange } from '@urql/exchange-auth';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { user } from '../auth/authStore'; import { user, refreshToken } from '../auth/authStore';
export function createClient() { export function createClient() {
return new Client({ return new Client({
url: import.meta.env.PUBLIC_GRAPHQL_URI, url: import.meta.env.PUBLIC_GRAPHQL_URI,
exchanges: [cacheExchange, fetchExchange], exchanges: [
fetchOptions: () => { cacheExchange,
authExchange(async (utils) => ({
addAuthToOperation(operation) {
const currentUser = get(user); const currentUser = get(user);
return { if (!currentUser?.access_token) return operation;
headers: currentUser?.access_token return utils.appendHeaders(operation, {
? { Authorization: `Bearer ${currentUser.access_token}` } Authorization: `Bearer ${currentUser.access_token}`,
: {}, });
};
}, },
didAuthError(error) {
return error.graphQLErrors?.some(
(e) => e.extensions?.code === 'AUTH_NOT_AUTHENTICATED'
);
},
async refreshAuth() {
const newUser = await refreshToken();
if (!newUser) {
// Refresh failed, redirect to login
window.location.href = '/';
}
},
})),
fetchExchange,
],
}); });
} }