mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-24 01:26:57 -05:00
feat: add GRPC based login flow
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -23,7 +23,8 @@
|
||||
"nice-grpc": "^2.1.17",
|
||||
"nuxt": "^4.5.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.2.0"
|
||||
"vue-router": "^5.2.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/fa7-brands": "^1.2.4",
|
||||
@@ -2800,6 +2801,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nuxt/content/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@nuxt/devalue": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz",
|
||||
@@ -21525,9 +21535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"nice-grpc": "^2.1.17",
|
||||
"nuxt": "^4.5.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.2.0"
|
||||
"vue-router": "^5.2.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/fa7-brands": "^1.2.4",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
// TODO: endpoint for refreshing tokens
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
49
src/server/api/auth/login.post.ts
Normal file
49
src/server/api/auth/login.post.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
10
src/server/utils/readZodBody.ts
Normal file
10
src/server/utils/readZodBody.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user