mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
ca groundwork
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop"
|
||||||
|
|
||||||
|
CLIENT_CERTIFICATES="./.data/ca"
|
||||||
|
|
||||||
|
GIANT_BOMB_API_KEY=""
|
||||||
1
.yarnrc
Normal file
1
.yarnrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
"@drop:registry" "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/"
|
||||||
4
app.vue
4
app.vue
@ -3,3 +3,7 @@
|
|||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
await updateUser();
|
||||||
|
</script>
|
||||||
|
|||||||
@ -36,4 +36,16 @@ $helvetica: (
|
|||||||
font-weight: $weight;
|
font-weight: $weight;
|
||||||
font-style: $style;
|
font-style: $style;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
src: url("/fonts/inter/InterVariable.ttf");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
src: url("/fonts/inter/InterVariable-Italic.ttf");
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
68
components/HeaderUserWidget.vue
Normal file
68
components/HeaderUserWidget.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<Menu as="div" class="relative inline-block">
|
||||||
|
<MenuButton>
|
||||||
|
<InlineWidget>
|
||||||
|
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||||
|
<img :src="userData.image" class="w-5 h-5 rounded-sm" />
|
||||||
|
<span class="ml-2 text-sm font-bold">{{ userData.name }}</span>
|
||||||
|
<ChevronDownIcon class="ml-3 h-4" />
|
||||||
|
</div>
|
||||||
|
</InlineWidget>
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<transition enter-active-class="transition ease-out duration-100"
|
||||||
|
enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
|
<MenuItems class="absolute right-0 top-10 z-10 w-56 origin-top-right focus:outline-none">
|
||||||
|
<PanelWidget class="flex-col gap-y-2">
|
||||||
|
<NuxtLink to="/id/me"
|
||||||
|
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800">
|
||||||
|
<div class="inline-flex items-center text-zinc-300">
|
||||||
|
<img :src="userData.image" class="w-5 h-5 rounded-sm" />
|
||||||
|
<span class="ml-2 text-sm font-bold">{{ userData.name }}</span>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<MenuItem v-for="(nav, navIdx) in navigation" v-slot="{ active }">
|
||||||
|
<NuxtLink :href="nav.route"
|
||||||
|
:class="[active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400', 'transition block px-4 py-2 text-sm']">
|
||||||
|
{{
|
||||||
|
nav.label }}</NuxtLink>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</PanelWidget>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/vue/16/solid';
|
||||||
|
import type { NavigationItem } from '../composables/types';
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
image: "https://avatars.githubusercontent.com/u/64579723?v=4",
|
||||||
|
name: "DecDuck",
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigation: NavigationItem[] = [
|
||||||
|
{
|
||||||
|
label: "Admin Dashboard",
|
||||||
|
route: "/admin",
|
||||||
|
prefix: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Account settings",
|
||||||
|
route: "/account",
|
||||||
|
prefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sign out",
|
||||||
|
route: "/signout",
|
||||||
|
prefix: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<template>
|
|
||||||
<InlineWidget>
|
|
||||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
|
||||||
<img :src="userData.image" class="w-5 h-5 rounded-sm" />
|
|
||||||
<span class="ml-2 -mb-1 text-sm font-bold">{{ userData.name }}</span>
|
|
||||||
<ChevronDownIcon class="ml-3 h-4" />
|
|
||||||
</div>
|
|
||||||
</InlineWidget>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ChevronDownIcon } from '@heroicons/vue/16/solid';
|
|
||||||
|
|
||||||
const userData = {
|
|
||||||
image: "https://avatars.githubusercontent.com/u/64579723?v=4",
|
|
||||||
name: "DecDuck",
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
31
components/LoadingButton.vue
Normal file
31
components/LoadingButton.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex h-9 items-center justify-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
|
>
|
||||||
|
<div v-if="props.loading" role="status">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="w-5 h-5 text-transparent animate-spin fill-white"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<span v-else> <slot /> </span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ loading: boolean }>();
|
||||||
|
</script>
|
||||||
5
components/PanelWidget.vue
Normal file
5
components/PanelWidget.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex rounded-sm px-2 py-2 bg-zinc-900 text-zinc-600">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -18,18 +18,18 @@
|
|||||||
<div class="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
<div class="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
||||||
<div class="md:grid md:grid-cols-2 md:gap-8">
|
<div class="md:grid md:grid-cols-2 md:gap-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold leading-6 text-white">Solutions</h3>
|
<h3 class="text-sm font-semibold leading-6 text-white">Games</h3>
|
||||||
<ul role="list" class="mt-6 space-y-4">
|
<ul role="list" class="mt-6 space-y-4">
|
||||||
<li v-for="item in navigation.solutions" :key="item.name">
|
<li v-for="item in navigation.games" :key="item.name">
|
||||||
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
||||||
item.name }}</a>
|
item.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10 md:mt-0">
|
<div class="mt-10 md:mt-0">
|
||||||
<h3 class="text-sm font-semibold leading-6 text-white">Support</h3>
|
<h3 class="text-sm font-semibold leading-6 text-white">Community</h3>
|
||||||
<ul role="list" class="mt-6 space-y-4">
|
<ul role="list" class="mt-6 space-y-4">
|
||||||
<li v-for="item in navigation.support" :key="item.name">
|
<li v-for="item in navigation.community" :key="item.name">
|
||||||
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
||||||
item.name }}</a>
|
item.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
@ -38,18 +38,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:grid md:grid-cols-2 md:gap-8">
|
<div class="md:grid md:grid-cols-2 md:gap-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold leading-6 text-white">Company</h3>
|
<h3 class="text-sm font-semibold leading-6 text-white">Documentation</h3>
|
||||||
<ul role="list" class="mt-6 space-y-4">
|
<ul role="list" class="mt-6 space-y-4">
|
||||||
<li v-for="item in navigation.company" :key="item.name">
|
<li v-for="item in navigation.documentation" :key="item.name">
|
||||||
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
||||||
item.name }}</a>
|
item.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10 md:mt-0">
|
<div class="mt-10 md:mt-0">
|
||||||
<h3 class="text-sm font-semibold leading-6 text-white">Legal</h3>
|
<h3 class="text-sm font-semibold leading-6 text-white">About</h3>
|
||||||
<ul role="list" class="mt-6 space-y-4">
|
<ul role="list" class="mt-6 space-y-4">
|
||||||
<li v-for="item in navigation.legal" :key="item.name">
|
<li v-for="item in navigation.about" :key="item.name">
|
||||||
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
<a :href="item.href" class="text-sm leading-6 text-gray-300 hover:text-white">{{
|
||||||
item.name }}</a>
|
item.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
@ -67,29 +67,26 @@ import GithubLogo from './GithubLogo.vue';
|
|||||||
import DiscordLogo from './DiscordLogo.vue';
|
import DiscordLogo from './DiscordLogo.vue';
|
||||||
|
|
||||||
const navigation = {
|
const navigation = {
|
||||||
solutions: [
|
games: [
|
||||||
{ name: 'Marketing', href: '#' },
|
{ name: 'Newly Added', href: '#' },
|
||||||
{ name: 'Analytics', href: '#' },
|
{ name: 'New Releases', href: '#' },
|
||||||
{ name: 'Commerce', href: '#' },
|
{ name: 'Top Sellers', href: '#' },
|
||||||
{ name: 'Insights', href: '#' },
|
{ name: 'Find a Game', href: '#' },
|
||||||
],
|
],
|
||||||
support: [
|
community: [
|
||||||
{ name: 'Pricing', href: '#' },
|
{ name: 'Friends', href: '#' },
|
||||||
{ name: 'Documentation', href: '#' },
|
{ name: 'Groups', href: '#' },
|
||||||
{ name: 'Guides', href: '#' },
|
{ name: 'Servers', href: '#' },
|
||||||
{ name: 'API Status', href: '#' },
|
|
||||||
],
|
],
|
||||||
company: [
|
documentation: [
|
||||||
{ name: 'About', href: '#' },
|
{ name: 'API', href: '#' },
|
||||||
{ name: 'Blog', href: '#' },
|
{ name: 'Server Docs', href: '#' },
|
||||||
{ name: 'Jobs', href: '#' },
|
{ name: 'Client Docs', href: '#' },
|
||||||
{ name: 'Press', href: '#' },
|
|
||||||
{ name: 'Partners', href: '#' },
|
|
||||||
],
|
],
|
||||||
legal: [
|
about: [
|
||||||
{ name: 'Claim', href: '#' },
|
{ name: 'About Drop', href: '#' },
|
||||||
{ name: 'Privacy', href: '#' },
|
{ name: 'Features', href: '#' },
|
||||||
{ name: 'Terms', href: '#' },
|
{ name: 'FAQ', href: '#' },
|
||||||
],
|
],
|
||||||
social: [
|
social: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div class="grow inline-flex items-center gap-x-20">
|
<div class="grow inline-flex items-center gap-x-20">
|
||||||
<Wordmark class="h-8" />
|
<Wordmark class="h-8" />
|
||||||
<nav class="inline-flex items-center">
|
<nav class="inline-flex items-center">
|
||||||
<ol class="inline-flex items-center gap-x-12 mt-1">
|
<ol class="inline-flex items-center gap-x-12">
|
||||||
<li class="transition text-gray-300 hover:text-gray-100 uppercase font-display font-semibold text-md"
|
<li class="transition text-gray-300 hover:text-gray-100 uppercase font-display font-semibold text-md"
|
||||||
v-for="(nav, navIdx) in navigation">
|
v-for="(nav, navIdx) in navigation">
|
||||||
{{ nav.label }}
|
{{ nav.label }}
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<component class="h-5" :is="item.icon" />
|
<component class="h-5" :is="item.icon" />
|
||||||
</InlineWidget>
|
</InlineWidget>
|
||||||
</li>
|
</li>
|
||||||
<InlineUserWidget />
|
<HeaderUserWidget />
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -26,8 +26,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BellIcon, UserGroupIcon } from '@heroicons/vue/16/solid';
|
import { BellIcon, UserGroupIcon } from '@heroicons/vue/16/solid';
|
||||||
import type { NavigationItem, QuickActionNav } from './types';
|
import type { NavigationItem, QuickActionNav } from '../composables/types';
|
||||||
|
|
||||||
|
|
||||||
const navigation: Array<NavigationItem> = [
|
const navigation: Array<NavigationItem> = [
|
||||||
{
|
{
|
||||||
|
|||||||
16
composables/user.ts
Normal file
16
composables/user.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { User } from "@prisma/client";
|
||||||
|
|
||||||
|
// undefined = haven't check
|
||||||
|
// null = check, no user
|
||||||
|
// {} = check, user
|
||||||
|
|
||||||
|
export const useUser = () => useState<User | undefined | null>(undefined);
|
||||||
|
export const updateUser = async () => {
|
||||||
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
|
|
||||||
|
const user = useUser();
|
||||||
|
if (user.value === null) return;
|
||||||
|
|
||||||
|
// SSR calls have to be after uses
|
||||||
|
user.value = await $fetch<User | null>("/api/v1/whoami", { headers });
|
||||||
|
};
|
||||||
2
layouts/admin.vue
Normal file
2
layouts/admin.vue
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<template>
|
||||||
|
</template>
|
||||||
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<content class="flex flex-col w-full min-h-screen">
|
<content class="flex flex-col w-full min-h-screen">
|
||||||
<UserHeader />
|
<UserHeader />
|
||||||
<div class="grow flex">
|
<div class="grow flex">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
<UserFooter />
|
<UserFooter />
|
||||||
</content>
|
</content>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({
|
useHead({
|
||||||
titleTemplate(title) {
|
titleTemplate(title) {
|
||||||
if (title) return `${title} | Drop`;
|
if (title) return `${title} | Drop`;
|
||||||
return `Drop`;
|
return `Drop`;
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
15
middleware/require-user.global.ts
Normal file
15
middleware/require-user.global.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const whitelistedPrefixes = ["/signin", "/register"];
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
if (import.meta.server) return;
|
||||||
|
if (whitelistedPrefixes.findIndex((e) => to.fullPath.startsWith(e)) != -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const user = useUser();
|
||||||
|
if (user === undefined) {
|
||||||
|
await updateUser();
|
||||||
|
}
|
||||||
|
if (!user.value) {
|
||||||
|
return navigateTo({ path: "/signin", query: { redirect: to.fullPath } });
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -10,6 +10,8 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@drop/droplet": "^0.2.0",
|
||||||
|
"@drop/droplet-linux-x64-gnu": "^0.2.0",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
"@prisma/client": "5.20.0",
|
"@prisma/client": "5.20.0",
|
||||||
@ -25,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/turndown": "^5.0.5",
|
"@types/turndown": "^5.0.5",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
|||||||
8
pages/admin/index.vue
Normal file
8
pages/admin/index.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: "admin"
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
|
{{ user ?? "no user" }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({
|
useHead({
|
||||||
title: "Home"
|
title: "Home",
|
||||||
})
|
});
|
||||||
</script>
|
|
||||||
|
const user = useUser();
|
||||||
|
</script>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const username = ref("");
|
|||||||
const password = ref("");
|
const password = ref("");
|
||||||
|
|
||||||
async function register() {
|
async function register() {
|
||||||
await $fetch('/api/v1/signup/simple', {
|
await $fetch('/api/v1/auth/signup/simple', {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
|
|||||||
175
pages/signin.vue
175
pages/signin.vue
@ -1,23 +1,172 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="register">
|
<div class="flex min-h-screen flex-1 bg-zinc-900">
|
||||||
<input type="text" v-model="username" placeholder="username" />
|
<div
|
||||||
<input type="text" v-model="password" placeholder="password" />
|
class="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24"
|
||||||
|
>
|
||||||
|
<div class="mx-auto w-full max-w-sm lg:w-96">
|
||||||
|
<div>
|
||||||
|
<Logo class="h-10 w-auto" />
|
||||||
|
<h2
|
||||||
|
class="mt-8 text-2xl font-bold font-display leading-9 tracking-tight text-zinc-100"
|
||||||
|
>
|
||||||
|
Sign in to your account
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-zinc-400">
|
||||||
|
Don't have an account? Ask an admin to create one for you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<div class="mt-10">
|
||||||
</form>
|
<div>
|
||||||
|
<form @submit.prevent="signin_wrapper" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="username"
|
||||||
|
class="block text-sm font-medium leading-6 text-zinc-300"
|
||||||
|
>Username</label
|
||||||
|
>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="username"
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||||
|
v-model="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="password"
|
||||||
|
class="block text-sm font-medium leading-6 text-zinc-300"
|
||||||
|
>Password</label
|
||||||
|
>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
v-model="password"
|
||||||
|
required
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember-me"
|
||||||
|
name="remember-me"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="rememberMe"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="remember-me"
|
||||||
|
class="ml-3 block text-sm leading-6 text-zinc-400"
|
||||||
|
>Remember me</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm leading-6">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="font-semibold text-blue-600 hover:text-blue-500"
|
||||||
|
>Forgot password?</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<LoadingButton class="w-full" :loading="loading">
|
||||||
|
Sign in</LoadingButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<XCircleIcon
|
||||||
|
class="h-5 w-5 text-red-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-600">
|
||||||
|
{{ error }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative hidden w-0 flex-1 lg:block">
|
||||||
|
<img
|
||||||
|
class="absolute inset-0 h-full w-full object-cover"
|
||||||
|
src="/wallpapers/signin.jpg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { XCircleIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import type { User } from "@prisma/client";
|
||||||
|
import LoadingButton from "~/components/LoadingButton.vue";
|
||||||
|
import Logo from "~/components/Logo.vue";
|
||||||
|
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
|
const rememberMe = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
async function register() {
|
const route = useRoute();
|
||||||
await $fetch('/api/v1/signin/simple', {
|
const router = useRouter();
|
||||||
method: "POST",
|
|
||||||
body: {
|
const error = ref<string | undefined>();
|
||||||
username: username.value,
|
|
||||||
password: password.value
|
function signin_wrapper() {
|
||||||
}
|
loading.value = true;
|
||||||
|
signin()
|
||||||
|
.then(() => {
|
||||||
|
router.push(route.query.redirect?.toString() ?? "/");
|
||||||
})
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
const message = response.statusMessage || "An unknown error occurred";
|
||||||
|
error.value = message;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
async function signin() {
|
||||||
|
await $fetch("/api/v1/auth/signin/simple", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
username: username.value,
|
||||||
|
password: password.value,
|
||||||
|
rememberMe: rememberMe.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const user = useUser();
|
||||||
|
user.value = await $fetch<User | null>("/api/v1/whoami");
|
||||||
|
}
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: "Sign in to Drop",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "admin" BOOLEAN NOT NULL DEFAULT false;
|
||||||
15
prisma/migrations/20241007065541_add_client/migration.sql
Normal file
15
prisma/migrations/20241007065541_add_client/migration.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ClientCapabilities" AS ENUM ('DownloadAggregation');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Client" (
|
||||||
|
"sharedToken" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"endpoint" TEXT NOT NULL,
|
||||||
|
"capabilities" "ClientCapabilities"[],
|
||||||
|
|
||||||
|
CONSTRAINT "Client_pkey" PRIMARY KEY ("sharedToken")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Client" ADD CONSTRAINT "Client_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@ -14,10 +14,12 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
username String @unique
|
username String @unique
|
||||||
|
admin Boolean @default(false)
|
||||||
|
|
||||||
authMecs LinkedAuthMec[]
|
authMecs LinkedAuthMec[]
|
||||||
|
clients Client[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthMec {
|
enum AuthMec {
|
||||||
@ -35,6 +37,20 @@ model LinkedAuthMec {
|
|||||||
@@id([userId, mec])
|
@@id([userId, mec])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ClientCapabilities {
|
||||||
|
DownloadAggregation
|
||||||
|
}
|
||||||
|
|
||||||
|
// References a device
|
||||||
|
model Client {
|
||||||
|
sharedToken String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
endpoint String
|
||||||
|
capabilities ClientCapabilities[]
|
||||||
|
}
|
||||||
|
|
||||||
enum MetadataSource {
|
enum MetadataSource {
|
||||||
Custom
|
Custom
|
||||||
GiantBomb
|
GiantBomb
|
||||||
|
|||||||
BIN
public/fonts/inter/InterVariable-Italic.ttf
Normal file
BIN
public/fonts/inter/InterVariable-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/inter/InterVariable.ttf
Normal file
BIN
public/fonts/inter/InterVariable.ttf
Normal file
Binary file not shown.
BIN
public/wallpapers/signin.jpg
Normal file
BIN
public/wallpapers/signin.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 940 KiB |
@ -8,6 +8,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
|
|
||||||
const username = body.username;
|
const username = body.username;
|
||||||
const password = body.password;
|
const password = body.password;
|
||||||
|
const rememberMe = body.rememberMe ?? false;
|
||||||
if (username === undefined || password === undefined)
|
if (username === undefined || password === undefined)
|
||||||
throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." });
|
throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." });
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
if (!await checkHash(password, hash.toString()))
|
if (!await checkHash(password, hash.toString()))
|
||||||
throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
|
throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
|
||||||
|
|
||||||
await h3.context.session.setUserId(h3, authMek.userId);
|
await h3.context.session.setUserId(h3, authMek.userId, rememberMe);
|
||||||
|
|
||||||
return { result: true, userId: authMek.userId }
|
return { result: true, userId: authMek.userId }
|
||||||
});
|
});
|
||||||
3
server/api/v1/client/handshake.post.ts
Normal file
3
server/api/v1/client/handshake.post.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineEventHandler((h3) => {
|
||||||
|
|
||||||
|
})
|
||||||
3
server/api/v1/client/initiate.post.ts
Normal file
3
server/api/v1/client/initiate.post.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
|
||||||
|
});
|
||||||
3
server/api/v1/client/session.post.ts
Normal file
3
server/api/v1/client/session.post.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineEventHandler((h3) => {
|
||||||
|
|
||||||
|
})
|
||||||
@ -1,5 +1,4 @@
|
|||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const user = await h3.context.session.getUser(h3);
|
const user = await h3.context.session.getUser(h3);
|
||||||
|
return user ?? null;
|
||||||
return user ?? {};
|
|
||||||
});
|
});
|
||||||
26
server/internal/clients/README.md
Normal file
26
server/internal/clients/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Client Handshake process
|
||||||
|
|
||||||
|
Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections.
|
||||||
|
|
||||||
|
## 1. Client requests a handshake
|
||||||
|
Client makes request: `POST /api/v1/client/initiate` with information about the client.
|
||||||
|
|
||||||
|
Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached.
|
||||||
|
|
||||||
|
## 2. User signs in
|
||||||
|
Client sends user to the provided URL (in external browser). User signs in using the existing authentication stack.
|
||||||
|
|
||||||
|
Server sends redirect to drop://handshake/[id]/[token], where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server.
|
||||||
|
|
||||||
|
## 3. Client requests certificates
|
||||||
|
Client makes request: `POST /api/v1/client/handshake` with the token recieved in the previous step.
|
||||||
|
|
||||||
|
The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all.
|
||||||
|
|
||||||
|
The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry.
|
||||||
|
|
||||||
|
## 4.a Client requests one-time device endpoint
|
||||||
|
The client generates a nonce and signs it with their private key. This is then attached to any device-related request.
|
||||||
|
|
||||||
|
## 4.b Client wants a long-lived session
|
||||||
|
The client does the same as above, but instead makes the request to `POST /api/v1/client/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication.
|
||||||
34
server/internal/clients/ca.ts
Normal file
34
server/internal/clients/ca.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import path from "path";
|
||||||
|
import droplet from "@drop/droplet";
|
||||||
|
import { CertificateStore } from "./store";
|
||||||
|
|
||||||
|
export type CertificateBundle = {
|
||||||
|
priv: string;
|
||||||
|
pub: string;
|
||||||
|
cert: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is designed to handle client certificates, as described in the README.md
|
||||||
|
*/
|
||||||
|
export class CertificateAuthority {
|
||||||
|
private certificateStore: CertificateStore;
|
||||||
|
|
||||||
|
private root: CertificateBundle;
|
||||||
|
|
||||||
|
constructor(store: CertificateStore, root: CertificateBundle) {
|
||||||
|
this.certificateStore = store;
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async new(store: CertificateStore) {
|
||||||
|
const root = await store.fetch("ca");
|
||||||
|
if (root === undefined) {
|
||||||
|
const [priv, pub, cert] = droplet.generateRootCa();
|
||||||
|
const bundle: CertificateBundle = { priv, pub, cert };
|
||||||
|
await store.store("ca", bundle);
|
||||||
|
return new CertificateAuthority(store, bundle);
|
||||||
|
}
|
||||||
|
return new CertificateAuthority(store, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
server/internal/clients/store.ts
Normal file
23
server/internal/clients/store.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { CertificateBundle } from "./ca";
|
||||||
|
|
||||||
|
export type CertificateStore = {
|
||||||
|
store(name: string, data: CertificateBundle): Promise<void>;
|
||||||
|
fetch(name: string): Promise<CertificateBundle | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fsCertificateStore = (base: string) => {
|
||||||
|
const store: CertificateStore = {
|
||||||
|
async store(name: string, data: CertificateBundle) {
|
||||||
|
const filepath = path.join(base, name);
|
||||||
|
fs.writeFileSync(filepath, JSON.stringify(data));
|
||||||
|
},
|
||||||
|
async fetch(name: string) {
|
||||||
|
const filepath = path.join(base, name);
|
||||||
|
if (!fs.existsSync(filepath)) return undefined;
|
||||||
|
return JSON.parse(fs.readFileSync(filepath, "utf-8"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return store;
|
||||||
|
};
|
||||||
5
server/internal/downloads/README.md
Normal file
5
server/internal/downloads/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Drop Download System
|
||||||
|
The Drop download system uses a torrent-*like* system. It is not torrenting, nor is it compatible with torrenting clients.
|
||||||
|
|
||||||
|
## Clients
|
||||||
|
Drop clients have built-in HTTP APIs that they forward with UPnP. This API exposes different capabilities for different Drop features, like download aggegration and P2P networking. When they sign on, they send a list of supported capabilities to the server.
|
||||||
10
server/internal/downloads/coordinator.ts
Normal file
10
server/internal/downloads/coordinator.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
The download co-ordinator's job is to keep track of all the currently online clients.
|
||||||
|
|
||||||
|
When a client signs on and registers itself as a peer
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DownloadCoordinator {
|
||||||
|
|
||||||
|
}
|
||||||
@ -26,11 +26,11 @@ export class SessionHandler {
|
|||||||
|
|
||||||
return data[userSessionKey];
|
return data[userSessionKey];
|
||||||
}
|
}
|
||||||
async setSession(h3: H3Event, data: any) {
|
async setSession(h3: H3Event, data: any, expend = false) {
|
||||||
const result = await this.sessionProvider.updateSession(h3, userSessionKey, data);
|
const result = await this.sessionProvider.updateSession(h3, userSessionKey, data);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const toCreate = { [userSessionKey]: data };
|
const toCreate = { [userSessionKey]: data };
|
||||||
await this.sessionProvider.setSession(h3, toCreate);
|
await this.sessionProvider.setSession(h3, toCreate, expend);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async clearSession(h3: H3Event) {
|
async clearSession(h3: H3Event) {
|
||||||
@ -52,11 +52,11 @@ export class SessionHandler {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserId(h3: H3Event, userId: string) {
|
async setUserId(h3: H3Event, userId: string, extend = false) {
|
||||||
const result = await this.sessionProvider.updateSession(h3, userIdKey, userId);
|
const result = await this.sessionProvider.updateSession(h3, userIdKey, userId);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const toCreate = { [userIdKey]: userId };
|
const toCreate = { [userIdKey]: userId };
|
||||||
await this.sessionProvider.setSession(h3, toCreate);
|
await this.sessionProvider.setSession(h3, toCreate, extend);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +1,45 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Session, SessionProvider } from "./types";
|
import { Session, SessionProvider } from "./types";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
export default function createMemorySessionHandler() {
|
export default function createMemorySessionHandler() {
|
||||||
const sessions: { [key: string]: Session } = {}
|
const sessions: { [key: string]: Session } = {};
|
||||||
|
|
||||||
const sessionCookieName = "drop-session";
|
const sessionCookieName = "drop-session";
|
||||||
|
|
||||||
const memoryProvider: SessionProvider = {
|
const memoryProvider: SessionProvider = {
|
||||||
async setSession(h3, data) {
|
async setSession(h3, data, extend = false) {
|
||||||
const existingCookie = getCookie(h3, sessionCookieName);
|
const existingCookie = getCookie(h3, sessionCookieName);
|
||||||
if (existingCookie) delete sessions[existingCookie]; // Clear any previous session
|
if (existingCookie) delete sessions[existingCookie]; // Clear any previous session
|
||||||
|
|
||||||
const cookie = uuidv4();
|
const cookie = uuidv4();
|
||||||
const expiry = moment().add(31, 'day');
|
const expiry = moment().add(31, extend ? "month" : "day");
|
||||||
setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() });
|
setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() });
|
||||||
|
|
||||||
sessions[cookie] = data;
|
sessions[cookie] = data;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
async updateSession(h3, key, data) {
|
async updateSession(h3, key, data) {
|
||||||
const cookie = getCookie(h3, sessionCookieName);
|
const cookie = getCookie(h3, sessionCookieName);
|
||||||
if (!cookie) return false;
|
if (!cookie) return false;
|
||||||
|
|
||||||
sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data });
|
sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
async getSession(h3) {
|
async getSession(h3) {
|
||||||
const cookie = getCookie(h3, sessionCookieName);
|
const cookie = getCookie(h3, sessionCookieName);
|
||||||
if (!cookie) return undefined;
|
if (!cookie) return undefined;
|
||||||
|
|
||||||
return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want
|
return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want
|
||||||
},
|
},
|
||||||
async clearSession(h3) {
|
async clearSession(h3) {
|
||||||
const cookie = getCookie(h3, sessionCookieName);
|
const cookie = getCookie(h3, sessionCookieName);
|
||||||
if (!cookie) return;
|
if (!cookie) return;
|
||||||
|
|
||||||
delete sessions[cookie];
|
delete sessions[cookie];
|
||||||
deleteCookie(h3, sessionCookieName);
|
deleteCookie(h3, sessionCookieName);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return memoryProvider;
|
return memoryProvider;
|
||||||
}
|
}
|
||||||
|
|||||||
14
server/internal/session/types.d.ts
vendored
14
server/internal/session/types.d.ts
vendored
@ -3,8 +3,12 @@ import { H3Event } from "h3";
|
|||||||
export type Session = { [key: string]: any };
|
export type Session = { [key: string]: any };
|
||||||
|
|
||||||
export interface SessionProvider {
|
export interface SessionProvider {
|
||||||
setSession: (h3: H3Event, data: Session) => Promise<boolean>;
|
setSession: (
|
||||||
updateSession: (h3: H3Event, key: string, data: any) => Promise<boolean>;
|
h3: H3Event,
|
||||||
getSession: <T extends Session>(h3: H3Event) => Promise<T | undefined>;
|
data: Session,
|
||||||
clearSession: (h3: H3Event) => Promise<void>;
|
extend?: boolean
|
||||||
}
|
) => Promise<boolean>;
|
||||||
|
updateSession: (h3: H3Event, key: string, data: any) => Promise<boolean>;
|
||||||
|
getSession: <T extends Session>(h3: H3Event) => Promise<T | undefined>;
|
||||||
|
clearSession: (h3: H3Event) => Promise<void>;
|
||||||
|
}
|
||||||
|
|||||||
18
server/plugins/ca.ts
Normal file
18
server/plugins/ca.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { CertificateAuthority } from "../internal/clients/ca";
|
||||||
|
import fs from "fs";
|
||||||
|
import { fsCertificateStore } from "../internal/clients/store";
|
||||||
|
|
||||||
|
let ca: CertificateAuthority | undefined;
|
||||||
|
|
||||||
|
export const useGlobalCertificateAuthority = () => {
|
||||||
|
if (!ca) throw new Error("CA not initialised");
|
||||||
|
return ca;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineNitroPlugin(async (nitro) => {
|
||||||
|
const basePath = process.env.CLIENT_CERTIFICATES ?? "./certs";
|
||||||
|
fs.mkdirSync(basePath, { recursive: true });
|
||||||
|
const store = fsCertificateStore(basePath);
|
||||||
|
|
||||||
|
ca = await CertificateAuthority.new(store);
|
||||||
|
});
|
||||||
5
server/routes/signout.get.ts
Normal file
5
server/routes/signout.get.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
await h3.context.session.clearSession(h3);
|
||||||
|
|
||||||
|
return sendRedirect(h3, "/signin");
|
||||||
|
});
|
||||||
@ -11,11 +11,13 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Helvetica"],
|
sans: ["Inter"],
|
||||||
display: ["Motiva Sans"]
|
display: ["Motiva Sans"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require('@tailwindcss/forms'),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
yarn.lock
30
yarn.lock
@ -296,6 +296,24 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime "^3.0.0"
|
mime "^3.0.0"
|
||||||
|
|
||||||
|
"@drop/droplet-linux-x64-gnu@0.2.0", "@drop/droplet-linux-x64-gnu@^0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.2.0.tgz#e1c0133abc38cf63cc8beaf5826db1946beb1165"
|
||||||
|
integrity sha1-4cATOrw4z2PMi+r1gm2xlGvrEWU=
|
||||||
|
|
||||||
|
"@drop/droplet-win32-x64-msvc@0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.2.0.tgz#0531e51e225530c277afcc7ac4230c8d99c8365e"
|
||||||
|
integrity sha1-BTHlHiJVMMJ3r8x6xCMMjZnINl4=
|
||||||
|
|
||||||
|
"@drop/droplet@^0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.2.0.tgz#e4b6d2cf2bd5c0416fd3452ffa5b7c34267e160a"
|
||||||
|
integrity sha1-5LbSzyvVwEFv00Uv+lt8NCZ+Fgo=
|
||||||
|
optionalDependencies:
|
||||||
|
"@drop/droplet-linux-x64-gnu" "0.2.0"
|
||||||
|
"@drop/droplet-win32-x64-msvc" "0.2.0"
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@0.20.2":
|
"@esbuild/aix-ppc64@0.20.2":
|
||||||
version "0.20.2"
|
version "0.20.2"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
||||||
@ -1279,6 +1297,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958"
|
||||||
integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==
|
integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==
|
||||||
|
|
||||||
|
"@tailwindcss/forms@^0.5.9":
|
||||||
|
version "0.5.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.9.tgz#b495c12575d6eae5865b2cbd9876b26d89f16f61"
|
||||||
|
integrity sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==
|
||||||
|
dependencies:
|
||||||
|
mini-svg-data-uri "^1.2.3"
|
||||||
|
|
||||||
"@tanstack/virtual-core@3.10.8":
|
"@tanstack/virtual-core@3.10.8":
|
||||||
version "3.10.8"
|
version "3.10.8"
|
||||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4"
|
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4"
|
||||||
@ -3502,6 +3527,11 @@ mimic-fn@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
||||||
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
|
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
|
||||||
|
|
||||||
|
mini-svg-data-uri@^1.2.3:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
|
||||||
|
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
|
||||||
|
|
||||||
minimatch@^3.0.4, minimatch@^3.1.1:
|
minimatch@^3.0.4, minimatch@^3.1.1:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||||
|
|||||||
Reference in New Issue
Block a user