mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
Merge branch 'develop' into db-store
This commit is contained in:
64
.github/workflows/release.yml
vendored
Normal file
64
.github/workflows/release.yml
vendored
Normal 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
35
.vscode/settings.json
vendored
@ -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$"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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 &&
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
15
package.json
15
package.json
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LinkedAuthMec" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;
|
||||||
@ -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])
|
||||||
|
|||||||
@ -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.",
|
||||||
|
|||||||
@ -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;
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "../.nuxt/tsconfig.server.json"
|
"extends": "../.nuxt/tsconfig.server.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"exactOptionalPropertyTypes": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user