diff --git a/package.json b/package.json index 25d1884..3465093 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "prisma": "^5.11.0", "svelte": "^4.2.7", "tailwindcss": "^3.4.1", + "tslib": "^2.6.3", + "typescript": "^5.5.3", "vite": "^5.0.3" }, "type": "module", diff --git a/prisma/migrations/20240705200803_role_key/migration.sql b/prisma/migrations/20240705200803_role_key/migration.sql new file mode 100644 index 0000000..6427c03 --- /dev/null +++ b/prisma/migrations/20240705200803_role_key/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `role` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `User` ADD COLUMN `role` ENUM('ADMINISTRATOR', 'USER', 'BANNED') NOT NULL; diff --git a/prisma/migrations/20240706025748_longtext_lol/migration.sql b/prisma/migrations/20240706025748_longtext_lol/migration.sql new file mode 100644 index 0000000..153cf71 --- /dev/null +++ b/prisma/migrations/20240706025748_longtext_lol/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE `Upload` MODIFY `fileName` LONGTEXT NOT NULL, + MODIFY `public` BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE `UserSettings` MODIFY `newPostsPublic` BOOLEAN NOT NULL DEFAULT true; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dcd3e1d..9f4097c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,7 @@ model User { username String @unique email String @unique password String + role Role createdAt DateTime @default(now()) lastSeen DateTime @default(now()) settings UserSettings? @@ -37,7 +38,7 @@ model UserSettings { user User @relation(fields: [userId], references: [id]) userId Int @unique - newPostsPublic Boolean @default(false) + newPostsPublic Boolean @default(true) linkToRaw Boolean @default(false) embedTitle String @default("{{file}}") @@ -50,7 +51,13 @@ model Upload { uploader User @relation(fields: [uploaderId], references: [id]) uploaderId Int - fileName String - public Boolean @default(false) + fileName String @db.LongText + public Boolean @default(true) uploaded DateTime @default(now()) } + +enum Role { + ADMINISTRATOR + USER + BANNED +} diff --git a/src/app.d.ts b/src/app.d.ts index 860cdab..35d5351 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,3 +1,5 @@ +import type { User } from '@prisma/client'; + declare global { namespace App { interface Error { diff --git a/src/error.html b/src/error.html.disabled similarity index 100% rename from src/error.html rename to src/error.html.disabled diff --git a/src/lib/components/Dropdown.svelte b/src/lib/components/Dropdown.svelte index 8b7756c..cb32b6d 100644 --- a/src/lib/components/Dropdown.svelte +++ b/src/lib/components/Dropdown.svelte @@ -9,6 +9,15 @@ } + { + if (visible) + setTimeout(() => { + visible = false; + }, 150); + }} +/> +
+ {/if}
diff --git a/src/lib/components/Inputs/Link.svelte b/src/lib/components/Inputs/Link.svelte index da926e2..ed9391f 100644 --- a/src/lib/components/Inputs/Link.svelte +++ b/src/lib/components/Inputs/Link.svelte @@ -10,8 +10,7 @@ {:else if style === 'button'} diff --git a/src/lib/index.js b/src/lib/index.js deleted file mode 100644 index d5bdc0c..0000000 --- a/src/lib/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import { browser } from '$app/environment'; - -export function goBack() { - if (browser) { - history.back(); - } -} - -/** @param bytes {Number} */ -export function bytesToHumanReadable(bytes) { - if (bytes === 0) { - return '0 B'; - } - - let e = Math.floor(Math.log(bytes) / Math.log(1024)); - return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B'; -} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..c774401 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,45 @@ +import { browser } from '$app/environment'; + +export function goBack() { + if (browser) { + history.back(); + } +} + +export function bytesToHumanReadable(bytes: number) { + if (bytes === 0) { + return '0 B'; + } + + let e = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B'; +} + +export function request( + data: FormData, + progress: Function +): Promise<{ success: boolean; body: string }> { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.addEventListener('loadend', () => { + resolve({ + success: xhr.readyState === 4 && xhr.status === 200, + body: xhr.responseText + }); + }); + + xhr.addEventListener('error', (event) => { + reject(event); + }); + + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + progress(Math.floor((event.loaded / event.total) * 100)); + } + }); + + xhr.open('POST', '/api/upload', true); + xhr.send(data); + }); +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..7788f04 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,27 @@ +import { COOKIE } from '$lib/config'; +import type { Cookies } from '@sveltejs/kit'; +import { getSession } from './database'; +import type { User, UserSettings } from '@prisma/client'; + +interface UserAndMaybeSettings extends User { + settings: UserSettings | null; +} + +export async function authenticate(request: Request, cookies: Cookies) { + const bearer = request.headers.get('Authorization')?.replace('Bearer ', ''); + const cookie = cookies.get(COOKIE); + + let user: UserAndMaybeSettings | false = false; + + if (bearer && !cookie) { + return false; + } + + if (cookie && !bearer) { + const session = await getSession(cookie); + if (!session) return false; + user = session.user; + } + + return user; +} diff --git a/src/lib/server/crypto.js b/src/lib/server/crypto.js deleted file mode 100644 index bd1afe7..0000000 --- a/src/lib/server/crypto.js +++ /dev/null @@ -1,9 +0,0 @@ -import { hash, verify } from 'argon2'; - -export async function createHash(input) { - return await hash(input); -} - -export async function verifyHash(hash, input) { - return await verify(hash, input); -} diff --git a/src/lib/server/crypto.ts b/src/lib/server/crypto.ts new file mode 100644 index 0000000..1647727 --- /dev/null +++ b/src/lib/server/crypto.ts @@ -0,0 +1,23 @@ +import { hash, verify } from 'argon2'; + +export const letterIdCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); +export const catIdCharacters = ['🐱', '😻', '😿', '😹', '😽', '😾', '🙀', '😸', '😺', '😼', '🐈']; + +export async function createHash(input: string) { + return await hash(input); +} + +export async function verifyHash(hash: string, input: string) { + return await verify(hash, input); +} + +export function generateId(characters: String[] = letterIdCharacters, length: number) { + length = Math.max(length, 6); + let id = ''; + + for (let i = 0; length > i; i++) { + id += characters[Math.floor(Math.random() * characters.length)]; + } + + return id; +} diff --git a/src/lib/server/database.js b/src/lib/server/database.js deleted file mode 100644 index aa80fbf..0000000 --- a/src/lib/server/database.js +++ /dev/null @@ -1,71 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { randomBytes } from 'node:crypto'; -import { createHash } from './crypto'; - -const prisma = new PrismaClient(); -export default prisma; - -export async function createUser(username, email, password) { - const user = await prisma.user.create({ - data: { - username, - email, - password: await createHash(password) - } - }); - - await prisma.userSettings.create({ - data: { - userId: user.id - } - }); - - return user; -} - -export async function createSession(userId, needsMfa = false) { - const session = await prisma.session.create({ - data: { - id: randomBytes(64).toString('base64'), - userId, - authorized: !needsMfa, - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 12) - } - }); - - return session; -} - -export async function findUser({ email, username }) { - if (!email && !username) return false; - - const user = await prisma.user.findFirst({ - where: { - email, - username - } - }); - - return user; -} - -export async function getSession(id) { - if (!id) return false; - - const session = await prisma.session.findFirst({ - where: { id }, - include: { - user: true - } - }); - - return session; -} - -export async function deleteSession(id) { - if (!id) return false; - - return await prisma.session.delete({ - where: { id } - }); -} diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts new file mode 100644 index 0000000..de5644b --- /dev/null +++ b/src/lib/server/database.ts @@ -0,0 +1,121 @@ +import { PrismaClient } from '@prisma/client'; +import { randomBytes } from 'node:crypto'; +import { createHash } from './crypto'; + +const prisma = new PrismaClient(); +export default prisma; + +interface FindUserQuery { + username?: string; + email?: string; +} + +export async function createUser(username: string, email: string, password: string) { + const user = await prisma.user.create({ + data: { + username, + email, + password: await createHash(password), + role: 'USER' + } + }); + + await prisma.userSettings.create({ + data: { + userId: user.id + } + }); + + return user; +} + +export async function createSession(userId: number, needsMfa = false) { + const session = await prisma.session.create({ + data: { + id: randomBytes(64).toString('base64'), + userId, + authorized: !needsMfa, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 12) + } + }); + + return session; +} + +export async function findUser({ email, username }: FindUserQuery) { + if (!email && !username) return false; + + const user = await prisma.user.findFirst({ + where: { + email, + username + } + }); + + return user; +} + +export async function getSession(id: string) { + if (!id) return false; + + const session = await prisma.session.findFirst({ + where: { id }, + include: { + user: { + select: { + id: true, + username: true, + email: true, + password: true, + role: true, + createdAt: true, + lastSeen: true, + maxUploadMB: true, + settings: true + } + } + } + }); + + return session; +} + +export async function deleteSession(id: string) { + if (!id) return false; + + return await prisma.session.delete({ + where: { id } + }); +} + +export async function createUpload(id: string, uploaderId: number, fileName: string) { + const settings = await prisma.userSettings.findFirst({ + where: { id: uploaderId } + }); + + return await prisma.upload.create({ + data: { + id, + uploaderId, + fileName, + public: settings?.newPostsPublic + } + }); +} + +export async function getUpload(id: string) { + if (!id) return false; + + return await prisma.upload.findFirst({ + where: { + id + }, + select: { + id: true, + fileName: true, + public: true, + uploaded: true, + uploader: true + } + }); +} diff --git a/src/lib/server/minio.ts b/src/lib/server/minio.ts new file mode 100644 index 0000000..671e54a --- /dev/null +++ b/src/lib/server/minio.ts @@ -0,0 +1,28 @@ +import { building } from '$app/environment'; +import { env } from '$env/dynamic/private'; +import * as Minio from 'minio'; +import { get, writable } from 'svelte/store'; + +const minio = new Minio.Client({ + endPoint: building ? 'building.local' : env.MINIO_URL, + useSSL: true, + accessKey: building ? 'building' : env.MINIO_ACCESS_KEY, + secretKey: building ? 'building' : env.MINIO_SECRET_KEY +}); + +export default minio; + +export const BUCKET = env.MINIO_BUCKET; + +export let USAGE = writable(0); + +function du() { + let usage = 0; + const stream = minio.listObjects(BUCKET, undefined, true); + stream.on('data', (object) => (usage += object.size)); + stream.on('end', () => USAGE.set(usage)); +} + +du(); + +setTimeout(du, 1000 * 60 * 10); diff --git a/src/lib/stores.js b/src/lib/stores.js deleted file mode 100644 index f012b30..0000000 --- a/src/lib/stores.js +++ /dev/null @@ -1,4 +0,0 @@ -import { writable } from 'svelte/store'; - -export const darkMode = writable(); -export const user = writable(); diff --git a/src/lib/stores.ts b/src/lib/stores.ts new file mode 100644 index 0000000..3553348 --- /dev/null +++ b/src/lib/stores.ts @@ -0,0 +1,7 @@ +import { writable, type Writable } from 'svelte/store'; + +export const darkMode = writable(); +export const user = writable(); + +// too lazy to do types for this +export const fileProgress: Writable<{ [key: string]: any }> = writable({}); diff --git a/src/lib/types/user.d.ts b/src/lib/types/user.d.ts deleted file mode 100644 index 88b5b37..0000000 --- a/src/lib/types/user.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface User { - id: number; - username: string; - email: string; -} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index af8d3d1..b61d6aa 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -6,18 +6,19 @@ import Footer from '$lib/components/Footer.svelte'; -
-
+
+
{#key $page.url} -
+
{/key} +
diff --git a/src/routes/(app)/dashboard/+page.svelte b/src/routes/(app)/dashboard/+page.svelte index 77d6074..4d47480 100644 --- a/src/routes/(app)/dashboard/+page.svelte +++ b/src/routes/(app)/dashboard/+page.svelte @@ -1,25 +1,68 @@ - - +
@@ -28,15 +71,16 @@

Your max upload size is 100 MB.

- {#if files?.length} - {#each Array.from(files) as file, i} - + {#key fileMap} + {#each fileMap.values() as file, i} + {/each} - {/if} + {/key} + {#if !running}
- {#if files?.length} + {#if fileMap.size}