Compare commits

...

27 commits

Author SHA1 Message Date
1d79f680df feat: add Channel component
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-26 22:49:43 +02:00
8ce2d52044 feat: add invite popup component 2025-05-26 22:49:43 +02:00
646ae78776 feat: add components for showing messages 2025-05-26 22:49:43 +02:00
68cd8e10ed feat: add Message class, may remove later 2025-05-26 22:49:43 +02:00
2f0ff0521f feat: update and improve auth composable 2025-05-26 22:49:43 +02:00
c9decc585e feat: add server page 2025-05-26 22:49:43 +02:00
136ca93ce8 feat: move some styles to app.vue 2025-05-26 22:49:43 +02:00
591599f499 feat: remove assets folder due to having moved to lucide icons 2025-05-26 22:49:43 +02:00
b900655896 feat: remove unused index file 2025-05-26 22:49:43 +02:00
64c5f99963 feat: add auth layout 2025-05-26 22:49:43 +02:00
2e3a4ae10d feat: add client layout 2025-05-26 22:49:43 +02:00
1085687c00 feat: add channel page 2025-05-26 22:49:43 +02:00
89cd8ec1bf feat: add file for storing types 2025-05-26 22:49:43 +02:00
358b950af4 feat: add uuid v7 to timestamp converter 2025-05-26 22:49:43 +02:00
3d1d1151bc feat: add sleep function utility 2025-05-26 22:49:43 +02:00
6d4b3d51bc feat: implement fetchWithAuth utility composable 2025-05-26 22:49:43 +02:00
fc9d21d4df feat: remove fetchWithAuth 2025-05-26 22:49:43 +02:00
986c5a90eb feat: add Nuxt Icon module 2025-05-26 22:49:43 +02:00
1c680e85e3 feat: add loading component 2025-05-26 22:49:43 +02:00
fecf6fb6e0 feat: change requests to use apiBase localStorage value 2025-05-26 22:49:43 +02:00
4364e9fa3b feat: move login/register to auth layout, add detecting and setting instance URL 2025-05-26 22:49:43 +02:00
e1f2a5a591 feat: change to use useAuth composable 2025-05-26 22:49:43 +02:00
a4b98ba58a feat: re-added automatic revoke 2025-05-26 22:49:43 +02:00
6aa725fb77 feat: add stats interface 2025-05-26 22:49:43 +02:00
f17aab4a6a feat: add check for access token presence in fetchWithAuth utility and added credentials to request 2025-05-26 22:49:43 +02:00
2bfd1aa833 feat: add useAuth composable for centralized auth management 2025-05-26 22:49:43 +02:00
f04c88b392 feat: add utility function to fetch with authrorization header 2025-05-26 22:49:43 +02:00
29 changed files with 1051 additions and 521 deletions

17
app.vue
View file

@ -16,4 +16,21 @@ body {
a { a {
color: aquamarine; 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);
}
</style> </style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1 +0,0 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M17.47 250.9C88.82 328.1 158 397.6 224.5 485.5c72.3-143.8 146.3-288.1 268.4-444.37L460 26.06C356.9 135.4 276.8 238.9 207.2 361.9c-48.4-43.6-126.62-105.3-174.38-137z" fill="#fff" fill-opacity="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 430 B

View file

@ -1 +0,0 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M182.78 80.125c3.367 19.498 10.608 52.67 19.126 84.813 8.126 30.663 17.73 60.122 25.063 75.062 57.968-.962 148.212-16.707 252.343-46.344-91.756-70.023-188.486-99.376-296.532-113.53zM164.657 85c-65.62 51.243-106.43 120.106-138.5 196.25 54.866-38.51 111.644-60.42 169.313-70.906-3.995-12.636-7.88-26.486-11.626-40.625-8.425-31.79-15.554-64.12-19.188-84.72zm322.281 125.906c-74.123 21.218-141.43 35.68-196.25 42.813 24.018 51.794 36.448 106.688 43.688 160.936 70.634-58.76 125.36-118.495 152.563-203.75zM201.53 228.28c-56.563 9.917-111.78 30.946-165.56 68.907 89.478 61.396 189.91 97.037 279.874 119.844-7.362-55.057-20.104-109.997-44.75-161.03-18.39 1.897-35.134 2.875-49.938 2.875h-5.344l-2.718-4.625c-3.898-6.69-7.77-15.598-11.563-25.97z" fill="#fff" fill-opacity="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 1,003 B

View file

@ -1 +0,0 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M256 19.27L25.637 249.638 19.27 256 32 268.73l6.363-6.367L256 44.727l217.637 217.636L480 268.73 492.73 256l-6.367-6.363zM96 48v107.273l64-64.002V48zm160 20.727l-192 192V486h64V320h96v166h224V260.727zM288 320h96v80h-96z" fill="#fff" fill-opacity="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 484 B

View file

@ -1 +0,0 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M41 25v78h430V25H41zm254 23h18v32h-18V48zm121 0a16 16 0 0 1 16 16 16 16 0 0 1-16 16 16 16 0 0 1-16-16 16 16 0 0 1 16-16zM64 55h48v18H64V55zm80 0h48v18h-48V55zm80 0h48v18h-48V55zm-119 66v30h302v-30H105zm-64 48v78h430v-78H41zm254 23h18v32h-18v-32zm121 0a16 16 0 0 1 16 16 16 16 0 0 1-16 16 16 16 0 0 1-16-16 16 16 0 0 1 16-16zm-352 7h48v18H64v-18zm80 0h48v18h-48v-18zm80 0h48v18h-48v-18zm-119 66v30h302v-30H105zm-64 48v78h430v-78H41zm254 23h18v32h-18v-32zm121 0a16 16 0 0 1 16 16 16 16 0 0 1-16 16 16 16 0 0 1-16-16 16 16 0 0 1 16-16zm-352 7h48v18H64v-18zm80 0h48v18h-48v-18zm80 0h48v18h-48v-18zm13 66v30h38v-30h-38zM25 457v30h130.2l20-30H25zm171.8 0l-20 30h158.4l-20-30H196.8zm140 0l20 30H487v-30H336.8z" fill="#fff" fill-opacity="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 968 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

19
classes/Message.ts Normal file
View file

@ -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);
}
}

