Compare commits

..

37 commits

Author SHA1 Message Date
9f289249a8 Merge pull request 'dev' (#1) from dev into main
All checks were successful
ci/woodpecker/manual/build-and-publish Pipeline was successful
Reviewed-on: #1
2025-05-29 03:01:50 +00:00
454633720b
feat: styled button a bit 2025-05-29 04:58:43 +02:00
e1578a1302
feat: disable input history in message text box 2025-05-29 04:58:28 +02:00
cedf3c201f
feat: set focus outline of all items to light gray 2025-05-29 04:52:53 +02:00
fb85a2a33f
feat: improve styling of message box area 2025-05-29 04:52:02 +02:00
94fa7dc1c0
feat: add top margin for message box 2025-05-29 04:30:24 +02:00
d80731a1c0
fix: props definition not allowing for null value on img
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-29 04:28:58 +02:00
bc94d22ef0
feat: switch to using user property of message object 2025-05-29 04:27:19 +02:00
46483c336a
feat: add user property to MessageResponse interface 2025-05-29 04:26:34 +02:00
b164abeda9
feat: implement autoscroll
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-29 04:13:02 +02:00
e0cc80230c
feat: add small additional check for refresh res variable 2025-05-29 04:12:42 +02:00
52eab190ee
fix: auth layout causing too much recursion due to having forgotten to remove <NuxtLayout> 2025-05-29 04:12:13 +02:00
25fc9e23c5
fix: redirect missing ? before query parameters 2025-05-29 04:11:03 +02:00
6658c28c85
feat: add utility to scroll to bottom of element 2025-05-29 04:09:44 +02:00
f44d67212b
feat: remove sleep I forgot to remove
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-29 02:38:18 +02:00
59c9acdc9e
feat: remove loading on this page in favor of layout-wide loading
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-29 02:36:44 +02:00
3b1581d950
feat: add loading on client layout 2025-05-29 02:36:10 +02:00
a324cc9300
style: fix indentation 2025-05-29 02:33:48 +02:00
a439f9481a
feat: manage loading state in auth middleware 2025-05-28 23:38:52 +02:00
379b85db4e
feat: remove Loading component from app.vue 2025-05-28 23:38:04 +02:00
751bdcfd9a
Merge branch 'main' into dev 2025-05-28 22:45:05 +02:00
a5f0e19716
feat: added check for if refresh returned access token 2025-05-28 22:40:26 +02:00
7ad2b6f299
style: fix indentation 2025-05-28 22:38:54 +02:00
6548f9329e
fix: redirect query set to undefined due to missing if statement check 2025-05-28 22:38:11 +02:00
e6bff0042d
feat: switched from setting height to 100% to using 100dvh 2025-05-28 22:37:25 +02:00
585e79dac6
feat: set baseURL back to root rather than /web 2025-05-28 22:36:14 +02:00
508af36704
feat: add Loading component to app.vue
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-28 02:30:18 +02:00
582435a3c7
feat: create custom error page, wip 2025-05-28 02:29:44 +02:00
1783928c36
feat: add / page 2025-05-28 02:29:26 +02:00
073e93cb57
feat: move Loading in auth layout to outside root container 2025-05-28 02:29:05 +02:00
e1cce87cdb
fix: always redirecting to / due to missing if statement in login page 2025-05-28 02:28:09 +02:00
8bfa17631c
feat: add support for redirect_to query param in register page 2025-05-28 02:27:22 +02:00
9f6490d744
feat: add verify-email page, wip 2025-05-28 02:26:17 +02:00
a15f85a082
fix: refresh returning 401 not properly logging you out of client 2025-05-28 02:24:54 +02:00
6a11108ec1
feat: add auth middleware 2025-05-28 02:18:29 +02:00
22ca450651
feat: make Loading component a spinning load circle 2025-05-28 02:17:54 +02:00
8140335518
feat: disable ssr to enable client-side rendering 2025-05-28 02:17:00 +02:00
18 changed files with 366 additions and 154 deletions

13
app.vue
View file

@ -1,7 +1,13 @@
<template> <template>
<NuxtPage /> <div>
<NuxtPage />
</div>
</template> </template>
<script lang="ts" setup>
</script>
<style> <style>
html, html,
body { body {
@ -9,10 +15,13 @@ body {
box-sizing: border-box; box-sizing: border-box;
color: rgb(190, 190, 190); color: rgb(190, 190, 190);
background-color: rgb(30, 30, 30); background-color: rgb(30, 30, 30);
height: 100%;
margin: 0; margin: 0;
} }
*:focus-visible {
outline: 1px solid rgb(150, 150, 150);
}
a { a {
color: aquamarine; color: aquamarine;
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div id="loading-container">
Loading... <Icon name="lucide:loader-circle" id="loading-circle" />
</div> </div>
</template> </template>
@ -8,6 +8,31 @@
</script> </script>
<style> <style scoped>
#loading-container {
position: fixed;
left: 50dvw;
top: 50dvh;
transform: translate(-50%, -50%);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#loading-circle {
animation-name: spin;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
font-size: 2rem;
}
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="message"> <div class="message">
<div> <div>
<img v-if="props.img" class="message-author-avatar" :src="img" :alt="username"> <img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username">
<Icon v-else name="lucide:user" class="message-author-avatar" /> <Icon v-else name="lucide:user" class="message-author-avatar" />
</div> </div>
<div class="message-data"> <div class="message-data">
@ -25,7 +25,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps<{ class?: string, img?: string, username: string, text: string, timestamp: number, format: "12" | "24" }>(); const props = defineProps<{ class?: string, img?: string | null, username: string, text: string, timestamp: number, format: "12" | "24" }>();
const messageDate = ref<string>(); const messageDate = ref<string>();
const showHover = ref(false); const showHover = ref(false);
@ -74,6 +74,7 @@ if (now.getUTCHours() >= 0) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1dvh; gap: 1dvh;
height: 100%;
} }
.message-author { .message-author {
@ -84,6 +85,7 @@ if (now.getUTCHours() >= 0) {
.message-author-avatar { .message-author-avatar {
margin-right: 1dvw; margin-right: 1dvw;
width: 3em; width: 3em;
border-radius: 50%;
} }
.author-username { .author-username {

View file

@ -1,13 +1,13 @@
<template> <template>
<div id="message-area"> <div id="message-area">
<div id="messages" ref="messagesElement"> <div id="messages" ref="messagesElement">
<Message v-for="message of messages" :username="displayNames[message.user_uuid]" :text="message.message" <Message v-for="message of messages" :username="message.user.display_name ?? message.user.username" :text="message.message"
:timestamp="uuidToTimestamp(message.uuid)" format="12" /> :timestamp="uuidToTimestamp(message.uuid)" :img="message.user.avatar" format="12" />
</div> </div>
<div id="message-box"> <div id="message-box">
<form id="message-form" @submit="sendMessage"> <form id="message-form" @submit="sendMessage">
<input v-model="messageInput" type="text" name="message-input" id="message-box-input"> <input v-model="messageInput" type="text" name="message-input" id="message-box-input" autocomplete="off">
<button type="submit"> <button id="submit-button" type="submit">
<Icon name="lucide:send" /> <Icon name="lucide:send" />
</button> </button>
</form> </form>
@ -17,7 +17,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MessageResponse } from '~/types/interfaces'; import type { MessageResponse } from '~/types/interfaces';
import fetchUser from '~/utils/fetchUser'; import scrollToBottom from '~/utils/scrollToBottom';
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number, reverse?: boolean }>(); const props = defineProps<{ channelUrl: string, amount?: number, offset?: number, reverse?: boolean }>();
@ -31,8 +31,6 @@ if (messagesRes && props.reverse) {
const messages = ref<MessageResponse[]>([]); const messages = ref<MessageResponse[]>([]);
const displayNames = ref<Record<string, string>>({});
const route = useRoute(); const route = useRoute();
const messageInput = ref<string>(); const messageInput = ref<string>();
@ -40,14 +38,6 @@ const messageInput = ref<string>();
const messagesElement = ref<HTMLDivElement>(); const messagesElement = ref<HTMLDivElement>();
if (messagesRes) messages.value = messagesRes; if (messagesRes) messages.value = messagesRes;
const displayNamesArr: Record<string, string> = {};
for (const message of messages.value) {
if (!displayNamesArr[message.user_uuid]) {
const displayName = await getDisplayName(message.user_uuid);
displayNamesArr[message.user_uuid] = displayName;
}
}
displayNames.value = displayNamesArr;
const accessToken = useCookie("access_token").value; const accessToken = useCookie("access_token").value;
const apiBase = useCookie("api_base").value; const apiBase = useCookie("api_base").value;
@ -71,23 +61,21 @@ ws.addEventListener("open", (event) => {
console.log("WebSocket connected!"); console.log("WebSocket connected!");
}); });
ws.addEventListener("message", (event) => { ws.addEventListener("message", async (event) => {
console.log("event data:", event.data); console.log("event data:", event.data);
messages.value?.push( messages.value?.push(
JSON.parse(event.data) JSON.parse(event.data)
); );
await nextTick();
if (messagesElement.value) {
console.log("scrolling to bottom");
scrollToBottom(messagesElement);
}
}); });
} else { } else {
await refresh(); await refresh();
} }
async function getDisplayName(memberId: string): Promise<string> {
//const user = await fetchMember((route.params.serverId as string), memberId);
const user = await fetchUser((route.params.serverId as string), memberId);
return user!.display_name ?? user!.username;
}
function sendMessage(e: Event) { function sendMessage(e: Event) {
e.preventDefault(); e.preventDefault();
const text = messageInput.value; const text = messageInput.value;
@ -100,7 +88,9 @@ function sendMessage(e: Event) {
} }
onMounted(async () => { onMounted(async () => {
messagesElement.value?.scrollTo({ top: messagesElement.value.scrollHeight }); if (messagesElement.value) {
scrollToBottom(messagesElement);
}
}); });
</script> </script>
@ -108,27 +98,38 @@ onMounted(async () => {
<style scoped> <style scoped>
#message-area { #message-area {
padding-top: 3dvh; display: grid;
} grid-template-columns: 1fr;
grid-template-rows: 8fr 1fr;
#message-area {
display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
padding-top: 3dvh;
padding-left: 1dvw; padding-left: 1dvw;
padding-right: 1dvw; padding-right: 1dvw;
overflow: hidden; overflow: hidden;
} }
#message-box { #message-box {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
border: 1px solid rgb(70, 70, 70); border: 1px solid rgb(70, 70, 70);
padding-bottom: 1dvh; padding-bottom: 1dvh;
padding-top: 1dvh; padding-top: 1dvh;
margin-bottom: 1dvh; margin-bottom: 1dvh;
margin-top: 1dvh;
} }
#message-input { #message-form {
width: 100%; display: flex;
justify-content: center;
height: 60%;
}
#message-box-input {
width: 80%;
background-color: rgb(50, 50, 50);
border: none;
} }
#messages { #messages {
@ -138,4 +139,14 @@ onMounted(async () => {
gap: 1dvh; gap: 1dvh;
} }
#submit-button {
background-color: inherit;
border: none;
color: white;
}
#submit-button:hover {
background-color: rgb(40, 40, 40);
}
</style> </style>

