Skip to content

Commit

Permalink
adding session info to SSE for unique streams
Browse files Browse the repository at this point in the history
  • Loading branch information
SelfhostedPro committed Mar 4, 2024
1 parent 78f532c commit 5d80c40
Show file tree
Hide file tree
Showing 14 changed files with 144 additions and 59 deletions.
61 changes: 42 additions & 19 deletions components/auth/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -1,47 +1,70 @@
<template>
<v-card-text>
<v-form fast-fail>
<v-text-field v-model="username.value.value" label="username" append-inner-icon="mdi-account-circle"
@keyup.enter="submit" />
<v-text-field v-model="password.value.value" label="password" type="password" append-inner-icon="mdi-shield-key"
@keyup.enter="submit" />
<v-text-field
v-model="username.value.value"
label="username"
append-inner-icon="mdi-account-circle"
@keyup.enter="submit"
/>
<v-text-field
v-model="password.value.value"
label="password"
type="password"
append-inner-icon="mdi-shield-key"
@keyup.enter="submit"
/>
</v-form>
<v-spacer />
<v-btn block color="primary" elevation="4" @click="submit">
submit
</v-btn>
<v-btn block color="primary" elevation="4" @click="submit"> submit </v-btn>
<span>{{ error }}</span>
</v-card-text>
</template>

<script setup lang="ts">
import { LoginUserFormSchema } from '~/types/auth';
import { LoginUserFormSchema } from "~/types/auth";
const { handleSubmit, handleReset } = useForm({
initialValues: {
username: "",
password: "",
},
validationSchema: toTypedSchema(LoginUserFormSchema),
keepValuesOnUnmount: true
})
keepValuesOnUnmount: true,
});
const username = useField('username')
const password = useField('password')
const username = useField("username");
const password = useField("password");
const error = ref<string | null>(null);
const user = useUser();
const submit = handleSubmit(async (values) => {
try {
await $fetch("/api/auth/login", {
method: "POST",
body: values
body: values,
});
await navigateTo('/')
const data = await useRequestFetch()("/api/auth/me");
if (data) {
user.value = data;
}
await navigateTo("/");
} catch (err) {
error.value = JSON.stringify(err)
error.value = JSON.stringify(err);
}
})
});
</script>
onMounted(async () => {
const config = useClientConfig();
if (config.value?.auth === false) await navigateTo("/");
try {
const data = await useRequestFetch()("/api/auth/me");
if (data) {
user.value = data;
await navigateTo("/");
}
} catch (e) {
/* Don't do anything here, just surpress duplicate 401 error notification */
}
});
</script>
1 change: 1 addition & 0 deletions components/auth/RegisterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const onSubmit = handleSubmit(async (values) => {
method: "POST",
body: values
});
navigateTo('/')
} catch (err) {
error.value = JSON.stringify(err)
}
Expand Down
74 changes: 55 additions & 19 deletions components/nav/AppBar.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
<template>
<v-app-bar class="app-bar" elevation="8">
<template #prepend>
<v-app-bar-nav-icon v-if="smAndDown" color="grey-lighten-5" variant="text" @click.stop="drawer = !drawer" />
<v-app-bar-nav-icon
v-if="smAndDown"
color="grey-lighten-5"
variant="text"
@click.stop="drawer = !drawer"
/>
</template>
<template #append>
<v-btn v-if="user" variant="elevated" color="surface" @click.stop="logout">
<v-btn
v-if="user"
variant="elevated"
color="surface"
@click.stop="logout"
>
logout
</v-btn>
</template>
<v-app-bar-title>
<v-img max-height="30" class="d-flex align-center mx-auto text-logo" src="~/assets/icons/yacht/text.svg"
style="filter: brightness(5)" />
<v-img
max-height="30"
class="d-flex align-center mx-auto text-logo"
src="~/assets/icons/yacht/text.svg"
style="filter: brightness(5)"
/>
</v-app-bar-title>
</v-app-bar>
<v-navigation-drawer v-model="drawer" app location="right" temporary>
<v-list nav dense>
<div v-for="(link, i) in links" :key="i">
<v-list-item v-if="!link.subLinks" :to="link.to" :title="link.text" :prepend-icon="link.icon" exact
class="mt-1" />
<v-list-group v-else :key="link.text" :prepend-icon="link.icon" :value="false">
<v-list-item
v-if="!link.subLinks"
:to="link.to"
:title="link.text"
:prepend-icon="link.icon"
exact
class="mt-1"
/>
<v-list-group
v-else
:key="link.text"
:prepend-icon="link.icon"
:value="false"
>
<template #activator="{ props }">
<v-list-item v-bind="props" :title="link.text" :prepend-icon="link.icon" />
<v-list-item
v-bind="props"
:title="link.text"
:prepend-icon="link.icon"
/>
</template>
<v-list-item v-for="sublink in link.subLinks" :key="sublink.text" :to="sublink.to" :title="sublink.text"
:prepend-icon="sublink.icon" exact class="mb-1" />
<v-list-item
v-for="sublink in link.subLinks"
:key="sublink.text"
:to="sublink.to"
:title="sublink.text"
:prepend-icon="sublink.icon"
exact
class="mb-1"
/>
</v-list-group>
<v-divider />
</div>
Expand All @@ -32,21 +68,21 @@
</template>