View file

@ -1,14 +1,19 @@
<template> <template>
<div> <div v-if="current" class="channel-list-link-container">
<a class="channel-list-link" :href="href"> <NuxtLink class="channel-list-link" :href="props.href">
{{ name }} # {{ name }}
</a> </NuxtLink>
</div>
<div v-else class="channel-list-link-container current-channel">
<NuxtLink class="channel-list-link" :href="props.href">
# {{ name }}
</NuxtLink>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps<{ name: string, href: string }>(); const props = defineProps<{ name: string, href: string, current?: boolean }>();
</script> </script>
@ -16,5 +21,18 @@ const props = defineProps<{ name: string, href: string }>();
.channel-list-link { .channel-list-link {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
padding-left: .5dvw;
padding-right: .5dvw;
}
.channel-list-link-container {
text-align: left;
display: flex;
height: 4dvh;
white-space: nowrap;
}
.current-channel {
background-color: rgb(70, 70, 70);
} }
</style> </style>

View file

@ -0,0 +1,40 @@
<template>
<div id="invite-popup">
<div v-if="invite">
<p>{{ invite }}</p>
<button @click="copyInvite">Copy Link</button>
</div>
<div v-else>
<button @click="generateInvite">Generate Invite</button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { InviteResponse } from '~/types/interfaces';
const invite = ref<string>();
const route = useRoute();
async function generateInvite(): Promise<void> {
const createdInvite: InviteResponse | undefined = await fetchWithApi(
`/servers/${route.params.serverId}/invites`,
{ method: "POST", body: { custom_id: "oijewfoiewf" } }
);
invite.value = createdInvite?.id;
return;
}
function copyInvite() {
const inviteUrl = URL.parse(`invite/${invite.value}`, `${window.location.protocol}//${window.location.host}`);
navigator.clipboard.writeText(inviteUrl!.href);
}
</script>
<style>
</style>

13
components/Loading.vue Normal file
View file

@ -0,0 +1,13 @@
<template>
<div>
Loading...
</div>
</template>
<script lang="ts" setup>
</script>
<style>
</style>

View file

@ -1,13 +1,20 @@
<template> <template>
<div class="message"> <div class="message">
<img class="message-author-pfp" :src="img" :alt="username"> <div>
<img v-if="props.img" class="message-author-avatar" :src="img" :alt="username">
<Icon v-else name="lucide:user" class="message-author-avatar" />
</div>
<div class="message-data"> <div class="message-data">
<div class="message-metadata"> <div class="message-metadata">
<span class="message-author-username"> <span class="message-author-username">
{{ username }} {{ username }}
</span> </span>
<span class="message-date"> <span class="message-date" :title="date.toString()">
{{ messageDate }} {{ messageDate }}
<!--
<div class="message-date-hover" v-if="showHover">
</div>
-->
</span> </span>
</div> </div>
<div class="message-text"> <div class="message-text">
@ -21,6 +28,7 @@
const props = defineProps<{ class?: string, img?: string, username: string, text: string, timestamp: number, format: "12" | "24" }>(); const props = defineProps<{ class?: string, img?: string, username: string, text: string, timestamp: number, format: "12" | "24" }>();
const messageDate = ref<string>(); const messageDate = ref<string>();
const showHover = ref(false);
const date = new Date(props.timestamp); const date = new Date(props.timestamp);
const now = new Date() const now = new Date()
@ -32,12 +40,16 @@ if (now.getUTCHours() >= 0) {
if (dateHour > 12) { if (dateHour > 12) {
dateHour = dateHour - 12; dateHour = dateHour - 12;
} }
messageDate.value = `${dateHour}:${dateMinute} ${dateHour > 0 && dateHour < 13 ? "AM" : "PM"}` messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute} ${dateHour > 0 && dateHour < 13 ? "AM" : "PM"}`
} else { } else {
messageDate.value = `${dateHour}:${dateMinute}` messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute}`
} }
} }
//function toggleTooltip(e: Event) {
// showHover.value = !showHover.value;
//}
</script> </script>
<style scoped> <style scoped>
@ -46,7 +58,8 @@ if (now.getUTCHours() >= 0) {
/* border: 1px solid lightcoral; */ /* border: 1px solid lightcoral; */
margin-bottom: 1dvh; margin-bottom: 1dvh;
display: grid; display: grid;
grid-template-columns: 3em auto; grid-template-columns: auto 1fr;
align-items: center;
} }
.message-metadata { .message-metadata {
@ -65,7 +78,7 @@ if (now.getUTCHours() >= 0) {
align-items: center; align-items: center;
} }
.message-author-pfp { .message-author-avatar {
margin-right: 1dvw; margin-right: 1dvw;
width: 3em; width: 3em;
} }
@ -79,4 +92,15 @@ if (now.getUTCHours() >= 0) {
font-size: small; font-size: small;
color: rgb(150, 150, 150); color: rgb(150, 150, 150);
} }
.message-date:hover {
cursor: default;
}
/*
.message-date-tooltip {
height: 20px;;
width: 20px;
}
*/
</style> </style>

