From 47d703f6fd79df55bdec11a7bd0bd28ec1604cfb Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 16 Aug 2026 22:17:49 +0200 Subject: [PATCH] feat: add support for refresh tokens in internal stores --- server/api/auth/refresh.post.ts | 27 +++++++++++++++++++++++++++ server/utils/enforceAuth.ts | 5 +---- shared/api-types.ts | 5 +++++ shared/errors.ts | 6 ++++-- src/composables/apiFetch.ts | 6 +++--- src/middleware/1.auth.global.ts | 6 +++--- src/stores/auth.ts | 13 ++++++++++--- 7 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 server/api/auth/refresh.post.ts diff --git a/server/api/auth/refresh.post.ts b/server/api/auth/refresh.post.ts new file mode 100644 index 0000000..9508964 --- /dev/null +++ b/server/api/auth/refresh.post.ts @@ -0,0 +1,27 @@ +import { ClientError } from 'nice-grpc'; +import { RefreshSchema } from '#shared/api-types'; +import type { ApiAuthLogin } from '#shared/api-types'; + +export default defineEventHandler(async (event): Promise => { + const body = await readZodBody(event, RefreshSchema); + const grpc = useApiGrpc(event); + + try { + const res = await grpc.login({ + refreshToken: body.token, + grantType: 'refresh_token' + }); + + return { + accessToken: res.accessToken, + refreshToken: res.refreshToken + }; + } catch (error: unknown) { + if (error instanceof ClientError) { + if (error.details === 'INVALID_ARGUMENT: Invalid or missing refresh token') { + throw createApiError('UNAUTHENTICATED'); + } + } + throw error; + } +}); diff --git a/server/utils/enforceAuth.ts b/server/utils/enforceAuth.ts index c77b99a..ae83b00 100644 --- a/server/utils/enforceAuth.ts +++ b/server/utils/enforceAuth.ts @@ -20,10 +20,7 @@ export function getAuthContext(event: H3Event): AuthContext | null { export function enforceLoggedIn(event: H3Event): AuthContext { const context = getAuthContext(event); if (!context) { - throw createError({ - status: 401, - message: 'This action requires authentication' - }); + throw createApiError('UNAUTHENTICATED'); } return context; } diff --git a/shared/api-types.ts b/shared/api-types.ts index 3d2e9a5..293cc8f 100644 --- a/shared/api-types.ts +++ b/shared/api-types.ts @@ -90,6 +90,11 @@ export const LoginSchema = z.object({ }); export type ApiAuthLoginRequest = z.infer; +export const RefreshSchema = z.object({ + token: z.string() +}); +export type ApiAuthRefreshRequest = z.infer; + export const RegisterSchema = z.object({ email: z.email(), username: z.string(), diff --git a/shared/errors.ts b/shared/errors.ts index fb2d527..4c0373c 100644 --- a/shared/errors.ts +++ b/shared/errors.ts @@ -6,7 +6,8 @@ const apiErrorCodes = { STAFF_NO_DONATE: 'Staff members do not need to purchase tiers', INVALID_CAPTCHA: 'Invalid captcha, try again', INVALID_USERNAME: 'Could not find user', - INVALID_PASSWORD: 'Incorrect password' + INVALID_PASSWORD: 'Incorrect password', + UNAUTHENTICATED: 'This request needs authentication' } as const; export type ApiErrorCodes = keyof typeof apiErrorCodes; @@ -19,7 +20,8 @@ export const apiErrorCodeStatus: Record = { STAFF_NO_DONATE: 400, INVALID_CAPTCHA: 400, INVALID_USERNAME: 400, - INVALID_PASSWORD: 400 + INVALID_PASSWORD: 400, + UNAUTHENTICATED: 401 }; export function getTextForApiErrorCode(code: ApiErrorCodes): string { diff --git a/src/composables/apiFetch.ts b/src/composables/apiFetch.ts index c867064..3b6d532 100644 --- a/src/composables/apiFetch.ts +++ b/src/composables/apiFetch.ts @@ -1,8 +1,8 @@ export const apiFetch = $fetch.create({ onRequest({ options }) { - const token = useAuthStore().getToken(); - if (token) { - options.headers.set('Authorization', 'Bearer ' + token); + const tokens = useAuthStore().getTokens(); + if (tokens) { + options.headers.set('Authorization', 'Bearer ' + tokens.accessToken); } }, async onResponseError({ response }) { diff --git a/src/middleware/1.auth.global.ts b/src/middleware/1.auth.global.ts index 5817450..1adafe5 100644 --- a/src/middleware/1.auth.global.ts +++ b/src/middleware/1.auth.global.ts @@ -8,8 +8,8 @@ export default defineNuxtRouteMiddleware(async () => { const authStore = useAuthStore(); authStore.refresh(); - const token = authStore.getToken(); - if (!token) { + const tokens = authStore.getTokens(); + if (!tokens) { meStore.setMe(null); return; // No token } @@ -17,7 +17,7 @@ export default defineNuxtRouteMiddleware(async () => { try { const res = await $fetch('/api/auth/me', { headers: { - Authorization: `Bearer ${token}` + Authorization: `Bearer ${tokens.accessToken}` } }); meStore.setMe({ diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 822b3ab..e7391f3 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -45,8 +45,15 @@ export function useAuthStore() { const refreshTokenCookie = useCookie('refresh_token', oldOpts); const tokenTypeCookie = useCookie('token_type', oldOpts); - function getToken() { - return authState.value?.accessToken ?? null; + function getTokens() { + if (!authState.value) { + return null; + } + + return { + accessToken: authState.value.accessToken, + refreshToken: authState.value.refreshToken + }; } function refresh() { @@ -76,7 +83,7 @@ export function useAuthStore() { } return { - getToken, + getTokens, refresh, set, logout