feat: add GRPC based login flow

This commit is contained in:
mrjvs
2026-08-07 13:41:07 +02:00
parent fb0efeb4ed
commit 128b82c9e7
8 changed files with 82 additions and 81 deletions

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { FetchError } from 'ofetch';
import type { ApiAuthLogin, ApiAuthLoginRequest } from '~/server/api/auth/login.post';
const route = useRoute();
const redirect = computed(() => route.query.redirect);
@@ -10,9 +11,12 @@ const errorMessage = ref<string | null>();
async function loginSubmission() {
try {
await $fetch('/api/account/login', {
await $fetch<ApiAuthLogin>('/api/auth/login', {
method: 'POST',
body: loginForm
body: {
username: loginForm.username,
password: loginForm.password,
} satisfies ApiAuthLoginRequest
});
if (typeof redirect.value === 'string') {
@@ -24,7 +28,7 @@ async function loginSubmission() {
if (error instanceof FetchError) {
errorMessage.value = error.statusText;
} else {
errorMessage.value = `Error during registration: ${error}`; // TODO: localize
errorMessage.value = `Error during login: ${error}`; // TODO: localize
}
setTimeout(() => { // TODO: replace this toast

View File

@@ -1,37 +0,0 @@
import { FetchError } from 'ofetch';
interface LoginCCResponse {
refresh_token: string;
access_token: string;
token_type: string;
expires_in: number;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
try {
const apiResponse = await $fetch<LoginCCResponse>(`/v1/login`, {
method: 'POST',
baseURL: useRuntimeConfig(event).public.apiBase,
body: { ...body, grant_type: 'password' }
});
setCookie(event, 'refresh_token', apiResponse.refresh_token);
setCookie(event, 'access_token', apiResponse.access_token);
setCookie(event, 'token_type', apiResponse.token_type);
setResponseStatus(event, 200);
event.node.res.end();
} catch (error: unknown) {
if (error instanceof FetchError) {
setResponseStatus(event, error.status, error.data.error);
} else {
setResponseStatus(event, 500);
}
event.node.res.end();
return;
}
});

View File

@@ -1 +0,0 @@
// TODO: endpoint for refreshing tokens

View File

@@ -1,35 +0,0 @@
import { FetchError } from 'ofetch';
interface RegisterCCResponse { // TODO: this is the same as the login response. figure out where we should put types and merge into AuthCCReponse!
refresh_token: string;
access_token: string;
token_type: string;
expires_in: number;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
try {
const apiResponse = await $fetch<RegisterCCResponse>(`/v1/register`, {
method: 'POST',
baseURL: useRuntimeConfig(event).public.apiBase,
body: body
});
setCookie(event, 'refresh_token', apiResponse.refresh_token);
setCookie(event, 'access_token', apiResponse.access_token);
setCookie(event, 'token_type', apiResponse.token_type);
setResponseStatus(event, 200);
event.node.res.end();
} catch (error: unknown) {
if (error instanceof FetchError) {
setResponseStatus(event, error.status, error.data.error);
} else {
setResponseStatus(event, 500);
}
event.node.res.end();
}
});

View File

@@ -0,0 +1,49 @@
import z from 'zod';
import { readZodBody } from '~/server/utils/readZodBody';
import { useGrpc } from '~/server/utils/useGrpc';
import { ServerError } from 'nice-grpc';
export type ApiAuthLogin = {
accessToken: string
refreshToken: string;
}
const LoginSchema = z.object({
username: z.string(),
password: z.string(),
})
export type ApiAuthLoginRequest = z.infer<typeof LoginSchema>
export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
const body = await readZodBody(event, LoginSchema);
const grpc = useGrpc(event);
try {
const res = await grpc.login({
username: body.username,
password: body.password,
grantType: 'password'
});
return {
accessToken: res.accessToken,
refreshToken: res.refreshToken,
}
} catch (error: unknown) {
if (error instanceof ServerError) {
if (error.details === 'INVALID_ARGUMENT: User not found') {
throw createError({
status: 400,
statusText: 'User not found',
})
}
if (error.details === 'INVALID_ARGUMENT: Password is incorrect') {
throw createError({
status: 400,
statusText: 'Password was incorrect',
})
}
}
throw error;
}
});

View File

@@ -0,0 +1,10 @@
import type { ZodType } from "zod";
export async function readZodBody<T extends ZodType>(event: H3Event, schema: T) {
const body = await readValidatedBody(event, schema.safeParse);
if (!body.success) throw createError({
status: 400,
statusText: 'Invalid input'
})
return body.data;
}