122
components/MessageArea.vue Normal file
View file

@ -0,0 +1,122 @@
<template>
<div id="message-area">
<div id="messages">
<Message v-for="message of messages" :username="displayName" :text="message.message"
:timestamp="uuidToTimestamp(message.uuid)" format="12" />
</div>
<div id="message-box">
<form id="message-form" @submit="sendMessage">
<input v-model="messageInput" type="text" name="message-input" id="message-box-input">
<button type="submit">
<Icon name="lucide:send" />
</button>
</form>
</div>
</div>
</template>
<script lang="ts" setup>
import type { MessageResponse } from '~/types/interfaces';
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number, reverse?: boolean }>();
const messagesRes: MessageResponse[] | undefined = await fetchWithApi(
`${props.channelUrl}/messages`,
{ query: { "amount": props.amount ?? 100, "offset": props.offset ?? 0 } }
);
if (messagesRes && props.reverse) {
messagesRes.reverse();
}
const messages = ref(messagesRes);
const { fetchUser } = useAuth();
const user = await fetchUser();
const displayName = user!.display_name ?? user!.username
const messageInput = ref<string>();
const accessToken = useCookie("access_token").value;
const apiBase = useCookie("api_base").value;
const { refresh } = useAuth();
let ws: WebSocket;
if (accessToken && apiBase) {
console.log("channel url:", `${apiBase.replace("http", "ws")}/${props.channelUrl}/socket`);
console.log("access token:", accessToken);
do {
console.log("Trying to connect to channel WebSocket...");
ws = new WebSocket(`${apiBase.replace("http", "ws").replace("3000", "8080")}/${props.channelUrl}/socket`,
["Authorization", accessToken]
);
if (ws) break;
await sleep(10000);
} while (!ws);
ws.addEventListener("open", (event) => {
console.log("WebSocket connected!");
});
ws.addEventListener("message", (event) => {
console.log("event data:", event.data);
messages.value?.push(
JSON.parse(event.data)
)
});
} else {
await refresh();
}
function sendMessage(e: Event) {
e.preventDefault();
const text = messageInput.value;
console.log("text:", text);
if (text) {
ws.send(text);
messageInput.value = "";
console.log("MESSAGE SENT!!!");
}
}
</script>
<style scoped>
#message-area {
padding-top: 3dvh;
}
#message-area {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 1dvw;
padding-right: 1dvw;
overflow: hidden;
}
#message-box {
border: 1px solid rgb(70, 70, 70);
margin-bottom: 1dvh;
height: 7%;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
#message-form {
height: 50%;
width: 100%;
}
#message-input {
width: 100%;
}
#messages {
overflow-y: scroll;
}
</style>

100
composables/auth.ts Normal file
View file