View file

@ -30,11 +30,11 @@ export const useAuth = () => {
{ {
username, password: hashedPass, device_name: "Linux Laptop" username, password: hashedPass, device_name: "Linux Laptop"
} }
}) as { access_token: string, refresh_token: string }; fetch }) as { access_token: string, refresh_token: string };
console.log("hi"); console.log("hi");
accessToken.value = res.access_token; accessToken.value = res.access_token;
console.log("access token:", accessToken.value); console.log("access token:", accessToken.value);
await fetchUser(); //await fetchUser();
} }
async function logout(password: string) { async function logout(password: string) {
@ -60,19 +60,21 @@ export const useAuth = () => {
async function refresh() { async function refresh() {
console.log("refreshing"); console.log("refreshing");
try { const res = await fetchWithApi("/auth/refresh", {
const res = await fetchWithApi("/auth/refresh", { method: "POST"
method: "POST" }) as any;
}) as { access_token: string }; console.log("finished refreshing:", res);
if (res && res.access_token) {
accessToken.value = res.access_token; accessToken.value = res.access_token;
console.log("set new access token"); console.log("set new access token");
} catch (error) { } else {
console.error("refresh error:", error); console.log("refresh didn't return access token");
} }
} }
async function fetchUser() { async function fetchUser() {
if (!accessToken.value) return; if (!accessToken.value) return;
console.log("fetchuser access token:", accessToken.value);
const res = await fetchWithApi("/users/me") as UserResponse; const res = await fetchWithApi("/users/me") as UserResponse;
user.value = res; user.value = res;
return user.value; return user.value;

29
error.vue Normal file
View file

@ -0,0 +1,29 @@
<template>
<div id="error-container">
<h2>{{ error?.statusCode }}</h2>
<p>{{ error?.message }}</p>
</div>
</template>
<script lang="ts" setup>
import type { NuxtError } from '#app';
const props = defineProps({
error: Object as () => NuxtError
});
if (props.error?.statusCode == 401) {
console.log("HELLO THERE");
clearError({ redirect: `/login?redirect_to=${useRoute().fullPath}` });
}
</script>
<style scoped>
#error-container {
text-align: center;
margin: auto;
}
</style>

View file

@ -1,7 +1,6 @@
<template> <template>
<div id="root-container" style="margin-top: 5dvh;"> <div id="root-container" style="margin-top: 5dvh;">
<Loading v-if="!mounted" /> <div id="main-container">
<div v-else id="main-container">
<div v-if="!instanceUrl"> <div v-if="!instanceUrl">
<div v-if="instanceError" style="color: red;"> <div v-if="instanceError" style="color: red;">
{{ instanceError }} {{ instanceError }}
@ -49,7 +48,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { FetchError } from 'ofetch'; import { FetchError } from 'ofetch';
const mounted = ref(false);
const redirectTo = useRoute().query.redirect_to; const redirectTo = useRoute().query.redirect_to;
const apiVersion = useRuntimeConfig().public.apiVersion; const apiVersion = useRuntimeConfig().public.apiVersion;
@ -64,7 +62,6 @@ if (auth.accessToken.value) {
} }
onMounted(() => { onMounted(() => {
mounted.value = true;
const cookie = useCookie("instance_url").value; const cookie = useCookie("instance_url").value;
instanceUrl.value = cookie; instanceUrl.value = cookie;
console.log(cookie); console.log(cookie);

View file

@ -1,5 +1,6 @@
<template> <template>
<div id="client-root"> <Loading v-show="loading" />
<div :class="{ hidden: loading, visible: !loading }" id="client-root">
<div id="homebar"> <div id="homebar">
<div class="homebar-item"> <div class="homebar-item">
main bar main bar
@ -21,6 +22,8 @@
<script lang="ts" setup> <script lang="ts" setup>
const loading = useState("loading", () => false);
const servers = [ const servers = [
{ {
name: "Test", name: "Test",
@ -90,11 +93,21 @@ function sendMessage(e: Event) {
<style> <style>
#client-root { #client-root {
/* border: 1px solid white; */ /* border: 1px solid white; */
height: 100%; height: 100dvh;
display: grid; display: grid;
grid-template-columns: 1fr 4fr 18fr 4fr; grid-template-columns: 1fr 4fr 18fr 4fr;
grid-template-rows: 4dvh auto; grid-template-rows: 4dvh auto;
text-align: center; text-align: center;
}
.hidden {
opacity: 0%;
}
.visible {
opacity: 100%;
transition-duration: 500ms;
} }
#homebar { #homebar {
@ -114,7 +127,6 @@ function sendMessage(e: Event) {
#__nuxt { #__nuxt {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
height: 100%;
} }
.grid-column { .grid-column {

24
middleware/auth.global.ts Normal file
View file

@ -0,0 +1,24 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
console.log("to.fullPath:", to.fullPath);
const loading = useState("loading");
const accessToken = useCookie("access_token").value;
if (["/login", "/register"].includes(to.path)) {
if (accessToken) {
return await navigateTo("/");
}
return;
};
if (!accessToken) {
loading.value = true;
console.log("set loading to true");
const { refresh } = useAuth();
console.log("hi");
await refresh();
const query = new URLSearchParams();
query.set("redirect_to", to.path);
loading.value = false;
console.log("set loading to false");
return await navigateTo("/login?" + (query ?? ""));
}
})

View file

@ -3,13 +3,14 @@ export default defineNuxtConfig({
compatibilityDate: '2024-11-01', compatibilityDate: '2024-11-01',
devtools: { enabled: true }, devtools: { enabled: true },
modules: ['@nuxt/eslint', '@nuxt/image', "@pinia/nuxt", "@nuxt/icon"], modules: ['@nuxt/eslint', '@nuxt/image', "@pinia/nuxt", "@nuxt/icon"],
ssr: false,
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: "/web", baseURL: "/",
head: { head: {
title: 'Gorb', title: 'Gorb',
// this is purely used to embed in that other chat app, and similar stuff // this is purely used to embed in that other chat app, and similar stuff

17
pages/index.vue Normal file
View file

@ -0,0 +1,17 @@
<template>
<NuxtLayout>
</NuxtLayout>
</template>
<script lang="ts" setup>
definePageMeta({
layout: "client"
});
</script>
<style>
</style>

View file

@ -1,46 +1,60 @@
<template> <template>
<NuxtLayout> <NuxtLayout>
<form @submit="formLogin"> <form @submit="formLogin">
<div> <div>
<label for="username">Username/Email</label> <label for="username">Username/Email</label>
<br> <br>
<input type="text" name="username" id="username" v-model="form.username"> <input type="text" name="username" id="username" v-model="form.username">
</div> </div>
<div> <div>
<label for="password">Password</label> <label for="password">Password</label>
<br> <br>
<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>
<button type="submit">Login</button> <button type="submit">Login</button>
</div> </div>
</form> </form>
<div> <div>
Don't have an account? <NuxtLink href="/register">Register</NuxtLink> one! Don't have an account? <NuxtLink :href="registerUrl">Register</NuxtLink> one!
</div> </div>
</NuxtLayout> </NuxtLayout>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
definePageMeta({ definePageMeta({
layout: "auth" layout: "auth"
}) })
const form = reactive({ const form = reactive({
username: "", username: "",
password: "", password: "",
}); });
//const authStore = useAuthStore(); //const authStore = useAuthStore();
const query = useRoute().query as Record<string, string>;
const searchParams = new URLSearchParams(query);
const registerUrl = `/register?${searchParams}`
const { login } = useAuth(); const { login } = useAuth();
async function formLogin(e: Event) { async function formLogin(e: Event) {
e.preventDefault(); e.preventDefault();
console.log("Sending login data"); console.log("Sending login data");
await login(form.username, form.password, "Linux Laptop"); try {
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); await login(form.username, form.password, "Linux Laptop");
console.log("logged in");
if (query.redirect_to) {
console.log("redirecting to:", query.redirect_to);
return await navigateTo(query.redirect_to);
}
return await navigateTo("/");
} catch (error) {
console.error("Error logging in:", error);
}
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
} }
</script> </script>

View file

@ -33,7 +33,7 @@
</div> </div>
</form> </form>
<div> <div>
Already have an account? <NuxtLink href="/login">Log in</NuxtLink>! Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
</div> </div>
</NuxtLayout> </NuxtLayout>
</template> </template>
@ -74,7 +74,9 @@ const errorMessages = reactive({
//const authStore = useAuthStore(); //const authStore = useAuthStore();
const auth = useAuth(); const auth = useAuth();
const redirectTo = useRoute().query.redirect_to; const query = useRoute().query as Record<string, string>;
const searchParams = new URLSearchParams(query);
const loginUrl = `/login?${searchParams}`
onMounted(() => { onMounted(() => {
if (auth.accessToken.value) { if (auth.accessToken.value) {
@ -120,7 +122,12 @@ 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");
await auth.register(form.username, form.email, form.password); try {
await auth.register(form.username, form.email, form.password);
return await navigateTo(query.redirect_to);
} catch (error) {
console.error("Error registering:", error);
}
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
} }

View file

@ -39,25 +39,20 @@
const route = useRoute(); const route = useRoute();
const server: GuildResponse | undefined = await fetchWithApi(`servers/${route.params.serverId}`); const loading = useState("loading");
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}`; const channelUrlPath = `servers/${route.params.serverId}/channels/${route.params.channelId}`;
console.log("channel:", channel); const server = ref<GuildResponse | undefined>();
const channels = ref<ChannelResponse[] | undefined>();
const channel = ref<ChannelResponse | undefined>();
const showInvitePopup = ref(false); const showInvitePopup = ref(false);
import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces"; import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[]; //const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("servers:", servers); //console.log("channelid: servers:", servers);
const members = [ const members = [
{ {
id: "3287484395", id: "3287484395",
@ -106,6 +101,23 @@ const members = [
} }
]; ];
onMounted(async () => {
loading.value = true;
console.log("channelid: set loading to true");
server.value = await fetchWithApi(`servers/${route.params.serverId}`);
channels.value = await fetchWithApi(
`servers/${route.params.serverId}/channels`
);
channel.value = await fetchWithApi(
route.path
);
console.log("channelid: channel:", channel);
loading.value = false;
console.log("channelid: set loading to false");
});
function showServerSettings() { } function showServerSettings() { }
function toggleInvitePopup(e: Event) { function toggleInvitePopup(e: Event) {

32
pages/verify-email.vue Normal file
View file

@ -0,0 +1,32 @@
<template>
<div id="container">
<div v-if="errorMessage">
{{ errorMessage }}
</div>
</div>
</template>
<script lang="ts" setup>
const errorMessage = ref();
const token = useRoute().query.token;
try {
const res = await fetchWithApi("/auth/verify-email", { query: { token } });
console.log("hi");
} catch (error) {
console.error("Error verifying email:", error);
errorMessage.value = error;
}
</script>
<style scoped>
#container {
text-align: center;
margin: auto;
}
</style>

View file

@ -32,10 +32,11 @@ export interface ChannelResponse {
} }
export interface MessageResponse { export interface MessageResponse {
uuid: string uuid: string,
channel_uuid: string channel_uuid: string,
user_uuid: string user_uuid: string,
message: string message: string,
user: UserResponse
} }
export interface InviteResponse { export interface InviteResponse {
@ -49,6 +50,6 @@ export interface UserResponse {
username: string, username: string,
display_name: string | null, display_name: string | null,
avatar: string | null, avatar: string | null,
email: string, email?: string,
email_verified: boolean email_verified?: boolean
} }

View file

@ -9,63 +9,74 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
path = path.slice(0, path.lastIndexOf("/")); path = path.slice(0, path.lastIndexOf("/"));
} }
console.log("formatted path:", path); console.log("formatted path:", path);
try { const accessToken = useCookie("access_token");
const accessToken = useCookie("access_token"); console.log("access token:", accessToken.value);
console.log("access token:", accessToken.value); const apiBase = useCookie("api_base").value;
const apiBase = useCookie("api_base").value; const apiVersion = useRuntimeConfig().public.apiVersion;
const apiVersion = useRuntimeConfig().public.apiVersion; console.log("heyoooo")
console.log("heyoooo") console.log("apiBase:", apiBase);
console.log("apiBase:", apiBase); if (!apiBase) {
if (!apiBase) { console.log("no api base");
console.log("no api base"); return;
return; }
} console.log("path:", path)
console.log("path:", path) const { revoke, refresh } = useAuth();
const { revoke, refresh } = useAuth(); console.log("access token 2:", accessToken.value);
console.log("access token 2:", accessToken.value);
let headers: HeadersInit = {};
let headers: HeadersInit = {};
if (accessToken.value) {
if (accessToken.value) { headers = {
headers = { ...options.headers,
...options.headers, "Authorization": `Bearer ${accessToken.value}`
"Authorization": `Bearer ${accessToken.value}` };
}; } else {
} else { headers = {
headers = { ...options.headers
...options.headers };
}; }
}
let reauthFailed = false;
let reauthFailed = false; while (!reauthFailed) {
while (!reauthFailed) { try {
try { console.log("fetching:", URL.parse(apiBase + path));
console.log("fetching:", URL.parse(apiBase + path)); const res = await $fetch<T>(URL.parse(apiBase + path)!.href, {
const res = await $fetch<T>(URL.parse(apiBase + path)!.href, { ...options,
...options, headers,
headers, credentials: "include"
credentials: "include" });
});
return res;
return res; } catch (error: any) {
} catch (error: any) { console.error("Error fetching resource");
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
if (!path.startsWith("/auth/refresh")) { console.log("Error status is 401");
try { if (!path.startsWith("/auth/refresh")) {
await refresh(); console.log("Path is not refresh endpoint");
} catch (error: any) { try {
if (error?.response?.status === 401) { console.log("Trying to refresh");
reauthFailed = true; await refresh();
await revoke(); console.log("Successfully refreshed token");
return; } catch (error: any) {
} console.log("Failed to refresh token");
if (error?.response?.status === 401) {
console.log("Refresh returned 401");
reauthFailed = true;
console.log("Revoking");
await revoke();
console.log("Redirecting to login");
await navigateTo("/login");
console.log("redirected");
return;
} }
} }
} else {
console.log("Path is refresh endpoint, throwing error");
throw error;
} }
throw error;
} }
console.log("throwing error");
throw error;
} }
} catch (error) {
console.error("error:", error);
} }
} }

6
utils/scrollToBottom.ts Normal file
View file

@ -0,0 +1,6 @@
export default (element: Ref<HTMLElement | undefined, HTMLElement | undefined>) => {
if (element.value) {
element.value.scrollTo({ top: element.value.scrollHeight });
return;
}
}