<script lang="ts" setup>
import { useDisplay } from 'vuetify'
const user = useUser()
defineProps(['links'])
const drawer = ref(false)
const { smAndDown } = useDisplay()
import { useDisplay } from "vuetify";
const user = useUser();
defineProps(["links"]);
const drawer = ref(false);
const { smAndDown } = useDisplay();
const logout = async () => {
await $fetch('/api/auth/logout', { method: "POST" })
navigateTo('/login')
}
await $fetch("/api/auth/logout", { method: "POST" });
navigateTo("/login");
};
</script>

<style>
.app-bar {
color: rgba(var(--v-theme-primary), 0.9) !important;
background-color: rgba(var(--v-theme-primary), 0.9) !important;
}
</style>
</style>
5 changes: 3 additions & 2 deletions middleware/2.auth.global.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
export default defineNuxtRouteMiddleware(async (from, to) => {
// console.log(`${from.path} => ${to.path}`)
const config = useClientConfig()
if (
process.client
|| config.value?.auth === false
|| from.path === '/login'
) return
else {
if (from.path.startsWith('/login')) console.log("You should never see this")
const user = useUser();
try {
const data = await useRequestFetch()("/api/auth/me");
if (data) {
user.value = data;
}
console.log(data)
} catch (e) {
if (config.value?.auth === false) return
useToast({ title: 'Authentication Error', message: 'not able to fetch user information. Please login again.', level: 'error', dedupe: false })
return await navigateTo({ path: '/login' }, { replace: true })
}
Expand Down
3 changes: 2 additions & 1 deletion server/api/auth/login.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { db } from "~/server/utils/db";
import { generateId } from "lucia";
import { LoginUserFormSchema } from "~/types/auth";
import type { DBUser, LoginUserForm } from "~/types/auth";
import { authHooks } from "~/server/utils/auth";

export default eventHandler(async (event) => {
const { username, password }: LoginUserForm = await readValidatedBody(event, body => LoginUserFormSchema.parse(body))
Expand All @@ -23,7 +24,6 @@ export default eventHandler(async (event) => {
statusCode: 400
});
}
// db.select().from(userTable).where(eq(userTable.username, username)).get()
const existingUser = await db.selectFrom('user').selectAll().where('username', '==', username).executeTakeFirst() as
| DBUser
| undefined;
Expand All @@ -45,5 +45,6 @@ export default eventHandler(async (event) => {

const session = await lucia.createSession(existingUser.id, {});
appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize());
authHooks.callHook('login', session.id)
return { status: 'success', message: 'logged in' }
});
2 changes: 2 additions & 0 deletions server/api/auth/logout.post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { authHooks } from "~/server/utils/auth";

export default eventHandler(async (event) => {
if (!event.context.session) {
Expand All @@ -7,5 +8,6 @@ export default eventHandler(async (event) => {
}
await lucia.invalidateSession(event.context.session.id);
appendHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize());
await authHooks.callHook('logout', event.context.session.id)
return { status: 'success', message: 'logged out' }
});
4 changes: 2 additions & 2 deletions server/api/containers/[server]/[id]/logs.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => {
const containerId = event.context.params?.id
if (!server || !containerId) throw createError('Server or container not specified')

const { close } = useSSE(event, "sse:containerLogs")
getContainerLogs(server, containerId, close)
const { close, send } = useSSE(event, "sse:containerLogs")
getContainerLogs(server, containerId, close, send)
event.node.req.on("close", () => close())
})
4 changes: 2 additions & 2 deletions server/api/containers/stats.get.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getContainerStats } from "~/server/services/containers/streams"

export default defineEventHandler(async (event) => {
const { close } = useSSE(event, "sse:containerStats")
getContainerStats(close)
const { close, send } = useSSE(event, "sse:containerStats")
getContainerStats(close, send)
event.node.req.on("close", () => close())
})
2 changes: 1 addition & 1 deletion server/api/notifications.get.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Notification } from '~/types/notifications'

export default defineEventHandler(async (event) => {
const { send, close } = useSSE(event, "sse:notification")
const { send, close } = useSSE(event, "sse:notification", false)
const initNotification: Notification = { title: "Connected", message: "Connected to notifications.", level: 'info', from: "/notifications", timeout: 3000, dedupe: true }
send(() => (initNotification))

Expand Down
2 changes: 1 addition & 1 deletion server/api/progress.get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

export default defineEventHandler(async (event) => {
const { send, close } = useSSE(event, "sse:progress")
const { send, close } = useSSE(event, "sse:progress", false)
event.node.req.on("close", () => close())
})
9 changes: 5 additions & 4 deletions server/services/containers/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import debounce from "lodash/debounce"
import { useServers } from '../servers'
import type { ServerDict } from "~/types/servers"

export const getContainerStats = async (close: () => void) => {
export const getContainerStats = async (close: () => void, send: (callback: (id: number) => any) => void) => {
const servers = Object.entries(await useServers())
// Get all servers
servers.map(
Expand Down Expand Up @@ -49,6 +49,7 @@ export const getContainerStats = async (close: () => void) => {
containerStats.memory_stats.usage !== cachedStats.memory_stats.usage
) {
cachedStats = containerStats;
send(() => formatStats(containerStats))
sseHooks.callHook("sse:containerStats", formatStats(containerStats));
}
});
Expand All @@ -63,8 +64,7 @@ export const getContainerStats = async (close: () => void) => {
)
}


export const getContainerLogs = async (server: string, id: string, close: () => void) => {
export const getContainerLogs = async (server: string, id: string, close: () => void, send: (callback: (id: number) => any) => void) => {
const _server = await useServers().then((servers: ServerDict) => servers[server])
if (!_server) throw YachtError(new Error(`Server ${server} not found!`), '/services/containers/streams - getContainerLogs')
const container = _server.getContainer(id)
Expand All @@ -73,7 +73,8 @@ export const getContainerLogs = async (server: string, id: string, close: () =>
// Placeholder string to assemble chunks of data
const logStream = new StreamPassThrough()
logStream.on('data', (chunk) => {
sseHooks.callHook("sse:containerLogs", chunk.toString('utf8'));
send(() => chunk.toString('utf8'))
// sseHooks.callHook("sse:containerLogs", chunk.toString('utf8'));
});
// Start streaming logs
container.logs({
Expand Down
12 changes: 12 additions & 0 deletions server/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@ import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
import { rawDB } from "./db";
import type { DBUser } from "~/types/auth";

import { createHooks } from 'hookable'

const adapter = new BetterSqlite3Adapter(rawDB, {
user: 'user',
session: 'session'
});

// export interface AuthHooks {
// [hook: string]: <T, R>(data: T) => R | void
// }

interface AuthHooks {
login: (session: string) => void,
logout: (session: string) => void,
}

export const authHooks = createHooks<AuthHooks>()

export const lucia = new Lucia(adapter, {
sessionCookie: {
// IMPORTANT!
Expand Down
3 changes: 2 additions & 1 deletion server/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { sseHooks } from './sse'
import type { Notification } from '~/types/notifications'
import { type ConsolaOptions, createConsola, } from 'consola'

const logger = createConsola({ level: 3, formatOptions: { compact: true } })


export function useLog(tag?: string, options: Partial<ConsolaOptions> = {}) {
const logger = createConsola({ level: 3, formatOptions: { compact: true } })
return tag ? logger.create(options).withTag(tag) : logger
}

export const YachtLog = (event: Notification, error?: H3Error, quiet?: boolean,) => {
const logger = createConsola({ level: 3, formatOptions: { compact: true } })
logger.withTag(event['from'] || event.title || 'unknown')[event.level](`${event.message} ${error ? `\nError: ${JSON.stringify(error)}` : ''}`)
if (!quiet) {
sseHooks.callHook("sse:notification", event)
Expand Down
Loading

0 comments on commit 5d80c40

Please sign in to comment.