@ -0,0 +1,100 @@
import type { UserResponse } from "~/types/interfaces";
export const useAuth = () => {
const accessToken = useCookie("access_token");
const user = useState<UserResponse | null>("user", () => null);
async function clearAuth() {
accessToken.value = null;
user.value = null;
}
async function register(username: string, email: string, password: string) {
const hashedPass = await hashPassword(password);
const res = await fetchWithApi("/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 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) {
console.log("password:", password);
console.log("access:", accessToken.value);
const hashedPass = await hashPassword(password);
console.log("hashed");
const res = await fetchWithApi("/auth/revoke", {
method: "POST",
body:
{
password: hashedPass, device_name: "Linux Laptop"
}
});
clearAuth();
}
async function revoke() {
clearAuth();
}
async function refresh() {
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() {
if (!accessToken.value) return;
const res = await fetchWithApi("/users/me") as UserResponse;
user.value = res;
return user.value;
}
async function getUser() {
if (!accessToken) return;
if (!user.value) {
await fetchUser();
}
return user.value;
}
return {
accessToken,
register,
login,
logout,
revoke,
refresh,
getUser,
fetchUser,
user
}
}

View file

@ -1,40 +1,133 @@
<template> <template>
<div id="main-container"> <div id="root-container" style="margin-top: 5dvh;">
<div id="auth-form-container"> <Loading v-if="!mounted" />
<div v-else id="main-container">
<div v-if="!instanceUrl">
<div v-if="instanceError" style="color: red;">
{{ instanceError }}
</div>
<form @submit="selectInstance">
<div>
<label for="instance-url">Instance URL</label>
<br>
<input type="url" name="instance-url" id="instance-url" required v-model="instanceUrlInput">
</div>
<div>
<button type="submit">Next</button>
</div>
</form>
</div>
<div v-else id="auth-form-container">
<slot /> <slot />
</div> </div>
<div v-if="accessToken"> <div v-if="auth.accessToken.value">
You're logged in! You're logged in!
<button @click="logout">Log out</button> <form @submit="logout">
<div>
<label for="logout-password">Password</label>
<br>
<input type="password" name="logout-password" id="logout-password" v-model="form.password" required>
</div>
<div>
<button type="submit">Log out</button>
</div>
</form>
<div>
<button @click="refresh">Refresh</button>
</div>
<div>
<button @click="showUser">Show user</button>
</div>
<div>
<button @click="getUser">Get me</button>
</div>
</div> </div>
<div v-if="res">
Response:
<p>
{{ res }}
</p>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { FetchError } from 'ofetch';
const mounted = ref(false);
const redirectTo = useRoute().query.redirect_to;
const apiVersion = useRuntimeConfig().public.apiVersion; const apiVersion = useRuntimeConfig().public.apiVersion;
const instanceUrl = ref<string | null | undefined>(null);
const instanceUrlInput = ref<string>();
const instanceError = ref<string>();
const accessToken = useCookie("access_token"); const auth = useAuth();
const res = ref(); if (auth.accessToken.value) {
//navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
}
onMounted(() => {
mounted.value = true;
const cookie = useCookie("instance_url").value;
instanceUrl.value = cookie;
console.log(cookie);
console.log("set instance url to:", instanceUrl.value);
});
async function selectInstance(e: Event) {
e.preventDefault();
if (instanceUrlInput.value) {
const instanceUrlObj = new URL(`api/v${apiVersion}/stats`, instanceUrlInput.value.endsWith("/") ? instanceUrlInput.value : instanceUrlInput.value + "/");
try {
const res = await $fetch.raw(instanceUrlObj.href);
console.log("instance res:", res);
instanceError.value = "";
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) {
console.log("Status code:", error.response?.status);
if (error.response?.status == 404) {
instanceError.value = "An instance with that URL does not exist or is currently down.";
}
}
console.error("Error:", error);
}
}
}
const form = reactive({
password: ""
});
async function logout(e: Event) { async function logout(e: Event) {
e.preventDefault(); e.preventDefault();
accessToken.value = null; await auth.logout(form.password);
useCookie("refresh_token").value = null; console.log("logout");
res.value = await $fetch(`/api/v${apiVersion}/auth/revoke`, { credentials: "include" }); }
async function refresh(e: Event) {
e.preventDefault();
await auth.refresh();
console.log("refreshed");
}
async function getUser(e: Event) {
e.preventDefault();
await auth.getUser();
console.log("user:", auth.user.value);
}
async function showUser(e: Event) {
e.preventDefault();
console.log("user:", auth.user.value);
} }
</script> </script>
<style> <style>
#main-container { #root-container, #main-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View file

@ -1,59 +1,43 @@
<template> <template>
<div id="client-root"> <div id="client-root">
<div id="home" class=".homebar-item"> <div id="homebar">
<NuxtLink href="/web"> <div class="homebar-item">
<img src="~/assets/img/house.svg" alt="Home">
</NuxtLink>
</div>
<div id="current-channel" class=".homebar-item">
main bar main bar
</div> </div>
<div id="test" class=".homebar-item">test</div> </div>
<div id="test2" class=".homebar-item">test2</div> <div id="left-column">
<NuxtLink id="home-button" href="/">
<Icon name="lucide:house" class="white" size="2rem" />
</NuxtLink>
<div id="servers-list"> <div id="servers-list">
<NuxtLink v-for="server of servers" :href="'web' + server.url"> <NuxtLink v-for="server of servers" :href="`/servers/${server.uuid}`">
<img src="~/assets/img/server.svg" :alt="server.name"> <Icon name="lucide:server" class="white" size="2rem" />
</NuxtLink> </NuxtLink>
</div> </div>
<div id="channels-list" class="main-grid-row">
<Channel v-for="channel of channels" :name="channel.name" :href="`${useRoute().path}/${channel.id}`" />
</div> </div>
<div id="message-history" class="main-grid-row"> <slot />
<Message v-for="message of messages" :img="pfp" :username="message.author.username" :text="message.text"
:timestamp="message.timestamp" format="12" />
<div id="message-box" class="main-grid-row">
<input type="text" name="message-box-input" id="message-box-input">
</div> </div>
</div>
<div id="members-list">
<div class="member-item" v-for="member of members">
<img src="~/assets/img/tiger-head.svg" :alt="member.displayName" width="30dvw">
<span class="member-display-name">{{ member.displayName }}</span>
</div>
</div>
</div>
<NuxtPage />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import pfp from "~/assets/img/tiger-head.svg";
const servers = [ const servers = [
{ {
name: "Test", name: "Test",
url: "/servers/284103257435" uuid: "0197088b-e4e8-7033-8e6b-7ceb065e9acd"
}, },
{ {
name: "Test", name: "Test",
url: "/servers/284103257435" uuid: "0197088b-e4e8-7033-8e6b-7ceb065e9acd"
}, },
{ {
name: "Test", name: "Test",
url: "/servers/284103257435" uuid: "0197088b-e4e8-7033-8e6b-7ceb065e9acd"
} }
]; ];
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("servers:", servers);
const members = [ const members = [
{ {
id: "3287484395", id: "3287484395",
@ -93,59 +77,13 @@ const members = [
} }
]; ];
const messages = [ function sendMessage(e: Event) {
{ e.preventDefault();
author: { const textInput = document.getElementById("message-box-input") as HTMLInputElement;
id: "3287484395", const text = textInput.value;
username: "SauceyRed", console.log("MESSAGE SENT!!!");
avatar: "~/assets/img/tiger-head.svg" console.log("text:", text);
},
text: "hello gamers!",
timestamp: 1745948498000
},
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "yo what's up!",
timestamp: 1745948498000
},
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "how are you guys?",
timestamp: 1745948498000
},
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "im doing well",
timestamp: 1745948498000
} }
]
const channels = [
{
name: "#super-cool-channel",
id: "8gh9548rg44"
},
{
name: "#super-lame-channel",
id: "hgff45387hy"
},
{
name: "#secret-channel",
id: "g8f734h87gt"
},
]
</script> </script>
@ -155,20 +93,24 @@ const channels = [
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-columns: 1fr 4fr 18fr 4fr; grid-template-columns: 1fr 4fr 18fr 4fr;
grid-template-rows: 8dvh auto; grid-template-rows: 4dvh auto;
text-align: center; 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) { #client-root>div:nth-child(-n+4) {
border-bottom: 1px solid rgb(70, 70, 70); border-bottom: 1px solid rgb(70, 70, 70);
} }
#client-root div {
/* border: 1px solid cyan; */
}
#main-bar {}
#__nuxt { #__nuxt {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
@ -180,8 +122,8 @@ const channels = [
} }
#home { #home {
grid-column: 1; padding-left: .5dvw;
grid-row: 1; padding-right: .5dvw;
} }
#current-info { #current-info {
@ -194,64 +136,50 @@ const channels = [
grid-row: 1; 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 { .member-item {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.bottom-border { #message-history,
border-bottom: 1px solid rgb(70, 70, 70); #members-list {
padding-top: 3dvh;
} }
.left-border { #message-history {
border-left: 1px solid rgb(70, 70, 70); 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); border-right: 1px solid rgb(70, 70, 70);
} }
#main-content { #home-button {
display: grid; border-bottom: 1px solid rgb(70, 70, 70);
grid-template-rows: 1fr 15fr 30fr 2fr; padding-bottom: 1dvh;
text-align: center;
margin-left: 1dvw;
} }
#message-box { #servers-list {
border: 1px solid rgb(70, 70, 70); display: flex;
width: 100%; flex-direction: column;
margin-bottom: 1dvh; 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;
}
</style> </style>

