From f04c88b392ce40a927df4d7a7f798c9d91e1c857 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Tue, 6 May 2025 00:55:46 +0200 Subject: [PATCH 01/27] feat: add utility function to fetch with authrorization header --- utils/fetchWithAuth.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 utils/fetchWithAuth.ts diff --git a/utils/fetchWithAuth.ts b/utils/fetchWithAuth.ts new file mode 100644 index 0000000..5a3bfaf --- /dev/null +++ b/utils/fetchWithAuth.ts @@ -0,0 +1,23 @@ +import type { NitroFetchRequest, NitroFetchOptions } from "nitropack"; + +export default async (request: NitroFetchRequest, options: NitroFetchOptions = {}) => { + const accessToken = useCookie("access_token"); + console.log("access token 2:", accessToken.value); + + try { + const res = await $fetch(request, { + ...options, + headers: { + ...options.headers, + "Authorization": `Bearer ${accessToken.value}` + } + }); + + return res; + } catch (error: any) { + if (error?.response?.status === 401) { + // auth.revoke(); + } + throw error; + } +} From 2bfd1aa8332988a27e4f84323ce359276d90f756 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Tue, 6 May 2025 00:56:17 +0200 Subject: [PATCH 02/27] feat: add useAuth composable for centralized auth management --- composables/auth.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 composables/auth.ts diff --git a/composables/auth.ts b/composables/auth.ts new file mode 100644 index 0000000..0e1c26b --- /dev/null +++ b/composables/auth.ts @@ -0,0 +1,88 @@ +export const useAuth = () => { + const apiVersion = useRuntimeConfig().public.apiVersion; + const accessToken = useCookie("access_token"); + const user = useState("user", () => null); + + async function register(username: string, email: string, password: string) { + const hashedPass = await hashPassword(password); + const res = await $fetch(`/api/v${apiVersion}/auth/register`, { + method: "POST", body: + { + email, identifier: username, password: hashedPass, device_name: "Linux Laptop" + } + }) as { access_token: string, refresh_token: string }; + //authStore.setAccessToken(accessToken); + accessToken.value = res.access_token; + } + + async function login(username: string, password: string, device_name: string) { + const hashedPass = await hashPassword(password); + console.log("hashedPass:", hashedPass); + //authStore.setAccessToken(accessToken); + const res = await $fetch(`/api/v${apiVersion}/auth/login`, { + method: "POST", body: + { + username, password: hashedPass, device_name: "Linux Laptop" + } + }) as { access_token: string, refresh_token: string }; fetch + + accessToken.value = res.access_token; + console.log("access token:", accessToken.value); + await fetchUser(); + } + + async function logout(password: string) { + console.log("password:", password); + console.log("access:", accessToken.value); + const hashedPass = await hashPassword(password); + console.log("hashed"); + + const res = await fetchWithAuth(`/api/v${apiVersion}/auth/revoke`, { + method: "POST", + body: + { + password: hashedPass, device_name: "Linux Laptop" + } + }); + + accessToken.value = null; + user.value = null; + } + + async function revoke() { + accessToken.value = null; + } + + async function refresh() { + const res = await $fetch(`/api/v${apiVersion}/auth/refresh`, { + method: "POST" + }) as { access_token: string }; + accessToken.value = res.access_token; + } + + async function fetchUser() { + if (!accessToken.value) return; + const res = await fetchWithAuth(`/api/v${apiVersion}/users/me`) as any; + user.value = res; + } + + async function getUser() { + if (!accessToken) return; + if (!user.value) { + await fetchUser(); + } + return user.value; + } + + return { + accessToken, + register, + login, + logout, + revoke, + refresh, + getUser, + fetchUser, + user + } +} From f17aab4a6a78778fac0032f80b91b1cd81aa90e4 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Tue, 6 May 2025 01:44:09 +0200 Subject: [PATCH 03/27] feat: add check for access token presence in fetchWithAuth utility and added credentials to request --- utils/fetchWithAuth.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/utils/fetchWithAuth.ts b/utils/fetchWithAuth.ts index 5a3bfaf..cae3853 100644 --- a/utils/fetchWithAuth.ts +++ b/utils/fetchWithAuth.ts @@ -1,16 +1,27 @@ import type { NitroFetchRequest, NitroFetchOptions } from "nitropack"; export default async (request: NitroFetchRequest, options: NitroFetchOptions = {}) => { - const accessToken = useCookie("access_token"); - console.log("access token 2:", accessToken.value); + const accessToken = useCookie("access_token").value; + console.log("access token 2:", accessToken); + + let headers: HeadersInit = {}; + + if (accessToken) { + headers = { + ...options.headers, + "Authorization": `Bearer ${accessToken}` + }; + } else { + headers = { + ...options.headers + }; + } try { const res = await $fetch(request, { ...options, - headers: { - ...options.headers, - "Authorization": `Bearer ${accessToken.value}` - } + headers, + credentials: "include" }); return res; From 6aa725fb77f2a896478214f7fa3d2e19169e6db7 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Wed, 7 May 2025 19:38:15 +0200 Subject: [PATCH 04/27] feat: add stats interface --- types.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 types.d.ts diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..1ed1ce8 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,6 @@ +export interface GorbStats { + accounts: number, + uptime: number, + version: string, + build_number: string +} \ No newline at end of file From a4b98ba58a76dcc8613dc322f2fee509cdd91621 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Wed, 7 May 2025 19:39:22 +0200 Subject: [PATCH 05/27] feat: re-added automatic revoke --- utils/fetchWithAuth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/fetchWithAuth.ts b/utils/fetchWithAuth.ts index cae3853..df1f7bd 100644 --- a/utils/fetchWithAuth.ts +++ b/utils/fetchWithAuth.ts @@ -2,6 +2,7 @@ import type { NitroFetchRequest, NitroFetchOptions } from "nitropack"; export default async (request: NitroFetchRequest, options: NitroFetchOptions = {}) => { const accessToken = useCookie("access_token").value; + const { revoke } = useAuth(); console.log("access token 2:", accessToken); let headers: HeadersInit = {}; @@ -27,7 +28,7 @@ export default async (request: NitroFetchRequest, options: NitroFetchOptions< return res; } catch (error: any) { if (error?.response?.status === 401) { - // auth.revoke(); + revoke(); } throw error; } From e1f2a5a5911961dfc79f19804edb22ace6d582e9 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Wed, 7 May 2025 19:43:39 +0200 Subject: [PATCH 06/27] feat: change to use useAuth composable --- pages/login.vue | 44 ++++---------------------------------------- pages/register.vue | 35 ++++------------------------------- 2 files changed, 8 insertions(+), 71 deletions(-) diff --git a/pages/login.vue b/pages/login.vue index 9899a87..485166d 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -1,6 +1,6 @@ @@ -38,44 +32,14 @@ const form = reactive({ password: "", }); -const response = ref(); - //const authStore = useAuthStore(); -const accessToken = useCookie("access_token"); -const refreshToken = useCookie("refresh_token"); -const redirectTo = useRoute().query.redirect_to; -console.log("access token:", accessToken.value); -console.log("refresh token:", refreshToken.value); +const { login } = useAuth(); -onMounted(() => { - console.log("accessToken:", accessToken.value); - console.log("refreshToken:", refreshToken.value); - - if (accessToken.value) { - //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); - } -}); - -const apiVersion = useRuntimeConfig().public.apiVersion; - -async function login(e: Event) { +async function formLogin(e: Event) { e.preventDefault(); console.log("Sending login data"); - const hashedPass = await hashPassword(form.password); - console.log("hashedPass:", hashedPass); - //authStore.setAccessToken(accessToken); - const res = await $fetch(`/api/v${apiVersion}/auth/login`, { - method: "POST", body: - { - username: form.username, password: hashedPass - } - }) as { access_token: string, refresh_token: string }; - response.value = res; - accessToken.value = res.access_token; - console.log("set access token:", accessToken.value); - const refreshToken = useCookie("refresh_token", { secure: true, httpOnly: false }); - refreshToken.value = res.refresh_token; + await login(form.username, form.password, "Linux Laptop"); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); } diff --git a/pages/register.vue b/pages/register.vue index 48fae6b..cd17005 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -24,7 +24,7 @@
- +
@@ -35,12 +35,6 @@
Already have an account? Log in!
-
- Response: -

- {{ response }} -

-
@@ -57,8 +51,6 @@ const form = reactive({ repeatPassword: "" }); -const response = ref(); - /* const errorMessages = reactive({ username: { @@ -81,18 +73,11 @@ const errorMessages = reactive({ */ //const authStore = useAuthStore(); -const accessToken = useCookie("access_token"); -const refreshToken = useCookie("refresh_token"); +const auth = useAuth(); const redirectTo = useRoute().query.redirect_to; -console.log("access token:", accessToken.value); -console.log("refresh token:", refreshToken.value); - onMounted(() => { - console.log("accessToken:", accessToken.value); - console.log("refreshToken:", refreshToken.value); - - if (accessToken.value) { + if (auth.accessToken.value) { //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); } }); @@ -135,19 +120,7 @@ const apiVersion = useRuntimeConfig().public.apiVersion; async function register(e: Event) { e.preventDefault(); console.log("Sending registration data"); - const hashedPass = await hashPassword(form.password); - const res = await $fetch(`/api/v${apiVersion}/auth/register`, { - method: "POST", body: - { - email: form.email, username: form.username, password: hashedPass - } - }) as { access_token: string, refresh_token: string }; - response.value = res; - //authStore.setAccessToken(accessToken); - accessToken.value = res.access_token; - console.log("set access token:", accessToken.value); - const refreshToken = useCookie("refresh_token", { secure: true, httpOnly: false }); - refreshToken.value = res.refresh_token; + await auth.register(form.username, form.email, form.password); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); } From 4364e9fa3ba845fc5638bc43f764bf6a71bce942 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Wed, 7 May 2025 19:46:10 +0200 Subject: [PATCH 07/27] feat: move login/register to auth layout, add detecting and setting instance URL --- layouts/auth.vue | 129 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 19 deletions(-) diff --git a/layouts/auth.vue b/layouts/auth.vue index 73d4603..fd0f820 100644 --- a/layouts/auth.vue +++ b/layouts/auth.vue @@ -1,40 +1,131 @@ \ No newline at end of file From 986c5a90eba09d420529235b26f4dc0c62189cef Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:36:29 +0200 Subject: [PATCH 10/27] feat: add Nuxt Icon module --- nuxt.config.ts | 16 +++- package.json | 4 +- pnpm-lock.yaml | 207 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 4 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index feb8c46..1b29f61 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,18 +2,28 @@ export default defineNuxtConfig({ compatibilityDate: '2024-11-01', devtools: { enabled: true }, - modules: ['@nuxt/eslint', '@nuxt/image', "@pinia/nuxt"], + modules: ['@nuxt/eslint', '@nuxt/image', "@pinia/nuxt", "@nuxt/icon"], app: { /* Defines what prefix the client runs on E.g.: baseURL set to "/web" would host at https://gorb.app/web Default is "/" (aka root), which hosts at https://gorb.app/ */ - baseURL: "/", + baseURL: "/" }, runtimeConfig: { public: { apiVersion: 1 } - } + }, + /* nitro: { + devProxy: { + "/api": { + target: "http://localhost:8080/api", + changeOrigin: true, + prependPath: true, + ws: true + } + } + } */ }) \ No newline at end of file diff --git a/package.json b/package.json index a33286e..80a9ab4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@nuxt/eslint": "^1.3.0", + "@nuxt/icon": "1.13.0", "@nuxt/image": "1.10.0", "@pinia/nuxt": "0.11.0", "nuxt": "^3.17.0", @@ -20,9 +21,10 @@ "vue": "^3.5.13", "vue-router": "^4.5.1" }, - "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39", + "packageManager": "pnpm@10.11.0", "license": "MIT", "devDependencies": { + "@iconify-json/lucide": "^1.2.44", "@types/node": "^22.15.3", "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.2.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d78b130..4d4de9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@nuxt/eslint': specifier: ^1.3.0 version: 1.3.0(@vue/compiler-sfc@3.5.13)(eslint@9.25.1(jiti@2.4.2))(magicast@0.3.5)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1)) + '@nuxt/icon': + specifier: 1.13.0 + version: 1.13.0(magicast@0.3.5)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3)) '@nuxt/image': specifier: 1.10.0 version: 1.10.0(@netlify/blobs@8.2.0)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5) @@ -36,6 +39,9 @@ importers: specifier: ^4.5.1 version: 4.5.1(vue@3.5.13(typescript@5.8.3)) devDependencies: + '@iconify-json/lucide': + specifier: ^1.2.44 + version: 1.2.44 '@types/node': specifier: ^22.15.3 version: 22.15.3 @@ -55,6 +61,9 @@ packages: '@antfu/install-pkg@1.0.0': resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + '@apidevtools/json-schema-ref-parser@11.9.3': resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} @@ -578,6 +587,23 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@iconify-json/lucide@1.2.44': + resolution: {integrity: sha512-lOwXoOtkFm4Zj1duPj5xqhPKKMUvxVRMfIHi0YDfwLjEEgrhzsJZ4JCpE119L8P8p6zi4on4b9cPZdVXCUuDoQ==} + + '@iconify/collections@1.0.551': + resolution: {integrity: sha512-5Jy+BoI4nsNE1nHqukQh5ZxxppGThyU3VR794PLmZtbF7YJGYYa4SETnGRl4hz/5lKiTe8OhlXf5Ns45ZGLz5w==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + + '@iconify/vue@5.0.0': + resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==} + peerDependencies: + vue: '>=3' + '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -704,6 +730,11 @@ packages: peerDependencies: vite: '>=6.0' + '@nuxt/devtools-kit@2.4.1': + resolution: {integrity: sha512-taA2Nm03JiV3I+SEYS/u1AfjvLm3V9PO8lh0xLsUk/2mlUnL6GZ9xLXrp8VRg11HHt7EPXERGQh8h4iSPU2bSQ==} + peerDependencies: + vite: '>=6.0' + '@nuxt/devtools-wizard@2.4.0': resolution: {integrity: sha512-3/5S2zpl79rE1b/lh8M/2lDNsYiYIXXHZmCwsYPuFJA6DilLQo/VY44oq6cY0Q1up32HYB3h1Te/q3ELbsb+ag==} hasBin: true @@ -740,6 +771,9 @@ packages: vite-plugin-eslint2: optional: true + '@nuxt/icon@1.13.0': + resolution: {integrity: sha512-Sft1DZj/U5Up60DMnhp+hRDNDXRkMhwHocxgDkDkAPBxqR8PRyvzxcMIy3rjGMu0s+fB6ZdLs6vtaWzjWuerQQ==} + '@nuxt/image@1.10.0': resolution: {integrity: sha512-/B58GeEmme7bkmQUrXzEw8P9sJb9BkMaYZqLDtq8ZdDLEddE3P4nVya8RQPB+p4b7EdqWajpPqdy1A2ZPLev/A==} engines: {node: '>=18.20.6'} @@ -748,10 +782,18 @@ packages: resolution: {integrity: sha512-+aS+Enqqo2qSbyl0APPPxX8BPYsaRcZ8dFRbpCOfK38lv2ckoHKCWNkT8L/7q2w+1pjNZaxlUoW9Mku1vdEb/A==} engines: {node: '>=18.12.0'} + '@nuxt/kit@3.17.4': + resolution: {integrity: sha512-l+hY8sy2XFfg3PigZj+PTu6+KIJzmbACTRimn1ew/gtCz+F38f6KTF4sMRTN5CUxiB8TRENgEonASmkAWfpO9Q==} + engines: {node: '>=18.12.0'} + '@nuxt/schema@3.17.0': resolution: {integrity: sha512-BwHD1NBtZRlk+qPZYvNzzdp7MG8s4i5gmTQ+12hbxc9x09osB9RivAU2ekwMMLfykx90wDszDu0DJ5Zec4Svgg==} engines: {node: ^14.18.0 || >=16.10.0} + '@nuxt/schema@3.17.4': + resolution: {integrity: sha512-bsfJdWjKNYLkVQt7Ykr9YsAql1u8Tuo6iecSUOltTIhsvAIYsknRFPHoNKNmaiv/L6FgCQgUgQppPTPUAXiJQQ==} + engines: {node: ^14.18.0 || >=16.10.0} + '@nuxt/telemetry@2.6.6': resolution: {integrity: sha512-Zh4HJLjzvm3Cq9w6sfzIFyH9ozK5ePYVfCUzzUQNiZojFsI2k1QkSBrVI9BGc6ArKXj/O6rkI6w7qQ+ouL8Cag==} engines: {node: '>=18.12.0'} @@ -1409,6 +1451,9 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/shared@3.5.14': + resolution: {integrity: sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==} + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -1650,6 +1695,14 @@ packages: magicast: optional: true + c12@3.0.4: + resolution: {integrity: sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2546,6 +2599,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + globals@16.0.0: resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} engines: {node: '>=18'} @@ -2873,6 +2930,9 @@ packages: knitwork@1.2.0: resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} @@ -3917,6 +3977,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} @@ -4287,6 +4352,10 @@ packages: resolution: {integrity: sha512-8jL3T+FKDg+qLFX55X9j92uFRqH5vWrNlf/eJb5IQlQB5q5wjooXQDXP1ulhJJQHbosBmlKhBo/ZVS5jHlcJGA==} engines: {node: '>=18.12.0'} + unimport@5.0.1: + resolution: {integrity: sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==} + engines: {node: '>=18.12.0'} + unixify@1.0.0: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} engines: {node: '>=0.10.0'} @@ -4684,6 +4753,8 @@ snapshots: package-manager-detector: 0.2.11 tinyexec: 0.3.2 + '@antfu/utils@8.1.1': {} + '@apidevtools/json-schema-ref-parser@11.9.3': dependencies: '@jsdevtools/ono': 7.1.3 @@ -5143,6 +5214,34 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} + '@iconify-json/lucide@1.2.44': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/collections@1.0.551': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.3.0': + dependencies: + '@antfu/install-pkg': 1.0.0 + '@antfu/utils': 8.1.1 + '@iconify/types': 2.0.0 + debug: 4.4.0 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.1 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + + '@iconify/vue@5.0.0(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.13(typescript@5.8.3) + '@ioredis/commands@1.2.0': {} '@isaacs/cliui@8.0.2': @@ -5377,6 +5476,15 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/devtools-kit@2.4.1(magicast@0.3.5)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))': + dependencies: + '@nuxt/kit': 3.17.4(magicast@0.3.5) + '@nuxt/schema': 3.17.4 + execa: 8.0.1 + vite: 6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1) + transitivePeerDependencies: + - magicast + '@nuxt/devtools-wizard@2.4.0': dependencies: consola: 3.4.2 @@ -5492,6 +5600,28 @@ snapshots: - utf-8-validate - vite + '@nuxt/icon@1.13.0(magicast@0.3.5)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@iconify/collections': 1.0.551 + '@iconify/types': 2.0.0 + '@iconify/utils': 2.3.0 + '@iconify/vue': 5.0.0(vue@3.5.13(typescript@5.8.3)) + '@nuxt/devtools-kit': 2.4.1(magicast@0.3.5)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1)) + '@nuxt/kit': 3.17.4(magicast@0.3.5) + consola: 3.4.2 + local-pkg: 1.1.1 + mlly: 1.7.4 + ohash: 2.0.11 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinyglobby: 0.2.13 + transitivePeerDependencies: + - magicast + - supports-color + - vite + - vue + '@nuxt/image@1.10.0(@netlify/blobs@8.2.0)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)': dependencies: '@nuxt/kit': 3.17.0(magicast@0.3.5) @@ -5555,6 +5685,33 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/kit@3.17.4(magicast@0.3.5)': + dependencies: + c12: 3.0.4(magicast@0.3.5) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + errx: 0.1.0 + exsolve: 1.0.5 + ignore: 7.0.4 + jiti: 2.4.2 + klona: 2.0.6 + knitwork: 1.2.0 + mlly: 1.7.4 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.1.0 + scule: 1.3.0 + semver: 7.7.2 + std-env: 3.9.0 + tinyglobby: 0.2.13 + ufo: 1.6.1 + unctx: 2.4.1 + unimport: 5.0.1 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + '@nuxt/schema@3.17.0': dependencies: consola: 3.4.2 @@ -5562,6 +5719,14 @@ snapshots: pathe: 2.0.3 std-env: 3.9.0 + '@nuxt/schema@3.17.4': + dependencies: + '@vue/shared': 3.5.14 + consola: 3.4.2 + defu: 6.1.4 + pathe: 2.0.3 + std-env: 3.9.0 + '@nuxt/telemetry@2.6.6(magicast@0.3.5)': dependencies: '@nuxt/kit': 3.17.0(magicast@0.3.5) @@ -6270,6 +6435,8 @@ snapshots: '@vue/shared@3.5.13': {} + '@vue/shared@3.5.14': {} + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.1 @@ -6520,6 +6687,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@3.0.4(magicast@0.3.5): + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.5.0 + exsolve: 1.0.5 + giget: 2.0.0 + jiti: 2.4.2 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -7505,6 +7689,8 @@ snapshots: globals@14.0.0: {} + globals@15.15.0: {} + globals@16.0.0: {} globby@11.1.0: @@ -7824,6 +8010,8 @@ snapshots: knitwork@1.2.0: {} + kolorist@1.8.0: {} + kuler@2.0.0: {} lambda-local@2.2.0: @@ -9030,6 +9218,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.2: {} + send@1.2.0: dependencies: debug: 4.4.0 @@ -9475,6 +9665,23 @@ snapshots: unplugin: 2.3.2 unplugin-utils: 0.2.4 + unimport@5.0.1: + dependencies: + acorn: 8.14.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.1 + magic-string: 0.30.17 + mlly: 1.7.4 + pathe: 2.0.3 + picomatch: 4.0.2 + pkg-types: 2.1.0 + scule: 1.3.0 + strip-literal: 3.0.0 + tinyglobby: 0.2.13 + unplugin: 2.3.2 + unplugin-utils: 0.2.4 + unixify@1.0.0: dependencies: normalize-path: 2.1.1 From fc9d21d4df8169d1d53b33ffe662021fd4a4aebd Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:39:38 +0200 Subject: [PATCH 11/27] feat: remove fetchWithAuth --- utils/fetchWithAuth.ts | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 utils/fetchWithAuth.ts diff --git a/utils/fetchWithAuth.ts b/utils/fetchWithAuth.ts deleted file mode 100644 index df1f7bd..0000000 --- a/utils/fetchWithAuth.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { NitroFetchRequest, NitroFetchOptions } from "nitropack"; - -export default async (request: NitroFetchRequest, options: NitroFetchOptions = {}) => { - const accessToken = useCookie("access_token").value; - const { revoke } = useAuth(); - console.log("access token 2:", accessToken); - - let headers: HeadersInit = {}; - - if (accessToken) { - headers = { - ...options.headers, - "Authorization": `Bearer ${accessToken}` - }; - } else { - headers = { - ...options.headers - }; - } - - try { - const res = await $fetch(request, { - ...options, - headers, - credentials: "include" - }); - - return res; - } catch (error: any) { - if (error?.response?.status === 401) { - revoke(); - } - throw error; - } -} From 6d4b3d51bcfcec009debea9a3ee52769a27a1a0f Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:40:02 +0200 Subject: [PATCH 12/27] feat: implement fetchWithAuth utility composable --- utils/fetchWithApi.ts | 71 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 utils/fetchWithApi.ts diff --git a/utils/fetchWithApi.ts b/utils/fetchWithApi.ts new file mode 100644 index 0000000..c0ebf2e --- /dev/null +++ b/utils/fetchWithApi.ts @@ -0,0 +1,71 @@ +import type { NitroFetchRequest, NitroFetchOptions } from "nitropack"; + +export default async (path: string, options: NitroFetchOptions = {}) => { + console.log("path received:", path); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.endsWith("/")) { + path = path.slice(0, path.lastIndexOf("/")); + } + console.log("formatted path:", path); + try { + const accessToken = useCookie("access_token"); + console.log("access token:", accessToken.value); + const apiBase = useCookie("api_base").value; + const apiVersion = useRuntimeConfig().public.apiVersion; + console.log("heyoooo") + console.log("apiBase:", apiBase); + if (!apiBase) { + console.log("no api base"); + return; + } + console.log("path:", path) + const { revoke, refresh } = useAuth(); + console.log("access token 2:", accessToken.value); + + let headers: HeadersInit = {}; + + if (accessToken.value) { + headers = { + ...options.headers, + "Authorization": `Bearer ${accessToken.value}` + }; + } else { + headers = { + ...options.headers + }; + } + + let reauthFailed = false; + while (!reauthFailed) { + try { + console.log("fetching:", URL.parse(apiBase + path)); + const res = await $fetch(URL.parse(apiBase + path)!.href, { + ...options, + headers, + credentials: "include" + }); + + return res; + } catch (error: any) { + if (error?.response?.status === 401) { + if (!path.startsWith("/auth/refresh")) { + try { + await refresh(); + } catch (error: any) { + if (error?.response?.status === 401) { + reauthFailed = true; + await revoke(); + return; + } + } + } + } + throw error; + } + } + } catch (error) { + console.error("error:", error); + } +} From 3d1d1151bc0fab7e0819af3e059771120e938575 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:40:19 +0200 Subject: [PATCH 13/27] feat: add sleep function utility --- utils/sleep.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 utils/sleep.ts diff --git a/utils/sleep.ts b/utils/sleep.ts new file mode 100644 index 0000000..b4ef59b --- /dev/null +++ b/utils/sleep.ts @@ -0,0 +1,3 @@ +export default async (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); +} From 358b950af482191ba508e6566bb32a10d6b08106 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:40:36 +0200 Subject: [PATCH 14/27] feat: add uuid v7 to timestamp converter --- utils/uuidToTimestamp.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 utils/uuidToTimestamp.ts diff --git a/utils/uuidToTimestamp.ts b/utils/uuidToTimestamp.ts new file mode 100644 index 0000000..78d6b12 --- /dev/null +++ b/utils/uuidToTimestamp.ts @@ -0,0 +1,6 @@ +export default (uuid: string) => { + const parts = uuid.split("-"); + const bits = parts[0] + parts[1].slice(0, 4); + const timestamp = parseInt(bits, 16); + return timestamp; +} From 89cd8ec1bfe6a9626d1be65b629a65ee88998cfc Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:40:56 +0200 Subject: [PATCH 15/27] feat: add file for storing types --- types/interfaces.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 types/interfaces.ts diff --git a/types/interfaces.ts b/types/interfaces.ts new file mode 100644 index 0000000..05830d2 --- /dev/null +++ b/types/interfaces.ts @@ -0,0 +1,54 @@ +export interface ChannelPermissionResponse { + channel_uuid: string, + role_uuid: string, + permissions: number +} + +export interface RoleResponse { + uuid: string, + guild_uuid: string, + name: string, + color: number, + position: number, + permissions: number +} + +export interface GuildResponse { + uuid: string, + name: string, + description: string | null, + icon: string | null, + owner_uuid: string, + roles: [], + member_count: number +} + +export interface ChannelResponse { + uuid: string, + guild_uuid: string, + name: string, + description: string, + permissions: ChannelPermissionResponse[] +} + +export interface MessageResponse { + uuid: string + channel_uuid: string + user_uuid: string + message: string +} + +export interface InviteResponse { + id: string, + user_uuid: string, + guild_uuid: string +} + +export interface UserResponse { + uuid: string, + username: string, + display_name: string | null, + avatar: string | null, + email: string, + email_verified: boolean + } From 1085687c0044fae83b315182077ff59ccfe1b003 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:41:44 +0200 Subject: [PATCH 16/27] feat: add channel page --- .../[serverId]/channels/[channelId].vue | 137 +++++++++++++++++- 1 file changed, 134 insertions(+), 3 deletions(-) diff --git a/pages/servers/[serverId]/channels/[channelId].vue b/pages/servers/[serverId]/channels/[channelId].vue index b9b4da4..2954af9 100644 --- a/pages/servers/[serverId]/channels/[channelId].vue +++ b/pages/servers/[serverId]/channels/[channelId].vue @@ -1,13 +1,144 @@ \ No newline at end of file From 2e3a4ae10d486afcd7b36cf63bb9df8676e4c0b9 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:42:00 +0200 Subject: [PATCH 17/27] feat: add client layout --- layouts/client.vue | 206 +++++++++++++++------------------------------ 1 file changed, 67 insertions(+), 139 deletions(-) diff --git a/layouts/client.vue b/layouts/client.vue index 240e59b..02f7ded 100644 --- a/layouts/client.vue +++ b/layouts/client.vue @@ -1,59 +1,43 @@ @@ -155,20 +93,24 @@ const channels = [ height: 100%; display: grid; grid-template-columns: 1fr 4fr 18fr 4fr; - grid-template-rows: 8dvh auto; + grid-template-rows: 4dvh auto; text-align: center; } +#homebar { + grid-row: 1; + grid-column: 1 / -1; + display: flex; + justify-content: space-evenly; + align-items: center; + padding-left: 5dvw; + padding-right: 5dvw; +} + #client-root>div:nth-child(-n+4) { border-bottom: 1px solid rgb(70, 70, 70); } -#client-root div { - /* border: 1px solid cyan; */ -} - -#main-bar {} - #__nuxt { display: flex; flex-flow: column; @@ -180,8 +122,8 @@ const channels = [ } #home { - grid-column: 1; - grid-row: 1; + padding-left: .5dvw; + padding-right: .5dvw; } #current-info { @@ -194,64 +136,50 @@ const channels = [ grid-row: 1; } -#utilities { - display: flex; - flex-direction: row; - margin-bottom: 3dvh; - justify-content: center; -} - -#left-sidebar-container, -#right-sidebar-container { - text-align: center; -} - .member-item { display: flex; justify-content: center; align-items: center; } -.bottom-border { - border-bottom: 1px solid rgb(70, 70, 70); +#message-history, +#members-list { + padding-top: 3dvh; } -.left-border { - border-left: 1px solid rgb(70, 70, 70); +#message-history { + display: flex; + flex-direction: column; + justify-content: space-between; + padding-left: 3dvw; + padding-right: 3dvw; } -.right-border { +#left-column { + display: flex; + flex-direction: column; + gap: 2dvh; + padding-left: .5dvw; + padding-right: .5dvw; + border-right: 1px solid rgb(70, 70, 70); + padding-top: 1.5dvh; +} + +#middle-left-column { + padding-left: 1dvw; + padding-right: 1dvw; border-right: 1px solid rgb(70, 70, 70); } -#main-content { - display: grid; - grid-template-rows: 1fr 15fr 30fr 2fr; - text-align: center; - margin-left: 1dvw; +#home-button { + border-bottom: 1px solid rgb(70, 70, 70); + padding-bottom: 1dvh; } -#message-box { - border: 1px solid rgb(70, 70, 70); - width: 100%; - margin-bottom: 1dvh; +#servers-list { + display: flex; + flex-direction: column; + gap: 1dvh; } -#message-box-input { - width: 80%; - height: 100%; -} - -.main-grid-row { - /* border: 1px solid cyan; */ -} - -#main-bar {} - -#servers-list, -#channels-list, -#message-history, -#members-list { - margin-top: 3dvh; -} \ No newline at end of file From 64c5f99963a397e723a84fef3556b4e37ae1e720 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:42:17 +0200 Subject: [PATCH 18/27] feat: add auth layout --- layouts/auth.vue | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/layouts/auth.vue b/layouts/auth.vue index fd0f820..f90dc84 100644 --- a/layouts/auth.vue +++ b/layouts/auth.vue @@ -50,24 +50,24 @@ import { FetchError } from 'ofetch'; const mounted = ref(false); +const redirectTo = useRoute().query.redirect_to; const apiVersion = useRuntimeConfig().public.apiVersion; -const instanceUrl = ref(null); - +const instanceUrl = ref(null); const instanceUrlInput = ref(); const instanceError = ref(); -const redirectTo = useRoute().query.redirect_to; - const auth = useAuth(); if (auth.accessToken.value) { - navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); + //navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); } onMounted(() => { mounted.value = true; - instanceUrl.value = localStorage.getItem("instanceUrl"); + const cookie = useCookie("instance_url").value; + instanceUrl.value = cookie; + console.log(cookie); console.log("set instance url to:", instanceUrl.value); }); @@ -82,6 +82,8 @@ async function selectInstance(e: Event) { const origin = new URL(res.url).origin; localStorage.setItem("instanceUrl", origin); instanceUrl.value = origin; + useCookie("instance_url").value = origin; + useCookie("api_base").value = origin + `/api/v${apiVersion}`; localStorage.setItem("apiBase", origin + `/api/v${apiVersion}`); } catch (error: any) { if (error instanceof FetchError) { From b90065589688c717b77c9ab2305f3580b4810e5d Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:45:00 +0200 Subject: [PATCH 19/27] feat: remove unused index file --- pages/index.vue | 268 ------------------------------------------------ 1 file changed, 268 deletions(-) delete mode 100644 pages/index.vue diff --git a/pages/index.vue b/pages/index.vue deleted file mode 100644 index 3e38b8a..0000000 --- a/pages/index.vue +++ /dev/null @@ -1,268 +0,0 @@ - - - - - \ No newline at end of file From 591599f4990582a1fb9beaa10d414860995a95d5 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:45:28 +0200 Subject: [PATCH 20/27] feat: remove assets folder due to having moved to lucide icons --- assets/img/check-mark.png | Bin 20751 -> 0 bytes assets/img/check-mark.svg | 1 - assets/img/envelope.svg | 1 - assets/img/house.svg | 1 - assets/img/server.svg | 1 - assets/img/tiger-head.svg | 1 - 6 files changed, 5 deletions(-) delete mode 100644 assets/img/check-mark.png delete mode 100644 assets/img/check-mark.svg delete mode 100644 assets/img/envelope.svg delete mode 100644 assets/img/house.svg delete mode 100644 assets/img/server.svg delete mode 100644 assets/img/tiger-head.svg diff --git a/assets/img/check-mark.png b/assets/img/check-mark.png deleted file mode 100644 index 597c805a824f4741cea6bb31b79d3198d1c164d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20751 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 VPB!wdzIrkMfv2mV%Q~loCIGyoLH_^% diff --git a/assets/img/check-mark.svg b/assets/img/check-mark.svg deleted file mode 100644 index c419bf2..0000000 --- a/assets/img/check-mark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/img/envelope.svg b/assets/img/envelope.svg deleted file mode 100644 index 00b5edd..0000000 --- a/assets/img/envelope.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/img/house.svg b/assets/img/house.svg deleted file mode 100644 index ad84b18..0000000 --- a/assets/img/house.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/img/server.svg b/assets/img/server.svg deleted file mode 100644 index 56d54fc..0000000 --- a/assets/img/server.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/img/tiger-head.svg b/assets/img/tiger-head.svg deleted file mode 100644 index dd68658..0000000 --- a/assets/img/tiger-head.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 136ca93ce8a51433f0a632548bde9812e853fa45 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:45:51 +0200 Subject: [PATCH 21/27] feat: move some styles to app.vue --- app.vue | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app.vue b/app.vue index 8b6727d..643fc86 100644 --- a/app.vue +++ b/app.vue @@ -16,4 +16,21 @@ body { a { color: aquamarine; } + +.white { + color: white; +} + +.bottom-border { + border-bottom: 1px solid rgb(70, 70, 70); +} + +.left-border { + border-left: 1px solid rgb(70, 70, 70); +} + +.right-border { + border-right: 1px solid rgb(70, 70, 70); +} + From c9decc585ef75559dad45cd02fe5245a2ffd90c4 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:46:13 +0200 Subject: [PATCH 22/27] feat: add server page --- pages/servers/[serverId]/index.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/servers/[serverId]/index.vue b/pages/servers/[serverId]/index.vue index b9b4da4..b45982a 100644 --- a/pages/servers/[serverId]/index.vue +++ b/pages/servers/[serverId]/index.vue @@ -1,10 +1,12 @@ From 2f0ff0521fb13ddb586bf8a24e383090c0086d18 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:46:44 +0200 Subject: [PATCH 23/27] feat: update and improve auth composable --- composables/auth.ts | 47 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/composables/auth.ts b/composables/auth.ts index 58e615b..ccb45ca 100644 --- a/composables/auth.ts +++ b/composables/auth.ts @@ -1,12 +1,17 @@ +import type { UserResponse } from "~/types/interfaces"; + export const useAuth = () => { - const config = useRuntimeConfig(); const accessToken = useCookie("access_token"); - const user = useState("user", () => null); + const user = useState("user", () => null); + + async function clearAuth() { + accessToken.value = null; + user.value = null; + } async function register(username: string, email: string, password: string) { - const apiBase = localStorage.getItem("apiBase"); const hashedPass = await hashPassword(password); - const res = await $fetch(`${apiBase}/auth/register`, { + const res = await fetchWithApi("/auth/register", { method: "POST", body: { email, identifier: username, password: hashedPass, device_name: "Linux Laptop" @@ -17,30 +22,28 @@ export const useAuth = () => { } async function login(username: string, password: string, device_name: string) { - const apiBase = localStorage.getItem("apiBase"); const hashedPass = await hashPassword(password); console.log("hashedPass:", hashedPass); //authStore.setAccessToken(accessToken); - const res = await fetchWithAuth(`${apiBase}/auth/login`, { + const res = await fetchWithApi("/auth/login", { method: "POST", body: { username, password: hashedPass, device_name: "Linux Laptop" } }) as { access_token: string, refresh_token: string }; fetch - + console.log("hi"); accessToken.value = res.access_token; console.log("access token:", accessToken.value); await fetchUser(); } async function logout(password: string) { - const apiBase = localStorage.getItem("apiBase"); console.log("password:", password); console.log("access:", accessToken.value); const hashedPass = await hashPassword(password); console.log("hashed"); - const res = await fetchWithAuth(`${apiBase}/auth/revoke`, { + const res = await fetchWithApi("/auth/revoke", { method: "POST", body: { @@ -48,29 +51,31 @@ export const useAuth = () => { } }); - accessToken.value = null; - user.value = null; + clearAuth(); } async function revoke() { - accessToken.value = null; - localStorage.removeItem("instanceUrl"); - localStorage.removeItem("apiBase"); + clearAuth(); } async function refresh() { - const apiBase = localStorage.getItem("apiBase"); - const res = await fetchWithAuth(`${apiBase}/auth/refresh`, { - method: "POST" - }) as { access_token: string }; - accessToken.value = res.access_token; + console.log("refreshing"); + try { + const res = await fetchWithApi("/auth/refresh", { + method: "POST" + }) as { access_token: string }; + accessToken.value = res.access_token; + console.log("set new access token"); + } catch (error) { + console.error("refresh error:", error); + } } async function fetchUser() { - const apiBase = localStorage.getItem("apiBase"); if (!accessToken.value) return; - const res = await fetchWithAuth(`${apiBase}/users/me`) as any; + const res = await fetchWithApi("/users/me") as UserResponse; user.value = res; + return user.value; } async function getUser() { From 68cd8e10edbcac5f0eb7a132fcd7402836b2291a Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:47:11 +0200 Subject: [PATCH 24/27] feat: add Message class, may remove later --- classes/Message.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 classes/Message.ts diff --git a/classes/Message.ts b/classes/Message.ts new file mode 100644 index 0000000..de0d12b --- /dev/null +++ b/classes/Message.ts @@ -0,0 +1,19 @@ +import type { MessageResponse } from "~/types/interfaces"; + +export default class Message { + uuid: string; + channelUuid: string; + userUuid: string; + message: string; + + constructor({ uuid, channel_uuid, user_uuid, message }: MessageResponse) { + this.uuid = uuid; + this.channelUuid = channel_uuid; + this.userUuid = user_uuid; + this.message = message; + } + + getTimestamp() { + return uuidToTimestamp(this.uuid); + } +} \ No newline at end of file From 646ae78776fd1aeaaf1022e79a1967ac69db2249 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Mon, 26 May 2025 22:47:37 +0200 Subject: [PATCH 25/27] feat: add components for showing messages --- components/Message.vue | 36 +++++++++-- components/MessageArea.vue | 122 +++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 components/MessageArea.vue diff --git a/components/Message.vue b/components/Message.vue index 44dc983..870738b 100644 --- a/components/Message.vue +++ b/components/Message.vue @@ -1,13 +1,20 @@