mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-09-25 18:25:40 -05:00
feat: implement auth stores
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
const { locales, setLocale } = useI18n();
|
||||
|
||||
const me = useMeStore();
|
||||
const user = computed(() => me.user);
|
||||
|
||||
const openDropdown = ref<boolean | string>(false);
|
||||
const loggedIn = ref(false);
|
||||
function handleDropdownButton(dropdown: boolean | string) {
|
||||
if (!openDropdown.value) {
|
||||
openDropdown.value = dropdown;
|
||||
@@ -451,7 +453,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loggedIn"
|
||||
v-if="user"
|
||||
class="user-widget-wrapper logged-in"
|
||||
>
|
||||
<div class="user-widget-toggle">
|
||||
@@ -491,7 +493,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!loggedIn"
|
||||
v-if="!user"
|
||||
class="user-widget-wrapper"
|
||||
>
|
||||
<a
|
||||
|
||||
27
src/middleware/1.auth.global.ts
Normal file
27
src/middleware/1.auth.global.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { GetApiAuthMe } from "~/server/api/auth/me.get";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const meStore = useMeStore();
|
||||
if (meStore.loaded) return; // Already loaded
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const token = authStore.getToken();
|
||||
if (!token) {
|
||||
meStore.setMe(null);
|
||||
return; // No token
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await $fetch<GetApiAuthMe>('/api/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
meStore.setMe({
|
||||
pid: res.pid,
|
||||
username: res.username,
|
||||
});
|
||||
} catch {
|
||||
meStore.setMe(null);
|
||||
}
|
||||
});
|
||||
13
src/middleware/2.enforce.global.ts
Normal file
13
src/middleware/2.enforce.global.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
function notAllowed() {
|
||||
return navigateTo("/");
|
||||
}
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const meStore = useMeStore();
|
||||
if (!meStore.loaded) throw new Error("Mestore must be loaded before reaching this middleware");
|
||||
|
||||
// NeedsAuth
|
||||
if (to.meta.needsAuth) {
|
||||
if (!meStore.user) return notAllowed();
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
needsAuth: true,
|
||||
})
|
||||
|
||||
const me = useMeStore();
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Hello {{ me.user?.username }}</h1>
|
||||
<p>TODO: account stub page</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
7
src/plugins/types.d.ts
vendored
Normal file
7
src/plugins/types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module "#app" {
|
||||
interface PageMeta {
|
||||
needsAuth?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
@@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const apiResponse = await $fetch<LoginCCResponse>(`/v1/login`, {
|
||||
method: 'POST',
|
||||
baseURL: useRuntimeConfig(event).apiBase,
|
||||
baseURL: useRuntimeConfig(event).public.apiBase,
|
||||
body: { ...body, grant_type: 'password' }
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const apiResponse = await $fetch<RegisterCCResponse>(`/v1/register`, {
|
||||
method: 'POST',
|
||||
baseURL: useRuntimeConfig(event).apiBase,
|
||||
baseURL: useRuntimeConfig(event).public.apiBase,
|
||||
body: body
|
||||
});
|
||||
|
||||
|
||||
18
src/server/api/auth/me.get.ts
Normal file
18
src/server/api/auth/me.get.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { enforceLoggedIn } from '~/server/utils/enforceAuth';
|
||||
import { useGrpc } from '~/server/utils/useGrpc';
|
||||
|
||||
export interface GetApiAuthMe {
|
||||
pid: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<GetApiAuthMe> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
|
||||
const grpc = useGrpc(event);
|
||||
const data = await grpc.getUserData({ pid: auth.pid });
|
||||
return {
|
||||
pid: data.pid,
|
||||
username: data.username,
|
||||
}
|
||||
});
|
||||
28
src/server/middleware/auth.ts
Normal file
28
src/server/middleware/auth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Metadata } from "nice-grpc";
|
||||
import { useGrpc } from "../utils/useGrpc";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const authHeader = getRequestHeader(event, "authorization");
|
||||
event.context.auth = null;
|
||||
if (authHeader) {
|
||||
const grpc = useGrpc(event);
|
||||
try {
|
||||
const [type, token] = authHeader.split(" ", 2);
|
||||
if (type !== "Bearer") throw new Error("Invalid token type");
|
||||
if (!token) throw new Error("Invalid token");
|
||||
const userData = await grpc.getUserData({}, {
|
||||
metadata: new Metadata({
|
||||
'X-Token': token
|
||||
})
|
||||
});
|
||||
|
||||
event.context.auth = {
|
||||
pid: userData.pid,
|
||||
username: userData.username,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to request user data: ", err);
|
||||
return; // Continue like nothing happened, further steps will validate if authed
|
||||
}
|
||||
}
|
||||
})
|
||||
22
src/server/utils/enforceAuth.ts
Normal file
22
src/server/utils/enforceAuth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type AuthContext = {
|
||||
pid: number;
|
||||
}
|
||||
|
||||
export function setAuthContext(event: H3Event, context: AuthContext) {
|
||||
event.context.auth = context;
|
||||
}
|
||||
|
||||
|
||||
export function getAuthContext(event: H3Event): AuthContext | null {
|
||||
return event.context.auth ?? null;
|
||||
}
|
||||
|
||||
|
||||
export function enforceLoggedIn(event: H3Event): AuthContext {
|
||||
const context = getAuthContext(event);
|
||||
if (!context) throw createError({
|
||||
status: 401,
|
||||
statusText: "This action requires authentication",
|
||||
})
|
||||
return context;
|
||||
}
|
||||
31
src/server/utils/useGrpc.ts
Normal file
31
src/server/utils/useGrpc.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { APIDefinition } from "@pretendonetwork/grpc/api/api_service";
|
||||
import { createChannel, createClient, Channel, type Client, Metadata } from "nice-grpc";
|
||||
|
||||
let grpc: { channel: Channel, client: Client<APIDefinition> } | null = null;
|
||||
|
||||
function getGrpcClient(event: H3Event): Client<APIDefinition> {
|
||||
if (!grpc) {
|
||||
const config = useRuntimeConfig();
|
||||
if (!config.grpcHost || !config.grpcApiKey) {
|
||||
throw new Error("GRPC not configured");
|
||||
}
|
||||
|
||||
const channel = createChannel(config.grpcHost);
|
||||
const metadata = new Metadata();
|
||||
metadata.append("X-API-Key", config.grpcApiKey);
|
||||
grpc = {
|
||||
channel,
|
||||
client: createClient(APIDefinition, channel, {
|
||||
"*": {
|
||||
metadata
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return grpc.client;
|
||||
}
|
||||
|
||||
export function useGrpc(event: H3Event) {
|
||||
return getGrpcClient(event);
|
||||
}
|
||||
74
src/stores/auth.ts
Normal file
74
src/stores/auth.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { CookieOptions } from "#app";
|
||||
|
||||
type AuthState = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
// Cookies on pretendo are weird:
|
||||
// `pretendo::auth` has the real authentication state that the website uses.
|
||||
// This has been made as secure as possible and must only be used in the website codebase.
|
||||
//
|
||||
// Then we have the rest of the cookies:
|
||||
// - `access_token`
|
||||
// - `refresh_token`
|
||||
// - `token_type`
|
||||
// These are for backwards compatibility. They must be configured exactly as implemented below
|
||||
// because other services use them (and overwrite them)
|
||||
// These are not used by the website codebase for security purposes.
|
||||
|
||||
const oldCookieTokenType = "Bearer"
|
||||
const cookieExpirySec = 7 * 24 * 60 * 60 // 7 days
|
||||
const oldCookieOptions = {
|
||||
domain: '.pretendo.network',
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
maxAge: cookieExpirySec,
|
||||
refresh: true,
|
||||
readonly: false,
|
||||
} satisfies CookieOptions<string> & { readonly: false }
|
||||
|
||||
// Not actually a store, but may as well be
|
||||
export function useAuthStore() {
|
||||
const authState = useCookie<AuthState | null>("pretendo::auth", {
|
||||
sameSite: "strict",
|
||||
secure: useRuntimeConfig().public.cookieSecure,
|
||||
refresh: true,
|
||||
default: () => null,
|
||||
});
|
||||
const accessTokenCookie = useCookie<string | null>("access_token", oldCookieOptions);
|
||||
const refreshTokenCookie = useCookie<string | null>("refresh_token", oldCookieOptions);
|
||||
const tokenTypeCookie = useCookie<string | null>("token_type", oldCookieOptions);
|
||||
|
||||
function getToken() {
|
||||
return authState.value?.accessToken ?? null;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
authState.value = authState.value;
|
||||
accessTokenCookie.value = accessTokenCookie.value;
|
||||
refreshTokenCookie.value = refreshTokenCookie.value;
|
||||
tokenTypeCookie.value = tokenTypeCookie.value;
|
||||
}
|
||||
|
||||
function set(val: AuthState | null) {
|
||||
if (val) {
|
||||
authState.value = val
|
||||
accessTokenCookie.value = val.accessToken;
|
||||
refreshTokenCookie.value = val.refreshToken;
|
||||
tokenTypeCookie.value = oldCookieTokenType;
|
||||
}
|
||||
else {
|
||||
authState.value = null;
|
||||
accessTokenCookie.value = null;
|
||||
refreshTokenCookie.value = null;
|
||||
tokenTypeCookie.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getToken,
|
||||
refresh,
|
||||
set,
|
||||
};
|
||||
}
|
||||
25
src/stores/me.ts
Normal file
25
src/stores/me.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type Me = {
|
||||
pid: number;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export const useMeStore = defineStore("me", () => {
|
||||
const data = ref<Me | null>(null);
|
||||
const loaded = ref(false);
|
||||
|
||||
function setMe(input: Me | null) {
|
||||
data.value = input;
|
||||
loaded.value = true;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
data.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
user: computed(() => data.value),
|
||||
loaded: computed(() => loaded.value),
|
||||
setMe,
|
||||
clear,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user