View file

@ -2,18 +2,28 @@
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2024-11-01', compatibilityDate: '2024-11-01',
devtools: { enabled: true }, devtools: { enabled: true },
modules: ['@nuxt/eslint', '@nuxt/image', "@pinia/nuxt"], modules: ['@nuxt/eslint', '@nuxt/image', "@pinia/nuxt", "@nuxt/icon"],
app: { app: {
/* /*
Defines what prefix the client runs on Defines what prefix the client runs on
E.g.: baseURL set to "/web" would host at https://gorb.app/web E.g.: baseURL set to "/web" would host at https://gorb.app/web
Default is "/" (aka root), which hosts at https://gorb.app/ Default is "/" (aka root), which hosts at https://gorb.app/
*/ */
baseURL: "/", baseURL: "/"
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {
apiVersion: 1 apiVersion: 1
} }
},
/* nitro: {
devProxy: {
"/api": {
target: "http://localhost:8080/api",
changeOrigin: true,
prependPath: true,
ws: true
} }
}
} */
}) })

View file

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/eslint": "^1.3.0", "@nuxt/eslint": "^1.3.0",
"@nuxt/icon": "1.13.0",
"@nuxt/image": "1.10.0", "@nuxt/image": "1.10.0",
"@pinia/nuxt": "0.11.0", "@pinia/nuxt": "0.11.0",
"nuxt": "^3.17.0", "nuxt": "^3.17.0",
@ -20,9 +21,10 @@
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39", "packageManager": "pnpm@10.11.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@iconify-json/lucide": "^1.2.44",
"@types/node": "^22.15.3", "@types/node": "^22.15.3",
"eslint-config-prettier": "^10.1.2", "eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6" "eslint-plugin-prettier": "^5.2.6"

View file

@ -1,268 +0,0 @@
<template>
<div id="client-root">
<div id="home" class=".homebar-item">
<NuxtLink href="/web">
<img src="~/assets/img/house.svg" alt="Home">
</NuxtLink>
</div>
<div id="current-channel" class=".homebar-item">
main bar
</div>
<div id="test" class=".homebar-item">test</div>
<div id="test2" class=".homebar-item">test2</div>
<div id="servers-list">
<NuxtLink v-for="server of servers" :href="'web' + server.url">
<img src="~/assets/img/server.svg" :alt="server.name">
</NuxtLink>
</div>
<div id="channels-list" class="main-grid-row">
<Channel v-for="channel of channels" :name="channel.name" :href="`${useRoute().path}/${channel.id}`" />
</div>
<div id="message-history" class="main-grid-row">
<div id="messages">
<Message v-for="message of messages" :img="pfp" :username="message.author.username" :text="message.text"
:timestamp="message.timestamp" format="12" />
</div>
<div id="message-box" class="main-grid-row">
<form @submit="sendMessage">
<input type="text" name="message-box-input" id="message-box-input">
<button type="submit">
<img :src="checkmark" alt="Send" width="20">
</button>
</form>
</div>
</div>
<div id="members-list">
<div class="member-item" v-for="member of members">
<img src="~/assets/img/tiger-head.svg" :alt="member.displayName" width="30dvw">
<span class="member-display-name">{{ member.displayName }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import pfp from "~/assets/img/tiger-head.svg";
import checkmark from "~/assets/img/check-mark.png";
const servers = [
{
name: "Test",
url: "/servers/284103257435"
},
{
name: "Test",
url: "/servers/284103257435"
},
{
name: "Test",
url: "/servers/284103257435"
}
];
const members = [
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
}
];
const messages = [
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "hello gamers!",
timestamp: 1745948498000
},
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "yo what's up!",
timestamp: 1745948498000
},
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "how are you guys?",
timestamp: 1745948498000
},
{
author: {
id: "3287484395",
username: "SauceyRed",
avatar: "~/assets/img/tiger-head.svg"
},
text: "im doing well",
timestamp: 1745948498000
}
]
const channels = [
{
name: "#super-cool-channel",
id: "8gh9548rg44"
},
{
name: "#super-lame-channel",
id: "hgff45387hy"
},
{
name: "#secret-channel",
id: "g8f734h87gt"
},
]
function sendMessage(e: Event) {
e.preventDefault();
const textInput = document.getElementById("message-box-input") as HTMLInputElement;
const text = textInput.value;
console.log("MESSAGE SENT!!!");
console.log("text:", text);
}
</script>
<style>
#client-root {
/* border: 1px solid white; */
height: 100%;
display: grid;
grid-template-columns: 1fr 4fr 18fr 4fr;
grid-template-rows: 8dvh auto;
text-align: center;
}
#client-root>div:nth-child(-n+4) {
border-bottom: 1px solid rgb(70, 70, 70);
}
#__nuxt {
display: flex;
flex-flow: column;
height: 100%;
}
.grid-column {
padding-top: 1dvh;
}
#home {
grid-column: 1;
grid-row: 1;
}
#current-info {
grid-column: 2;
grid-row: 1;
}
#test {
grid-column: 3;
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);
}
.left-border {
border-left: 1px solid rgb(70, 70, 70);
}
.right-border {
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;
}
#message-box {
border: 1px solid rgb(70, 70, 70);
width: 100%;
margin-bottom: 1dvh;
height: 5%;
text-align: center;
}
#message-box-input {
width: 80%;
height: 100%;
}
#servers-list,
#channels-list,
#message-history,
#members-list {
margin-top: 3dvh;
}
#message-history {
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>

