feat: add support for refresh tokens in internal stores

This commit is contained in:
mrjvs
2026-08-16 22:17:49 +02:00
parent b3b44751ea
commit 47d703f6fd
7 changed files with 53 additions and 15 deletions

View File

@@ -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<ApiAuthLogin> => {
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;
}
});

View File

@@ -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;
}

View File

@@ -90,6 +90,11 @@ export const LoginSchema = z.object({
});
export type ApiAuthLoginRequest = z.infer<typeof LoginSchema>;
export const RefreshSchema = z.object({
token: z.string()
});
export type ApiAuthRefreshRequest = z.infer<typeof RefreshSchema>;
export const RegisterSchema = z.object({
email: z.email(),
username: z.string(),

View File

@@ -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<ApiErrorCodes, number> = {
STAFF_NO_DONATE: 400,
INVALID_CAPTCHA: 400,
INVALID_USERNAME: 400,
INVALID_PASSWORD: 400
INVALID_PASSWORD: 400,
UNAUTHENTICATED: 401
};
export function getTextForApiErrorCode(code: ApiErrorCodes): string {

View File

@@ -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 }) {

View File

@@ -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<GetApiAuthMe>('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`
Authorization: `Bearer ${tokens.accessToken}`
}
});
meStore.setMe({

View File

@@ -45,8 +45,15 @@ export function useAuthStore() {
const refreshTokenCookie = useCookie<string | null>('refresh_token', oldOpts);
const tokenTypeCookie = useCookie<string | null>('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