mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
initial commit
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
|
||||||
|
.data
|
||||||
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Drop lol
|
||||||
|
|
||||||
|
## To-do list
|
||||||
|
- User authentication (done)
|
||||||
|
- Sessions (done)
|
||||||
|
- Game database/API
|
||||||
|
- Metadata matching and import
|
||||||
|
- Frontend beginnings
|
||||||
5
app.vue
Normal file
5
app.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
23
dev-tools/compose.yml
Normal file
23
dev-tools/compose.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- ../.data/db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=drop
|
||||||
|
- POSTGRES_USER=drop
|
||||||
|
- POSTGRES_DB=drop
|
||||||
|
minio:
|
||||||
|
# honestly no idea where this image comes from
|
||||||
|
image: quay.io/minio/minio
|
||||||
|
ports:
|
||||||
|
- 9000:9000
|
||||||
|
- 9001:9001
|
||||||
|
volumes:
|
||||||
|
- ../.data/s3:/data
|
||||||
|
environment:
|
||||||
|
- MINIO_ROOT_USER=drop
|
||||||
|
- MINIO_ROOT_PASSWORD=drop
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
5
nuxt.config.ts
Normal file
5
nuxt.config.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-04-03',
|
||||||
|
devtools: { enabled: true },
|
||||||
|
})
|
||||||
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "nuxt-app",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "5.20.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"nuxt": "^3.13.0",
|
||||||
|
"prisma": "^5.20.0",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"vue": "latest",
|
||||||
|
"vue-router": "latest"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"h3": "^1.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
pages/index.vue
Normal file
3
pages/index.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
23
pages/register.vue
Normal file
23
pages/register.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="register">
|
||||||
|
<input type="text" v-model="username" placeholder="username" />
|
||||||
|
<input type="text" v-model="password" placeholder="password" />
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const username = ref("");
|
||||||
|
const password = ref("");
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
await $fetch('/api/v1/signup/simple', {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
username: username.value,
|
||||||
|
password: password.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
23
pages/signin.vue
Normal file
23
pages/signin.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="register">
|
||||||
|
<input type="text" v-model="username" placeholder="username" />
|
||||||
|
<input type="text" v-model="password" placeholder="password" />
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const username = ref("");
|
||||||
|
const password = ref("");
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
await $fetch('/api/v1/signin/simple', {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
username: username.value,
|
||||||
|
password: password.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AuthMec" AS ENUM ('Simple');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LinkedAuthMec" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"mec" "AuthMec" NOT NULL,
|
||||||
|
"credentials" TEXT[],
|
||||||
|
|
||||||
|
CONSTRAINT "LinkedAuthMec_pkey" PRIMARY KEY ("userId","mec")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LinkedAuthMec" ADD CONSTRAINT "LinkedAuthMec_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Changed the type of `credentials` on the `LinkedAuthMec` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LinkedAuthMec" DROP COLUMN "credentials",
|
||||||
|
ADD COLUMN "credentials" JSONB NOT NULL;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
36
prisma/schema.prisma
Normal file
36
prisma/schema.prisma
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
username String @unique
|
||||||
|
|
||||||
|
authMecs LinkedAuthMec[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthMec {
|
||||||
|
Simple
|
||||||
|
}
|
||||||
|
|
||||||
|
model LinkedAuthMec {
|
||||||
|
userId String
|
||||||
|
mec AuthMec
|
||||||
|
|
||||||
|
credentials Json
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@id([userId, mec])
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1
public/robots.txt
Normal file
1
public/robots.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
36
server/api/v1/signin/simple.post.ts
Normal file
36
server/api/v1/signin/simple.post.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { AuthMec } from "@prisma/client";
|
||||||
|
import { JsonArray } from "@prisma/client/runtime/library";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
import { checkHash } from "~/server/internal/security/simple";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const body = await readBody(h3);
|
||||||
|
|
||||||
|
const username = body.username;
|
||||||
|
const password = body.password;
|
||||||
|
if (username === undefined || password === undefined)
|
||||||
|
throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." });
|
||||||
|
|
||||||
|
const authMek = await prisma.linkedAuthMec.findFirst({
|
||||||
|
where: {
|
||||||
|
mec: AuthMec.Simple,
|
||||||
|
credentials: {
|
||||||
|
array_starts_with: username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authMek) throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
|
||||||
|
|
||||||
|
const credentials = authMek.credentials as JsonArray;
|
||||||
|
const hash = credentials.at(1);
|
||||||
|
|
||||||
|
if (!hash) throw createError({ statusCode: 403, statusMessage: "Invalid or disabled account. Please contact the server administrator." });
|
||||||
|
|
||||||
|
if (!await checkHash(password, hash.toString()))
|
||||||
|
throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
|
||||||
|
|
||||||
|
await h3.context.session.setUserId(h3, authMek.userId);
|
||||||
|
|
||||||
|
return { result: true, userId: authMek.userId }
|
||||||
|
});
|
||||||
32
server/api/v1/signup/simple.post.ts
Normal file
32
server/api/v1/signup/simple.post.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { AuthMec } from "@prisma/client";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
import { createHash } from "~/server/internal/security/simple";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const body = await readBody(h3);
|
||||||
|
|
||||||
|
const username = body.username;
|
||||||
|
const password = body.password;
|
||||||
|
if (username === undefined || password === undefined)
|
||||||
|
throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." });
|
||||||
|
|
||||||
|
const existing = await prisma.user.count({ where: { username: username } });
|
||||||
|
if (existing > 0) throw createError({ statusCode: 400, statusMessage: "Username already taken." })
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = await createHash(password);
|
||||||
|
const authMek = await prisma.linkedAuthMec.create({
|
||||||
|
data: {
|
||||||
|
mec: AuthMec.Simple,
|
||||||
|
credentials: [username, hash],
|
||||||
|
userId: user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
})
|
||||||
5
server/api/v1/whoami.get.ts
Normal file
5
server/api/v1/whoami.get.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const user = await h3.context.session.getUser(h3);
|
||||||
|
|
||||||
|
return user ?? {};
|
||||||
|
});
|
||||||
8
server/h3.d.ts
vendored
Normal file
8
server/h3.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { SessionHandler } from "./internal/session";
|
||||||
|
|
||||||
|
export * from "h3";
|
||||||
|
declare module "h3" {
|
||||||
|
interface H3EventContext {
|
||||||
|
session: SessionHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
15
server/internal/db/database.ts
Normal file
15
server/internal/db/database.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const prismaClientSingleton = () => {
|
||||||
|
return new PrismaClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const globalThis: {
|
||||||
|
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
||||||
|
} & typeof global;
|
||||||
|
|
||||||
|
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||||
|
|
||||||
|
export default prisma
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
|
||||||
11
server/internal/security/simple.ts
Normal file
11
server/internal/security/simple.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
const rounds = 10;
|
||||||
|
|
||||||
|
export async function createHash(password: string) {
|
||||||
|
return bcrypt.hashSync(password, rounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkHash(password: string, hash: string) {
|
||||||
|
return bcrypt.compareSync(password, hash);
|
||||||
|
}
|
||||||
64
server/internal/session/index.ts
Normal file
64
server/internal/session/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { H3Event, Session } from "h3";
|
||||||
|
import createMemorySessionProvider from "./memory";
|
||||||
|
import { SessionProvider } from "./types";
|
||||||
|
import prisma from "../db/database";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is a poorly organised implemention.
|
||||||
|
|
||||||
|
It exposes an API that should stay static, but there are plenty of opportunities for optimisation/organisation under the hood
|
||||||
|
*/
|
||||||
|
|
||||||
|
const userSessionKey = "_userSession";
|
||||||
|
const userIdKey = "_userId";
|
||||||
|
|
||||||
|
export class SessionHandler {
|
||||||
|
private sessionProvider: SessionProvider;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Create a new provider
|
||||||
|
this.sessionProvider = createMemorySessionProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSession<T extends Session>(h3: H3Event) {
|
||||||
|
const data = await this.sessionProvider.getSession<{ [userSessionKey]: T }>(h3);
|
||||||
|
if (!data) return undefined;
|
||||||
|
|
||||||
|
return data[userSessionKey];
|
||||||
|
}
|
||||||
|
async setSession(h3: H3Event, data: any) {
|
||||||
|
const result = await this.sessionProvider.updateSession(h3, userSessionKey, data);
|
||||||
|
if (!result) {
|
||||||
|
const toCreate = { [userSessionKey]: data };
|
||||||
|
await this.sessionProvider.setSession(h3, toCreate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async clearSession(h3: H3Event) {
|
||||||
|
await this.sessionProvider.clearSession(h3);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserId(h3: H3Event) {
|
||||||
|
const session = await this.sessionProvider.getSession<{ [userIdKey]: string | undefined }>(h3);
|
||||||
|
if (!session) return undefined;
|
||||||
|
|
||||||
|
return session[userIdKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(h3: H3Event) {
|
||||||
|
const userId = await this.getUserId(h3);
|
||||||
|
if (!userId) return undefined;
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirst({ where: { id: userId } });
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserId(h3: H3Event, userId: string) {
|
||||||
|
const result = await this.sessionProvider.updateSession(h3, userIdKey, userId);
|
||||||
|
if (!result) {
|
||||||
|
const toCreate = { [userIdKey]: userId };
|
||||||
|
await this.sessionProvider.setSession(h3, toCreate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SessionHandler();
|
||||||
45
server/internal/session/memory.ts
Normal file
45
server/internal/session/memory.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
import { Session, SessionProvider } from "./types";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export default function createMemorySessionHandler() {
|
||||||
|
const sessions: { [key: string]: Session } = {}
|
||||||
|
|
||||||
|
const sessionCookieName = "drop-session";
|
||||||
|
|
||||||
|
const memoryProvider: SessionProvider = {
|
||||||
|
async setSession(h3, data) {
|
||||||
|
const existingCookie = getCookie(h3, sessionCookieName);
|
||||||
|
if (existingCookie) delete sessions[existingCookie]; // Clear any previous session
|
||||||
|
|
||||||
|
const cookie = uuidv4();
|
||||||
|
const expiry = moment().add(31, 'day');
|
||||||
|
setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() });
|
||||||
|
|
||||||
|
sessions[cookie] = data;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async updateSession(h3, key, data) {
|
||||||
|
const cookie = getCookie(h3, sessionCookieName);
|
||||||
|
if (!cookie) return false;
|
||||||
|
|
||||||
|
sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async getSession(h3) {
|
||||||
|
const cookie = getCookie(h3, sessionCookieName);
|
||||||
|
if (!cookie) return undefined;
|
||||||
|
|
||||||
|
return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want
|
||||||
|
},
|
||||||
|
async clearSession(h3) {
|
||||||
|
const cookie = getCookie(h3, sessionCookieName);
|
||||||
|
if (!cookie) return;
|
||||||
|
|
||||||
|
delete sessions[cookie];
|
||||||
|
deleteCookie(h3, sessionCookieName);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return memoryProvider;
|
||||||
|
}
|
||||||
10
server/internal/session/types.d.ts
vendored
Normal file
10
server/internal/session/types.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { H3Event } from "h3";
|
||||||
|
|
||||||
|
export type Session = { [key: string]: any };
|
||||||
|
|
||||||
|
export interface SessionProvider {
|
||||||
|
setSession: (h3: H3Event, data: Session) => 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>;
|
||||||
|
}
|
||||||
7
server/plugins/session.ts
Normal file
7
server/plugins/session.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import session from "../internal/session";
|
||||||
|
|
||||||
|
export default defineNitroPlugin((nitro) => {
|
||||||
|
nitro.hooks.hook('request', (h3) => {
|
||||||
|
h3.context.session = session;
|
||||||
|
})
|
||||||
|
});
|
||||||
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json"
|
||||||
|
}
|
||||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user