View file

@ -1,6 +1,6 @@
<template> <template>
<NuxtLayout> <NuxtLayout>
<form @submit="login"> <form @submit="formLogin">
<div> <div>
<label for="username">Username/Email</label> <label for="username">Username/Email</label>
<br> <br>
@ -18,12 +18,6 @@
<div> <div>
Don't have an account? <NuxtLink href="/register">Register</NuxtLink> one! Don't have an account? <NuxtLink href="/register">Register</NuxtLink> one!
</div> </div>
<div v-if="response">
Response:
<p>
{{ response }}
</p>
</div>
</NuxtLayout> </NuxtLayout>
</template> </template>
@ -38,44 +32,14 @@ const form = reactive({
password: "", password: "",
}); });
const response = ref();
//const authStore = useAuthStore(); //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); const { login } = useAuth();
console.log("refresh token:", refreshToken.value);
onMounted(() => { async function formLogin(e: Event) {
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) {
e.preventDefault(); e.preventDefault();
console.log("Sending login data"); console.log("Sending login data");
const hashedPass = await hashPassword(form.password); await login(form.username, form.password, "Linux Laptop");
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;
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
} }

View file

@ -24,7 +24,7 @@
<input type="password" name="password" id="password" v-model="form.password"> <input type="password" name="password" id="password" v-model="form.password">
</div> </div>
<div> <div>
<label for="repeat-password">Password</label> <label for="repeat-password">Repeat password</label>
<br> <br>
<input type="password" name="repeat-password" id="repeat-password" v-model="form.repeatPassword"> <input type="password" name="repeat-password" id="repeat-password" v-model="form.repeatPassword">
</div> </div>
@ -35,12 +35,6 @@
<div> <div>
Already have an account? <NuxtLink href="/login">Log in</NuxtLink>! Already have an account? <NuxtLink href="/login">Log in</NuxtLink>!
</div> </div>
<div v-if="response">
Response:
<p>
{{ response }}
</p>
</div>
</NuxtLayout> </NuxtLayout>
</template> </template>
@ -57,8 +51,6 @@ const form = reactive({
repeatPassword: "" repeatPassword: ""
}); });
const response = ref();
/* /*
const errorMessages = reactive({ const errorMessages = reactive({
username: { username: {
@ -81,18 +73,11 @@ const errorMessages = reactive({
*/ */
//const authStore = useAuthStore(); //const authStore = useAuthStore();
const accessToken = useCookie("access_token"); const auth = useAuth();
const refreshToken = useCookie("refresh_token");
const redirectTo = useRoute().query.redirect_to; const redirectTo = useRoute().query.redirect_to;
console.log("access token:", accessToken.value);
console.log("refresh token:", refreshToken.value);
onMounted(() => { onMounted(() => {
console.log("accessToken:", accessToken.value); if (auth.accessToken.value) {
console.log("refreshToken:", refreshToken.value);
if (accessToken.value) {
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
} }
}); });
@ -135,19 +120,7 @@ const apiVersion = useRuntimeConfig().public.apiVersion;
async function register(e: Event) { async function register(e: Event) {
e.preventDefault(); e.preventDefault();
console.log("Sending registration data"); console.log("Sending registration data");
const hashedPass = await hashPassword(form.password); await auth.register(form.username, form.email, 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;
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
} }

View file

@ -1,13 +1,144 @@
<template> <template>
<div> <NuxtLayout name="client">
<div id="middle-left-column" class="main-grid-row">
<div id="server-title">
<h3>
{{ server?.name }}
<span>
<button @click="showServerSettings">
<Icon name="lucide:settings" />
</button>
</span>
<span>
<button @click="toggleInvitePopup">
<Icon name="lucide:share-2" />
</button>
</span>
<InvitePopup v-if="showInvitePopup" />
</h3>
</div> </div>
<div id="channels-list">
<Channel v-for="channel of channels" v-if="route.params.channelId == channel?.uuid" :name="channel.name"
:href="route.path" :current="true" />
<Channel v-for="channel of channels" v-else :name="channel.name"
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
</div>
</div>
<MessageArea :channel-url="channelUrlPath" />
<div id="members-list">
<div class="member-item" v-for="member of members">
<img v-if="member.avatar" :src="member.avatar" :alt="member.displayName" height="30" />
<Icon v-else name="lucide:user" size="30" />
<span class="member-display-name">{{ member.displayName }}</span>
</div>
</div>
</NuxtLayout>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const route = useRoute();
const server: GuildResponse | undefined = await fetchWithApi(`servers/${route.params.serverId}`);
const channels: ChannelResponse[] | undefined = await fetchWithApi(
`servers/${route.params.serverId}/channels`
);
const channel: ChannelResponse | undefined = await fetchWithApi(
route.path
);
const channelUrlPath = `servers/${route.params.serverId}/channels/${route.params.channelId}`;
console.log("channel:", channel);
const showInvitePopup = ref(false);
import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("servers:", servers);
const members = [
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
}
];
function showServerSettings() { }
function toggleInvitePopup(e: Event) {
e.preventDefault();
showInvitePopup.value = !showInvitePopup.value;
}
</script> </script>
<style> <style>
.member-item {
display: flex;
justify-content: center;
align-items: center;
margin-top: .5em;
margin-bottom: .5em;
}
#members-list {
padding-top: 3dvh;
}
#middle-left-column {
padding-left: 1dvw;
padding-right: 1dvw;
border-right: 1px solid rgb(70, 70, 70);
}
#members-list {
padding-left: 1dvw;
padding-right: 1dvw;
border-left: 1px solid rgb(70, 70, 70);
}
</style> </style>

View file

@ -1,10 +1,12 @@
<template> <template>
<div> <div>
Hello
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const server = await fetchWithApi(`/servers/${useRoute().params.serverId}`);
console.log("server:", server);
</script> </script>

207
pnpm-lock.yaml generated
View file

@ -11,6 +11,9 @@ importers:
'@nuxt/eslint': '@nuxt/eslint':
specifier: ^1.3.0 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)) 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': '@nuxt/image':
specifier: 1.10.0 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) 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 specifier: ^4.5.1
version: 4.5.1(vue@3.5.13(typescript@5.8.3)) version: 4.5.1(vue@3.5.13(typescript@5.8.3))
devDependencies: devDependencies:
'@iconify-json/lucide':
specifier: ^1.2.44
version: 1.2.44
'@types/node': '@types/node':
specifier: ^22.15.3 specifier: ^22.15.3
version: 22.15.3 version: 22.15.3
@ -55,6 +61,9 @@ packages:
'@antfu/install-pkg@1.0.0': '@antfu/install-pkg@1.0.0':
resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} 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': '@apidevtools/json-schema-ref-parser@11.9.3':
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
@ -578,6 +587,23 @@ packages:
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
engines: {node: '>=18.18'} 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': '@ioredis/commands@1.2.0':
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
@ -704,6 +730,11 @@ packages:
peerDependencies: peerDependencies:
vite: '>=6.0' 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': '@nuxt/devtools-wizard@2.4.0':
resolution: {integrity: sha512-3/5S2zpl79rE1b/lh8M/2lDNsYiYIXXHZmCwsYPuFJA6DilLQo/VY44oq6cY0Q1up32HYB3h1Te/q3ELbsb+ag==} resolution: {integrity: sha512-3/5S2zpl79rE1b/lh8M/2lDNsYiYIXXHZmCwsYPuFJA6DilLQo/VY44oq6cY0Q1up32HYB3h1Te/q3ELbsb+ag==}
hasBin: true hasBin: true
@ -740,6 +771,9 @@ packages:
vite-plugin-eslint2: vite-plugin-eslint2:
optional: true optional: true
'@nuxt/icon@1.13.0':
resolution: {integrity: sha512-Sft1DZj/U5Up60DMnhp+hRDNDXRkMhwHocxgDkDkAPBxqR8PRyvzxcMIy3rjGMu0s+fB6ZdLs6vtaWzjWuerQQ==}
'@nuxt/image@1.10.0': '@nuxt/image@1.10.0':
resolution: {integrity: sha512-/B58GeEmme7bkmQUrXzEw8P9sJb9BkMaYZqLDtq8ZdDLEddE3P4nVya8RQPB+p4b7EdqWajpPqdy1A2ZPLev/A==} resolution: {integrity: sha512-/B58GeEmme7bkmQUrXzEw8P9sJb9BkMaYZqLDtq8ZdDLEddE3P4nVya8RQPB+p4b7EdqWajpPqdy1A2ZPLev/A==}
engines: {node: '>=18.20.6'} engines: {node: '>=18.20.6'}
@ -748,10 +782,18 @@ packages:
resolution: {integrity: sha512-+aS+Enqqo2qSbyl0APPPxX8BPYsaRcZ8dFRbpCOfK38lv2ckoHKCWNkT8L/7q2w+1pjNZaxlUoW9Mku1vdEb/A==} resolution: {integrity: sha512-+aS+Enqqo2qSbyl0APPPxX8BPYsaRcZ8dFRbpCOfK38lv2ckoHKCWNkT8L/7q2w+1pjNZaxlUoW9Mku1vdEb/A==}
engines: {node: '>=18.12.0'} 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': '@nuxt/schema@3.17.0':
resolution: {integrity: sha512-BwHD1NBtZRlk+qPZYvNzzdp7MG8s4i5gmTQ+12hbxc9x09osB9RivAU2ekwMMLfykx90wDszDu0DJ5Zec4Svgg==} resolution: {integrity: sha512-BwHD1NBtZRlk+qPZYvNzzdp7MG8s4i5gmTQ+12hbxc9x09osB9RivAU2ekwMMLfykx90wDszDu0DJ5Zec4Svgg==}
engines: {node: ^14.18.0 || >=16.10.0} 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': '@nuxt/telemetry@2.6.6':
resolution: {integrity: sha512-Zh4HJLjzvm3Cq9w6sfzIFyH9ozK5ePYVfCUzzUQNiZojFsI2k1QkSBrVI9BGc6ArKXj/O6rkI6w7qQ+ouL8Cag==} resolution: {integrity: sha512-Zh4HJLjzvm3Cq9w6sfzIFyH9ozK5ePYVfCUzzUQNiZojFsI2k1QkSBrVI9BGc6ArKXj/O6rkI6w7qQ+ouL8Cag==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
@ -1409,6 +1451,9 @@ packages:
'@vue/shared@3.5.13': '@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vue/shared@3.5.14':
resolution: {integrity: sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==}
'@whatwg-node/disposablestack@0.0.6': '@whatwg-node/disposablestack@0.0.6':
resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -1650,6 +1695,14 @@ packages:
magicast: magicast:
optional: true 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: cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2546,6 +2599,10 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
globals@15.15.0:
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
engines: {node: '>=18'}
globals@16.0.0: globals@16.0.0:
resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2873,6 +2930,9 @@ packages:
knitwork@1.2.0: knitwork@1.2.0:
resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} 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: kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
@ -3917,6 +3977,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
send@1.2.0: send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
@ -4287,6 +4352,10 @@ packages:
resolution: {integrity: sha512-8jL3T+FKDg+qLFX55X9j92uFRqH5vWrNlf/eJb5IQlQB5q5wjooXQDXP1ulhJJQHbosBmlKhBo/ZVS5jHlcJGA==} resolution: {integrity: sha512-8jL3T+FKDg+qLFX55X9j92uFRqH5vWrNlf/eJb5IQlQB5q5wjooXQDXP1ulhJJQHbosBmlKhBo/ZVS5jHlcJGA==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
unimport@5.0.1:
resolution: {integrity: sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==}
engines: {node: '>=18.12.0'}
unixify@1.0.0: unixify@1.0.0:
resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -4684,6 +4753,8 @@ snapshots:
package-manager-detector: 0.2.11 package-manager-detector: 0.2.11
tinyexec: 0.3.2 tinyexec: 0.3.2
'@antfu/utils@8.1.1': {}
'@apidevtools/json-schema-ref-parser@11.9.3': '@apidevtools/json-schema-ref-parser@11.9.3':
dependencies: dependencies:
'@jsdevtools/ono': 7.1.3 '@jsdevtools/ono': 7.1.3
@ -5143,6 +5214,34 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {} '@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': {} '@ioredis/commands@1.2.0': {}
'@isaacs/cliui@8.0.2': '@isaacs/cliui@8.0.2':
@ -5377,6 +5476,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - 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': '@nuxt/devtools-wizard@2.4.0':
dependencies: dependencies:
consola: 3.4.2 consola: 3.4.2
@ -5492,6 +5600,28 @@ snapshots:
- utf-8-validate - utf-8-validate
- vite - 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)': '@nuxt/image@1.10.0(@netlify/blobs@8.2.0)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)':
dependencies: dependencies:
'@nuxt/kit': 3.17.0(magicast@0.3.5) '@nuxt/kit': 3.17.0(magicast@0.3.5)
@ -5555,6 +5685,33 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - 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': '@nuxt/schema@3.17.0':
dependencies: dependencies:
consola: 3.4.2 consola: 3.4.2
@ -5562,6 +5719,14 @@ snapshots:
pathe: 2.0.3 pathe: 2.0.3
std-env: 3.9.0 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)': '@nuxt/telemetry@2.6.6(magicast@0.3.5)':
dependencies: dependencies:
'@nuxt/kit': 3.17.0(magicast@0.3.5) '@nuxt/kit': 3.17.0(magicast@0.3.5)
@ -6270,6 +6435,8 @@ snapshots:
'@vue/shared@3.5.13': {} '@vue/shared@3.5.13': {}
'@vue/shared@3.5.14': {}
'@whatwg-node/disposablestack@0.0.6': '@whatwg-node/disposablestack@0.0.6':
dependencies: dependencies:
'@whatwg-node/promise-helpers': 1.3.1 '@whatwg-node/promise-helpers': 1.3.1
@ -6520,6 +6687,23 @@ snapshots:
optionalDependencies: optionalDependencies:
magicast: 0.3.5 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: {} cac@6.7.14: {}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
@ -7505,6 +7689,8 @@ snapshots:
globals@14.0.0: {} globals@14.0.0: {}
globals@15.15.0: {}
globals@16.0.0: {} globals@16.0.0: {}
globby@11.1.0: globby@11.1.0:
@ -7824,6 +8010,8 @@ snapshots:
knitwork@1.2.0: {} knitwork@1.2.0: {}
kolorist@1.8.0: {}
kuler@2.0.0: {} kuler@2.0.0: {}
lambda-local@2.2.0: lambda-local@2.2.0:
@ -9030,6 +9218,8 @@ snapshots:
semver@7.7.1: {} semver@7.7.1: {}
semver@7.7.2: {}
send@1.2.0: send@1.2.0:
dependencies: dependencies:
debug: 4.4.0 debug: 4.4.0
@ -9475,6 +9665,23 @@ snapshots:
unplugin: 2.3.2 unplugin: 2.3.2
unplugin-utils: 0.2.4 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: unixify@1.0.0:
dependencies: dependencies:
normalize-path: 2.1.1 normalize-path: 2.1.1

6
types.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
export interface GorbStats {
accounts: number,
uptime: number,
version: string,
build_number: string
}

54
types/interfaces.ts Normal file
View file

@ -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
}

71
utils/fetchWithApi.ts Normal file
View file

@ -0,0 +1,71 @@
import type { NitroFetchRequest, NitroFetchOptions } from "nitropack";
export default async <T>(path: string, options: NitroFetchOptions<string> = {}) => {
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<T>(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);
}
}

3
utils/sleep.ts Normal file
View file

@ -0,0 +1,3 @@
export default async (ms: number): Promise<NodeJS.Timeout> => {
return new Promise(resolve => setTimeout(resolve, ms));
}

6
utils/uuidToTimestamp.ts Normal file
View file

@ -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;
}