120 lines
3.1 KiB
TypeScript
120 lines
3.1 KiB
TypeScript
import { createError } from 'h3';
|
|
import type { AuthInfoResponse } from '~/types/auth';
|
|
import {
|
|
createUserSession,
|
|
deleteUserSession,
|
|
getUserSession,
|
|
} from '~/server/utils/sessionStore';
|
|
|
|
type SessionStoreRequest = {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
};
|
|
|
|
const DEFAULT_SESSION_SECONDS = 60 * 60; // 1 hour
|
|
|
|
|
|
const parseJwtPayload = (token: string): { exp?: number } | null => {
|
|
try {
|
|
const payload = token.split('.')[1];
|
|
if (!payload) return null;
|
|
|
|
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
|
|
const decoded = Buffer.from(padded, 'base64').toString();
|
|
|
|
return JSON.parse(decoded) as { exp?: number };
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const body = await readBody<SessionStoreRequest>(event);
|
|
|
|
const accessToken = body?.accessToken;
|
|
const refreshToken = body?.refreshToken;
|
|
|
|
if (!accessToken || !refreshToken) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'accessToken and refreshToken are required',
|
|
});
|
|
}
|
|
|
|
const config = useRuntimeConfig();
|
|
const baseUrl = config.public.baseUrl;
|
|
const authInfoUrl = `${baseUrl}/api/v1/auth/info`;
|
|
|
|
let authInfoResponse: AuthInfoResponse;
|
|
try {
|
|
authInfoResponse = await $fetch<AuthInfoResponse>(authInfoUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage:
|
|
error?.data?.message || error?.message || 'Failed to fetch auth info',
|
|
});
|
|
}
|
|
|
|
if (!authInfoResponse?.data) {
|
|
throw createError({
|
|
statusCode: 502,
|
|
statusMessage: 'Invalid auth info response',
|
|
});
|
|
}
|
|
|
|
const now = Date.now();
|
|
const refreshPayload = parseJwtPayload(refreshToken);
|
|
const refreshExpSeconds = refreshPayload?.exp;
|
|
|
|
const expiresAt =
|
|
typeof refreshExpSeconds === 'number' && refreshExpSeconds > 0
|
|
? refreshExpSeconds * 1000
|
|
: now + DEFAULT_SESSION_SECONDS * 1000;
|
|
|
|
const maxAge = Math.max(Math.floor((expiresAt - now) / 1000), 60);
|
|
|
|
const previousSessionId = getCookie(event, 'user_session');
|
|
if (previousSessionId && getUserSession(previousSessionId)) {
|
|
deleteUserSession(previousSessionId);
|
|
}
|
|
|
|
const sessionId = createUserSession({
|
|
user: {
|
|
auth_provider: authInfoResponse.data.auth_provider || 'jwt',
|
|
email: authInfoResponse.data.email,
|
|
name: authInfoResponse.data.name,
|
|
role: authInfoResponse.data.role,
|
|
user_id: authInfoResponse.data.user_id,
|
|
username: authInfoResponse.data.username,
|
|
},
|
|
accessToken,
|
|
refreshToken,
|
|
expiresAt,
|
|
});
|
|
|
|
const isSecure =
|
|
process.env.NODE_ENV === 'production' && event.node.req.headers['x-forwarded-proto'] === 'https';
|
|
|
|
setCookie(event, 'user_session', sessionId, {
|
|
httpOnly: true,
|
|
secure: isSecure,
|
|
sameSite: 'lax',
|
|
maxAge,
|
|
path: '/',
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Session stored successfully',
|
|
expiresAt,
|
|
};
|
|
});
|