Merge branch 'develop' into db-store

This commit is contained in:
Huskydog9988
2025-04-03 18:12:07 -04:00
25 changed files with 1469 additions and 172 deletions

64
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Release Workflow
on:
release:
types: [published]
# This can be used to automatically publish nightlies at UTC nighttime
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
jobs:
web:
name: Push website Docker image to registry
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
buildkitd-flags: --debug
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/drop-OSS/drop
tags: |
type=schedule,pattern=nightly
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
type=ref,event=branch
type=ref,event=pr
type=sha
# set latest tag for stable releases
type=raw,value=latest,enable=${{ github.event.release.prerelease == false }}
- name: Build and push image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max

35
.vscode/settings.json vendored
View File

@ -1,18 +1,21 @@
{ {
"spellchecker.ignoreWordsList": [ "spellchecker.ignoreWordsList": ["mTLS", "Wireguard"],
"mTLS", "sqltools.connections": [
"Wireguard" {
], "previewLimit": 50,
"sqltools.connections": [ "server": "localhost",
{ "port": 5432,
"previewLimit": 50, "driver": "PostgreSQL",
"server": "localhost", "name": "drop",
"port": 5432, "database": "drop",
"driver": "PostgreSQL", "username": "drop",
"name": "drop", "password": "drop"
"database": "drop", }
"username": "drop", ],
"password": "drop" // allow autocomplete for ArkType expressions like "string | num"
} "editor.quickSuggestions": {
] "strings": "on"
},
// prioritize ArkType's "type" for autoimports
"typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"]
} }

View File

@ -1,5 +1,5 @@
# pull pre-configured and updated build environment # pull pre-configured and updated build environment
FROM registry.deepcore.dev/drop-oss/drop-server-build-environment/main:latest AS build-system FROM debian:12.10-slim AS build-system
# setup workdir # setup workdir
RUN mkdir /build RUN mkdir /build

View File

@ -32,16 +32,18 @@ To just deploy Drop, we've set up a simple docker compose file in deploy-templat
3. Edit the compose.yml file (`nano compose.yml`) and copy your GiamtBomb API Key into the GIANT_BOMB_API_KEY environment variable 3. Edit the compose.yml file (`nano compose.yml`) and copy your GiamtBomb API Key into the GIANT_BOMB_API_KEY environment variable
4. Run `docker compose up -d` 4. Run `docker compose up -d`
Your drop server should now be running. To register the admin user, navigate to http://your.drop.server.ip:3000/register?id=admin Your drop server should now be running. To register the admin user, navigate to http://your.drop.server.ip:3000/register?id=admin
and fill in the required forms and fill in the required forms
### Adding a game ### Adding a game
To add a game to the drop library, do as follows: To add a game to the drop library, do as follows:
1. Ensure that the current user owns the library folder with `sudo chown -R $(id -u $(whoami)) library` 1. Ensure that the current user owns the library folder with `sudo chown -R $(id -u $(whoami)) library`
2. `cd library` 2. `cd library`
3. `mkdir <GAME_NAME>` with the name of the game which you would like to register 3. `mkdir <GAME_NAME>` with the name of the game which you would like to register
4. `cd <GAME_NAME>` 4. `cd <GAME_NAME>`
5. `mkdir <VERSION_NAME>` Upload files for the specific game version to this folder 5. `mkdir <VERSION_NAME>` Upload files for the specific game version to this folder
6. Navigate to http://your.drop.server.ip:3000/ 6. Navigate to http://your.drop.server.ip:3000/
7. Import game metadata (uses GiantBomb API Key) by selecting the game and specifying which entry to import 7. Import game metadata (uses GiantBomb API Key) by selecting the game and specifying which entry to import
8. Navigate to http://your.drop.server.ip:3000/admin/library 8. Navigate to http://your.drop.server.ip:3000/admin/library
@ -73,7 +75,7 @@ Steps:
As part of the first-time bootstrap, Drop creates an invitation with the fixed id of 'admin'. So, to create an admin account, go to: As part of the first-time bootstrap, Drop creates an invitation with the fixed id of 'admin'. So, to create an admin account, go to:
http://localhost:3000/register?id=admin http://localhost:3000/auth/register?id=admin
## Contributing ## Contributing

View File

