good grief
This commit is contained in:
parent
b1555b7fd7
commit
64069bfc33
30 changed files with 1198 additions and 810 deletions
|
@ -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",
|
||||
|
|
8
prisma/migrations/20240705200803_role_key/migration.sql
Normal file
8
prisma/migrations/20240705200803_role_key/migration.sql
Normal file
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
|
|
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
|
@ -1,3 +1,5 @@
|
|||
import type { User } from '@prisma/client';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Error {
|
||||
|
|
|
@ -9,6 +9,15 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
on:mousedown={() => {
|
||||
if (visible)
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex relative w-fit">
|
||||
<button class="my-auto w-min hover:text-overlay2 {visible && 'text-overlay2'}" on:click={toggle}>
|
||||
<Menu size="20"></Menu>
|
||||
|
|
|
@ -1,28 +1,41 @@
|
|||
<script>
|
||||
import { bytesToHumanReadable } from '$lib';
|
||||
import { CircleAlert } from 'lucide-svelte';
|
||||
<script lang="ts">
|
||||
import { bytesToHumanReadable } from '$lib/index';
|
||||
import { fileProgress } from '$lib/stores';
|
||||
import { CircleAlert, X } from 'lucide-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
/** @type {File} */
|
||||
export let file;
|
||||
export let file: File;
|
||||
export let i: number;
|
||||
export let remove: Function;
|
||||
export let running = false;
|
||||
|
||||
/** @type {Number} */
|
||||
export let i;
|
||||
let percent = 0,
|
||||
url = '',
|
||||
error = false;
|
||||
|
||||
let percent = 0;
|
||||
fileProgress.subscribe((_) => {
|
||||
let fileProgress = _[file.name];
|
||||
if (!fileProgress) return;
|
||||
|
||||
if (fileProgress.percent) percent = fileProgress.percent;
|
||||
if (fileProgress.url) url = fileProgress.url;
|
||||
if (fileProgress.error) error = fileProgress.error;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="rounded-md transition-all bg-mantle">
|
||||
<div
|
||||
in:fade|global={{ delay: 100 * i }}
|
||||
class="flex px-1.5 w-full h-14 rounded-md transition-all"
|
||||
style="
|
||||
background: linear-gradient(90deg, rgb(var(--ctp-surface0)) {percent}%, transparent
|
||||
{percent}%);"
|
||||
class="flex place-content-between px-1.5 w-full h-14 rounded-md transition-all {error
|
||||
? 'bg-red-300 dark:bg-red-700/60'
|
||||
: ''}"
|
||||
style={error
|
||||
? ''
|
||||
: `background: linear-gradient(90deg, rgb(var(--ctp-surface0)) ${percent}%, transparent ${percent}%);`}
|
||||
>
|
||||
<div class="flex overflow-x-scroll flex-col my-auto overflow-y-clip">
|
||||
{#if percent === 100}
|
||||
<a href="?" class="text-blue">{file.name}</a>
|
||||
{#if url}
|
||||
<a href={url} class="text-blue">{file.name}</a>
|
||||
{:else}
|
||||
<p>{file.name}</p>
|
||||
{/if}
|
||||
|
@ -38,5 +51,15 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if !running}
|
||||
<button
|
||||
class="hover:text-red-500"
|
||||
on:click={() => {
|
||||
remove(file.name);
|
||||
}}
|
||||
>
|
||||
<X size="20"></X></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
{:else if style === 'button'}
|
||||
<a
|
||||
{href}
|
||||
class="flex p-2 space-x-2 rounded-lg border-b-2 border-neutral-400
|
||||
hover:border-neutral-500 hover:dark:border-neutral-500 dark:border-neutral-700 bg-neutral-200 dark:bg-neutral-900"
|
||||
class="flex p-2 space-x-2 rounded-lg border-b-2 hadow-md bg-crust border-overlay2 hover:border-overlay0 group-disabled/button:border-overlay0 group-disabled/button:hover:border-overlay0"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
|
|
@ -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';
|
||||
}
|
45
src/lib/index.ts
Normal file
45
src/lib/index.ts
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
27
src/lib/server/auth.ts
Normal file
27
src/lib/server/auth.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
23
src/lib/server/crypto.ts
Normal file
23
src/lib/server/crypto.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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 }
|
||||
});
|
||||
}
|
121
src/lib/server/database.ts
Normal file
121
src/lib/server/database.ts
Normal file
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
28
src/lib/server/minio.ts
Normal file
28
src/lib/server/minio.ts
Normal file
|
@ -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);
|
|
@ -1,4 +0,0 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const darkMode = writable();
|
||||
export const user = writable();
|
7
src/lib/stores.ts
Normal file
7
src/lib/stores.ts
Normal file
|
@ -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({});
|
5
src/lib/types/user.d.ts
vendored
5
src/lib/types/user.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
}
|
|
@ -6,18 +6,19 @@
|
|||
import Footer from '$lib/components/Footer.svelte';
|
||||
</script>
|
||||
|
||||
<div class="container my-2 w-full min-h-screen">
|
||||
<div class="w-full h-12">
|
||||
<div class="container pt-2 w-full min-h-screen">
|
||||
<div class="mb-2 w-full h-12">
|
||||
<Header></Header>
|
||||
</div>
|
||||
|
||||
{#key $page.url}
|
||||
<div in:fade|global class="w-full min-h-[calc(100vh-4.5rem)] my-2">
|
||||
<div in:fade|global class="w-full min-h-[calc(100vh-4rem)] pb-2">
|
||||
<slot />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<!--
|
||||
<div class="w-full h-20">
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,68 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { Check, Upload } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores';
|
||||
import { fileProgress, user } from '$lib/stores';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import File from '$lib/components/File.svelte';
|
||||
import FileComponent from '$lib/components/File.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { request } from '$lib';
|
||||
|
||||
export let data;
|
||||
user.set(data?.user);
|
||||
|
||||
/** @type {HTMLInputElement} */
|
||||
let input;
|
||||
|
||||
/** @type {FileList} */
|
||||
let files;
|
||||
let input: HTMLInputElement,
|
||||
files: FileList,
|
||||
fileMap: Map<string, File> = new Map();
|
||||
|
||||
let running = false;
|
||||
|
||||
// lazy again
|
||||
function progress(name: string, data: any) {
|
||||
let _ = get(fileProgress);
|
||||
fileProgress.set(
|
||||
Object.assign(_, {
|
||||
[name]: data
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
|
||||
fileMap.forEach(async (v, k) => {
|
||||
const body = new FormData();
|
||||
body.append('file', v);
|
||||
|
||||
const response = await request(body, (percent: number) => {
|
||||
progress(k, { percent });
|
||||
}).catch(() => {
|
||||
progress(k, { error: true });
|
||||
});
|
||||
|
||||
if (response && response.success) progress(k, { url: response.body });
|
||||
else progress(k, { error: true });
|
||||
});
|
||||
}
|
||||
|
||||
function remove(name: string) {
|
||||
fileMap.delete(name);
|
||||
fileMap = fileMap;
|
||||
}
|
||||
|
||||
function change() {
|
||||
fileMap = new Map();
|
||||
|
||||
for (let i = 0; files.length > i; i++) {
|
||||
let file = files.item(i);
|
||||
if (!file) return;
|
||||
|
||||
fileMap.set(file.name, file);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="hidden" action="">
|
||||
<input type="file" name="file" id="file" multiple={true} bind:this={input} bind:files />
|
||||
</form>
|
||||
<input class="hidden" type="file" multiple={true} bind:this={input} bind:files on:change={change} />
|
||||
|
||||
<div class="w-[23rem] h-[calc(100vh-4.5rem)] flex mx-auto">
|
||||
<div class="flex flex-col gap-2 my-auto w-full">
|
||||
|
@ -28,15 +71,16 @@
|
|||
<p class="text-overlay1">Your max upload size is <span class="font-bold">100 MB</span>.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-2 mx-auto w-full rounded-lg shadow-lg bg-crust">
|
||||
{#if files?.length}
|
||||
{#each Array.from(files) as file, i}
|
||||
<File {file} {i}></File>
|
||||
{#key fileMap}
|
||||
{#each fileMap.values() as file, i}
|
||||
<FileComponent {file} {i} {remove} {running}></FileComponent>
|
||||
{/each}
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
{#if !running}
|
||||
<div out:slide class="flex gap-2">
|
||||
<button
|
||||
class="flex w-full {!files?.length
|
||||
class="flex w-full {!fileMap.size
|
||||
? 'h-36'
|
||||
: 'h-14'} rounded-md outline-2 outline-dotted outline-surface2 bg-mantle group"
|
||||
on:click={() => {
|
||||
|
@ -47,12 +91,10 @@
|
|||
<Upload></Upload>
|
||||
</div>
|
||||
</button>
|
||||
{#if files?.length}
|
||||
{#if fileMap.size}
|
||||
<button
|
||||
class="flex w-[25%] h-14 rounded-md transition-all outline-2 outline-dotted outline-surface2 bg-mantle group"
|
||||
on:click={() => {
|
||||
running = true;
|
||||
}}
|
||||
on:click={upload}
|
||||
>
|
||||
<div class="flex m-auto text-lg text-surface2 group-hover:text-green">
|
||||
<Check class=""></Check>
|
||||
|
|
|
@ -3,12 +3,9 @@
|
|||
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
import { CircleAlert, TriangleAlert, Info, Check, Loader } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { user } from '$lib/stores.js';
|
||||
import { user } from '$lib/stores';
|
||||
import ThemeHandler from '$lib/components/ThemeHandler.svelte';
|
||||
import PageMeta from '$lib/components/PageMeta.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let data;
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { COOKIE } from '$lib/config';
|
|||
import { createUser, createSession } from '$lib/server/database';
|
||||
import { email } from '$lib/server/validator';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function POST(event) {
|
||||
const { request, cookies, locals } = event;
|
||||
const body = await request.json();
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { bytesToHumanReadable } from '$lib';
|
||||
import prisma from '$lib/server/database';
|
||||
import minio, { USAGE } from '$lib/server/minio';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
/** @type {import("@sveltejs/kit").RequestHandler} */
|
||||
export async function GET() {
|
||||
return json({
|
||||
users: ':3',
|
||||
files: 'dick',
|
||||
storage: '100 gigafarts'
|
||||
users: await prisma.user.count({}),
|
||||
files: await prisma.upload.count({}),
|
||||
storage: bytesToHumanReadable(get(USAGE))
|
||||
});
|
||||
}
|
||||
|
|
41
src/routes/api/upload/+server.ts
Normal file
41
src/routes/api/upload/+server.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { authenticate } from '$lib/server/auth';
|
||||
import { catIdCharacters, generateId } from '$lib/server/crypto.js';
|
||||
import { createUpload } from '$lib/server/database';
|
||||
import minio, { BUCKET } from '$lib/server/minio';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const POST = async ({ request, cookies }) => {
|
||||
const contentType = request.headers.get('Content-Type');
|
||||
if (!contentType || !contentType.includes('multipart/form-data'))
|
||||
return error(400, { status: 400, message: 'Improper Content-Type' });
|
||||
|
||||
const user = await authenticate(request, cookies);
|
||||
if (!user) return error(403, { status: 403, message: 'Forbidden' });
|
||||
|
||||
const data = await request.formData();
|
||||
const file = data.get('file') as File;
|
||||
let id = generateId(undefined, 10);
|
||||
|
||||
console.log(id);
|
||||
|
||||
const object = await minio
|
||||
.putObject(
|
||||
BUCKET,
|
||||
`${user.id}/${file.name}`,
|
||||
Buffer.from(await file.arrayBuffer()),
|
||||
file.size,
|
||||
{
|
||||
'Content-Type': file.type
|
||||
}
|
||||
)
|
||||
.catch((e) => console.log(e));
|
||||
if (!object)
|
||||
return error(500, { status: 500, message: 'Internal Server Error - Contact Administrator' });
|
||||
|
||||
const objectRecord = await createUpload(id, user.id, file.name);
|
||||
if (!objectRecord)
|
||||
return error(500, { status: 500, message: 'Internal Server Error - Contact Administrator' });
|
||||
|
||||
if (user.settings?.linkToRaw) return new Response(`/download/${id}`);
|
||||
return new Response(`/file/${id}`);
|
||||
};
|
34
src/routes/download/[id]/+server.ts
Normal file
34
src/routes/download/[id]/+server.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { getUpload } from '$lib/server/database.js';
|
||||
import minio, { BUCKET } from '$lib/server/minio';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async ({ params }) => {
|
||||
const id = params.id;
|
||||
|
||||
const file = await getUpload(id);
|
||||
if (!file) throw error(404, { status: 404, message: 'File Not Found' });
|
||||
|
||||
const object = await minio.getObject(BUCKET, `${file.uploader.id}/${file.fileName}`);
|
||||
const metadata = await minio.statObject(BUCKET, `${file.uploader.id}/${file.fileName}`);
|
||||
|
||||
const ac = new AbortController();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
object.on('data', (chunk) => {
|
||||
controller.enqueue(chunk);
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
ac.abort();
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Disposition': `attachment; filename="${file.fileName}"`,
|
||||
'Content-Type': metadata.metaData['content-type'],
|
||||
'Content-Length': metadata.size.toString()
|
||||
}
|
||||
});
|
||||
};
|
23
src/routes/file/[id]/+page.server.ts
Normal file
23
src/routes/file/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { getUpload } from '$lib/server/database';
|
||||
import minio, { BUCKET } from '$lib/server/minio';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ params }) {
|
||||
const file = await getUpload(params.id);
|
||||
if (!file) throw error(404, { status: 404, message: 'File Not Found' });
|
||||
|
||||
const metadata = await minio.statObject(BUCKET, `${file.uploader.id}/${file.fileName}`);
|
||||
|
||||
return {
|
||||
file: {
|
||||
id: file.id,
|
||||
fileName: file.fileName,
|
||||
uploaded: file.uploaded,
|
||||
size: metadata.size,
|
||||
type: metadata.metaData['content-type']
|
||||
},
|
||||
uploader: {
|
||||
username: file.uploader.username
|
||||
}
|
||||
};
|
||||
}
|
34
src/routes/file/[id]/+page.svelte
Normal file
34
src/routes/file/[id]/+page.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script>
|
||||
import { bytesToHumanReadable } from '$lib';
|
||||
import Link from '$lib/components/Inputs/Link.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="h-[85vh] md:h-screen">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{data.file.fileName}</h1>
|
||||
<p class="text-overlay1">
|
||||
Uploaded by <span class="font-bold">{data.uploader.username}</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-2.5 rounded-lg shadow-lg bg-crust h-[50vh]">
|
||||
{#if data.file.type.includes('video')}
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video class="h-full" src="/download/{data.file.id}" controls></video>
|
||||
{:else if data.file.type.includes('image')}
|
||||
<img class="h-full" src="/download/{data.file.id}" alt={data.file.id} />
|
||||
{/if}
|
||||
</div>
|
||||
<Link style="button" href="/download/{data.file.id}">
|
||||
<p class="w-full font-bold text-center">
|
||||
Download ({bytesToHumanReadable(data.file.size)})
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue