initial commit

This commit is contained in:
DecDuck
2024-09-28 19:12:11 +10:00
commit e1a789fa36
28 changed files with 5669 additions and 0 deletions

27
.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

23
dev-tools/compose.yml Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
<template>
</template>

23
pages/register.vue Normal file
View 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
View 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>

View File

@ -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;

View File

@ -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;

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/robots.txt Normal file
View File

@ -0,0 +1 @@

View 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 }
});

View 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;
})

View 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
View File

@ -0,0 +1,8 @@
import { SessionHandler } from "./internal/session";
export * from "h3";
declare module "h3" {
interface H3EventContext {
session: SessionHandler
}
}

View 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

View 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);
}

View 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();

View 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
View 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>;
}

View 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
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

4
tsconfig.json Normal file
View File

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

5210
yarn.lock Normal file

File diff suppressed because it is too large Load Diff