@ -39,7 +39,11 @@
</NuxtLink> </NuxtLink>
<div class="h-0.5 rounded-full w-full bg-zinc-800" /> <div class="h-0.5 rounded-full w-full bg-zinc-800" />
<div class="flex flex-col"> <div class="flex flex-col">
<MenuItem v-for="(nav, navIdx) in navigation" v-slot="{ active, close }"> <MenuItem
v-for="(nav, navIdx) in navigation"
v-slot="{ active, close }"
hydrate-on-visible
>
<button <button
:href="nav.route" :href="nav.route"
@click="() => navigateTo(nav.route, close)" @click="() => navigateTo(nav.route, close)"
@ -48,8 +52,8 @@
'text-left transition block px-4 py-2 text-sm', 'text-left transition block px-4 py-2 text-sm',
]" ]"
> >
{{ nav.label }}</button {{ nav.label }}
> </button>
</MenuItem> </MenuItem>
</div> </div>
</PanelWidget> </PanelWidget>
@ -81,7 +85,7 @@ const navigation: NavigationItem[] = [
}, },
{ {
label: "Sign out", label: "Sign out",
route: "/signout", route: "/auth/signout",
prefix: "", prefix: "",
}, },
].filter((e) => e !== undefined); ].filter((e) => e !== undefined);

View File

@ -16,7 +16,7 @@ const showSignIn = statusCode ? statusCode == 403 || statusCode == 401 : false;
async function signIn() { async function signIn() {
clearError({ clearError({
redirect: `/signin?redirect=${encodeURIComponent(route.fullPath)}`, redirect: `/auth/signin?redirect=${encodeURIComponent(route.fullPath)}`,
}); });
} }

View File

@ -1,10 +1,10 @@
<template> <template>
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900"> <div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
<UserHeader class="z-50" /> <UserHeader class="z-50" hydrate-on-idle />
<div class="grow flex"> <div class="grow flex">
<NuxtPage /> <NuxtPage />
</div> </div>
<UserFooter class="z-50" hydrate-on-visible /> <UserFooter class="z-50" hydrate-on-interaction />
</div> </div>
<div class="flex w-full min-h-screen bg-zinc-900" v-else> <div class="flex w-full min-h-screen bg-zinc-900" v-else>
<NuxtPage /> <NuxtPage />

View File

@ -1,4 +1,4 @@
const whitelistedPrefixes = ["/signin", "/register", "/api", "/setup"]; const whitelistedPrefixes = ["/auth/signin", "/register", "/api", "/setup"];
const requireAdmin = ["/admin"]; const requireAdmin = ["/admin"];
export default defineNuxtRouteMiddleware(async (to, from) => { export default defineNuxtRouteMiddleware(async (to, from) => {
@ -13,7 +13,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
await updateUser(); await updateUser();
} }
if (!user.value) { if (!user.value) {
return navigateTo({ path: "/signin", query: { redirect: to.fullPath } }); return navigateTo({
path: "/auth/signin",
query: { redirect: to.fullPath },
});
} }
if ( if (
requireAdmin.findIndex((e) => to.fullPath.startsWith(e)) != -1 && requireAdmin.findIndex((e) => to.fullPath.startsWith(e)) != -1 &&

View File

@ -9,10 +9,12 @@ export default defineNuxtConfig({
devtools: { enabled: false }, devtools: { enabled: false },
css: ["~/assets/tailwindcss.css", "~/assets/core.scss"], css: ["~/assets/tailwindcss.css", "~/assets/core.scss"],
experimental: {
buildCache: true,
},
vite: { vite: {
plugins: [ plugins: [tailwindcss()],
tailwindcss()
]
}, },
app: { app: {
@ -21,7 +23,22 @@ export default defineNuxtConfig({
}, },
}, },
routeRules: {
"/auth/signin": { prerender: true },
"/signout": { prerender: true },
"/api/**": { cors: true },
"/api/v1/client/object/*": {
security: {
rateLimiter: false,
},
},
},
nitro: { nitro: {
minify: true,
experimental: { experimental: {
websocket: true, websocket: true,
tasks: true, tasks: true,
@ -30,6 +47,8 @@ export default defineNuxtConfig({
scheduledTasks: { scheduledTasks: {
"0 * * * *": ["cleanup:invitations"], "0 * * * *": ["cleanup:invitations"],
}, },
compressPublicAssets: true,
}, },
extends: ["./drop-base"], extends: ["./drop-base"],
@ -39,6 +58,7 @@ export default defineNuxtConfig({
"vue3-carousel-nuxt", "vue3-carousel-nuxt",
"nuxt-security", "nuxt-security",
"@nuxt/image", "@nuxt/image",
"@nuxt/fonts",
], ],
carousel: { carousel: {
@ -48,6 +68,8 @@ export default defineNuxtConfig({
security: { security: {
headers: { headers: {
contentSecurityPolicy: { contentSecurityPolicy: {
"upgrade-insecure-requests": false,
"img-src": [ "img-src": [
"'self'", "'self'",
"data:", "data:",
@ -59,4 +81,4 @@ export default defineNuxtConfig({
strictTransportSecurity: false, strictTransportSecurity: false,
}, },
}, },
}); });

View File

@ -14,10 +14,13 @@
"@drop/droplet": "^0.7.0", "@drop/droplet": "^0.7.0",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@nuxt/fonts": "^0.11.0",
"@nuxt/image": "1.9.0", "@nuxt/image": "1.9.0",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@prisma/client": "^6.1.0", "@prisma/client": "^6.1.0",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"argon2": "^0.41.1",
"arktype": "^2.1.10",
"axios": "^1.7.7", "axios": "^1.7.7",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
@ -31,13 +34,14 @@
"nuxt-security": "2.2.0", "nuxt-security": "2.2.0",
"prisma": "^6.1.0", "prisma": "^6.1.0",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sharp": "^0.33.5",
"stream": "^0.0.3", "stream": "^0.0.3",
"stream-mime-type": "^2.0.0", "stream-mime-type": "^2.0.0",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vue": "latest", "vue": "latest",
"vue-router": "latest", "vue-router": "latest",
"vue3-carousel-nuxt": "^1.1.3", "vue3-carousel-nuxt": "^1.1.5",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -48,7 +52,7 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"h3": "^1.13.0", "h3": "^1.13.0",
"nitropack": "^2.9.7", "nitropack": "2.11.6",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"sass": "^1.79.4", "sass": "^1.79.4",
"tailwindcss": "^4.0.0" "tailwindcss": "^4.0.0"
@ -59,5 +63,10 @@
"@drop/droplet-linux-x64-gnu": "^0.7.0", "@drop/droplet-linux-x64-gnu": "^0.7.0",
"@drop/droplet-win32-x64-msvc": "^0.7.0" "@drop/droplet-win32-x64-msvc": "^0.7.0"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"overrides": {
"vue3-carousel-nuxt": {
"vue3-carousel": "^0.15.0"
}
}
} }

View File

@ -188,6 +188,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { XCircleIcon } from "@heroicons/vue/24/solid"; import { XCircleIcon } from "@heroicons/vue/24/solid";
import { type } from "arktype";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -208,14 +209,20 @@ const username = ref(invitation.data.value?.username);
const password = ref(""); const password = ref("");
const confirmPassword = ref(undefined); const confirmPassword = ref(undefined);
const mailRegex = /^\S+@\S+\.\S+$/; const emailValidator = type("string.email");
const validEmail = computed(() => mailRegex.test(email.value ?? "")); const validEmail = computed(
const validUsername = computed( () => !(emailValidator(email.value) instanceof type.errors)
() => );
(username.value?.length ?? 0) >= 5 &&
username.value?.toLowerCase() == username.value const usernameValidator = type("string.lower.preformatted >= 5");
const validUsername = computed(
() => !(usernameValidator(username.value) instanceof type.errors)
);
const passwordValidator = type("string >= 14");
const validPassword = computed(
() => !(passwordValidator(password.value) instanceof type.errors)
); );
const validPassword = computed(() => (password.value?.length ?? 0) >= 14);
const validConfirmPassword = computed( const validConfirmPassword = computed(
() => password.value == confirmPassword.value () => password.value == confirmPassword.value
); );
@ -248,7 +255,7 @@ function register_wrapper() {
loading.value = true; loading.value = true;
register() register()
.then(() => { .then(() => {
router.push("/signin"); router.push("/auth/signin");
}) })
.catch((response) => { .catch((response) => {
const message = response.statusMessage || "An unknown error occurred"; const message = response.statusMessage || "An unknown error occurred";

View File

@ -42,6 +42,6 @@ const user = useUser();
user.value = null; user.value = null;
// Redirect to signin page after signout // Redirect to signin page after signout
await $dropFetch("/signout"); await $dropFetch("/api/v1/auth/signout"); //TODO: add signout api route
router.push("/signin"); router.push("/auth/signin");
</script> </script>

View File

@ -80,7 +80,7 @@
</div> </div>
<!-- recently updated --> <!-- recently updated -->
<div class="px-4 sm:px-12 py-4"> <div class="px-4 sm:px-12 py-4" hydrate-on-visible>
<h1 class="text-zinc-100 text-2xl font-bold font-display"> <h1 class="text-zinc-100 text-2xl font-bold font-display">
Recently updated Recently updated
</h1> </h1>

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LinkedAuthMec" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;

View File

@ -7,6 +7,7 @@ model LinkedAuthMec {
mec AuthMec mec AuthMec
enabled Boolean @default(true) enabled Boolean @default(true)
version Int @default(1)
credentials Json credentials Json
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])

View File

@ -1,7 +1,11 @@
import { AuthMec } from "@prisma/client"; import { AuthMec } from "@prisma/client";
import { JsonArray } from "@prisma/client/runtime/library"; import { JsonArray } from "@prisma/client/runtime/library";
import { type } from "arktype";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { checkHash } from "~/server/internal/security/simple"; import {
checkHashArgon2,
checkHashBcrypt,
} from "~/server/internal/security/simple";
import sessionHandler from "~/server/internal/session"; import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
@ -19,10 +23,10 @@ export default defineEventHandler(async (h3) => {
const authMek = await prisma.linkedAuthMec.findFirst({ const authMek = await prisma.linkedAuthMec.findFirst({
where: { where: {
mec: AuthMec.Simple, mec: AuthMec.Simple,
credentials: {
array_starts_with: username,
},
enabled: true, enabled: true,
user: {
username,
},
}, },
include: { include: {
user: { user: {
@ -39,17 +43,46 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Invalid username or password.", statusMessage: "Invalid username or password.",
}); });
const credentials = authMek.credentials as JsonArray; if (!authMek.user.enabled)
const hash = credentials.at(1);
if (!hash || !authMek.user.enabled)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: statusMessage:
"Invalid or disabled account. Please contact the server administrator.", "Invalid or disabled account. Please contact the server administrator.",
}); });
if (!(await checkHash(password, hash.toString()))) // LEGACY bcrypt
if (authMek.version == 1) {
const credentials = authMek.credentials as JsonArray | null;
const hash = credentials?.at(1)?.toString();
if (!hash)
throw createError({
statusCode: 403,
statusMessage:
"Invalid password state. Please contact the server administrator.",
});
if (!(await checkHashBcrypt(password, hash)))
throw createError({
statusCode: 401,
statusMessage: "Invalid username or password.",
});
// TODO: send user to forgot password screen or something to force them to change their password to new system
await sessionHandler.setUserId(h3, authMek.userId, rememberMe);
return { result: true, userId: authMek.userId };
}
// V2: argon2
const hash = authMek.credentials as string | undefined;
if (!hash || typeof hash !== "string")
throw createError({
statusCode: 500,
statusMessage:
"Invalid password state. Please contact the server administrator.",
});
if (!(await checkHashArgon2(password, hash)))
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: "Invalid username or password.", statusMessage: "Invalid username or password.",

View File

@ -1,12 +1,20 @@
import { AuthMec, Invitation } from "@prisma/client"; import { AuthMec, Invitation } from "@prisma/client";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { createHash } from "~/server/internal/security/simple"; import {
createHashArgon2,
} from "~/server/internal/security/simple";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import * as jdenticon from "jdenticon"; import * as jdenticon from "jdenticon";
import objectHandler from "~/server/internal/objects"; import objectHandler from "~/server/internal/objects";
import { type } from "arktype";
import { writeNonLiteralDefaultMessage } from "arktype/internal/parser/shift/operator/default.ts";
// Only really a simple test, in case people mistype their emails const userValidator = type({
const mailRegex = /^\S+@\S+\.\S+$/; username: "string >= 5",
email: "string.email",
password: "string >= 14",
"displayName?": "string | undefined",
});
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const body = await readBody(h3); const body = await readBody(h3);
@ -27,59 +35,24 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Invalid or expired invitation.", statusMessage: "Invalid or expired invitation.",
}); });
const useInvitationOrBodyRequirement = ( const user = userValidator(body);
field: keyof Invitation, if (user instanceof type.errors) {
check: (v: string) => boolean // hover out.summary to see validation errors
) => { console.error(user.summary);
if (invitation[field]) {
return invitation[field].toString();
}
const v: string = body[field]?.toString();
const valid = check(v);
return valid ? v : undefined;
};
const username = useInvitationOrBodyRequirement(
"username",
(e) => e.length >= 5
);
const email = useInvitationOrBodyRequirement("email", (e) =>
mailRegex.test(e)
);
const password = body.password;
const displayName = body.displayName || username;
if (username === undefined)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Username is invalid. Must be more than 5 characters.", statusMessage: user.summary,
});
if (username.toLowerCase() != username)
throw createError({
statusCode: 400,
statusMessage: "Username must be all lowercase",
}); });
}
if (email === undefined) // reuse items from invite
throw createError({ if (invitation.username !== null) user.username = invitation.username;
statusCode: 400, if (invitation.email !== null) user.email = invitation.email;
statusMessage: "Invalid email. Must follow the format you@example.com",
});
if (!password) const existing = await prisma.user.count({
throw createError({ where: { username: user.username },
statusCode: 400, });
statusMessage: "Password empty or missing.",
});
if (password.length < 14)
throw createError({
statusCode: 400,
statusMessage: "Password must be 14 or more characters.",
});
const existing = await prisma.user.count({ where: { username: username } });
if (existing > 0) if (existing > 0)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
@ -91,30 +64,33 @@ export default defineEventHandler(async (h3) => {
const profilePictureId = uuidv4(); const profilePictureId = uuidv4();
await objectHandler.createFromSource( await objectHandler.createFromSource(
profilePictureId, profilePictureId,
async () => jdenticon.toPng(username, 256), async () => jdenticon.toPng(user.username, 256),
{}, {},
[`internal:read`, `${userId}:write`] [`internal:read`, `${userId}:write`]
); );
const user = await prisma.user.create({ const [linkMec] = await prisma.$transaction([
data: { prisma.linkedAuthMec.create({
username, data: {
displayName, mec: AuthMec.Simple,
email, credentials: await createHashArgon2(user.password),
profilePicture: profilePictureId, version: 2,
admin: invitation.isAdmin, user: {
}, create: {
}); id: userId,
username: user.username,
displayName: user.displayName ?? user.username,
email: user.email,
profilePicture: profilePictureId,
admin: invitation.isAdmin,
},
},
},
select: {
user: true,
},
}),
prisma.invitation.delete({ where: { id: invitationId } }),
]);
const hash = await createHash(password); return linkMec.user;
await prisma.linkedAuthMec.create({
data: {
mec: AuthMec.Simple,
credentials: [username, hash],
userId: user.id,
},
});
await prisma.invitation.delete({ where: { id: invitationId } });
return user;
}); });

View File

@ -1,11 +1,15 @@
import bcrypt from 'bcryptjs'; import bcrypt from "bcryptjs";
import * as argon2 from "argon2";
import { type } from "arktype";
const rounds = 10; export async function checkHashBcrypt(password: string, hash: string) {
return await bcrypt.compare(password, hash);
export async function createHash(password: string) {
return bcrypt.hashSync(password, rounds);
} }
export async function checkHash(password: string, hash: string) { export async function createHashArgon2(password: string) {
return bcrypt.compareSync(password, hash); return await argon2.hash(password);
} }
export async function checkHashArgon2(password: string, hash: string) {
return await argon2.verify(hash, password);
}

View File

@ -1,16 +1,13 @@
import { Platform } from "@prisma/client"; import { Platform } from "@prisma/client";
export function parsePlatform(platform: string) { export function parsePlatform(platform: string) {
switch (platform) { switch (platform.toLowerCase()) {
case "linux": case "linux":
case "Linux":
return Platform.Linux; return Platform.Linux;
case "windows": case "windows":
case "Windows":
return Platform.Windows; return Platform.Windows;
case "macOS":
case "MacOS":
case "mac": case "mac":
case "macos":
return Platform.macOS; return Platform.macOS;
} }

View File

@ -18,7 +18,7 @@ export default defineNitroPlugin((nitro) => {
if (userId) break; if (userId) break;
return sendRedirect( return sendRedirect(
event, event,
`/signin?redirect=${encodeURIComponent(event.path)}` `/auth/signin?redirect=${encodeURIComponent(event.path)}`
); );
} }
}); });

View File

@ -3,5 +3,5 @@ import sessionHandler from "../internal/session";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
await sessionHandler.clearSession(h3); await sessionHandler.clearSession(h3);
return sendRedirect(h3, "/signin"); return sendRedirect(h3, "/auth/signin");
}); });

View File

@ -1,3 +1,6 @@
{ {
"extends": "../.nuxt/tsconfig.server.json" "extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"exactOptionalPropertyTypes": true
}
} }

View File

@ -1,4 +1,7 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json" "extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"exactOptionalPropertyTypes": true
}
} }

1218
yarn.lock

File diff suppressed because it is too large Load Diff