yap #1
84 changed files with 2715 additions and 1188 deletions
|
@ -0,0 +1 @@
|
||||||
|
DATABASE_URL=
|
43
Dockerfile
43
Dockerfile
|
@ -3,33 +3,32 @@ FROM node:lts-alpine AS base
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
FROM base AS install
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json yarn.lock /temp/dev/
|
||||||
|
RUN cd /temp/dev && yarn install
|
||||||
|
|
||||||
RUN mkdir dev
|
RUN mkdir -p /temp/prod
|
||||||
COPY package.json dev/
|
COPY package.json yarn.lock /temp/prod/
|
||||||
COPY yarn.lock dev/
|
RUN cd /temp/prod && yarn install --omit=dev
|
||||||
RUN cd /usr/src/app/dev && yarn install
|
|
||||||
|
|
||||||
# Not needed as of now since adapter-node
|
FROM base AS prisma
|
||||||
# packs all of our dependencies for us.
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
|
|
||||||
# RUN mkdir prod
|
|
||||||
# COPY package.json prod/
|
|
||||||
# COPY yarn.lock prod/
|
|
||||||
# RUN cd /usr/src/app/prod && yarn install --production
|
|
||||||
|
|
||||||
FROM base AS build
|
|
||||||
|
|
||||||
COPY --from=install /usr/src/app/dev/node_modules node_modules
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn build
|
RUN npx prisma generate
|
||||||
|
|
||||||
FROM base AS app
|
FROM base AS prerelease
|
||||||
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# COPY --from=install /usr/src/app/prod/node_modules node_modules
|
RUN npx prisma generate
|
||||||
COPY --from=build /usr/src/app/package.json .
|
RUN npm run build
|
||||||
COPY --from=build /usr/src/app/build/ .
|
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=prisma /usr/src/app/prisma prisma
|
||||||
|
COPY --from=prisma /usr/src/app/node_modules node_modules
|
||||||
|
COPY --from=prerelease /usr/src/app/build/ ./
|
||||||
|
COPY --from=prerelease /usr/src/app/package.json .
|
||||||
|
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
CMD [ "node", "index.js" ]
|
CMD [ "node", "index.js" ]
|
||||||
|
|
|
@ -5,3 +5,11 @@
|
||||||
[![forthebadge](https://forthebadge.com/images/badges/license-mit.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/designed-in-ms-paint.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/gluten-free.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/powered-by-black-magic.svg)](https://forthebadge.com)
|
[![forthebadge](https://forthebadge.com/images/badges/license-mit.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/designed-in-ms-paint.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/gluten-free.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/powered-by-black-magic.svg)](https://forthebadge.com)
|
||||||
|
|
||||||
A file uploading website.
|
A file uploading website.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
You can start a development SQL server with Docker:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo docker run -e MYSQL_ROOT_PASSWORD=development -e MYSQL_DATABASE=default -d mysql:latest
|
||||||
|
```
|
||||||
|
|
9
docker-compose.dev.yml
Normal file
9
docker-compose.dev.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MARIADB_DATABASE: file-uploader
|
||||||
|
MARIADB_ROOT_PASSWORD: development
|
||||||
|
ports:
|
||||||
|
- '3306:3306'
|
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
container_name: 'file-uploader-db'
|
||||||
|
image: 'mariadb'
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MARIADB_DATABASE: file-uploader
|
||||||
|
MARIADB_ROOT_PASSWORD: file-uploader
|
||||||
|
file-uploader:
|
||||||
|
container_name: 'file-uploader'
|
||||||
|
image: file-uploader
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: mysql://root:file-uploader@mariadb:3306/file-uploader
|
19
jsconfig.json
Normal file
19
jsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
|
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
19
package.json
19
package.json
|
@ -4,30 +4,35 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": " vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "prettier --check .",
|
"lint": "prettier --check .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@catppuccin/tailwindcss": "^0.1.6",
|
||||||
"@sveltejs/adapter-node": "^5.0.1",
|
"@sveltejs/adapter-node": "^5.0.1",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
"drizzle-kit": "^0.20.14",
|
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
|
"prisma": "^5.11.0",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.7",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "^0.30.2",
|
"@prisma/client": "5.11.0",
|
||||||
|
"argon2": "^0.40.1",
|
||||||
"lucide-svelte": "^0.358.0",
|
"lucide-svelte": "^0.358.0",
|
||||||
"mysql2": "^3.9.2",
|
"mime": "^4.0.4",
|
||||||
"svelte-sonner": "^0.3.19"
|
"minio": "^7.1.3",
|
||||||
|
"svelte-sonner": "^0.3.19",
|
||||||
|
"validator": "^13.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
57
prisma/migrations/20240402035113_init/migration.sql
Normal file
57
prisma/migrations/20240402035113_init/migration.sql
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `User` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(191) NOT NULL,
|
||||||
|
`email` VARCHAR(191) NOT NULL,
|
||||||
|
`password` VARCHAR(191) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`lastSeen` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `User_username_key`(`username`),
|
||||||
|
UNIQUE INDEX `User_email_key`(`email`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Session` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`authorized` BOOLEAN NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`expiresAt` DATETIME(3) NOT NULL,
|
||||||
|
`remoteAddress` VARCHAR(191) NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `Session_id_key`(`id`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `UserSettings` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`newPostsPublic` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
UNIQUE INDEX `UserSettings_userId_key`(`userId`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Upload` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`uploaderId` INTEGER NOT NULL,
|
||||||
|
`fileName` VARCHAR(191) NOT NULL,
|
||||||
|
`public` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`uploaded` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `Upload_fileName_key`(`fileName`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `UserSettings` ADD CONSTRAINT `UserSettings_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Upload` ADD CONSTRAINT `Upload_uploaderId_fkey` FOREIGN KEY (`uploaderId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
16
prisma/migrations/20240501010657_dope/migration.sql
Normal file
16
prisma/migrations/20240501010657_dope/migration.sql
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `embedColor` to the `UserSettings` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `embedDescription` to the `UserSettings` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `embedTitle` to the `UserSettings` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `User` ADD COLUMN `maxUploadMB` INTEGER NOT NULL DEFAULT 100;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `UserSettings` ADD COLUMN `embedColor` INTEGER NOT NULL,
|
||||||
|
ADD COLUMN `embedDescription` VARCHAR(191) NOT NULL,
|
||||||
|
ADD COLUMN `embedTitle` VARCHAR(191) NOT NULL,
|
||||||
|
ADD COLUMN `linkToRaw` BOOLEAN NOT NULL DEFAULT false;
|
7
prisma/migrations/20240501012209_fix/migration.sql
Normal file
7
prisma/migrations/20240501012209_fix/migration.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX `Upload_fileName_key` ON `Upload`;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `UserSettings` MODIFY `embedColor` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
MODIFY `embedDescription` VARCHAR(191) NOT NULL DEFAULT 'Uploaded by {{username}} at {{time}}',
|
||||||
|
MODIFY `embedTitle` VARCHAR(191) NOT NULL DEFAULT '{{file}}';
|
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;
|
2
prisma/migrations/20240706040428_defaults/migration.sql
Normal file
2
prisma/migrations/20240706040428_defaults/migration.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `UserSettings` MODIFY `embedColor` INTEGER NOT NULL DEFAULT 3159110;
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `internalName` to the `Upload` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Upload` ADD COLUMN `internalName` LONGTEXT NOT NULL;
|
8
prisma/migrations/20240706074316_size_key/migration.sql
Normal file
8
prisma/migrations/20240706074316_size_key/migration.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `size` to the `Upload` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Upload` ADD COLUMN `size` BIGINT NOT NULL;
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to alter the column `size` on the `Upload` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Upload` MODIFY `size` INTEGER 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 = "mysql"
|
65
prisma/schema.prisma
Normal file
65
prisma/schema.prisma
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
datasource db {
|
||||||
|
provider = "mysql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
username String @unique
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
role Role
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
lastSeen DateTime @default(now())
|
||||||
|
settings UserSettings?
|
||||||
|
// STORED AS MEGABYTES !!
|
||||||
|
maxUploadMB Int @default(100)
|
||||||
|
uploads Upload[]
|
||||||
|
sessions Session[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @unique
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
|
||||||
|
authorized Boolean
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
expiresAt DateTime
|
||||||
|
remoteAddress String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSettings {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int @unique
|
||||||
|
|
||||||
|
newPostsPublic Boolean @default(true)
|
||||||
|
linkToRaw Boolean @default(false)
|
||||||
|
|
||||||
|
embedTitle String @default("{{file}}")
|
||||||
|
embedDescription String @default("Uploaded by {{username}} at {{time}}")
|
||||||
|
embedColor Int @default(3159110)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Upload {
|
||||||
|
id String @id
|
||||||
|
uploader User @relation(fields: [uploaderId], references: [id])
|
||||||
|
uploaderId Int
|
||||||
|
|
||||||
|
fileName String @db.LongText
|
||||||
|
internalName String @db.LongText
|
||||||
|
size Int
|
||||||
|
public Boolean @default(true)
|
||||||
|
uploaded DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
ADMINISTRATOR
|
||||||
|
USER
|
||||||
|
BANNED
|
||||||
|
}
|
20
src/app.css
20
src/app.css
|
@ -1,14 +1,22 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@200..1000&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
html {
|
html,
|
||||||
@apply dark:bg-neutral-950;
|
body {
|
||||||
@apply dark:text-neutral-200;
|
@apply bg-base;
|
||||||
@apply transition-colors;
|
@apply text-text;
|
||||||
|
|
||||||
|
@apply transition-all;
|
||||||
|
|
||||||
|
font-family: 'Nunito', sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
* {
|
||||||
@apply dark:text-white;
|
|
||||||
@apply transition-colors;
|
@apply transition-colors;
|
||||||
}
|
}
|
||||||
|
|
26
src/app.d.ts
vendored
Normal file
26
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import type { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface UserSafe {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
maxUploadMB: number;
|
||||||
|
role: Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
interface Error {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
interface Locals {
|
||||||
|
user: UserSafe;
|
||||||
|
}
|
||||||
|
interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
|
@ -28,14 +28,14 @@
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button on:click="history.back();">
|
<button on:click="history.back();">
|
||||||
<div
|
<div
|
||||||
class="flex p-2 space-x-2 rounded-lg border-b-2 transition-colors 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 border-neutral-400 hover:border-neutral-500 hover:dark:border-neutral-500 dark:border-neutral-700 bg-neutral-200 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<p>Go Back</p>
|
<p>Go Back</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="flex p-2 space-x-2 rounded-lg border-b-2 transition-colors 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 border-neutral-400 hover:border-neutral-500 hover:dark:border-neutral-500 dark:border-neutral-700 bg-neutral-200 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<p>Go Home</p>
|
<p>Go Home</p>
|
||||||
</a>
|
</a>
|
|
@ -1,12 +1,43 @@
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import { getSession } from '$lib/server/database';
|
||||||
|
import { COOKIE } from '$lib/config';
|
||||||
|
|
||||||
|
const PUBLIC_RESOURCES = [
|
||||||
|
'/',
|
||||||
|
'/api',
|
||||||
|
'/api/auth/register',
|
||||||
|
'/api/auth/login',
|
||||||
|
'/terms',
|
||||||
|
'/privacy'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
const { cookies, locals } = event;
|
||||||
|
const session = await getSession(cookies.get(COOKIE) || '');
|
||||||
|
|
||||||
|
if (session && session.user) {
|
||||||
|
locals.user = {
|
||||||
|
id: session.user.id,
|
||||||
|
username: session.user.username,
|
||||||
|
email: session.user.email,
|
||||||
|
maxUploadMB: session.user.maxUploadMB,
|
||||||
|
role: session.user.role
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (event.route.id) {
|
||||||
|
if (event.route.id.includes('(app)')) return redirect(303, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').HandleServerError} */
|
/** @type {import('@sveltejs/kit').HandleServerError} */
|
||||||
export async function handleError({ error, event, status, message }) {
|
export async function handleError({ error, event, status, message }) {
|
||||||
|
console.log(error);
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
|
||||||
status,
|
status,
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<script>
|
|
||||||
export let click;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button type="button" on:click={click}>
|
|
||||||
<div
|
|
||||||
class="flex p-2 space-x-2 rounded-lg border-b-2 transition-colors border-neutral-400 hover:border-neutral-500 hover:dark:border-neutral-500 dark:border-neutral-700 bg-neutral-200 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
33
src/lib/components/Dropdown.svelte
Normal file
33
src/lib/components/Dropdown.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { Menu } from 'lucide-svelte';
|
||||||
|
|
||||||
|
let visible = false;
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
visible = !visible;
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
</button>
|
||||||
|
{#if visible}
|
||||||
|
<div
|
||||||
|
transition:slide
|
||||||
|
class="w-[12rem] z-10 h-fit translate-y-12 py-2 bg-crust rounded-lg origin-top right-0 absolute transition-all shadow-md translate-x-3.5"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
7
src/lib/components/DropdownButton.svelte
Normal file
7
src/lib/components/DropdownButton.svelte
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
export let href = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a {href} class="flex px-3 py-1.5 space-x-1 transition-all hover:bg-overlay0">
|
||||||
|
<slot />
|
||||||
|
</a>
|
73
src/lib/components/File.svelte
Normal file
73
src/lib/components/File.svelte
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { bytesToHumanReadable } from '$lib/index';
|
||||||
|
import { fileProgress, user } from '$lib/stores';
|
||||||
|
import { CircleAlert, X } from 'lucide-svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
export let file: File;
|
||||||
|
export let i: number;
|
||||||
|
export let remove: Function;
|
||||||
|
export let running = false;
|
||||||
|
|
||||||
|
let percent = 0,
|
||||||
|
url = '',
|
||||||
|
error = false;
|
||||||
|
|
||||||
|
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 place-content-between px-1.5 w-full h-14 rounded-md transition-all
|
||||||
|
{url ? 'bg-blue/30' : ''} {error ? 'bg-red/30' : ''}"
|
||||||
|
style={error || url
|
||||||
|
? ''
|
||||||
|
: `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 url}
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
class="font-bold overflow-ellipsis whitespace-nowrap text-blue overflow-x-clip"
|
||||||
|
>{file.name}</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<p class="overflow-ellipsis whitespace-nowrap overflow-x-clip">{file.name}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex gap-0.5">
|
||||||
|
{#if file.size > get(user).maxUploadMB * 1048576}
|
||||||
|
<p class="font-bold text-red">
|
||||||
|
<CircleAlert class="w-4 h-4"></CircleAlert>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<p
|
||||||
|
class="text-xs my-auto {file.size > get(user).maxUploadMB * 1048576
|
||||||
|
? 'text-red font-bold'
|
||||||
|
: 'text-overlay1'}"
|
||||||
|
>
|
||||||
|
{bytesToHumanReadable(file.size)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if !running}
|
||||||
|
<button
|
||||||
|
class="hover:text-red-500"
|
||||||
|
on:click={() => {
|
||||||
|
remove(file.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size="20"></X></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
3
src/lib/components/Footer.svelte
Normal file
3
src/lib/components/Footer.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div
|
||||||
|
class="flex place-content-between px-4 w-full h-full rounded-xl shadow-md py-auto bg-crust"
|
||||||
|
></div>
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
export let type, name, id, placeholder;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex p-2 space-x-1 rounded-lg transition-colors bg-neutral-200 dark:bg-neutral-900">
|
|
||||||
<div class="py-0.5 pr-1 border-r-2 transition-colors border-neutral-400 dark:border-neutral-700">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
class="py-0.5 transition-colors bg-neutral-200 dark:bg-neutral-900"
|
|
||||||
{type}
|
|
||||||
{name}
|
|
||||||
{id}
|
|
||||||
{placeholder}
|
|
||||||
/>
|
|
||||||
</div>
|
|
90
src/lib/components/Forms/LoginForm.svelte
Normal file
90
src/lib/components/Forms/LoginForm.svelte
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<script>
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { Mail, SquareAsterisk, LogIn, Undo } from 'lucide-svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import FormInput from '$lib/components/Inputs/FormInput.svelte';
|
||||||
|
import Button from '$lib/components/Inputs/Button.svelte';
|
||||||
|
import ButtonText from '$lib/components/Inputs/ButtonText.svelte';
|
||||||
|
import ButtonIcon from '$lib/components/Inputs/ButtonIcon.svelte';
|
||||||
|
|
||||||
|
export let callback = () => {};
|
||||||
|
|
||||||
|
let disabled = false;
|
||||||
|
let email = '',
|
||||||
|
password = '';
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
disabled = true;
|
||||||
|
|
||||||
|
const id = toast.loading('Logging in...');
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
toast.error('Missing email.', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
toast.error('Missing password.', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
}).catch((_) => toast.error(_.message));
|
||||||
|
const body = await response.json().catch((_) => toast.error(_.message));
|
||||||
|
|
||||||
|
if (!body?.success) {
|
||||||
|
toast.error(body?.error || 'Unexpected Error', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Welcome, ' + body.data.username, { id });
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center h-full">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<div class=" fill-text">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<form on:submit|preventDefault>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<FormInput
|
||||||
|
type={'email'}
|
||||||
|
name={'email'}
|
||||||
|
id={'email'}
|
||||||
|
placeholder={'user@example.com'}
|
||||||
|
bind:value={email}
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<Mail />
|
||||||
|
</FormInput>
|
||||||
|
<FormInput
|
||||||
|
type={'password'}
|
||||||
|
name={'password'}
|
||||||
|
id={'password'}
|
||||||
|
placeholder={'•'.repeat(16)}
|
||||||
|
bind:value={password}
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<SquareAsterisk />
|
||||||
|
</FormInput>
|
||||||
|
<div class="flex place-content-between">
|
||||||
|
<Button click={callback} {disabled}>
|
||||||
|
<ButtonIcon><Undo /></ButtonIcon>
|
||||||
|
<ButtonText><p>Go Back</p></ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Button click={login} {disabled} pulse={disabled}>
|
||||||
|
<ButtonIcon><LogIn /></ButtonIcon>
|
||||||
|
<ButtonText><p>Login</p></ButtonText>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
126
src/lib/components/Forms/RegisterForm.svelte
Normal file
126
src/lib/components/Forms/RegisterForm.svelte
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<script>
|
||||||
|
import { Mail, SquareAsterisk, Undo, User, UserPlus } from 'lucide-svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import FormInput from '$lib/components/Inputs/FormInput.svelte';
|
||||||
|
import Button from '$lib/components/Inputs/Button.svelte';
|
||||||
|
import ButtonText from '$lib/components/Inputs/ButtonText.svelte';
|
||||||
|
import ButtonIcon from '$lib/components/Inputs/ButtonIcon.svelte';
|
||||||
|
|
||||||
|
export let callback = () => {};
|
||||||
|
|
||||||
|
let disabled = false;
|
||||||
|
let username = '',
|
||||||
|
email = '',
|
||||||
|
password = '',
|
||||||
|
cpassword = '';
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
disabled = true;
|
||||||
|
|
||||||
|
const id = toast.loading('Registering...');
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
toast.error('Missing username.', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
toast.error('Missing email.', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password || !cpassword) {
|
||||||
|
toast.error('Missing password.', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== cpassword) {
|
||||||
|
toast.error('Your passwords do not match.', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ username, email, password })
|
||||||
|
}).catch((_) => toast.error(_.message));
|
||||||
|
const body = await response.json().catch((_) => toast.error(_.message));
|
||||||
|
|
||||||
|
if (!body?.success) {
|
||||||
|
toast.error(body?.error || 'Unexpected Error', { id });
|
||||||
|
return (disabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Welcome, ' + body.data.username, { id });
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center h-full">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<div class=" fill-text">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<form on:submit|preventDefault>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<FormInput
|
||||||
|
type={'username'}
|
||||||
|
name={'username'}
|
||||||
|
id={'username'}
|
||||||
|
placeholder={'Username'}
|
||||||
|
bind:value={username}
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<User />
|
||||||
|
</FormInput>
|
||||||
|
<FormInput
|
||||||
|
type={'email'}
|
||||||
|
name={'email'}
|
||||||
|
id={'email'}
|
||||||
|
placeholder={'user@example.com'}
|
||||||
|
bind:value={email}
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<Mail />
|
||||||
|
</FormInput>
|
||||||
|
<FormInput
|
||||||
|
type={'password'}
|
||||||
|
name={'password'}
|
||||||
|
id={'password'}
|
||||||
|
placeholder={'•'.repeat(16)}
|
||||||
|
bind:value={password}
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<SquareAsterisk />
|
||||||
|
</FormInput>
|
||||||
|
<FormInput
|
||||||
|
type={'password'}
|
||||||
|
name={'cpassword'}
|
||||||
|
id={'cpassword'}
|
||||||
|
placeholder={'•'.repeat(16)}
|
||||||
|
bind:value={cpassword}
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<SquareAsterisk />
|
||||||
|
</FormInput>
|
||||||
|
<div class="flex place-content-between">
|
||||||
|
<Button click={callback} {disabled}>
|
||||||
|
<ButtonIcon><Undo /></ButtonIcon>
|
||||||
|
<ButtonText>Go Back</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Button click={register} {disabled} pulse={disabled}>
|
||||||
|
<ButtonIcon><UserPlus /></ButtonIcon>
|
||||||
|
<ButtonText>Register</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-center text-overlay1">
|
||||||
|
By registering an account you agree to the <br />
|
||||||
|
<a class="underline" href="/terms" data-sveltekit-reload>Terms of Service</a> and
|
||||||
|
<a class="underline" href="/privacy" data-sveltekit-reload>Privacy Policy</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
55
src/lib/components/Header.svelte
Normal file
55
src/lib/components/Header.svelte
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { Sun, Moon } from 'lucide-svelte';
|
||||||
|
|
||||||
|
import { user, darkMode } from '$lib/stores';
|
||||||
|
|
||||||
|
import HeaderLink from '$lib/components/HeaderLink.svelte';
|
||||||
|
import Dropdown from '$lib/components/Dropdown.svelte';
|
||||||
|
import DropdownButton from '$lib/components/DropdownButton.svelte';
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex place-content-between px-4 w-full h-full rounded-xl shadow-md py-auto bg-crust">
|
||||||
|
<div class="flex my-auto md:space-x-6">
|
||||||
|
<a
|
||||||
|
href="/dashboard"
|
||||||
|
class="hidden flex-none my-auto w-20 text-xl transition-all md:block fill-text hover:scale-105 focus:scale-105 active:scale-95"
|
||||||
|
>
|
||||||
|
<Logo></Logo>
|
||||||
|
</a>
|
||||||
|
<div class="flex my-auto space-x-3">
|
||||||
|
<HeaderLink href="/dashboard">Dashboard</HeaderLink>
|
||||||
|
<HeaderLink href="/uploads">Uploads</HeaderLink>
|
||||||
|
<HeaderLink href="/links">Links</HeaderLink>
|
||||||
|
{#if $user?.role === 'ADMINISTRATOR'}
|
||||||
|
<HeaderLink href="/admin">Admin</HeaderLink>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex my-auto space-x-2">
|
||||||
|
<p class="font-bold">{$user?.username}</p>
|
||||||
|
<Dropdown>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex px-3 py-1.5 space-x-1.5 w-full transition-all hover:bg-overlay0"
|
||||||
|
on:click={() => {
|
||||||
|
$darkMode = !$darkMode;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if $darkMode}
|
||||||
|
<Sun class="my-auto w-5 h-5" />
|
||||||
|
{:else}
|
||||||
|
<Moon class="my-auto w-5 h-5" />
|
||||||
|
{/if}
|
||||||
|
<p>Theme</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<DropdownButton href="/settings">
|
||||||
|
<p class="my-auto">Settings</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton href="/api/auth/logout">
|
||||||
|
<p class="my-auto text-red-500">Logout</p>
|
||||||
|
</DropdownButton>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
19
src/lib/components/HeaderLink.svelte
Normal file
19
src/lib/components/HeaderLink.svelte
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
export let href;
|
||||||
|
let selected = false;
|
||||||
|
|
||||||
|
page.subscribe((pg) => {
|
||||||
|
selected = pg.url.pathname === href;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
{href}
|
||||||
|
class="my-auto md:text-lg transition-all {selected
|
||||||
|
? 'font-bold tracking-wider'
|
||||||
|
: 'hover:text-overlay2 focus:text-overlay2'}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
18
src/lib/components/Inputs/Button.svelte
Normal file
18
src/lib/components/Inputs/Button.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
export let click,
|
||||||
|
disabled = false,
|
||||||
|
pulse = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="group/button {pulse ? 'animate-pulse cursor-wait' : ''} "
|
||||||
|
type="button"
|
||||||
|
on:click={click}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
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 />
|
||||||
|
</div>
|
||||||
|
</button>
|
3
src/lib/components/Inputs/ButtonIcon.svelte
Normal file
3
src/lib/components/Inputs/ButtonIcon.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="group-disabled/button:text-overlay1">
|
||||||
|
<slot />
|
||||||
|
</div>
|
3
src/lib/components/Inputs/ButtonText.svelte
Normal file
3
src/lib/components/Inputs/ButtonText.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<p class="group-disabled/button:text-overlay1">
|
||||||
|
<slot />
|
||||||
|
</p>
|
72
src/lib/components/Inputs/FormInput.svelte
Normal file
72
src/lib/components/Inputs/FormInput.svelte
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<script>
|
||||||
|
import { CircleAlert, Check } from 'lucide-svelte';
|
||||||
|
|
||||||
|
export let type, name, id, placeholder, value, required;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- insane that i have to do this because -->
|
||||||
|
<!-- 'type' attribute cannot be dynamic if input uses two-way binding -->
|
||||||
|
{#if type === 'username'}
|
||||||
|
<div class="flex p-2 space-x-1 rounded-lg shadow-md bg-crust">
|
||||||
|
<div class="py-0.5 pr-1 border-r-2 border-overlay2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="py-0.5 bg-crust peer placeholder:text-overlay1"
|
||||||
|
type="username"
|
||||||
|
{name}
|
||||||
|
{id}
|
||||||
|
{placeholder}
|
||||||
|
{required}
|
||||||
|
bind:value
|
||||||
|
/>
|
||||||
|
<div class="hidden my-auto peer-invalid:flex">
|
||||||
|
<CircleAlert />
|
||||||
|
</div>
|
||||||
|
<div class="my-auto peer-invalid:hidden">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if type === 'email'}
|
||||||
|
<div class="flex p-2 space-x-1 rounded-lg shadow-md bg-crust">
|
||||||
|
<div class="py-0.5 pr-1 border-r-2 border-overlay2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="py-0.5 bg-crust peer placeholder:text-overlay1"
|
||||||
|
type="email"
|
||||||
|
{name}
|
||||||
|
{id}
|
||||||
|
{placeholder}
|
||||||
|
{required}
|
||||||
|
bind:value
|
||||||
|
/>
|
||||||
|
<div class="hidden my-auto peer-invalid:flex">
|
||||||
|
<CircleAlert />
|
||||||
|
</div>
|
||||||
|
<div class="my-auto peer-invalid:hidden">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if type === 'password'}
|
||||||
|
<div class="flex p-2 space-x-1 rounded-lg shadow-md bg-crust">
|
||||||
|
<div class="py-0.5 pr-1 border-r-2 border-overlay2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="py-0.5 bg-crust peer placeholder:text-overlay1"
|
||||||
|
type="password"
|
||||||
|
{name}
|
||||||
|
{id}
|
||||||
|
{placeholder}
|
||||||
|
{required}
|
||||||
|
bind:value
|
||||||
|
/>
|
||||||
|
<div class="hidden my-auto peer-invalid:flex">
|
||||||
|
<CircleAlert />
|
||||||
|
</div>
|
||||||
|
<div class="my-auto peer-invalid:hidden">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
17
src/lib/components/Inputs/Link.svelte
Normal file
17
src/lib/components/Inputs/Link.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
export let href,
|
||||||
|
style = 'link';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if style === 'link'}
|
||||||
|
<a {href} class="text-blue-500">
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
{:else if style === 'button'}
|
||||||
|
<a
|
||||||
|
{href}
|
||||||
|
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>
|
||||||
|
{/if}
|
|
@ -1,17 +0,0 @@
|
||||||
<script>
|
|
||||||
export let href,
|
|
||||||
style = 'link';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if style === 'link'}
|
|
||||||
<a {href} class="text-blue-500">
|
|
||||||
<slot />
|
|
||||||
</a>
|
|
||||||
{:else if style === 'button'}
|
|
||||||
<a
|
|
||||||
{href}
|
|
||||||
class="flex p-2 space-x-2 rounded-lg border-b-2 transition-colors border-neutral-400 hover:border-neutral-500 hover:dark:border-neutral-500 dark:border-neutral-700 bg-neutral-200 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
|
@ -2,6 +2,7 @@
|
||||||
viewBox="0 0 1200 628"
|
viewBox="0 0 1200 628"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
|
class=" fill-inherit"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:
|
xmlns:
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 291 KiB |
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
import { darkMode } from '../stores';
|
import { darkMode } from '../stores';
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
|
@ -8,16 +9,20 @@
|
||||||
$darkMode = localStorage.getItem('darkMode') === 'true';
|
$darkMode = localStorage.getItem('darkMode') === 'true';
|
||||||
|
|
||||||
darkMode.subscribe(() => {
|
darkMode.subscribe(() => {
|
||||||
console.log(`[ThemeHandler.svelte] Current theme is ${$darkMode ? 'dark' : 'light'} mode`);
|
console.log(
|
||||||
|
`[ThemeHandler.svelte] Current theme is ${get(darkMode) ? 'dark' : 'light'} mode`
|
||||||
|
);
|
||||||
|
|
||||||
localStorage.setItem('darkMode', $darkMode);
|
localStorage.setItem('darkMode', get(darkMode).toString());
|
||||||
|
|
||||||
if ($darkMode) {
|
if ($darkMode) {
|
||||||
console.log('[ThemeHandler.svelte] Setting dark mode from store');
|
console.log('[ThemeHandler.svelte] Setting dark mode from store');
|
||||||
html.classList.add('dark');
|
html.classList.add('dark');
|
||||||
|
html.classList.add('frappe');
|
||||||
} else {
|
} else {
|
||||||
console.log('[ThemeHandler.svelte] Setting light mode from store');
|
console.log('[ThemeHandler.svelte] Setting light mode from store');
|
||||||
html.classList.remove('dark');
|
html.classList.remove('dark');
|
||||||
|
html.classList.remove('frappe');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -31,9 +36,11 @@
|
||||||
if (darkMode) {
|
if (darkMode) {
|
||||||
console.log('[ThemeHandler.svelte] Setting dark mode from localStorage');
|
console.log('[ThemeHandler.svelte] Setting dark mode from localStorage');
|
||||||
html.classList.add('dark');
|
html.classList.add('dark');
|
||||||
|
html.classList.add('frappe');
|
||||||
} else {
|
} else {
|
||||||
console.log('[ThemeHandler.svelte] Setting light mode from localStorage');
|
console.log('[ThemeHandler.svelte] Setting light mode from localStorage');
|
||||||
html.classList.remove('dark');
|
html.classList.remove('dark');
|
||||||
|
html.classList.remove('frappe');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Sun, Moon } from 'lucide-svelte';
|
import { Sun, Moon } from 'lucide-svelte';
|
||||||
import { darkMode } from '../stores';
|
import { darkMode } from '../stores';
|
||||||
import Button from './Button.svelte';
|
import Button from '$lib/components/Inputs/Button.svelte';
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
$darkMode = !$darkMode;
|
$darkMode = !$darkMode;
|
||||||
|
|
10
src/lib/config.js
Normal file
10
src/lib/config.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export const COOKIE = '.FILE-UPLOADER-SESSION';
|
||||||
|
|
||||||
|
export const MAIL_WHITELIST = [
|
||||||
|
'gmail.com',
|
||||||
|
'outlook.com',
|
||||||
|
'madhouselabs.net',
|
||||||
|
'dfuser.xyz',
|
||||||
|
'liloandstit.ch',
|
||||||
|
'vea.st'
|
||||||
|
];
|
|
@ -1,7 +0,0 @@
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
export function goBack() {
|
|
||||||
if (browser) {
|
|
||||||
history.back();
|
|
||||||
}
|
|
||||||
}
|
|
43
src/lib/index.ts
Normal file
43
src/lib/index.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export function goBack() {
|
||||||
|
if (browser) {
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToHumanReadable(bytes: number) {
|
||||||
|
let i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return (
|
||||||
|
+(bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'][i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
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;
|
||||||
|
}
|
140
src/lib/server/database.ts
Normal file
140
src/lib/server/database.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
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,
|
||||||
|
internalName: string,
|
||||||
|
size: number
|
||||||
|
) {
|
||||||
|
const settings = await prisma.userSettings.findFirst({
|
||||||
|
where: { id: uploaderId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.upload.create({
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
uploaderId,
|
||||||
|
fileName,
|
||||||
|
internalName,
|
||||||
|
size,
|
||||||
|
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,
|
||||||
|
internalName: true,
|
||||||
|
public: true,
|
||||||
|
uploaded: true,
|
||||||
|
uploader: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSettings(id: number) {
|
||||||
|
if (!id) return false;
|
||||||
|
|
||||||
|
return await prisma.userSettings.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
import { drizzle } from 'drizzle-orm/mysql2';
|
|
||||||
import mysql from 'mysql2/promise';
|
|
||||||
|
|
||||||
const connection = await mysql.createConnection({
|
|
||||||
host: process.env.DATABASE_HOST,
|
|
||||||
user: process.env.DATABASE_USER,
|
|
||||||
password: process.env.DATABASE_PASSWORD
|
|
||||||
});
|
|
||||||
|
|
||||||
export default drizzle(connection);
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { int, mysqlTable, text } from 'drizzle-orm/mysql-core';
|
|
||||||
|
|
||||||
export const users = mysqlTable('users', {
|
|
||||||
id: int('id').autoincrement().primaryKey(),
|
|
||||||
username: text('username').unique().notNull(),
|
|
||||||
password: text('password').notNull()
|
|
||||||
});
|
|
29
src/lib/server/minio.ts
Normal file
29
src/lib/server/minio.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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 = building ? 'building' : 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!building) {
|
||||||
|
du();
|
||||||
|
setTimeout(du, 1000 * 60 * 5);
|
||||||
|
}
|
30
src/lib/server/ratelimit.js
Normal file
30
src/lib/server/ratelimit.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { COOKIE } from '$lib/config.js';
|
||||||
|
|
||||||
|
const limits = new Map();
|
||||||
|
|
||||||
|
const CookieLimits = {
|
||||||
|
Minute: 100,
|
||||||
|
Hour: 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApiKeyLimits = {
|
||||||
|
Minute: 0,
|
||||||
|
Hour: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(function resetMinute() {}, 1000 * 60);
|
||||||
|
setInterval(function resetHour() {}, 1000 * 60 * 60);
|
||||||
|
|
||||||
|
function hash(input) {
|
||||||
|
createHash('sha256').update(input).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cookie({ cookies }) {
|
||||||
|
const hashed = hash(cookies.get(COOKIE));
|
||||||
|
console.log(hashed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiKey(event) {}
|
||||||
|
|
||||||
|
export async function handle(event) {}
|
8
src/lib/server/validator.js
Normal file
8
src/lib/server/validator.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import validator from 'validator';
|
||||||
|
import { MAIL_WHITELIST } from '$lib/config';
|
||||||
|
|
||||||
|
// https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
|
||||||
|
|
||||||
|
export function email(input) {
|
||||||
|
return validator.isEmail(input, { host_whitelist: MAIL_WHITELIST });
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const darkMode = writable();
|
|
8
src/lib/stores.ts
Normal file
8
src/lib/stores.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
import type { UserSafe } from '../app';
|
||||||
|
|
||||||
|
export const darkMode: Writable<boolean> = writable(true);
|
||||||
|
export const user: Writable<UserSafe> = writable();
|
||||||
|
|
||||||
|
// too lazy to do types for this
|
||||||
|
export const fileProgress: Writable<{ [key: string]: any }> = writable({});
|
24
src/routes/(app)/+layout.svelte
Normal file
24
src/routes/(app)/+layout.svelte
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
import Header from '$lib/components/Header.svelte';
|
||||||
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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-4rem)] pb-2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div class="w-full h-20">
|
||||||
|
<Footer></Footer>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
1
src/routes/(app)/admin/+page.svelte
Normal file
1
src/routes/(app)/admin/+page.svelte
Normal file
|
@ -0,0 +1 @@
|
||||||
|
adminnn
|
11
src/routes/(app)/dashboard/+page.server.js
Normal file
11
src/routes/(app)/dashboard/+page.server.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/** @type {import("@sveltejs/kit").ServerLoad} */
|
||||||
|
export function load({ locals, fetch }) {
|
||||||
|
const statistics = fetch('/api/statistics').then((response) => response.json());
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: locals?.user,
|
||||||
|
streamed: {
|
||||||
|
statistics
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
149
src/routes/(app)/dashboard/+page.svelte
Normal file
149
src/routes/(app)/dashboard/+page.svelte
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Check, Upload } from 'lucide-svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { fileProgress, user } from '$lib/stores';
|
||||||
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
import FileComponent from '$lib/components/File.svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { request } from '$lib';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
user.set(data?.user);
|
||||||
|
|
||||||
|
let input: HTMLInputElement,
|
||||||
|
files: FileList,
|
||||||
|
fileMap: Map<string, File> = new Map();
|
||||||
|
|
||||||
|
let running = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fileProgress.set({});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
toast.error(k + ' failed to upload.');
|
||||||
|
progress(k, { error: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success) progress(k, { url: response.body });
|
||||||
|
else {
|
||||||
|
if (response && response.body)
|
||||||
|
try {
|
||||||
|
let body = JSON.parse(response.body);
|
||||||
|
toast.error(k + ' failed: ' + body.message);
|
||||||
|
} catch (_) {
|
||||||
|
toast.error(k + ' failed to upload.');
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">Welcome, {data.user.username}.</h1>
|
||||||
|
<p class="text-overlay1">
|
||||||
|
Your max upload size is <span class="font-bold">{data.user.maxUploadMB} MiB</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 p-2 mx-auto w-full rounded-lg shadow-lg bg-crust">
|
||||||
|
{#key fileMap}
|
||||||
|
{#each fileMap.values() as file, i}
|
||||||
|
<FileComponent {file} {i} {remove} {running}></FileComponent>
|
||||||
|
{/each}
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
{#if !running}
|
||||||
|
<div out:slide class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="flex w-full {!fileMap.size
|
||||||
|
? 'h-36'
|
||||||
|
: 'h-14'} rounded-md outline-2 outline-dotted outline-surface2 bg-mantle group"
|
||||||
|
on:click={() => {
|
||||||
|
input.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex m-auto text-lg text-surface2 group-hover:text-overlay1">
|
||||||
|
<Upload></Upload>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{#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={upload}
|
||||||
|
>
|
||||||
|
<div class="flex m-auto text-lg text-surface2 group-hover:text-green">
|
||||||
|
<Check class=""></Check>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="p-2 mx-auto mb-auto w-full rounded-lg shadow-lg bg-crust">
|
||||||
|
<table class="mx-auto w-full text-sm table-auto">
|
||||||
|
<tbody>
|
||||||
|
{#await data?.streamed?.statistics}
|
||||||
|
<div class="h-[66px]"></div>
|
||||||
|
{:then statistics}
|
||||||
|
<tr in:fade={{ delay: 60 * 1 }}>
|
||||||
|
<td class="font-bold">Registered Users</td>
|
||||||
|
<td class="text-right">{statistics?.users}</td>
|
||||||
|
</tr>
|
||||||
|
<tr in:fade={{ delay: 60 * 3 }}>
|
||||||
|
<td class="font-bold">Files Hosted</td>
|
||||||
|
<td class="text-right">{statistics?.files}</td>
|
||||||
|
</tr>
|
||||||
|
<tr in:fade={{ delay: 60 * 6 }}>
|
||||||
|
<td class="font-bold">File Storage</td>
|
||||||
|
<td class="text-right">{statistics?.storage}</td>
|
||||||
|
</tr>
|
||||||
|
{/await}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
0
src/routes/(app)/settings/+page.svelte
Normal file
0
src/routes/(app)/settings/+page.svelte
Normal file
25
src/routes/(app)/uploads/+page.server.ts
Normal file
25
src/routes/(app)/uploads/+page.server.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import prisma from '$lib/server/database.js';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load({ locals, url }) {
|
||||||
|
if (!locals.user) return error(403, { status: 403, message: 'Forbidden' });
|
||||||
|
|
||||||
|
if (+(url.searchParams.get('i') || 0) < 0) error(400, { status: 403, message: 'Invalid Index' });
|
||||||
|
|
||||||
|
const totalUploads = await prisma.upload.count();
|
||||||
|
if (+(url.searchParams.get('i') || 0) >= Math.ceil(totalUploads / 15))
|
||||||
|
error(400, { status: 403, message: 'Invalid Index' });
|
||||||
|
|
||||||
|
const uploads = prisma.upload.findMany({
|
||||||
|
skip: +(url.searchParams.get('i') || 0) * 15,
|
||||||
|
take: 15,
|
||||||
|
where: {
|
||||||
|
uploaderId: locals.user.id
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
uploaded: 'desc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { uploads, totalUploads };
|
||||||
|
}
|
60
src/routes/(app)/uploads/+page.svelte
Normal file
60
src/routes/(app)/uploads/+page.svelte
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { File, ChevronLeft, ChevronRight } from 'lucide-svelte';
|
||||||
|
import { bytesToHumanReadable } from '$lib';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { writable, get } from 'svelte/store';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let pageIndex = writable(Number($page.url.searchParams.get('i')) || 0);
|
||||||
|
pageIndex.subscribe((_) => {
|
||||||
|
if (browser) {
|
||||||
|
goto('/uploads?i=' + get(pageIndex));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await data.uploads then uploads}
|
||||||
|
<div class="grid grid-cols-5 grid-rows-3 gap-2">
|
||||||
|
{#each uploads as upload, i}
|
||||||
|
<a
|
||||||
|
in:fade|global={{ duration: 500, delay: 100 * i }}
|
||||||
|
href="/file/{upload.id}"
|
||||||
|
class="flex flex-col gap-2 p-2 mx-auto w-full rounded-lg shadow-lg bg-crust"
|
||||||
|
>
|
||||||
|
<div class="flex w-full rounded-lg aspect-video bg-mantle">
|
||||||
|
<File size="32" class="m-auto text-surface2"></File>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold overflow-ellipsis whitespace-nowrap overflow-x-clip">
|
||||||
|
{upload.fileName}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-overlay1">{bytesToHumanReadable(upload.size)}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="flex mx-auto mt-2 space-x-1 rounded-md bg-crust w-fit">
|
||||||
|
<button
|
||||||
|
class="p-2 my-auto hover:text-overlay2"
|
||||||
|
on:click={() => {
|
||||||
|
if ($pageIndex <= 0) return;
|
||||||
|
$pageIndex -= 1;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronLeft></ChevronLeft>
|
||||||
|
</button>
|
||||||
|
<p class="p-2 my-auto">{$pageIndex + 1} / {Math.ceil(data.totalUploads / 15)}</p>
|
||||||
|
<button
|
||||||
|
class="p-2 my-auto hover:text-overlay2"
|
||||||
|
on:click={() => {
|
||||||
|
if ($pageIndex >= Math.ceil(data.totalUploads / 15) - 1) return;
|
||||||
|
$pageIndex += 1;
|
||||||
|
}}
|
||||||
|
><ChevronRight></ChevronRight>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/await}
|
10
src/routes/(landing)/+layout.svelte
Normal file
10
src/routes/(landing)/+layout.svelte
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key $page.url}
|
||||||
|
<div in:fade class="container">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
10
src/routes/(landing)/+page.server.ts
Normal file
10
src/routes/(landing)/+page.server.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/** @type {import("@sveltejs/kit").ServerLoad} */
|
||||||
|
export function load({ locals, fetch }) {
|
||||||
|
const statistics = fetch('/api/statistics').then((response) => response.json());
|
||||||
|
|
||||||
|
return {
|
||||||
|
streamed: {
|
||||||
|
statistics
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
65
src/routes/(landing)/+page.svelte
Normal file
65
src/routes/(landing)/+page.svelte
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script>
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { LogIn, UserPlus } from 'lucide-svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import ThemeSwitcher from '$lib/components/ThemeSwitcher.svelte';
|
||||||
|
import Button from '$lib/components/Inputs/Button.svelte';
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import LoginForm from '$lib/components/Forms/LoginForm.svelte';
|
||||||
|
import RegisterForm from '$lib/components/Forms/RegisterForm.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let state = writable('/landing');
|
||||||
|
|
||||||
|
if ($page.url.hash.replace('#', '') !== '') {
|
||||||
|
state.set($page.url.hash.replace('#', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.subscribe((value) => {
|
||||||
|
if (browser) {
|
||||||
|
goto('#' + value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-[85vh] md:h-screen">
|
||||||
|
{#if $state === '/landing'}
|
||||||
|
<div class="flex justify-center items-center h-full">
|
||||||
|
<div class="flex flex-col space-y-1.5">
|
||||||
|
<div class="text-text fill-text">
|
||||||
|
<Logo />
|
||||||
|
{#await data.streamed.statistics}
|
||||||
|
<p>Currently hosting <strong>...</strong> files.</p>
|
||||||
|
{:then stats}
|
||||||
|
<p>Currently hosting <strong>{stats.files}</strong> files.</p>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<p class="italic">The best file uploader <strong>ever!!!</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex place-content-around mx-auto space-x-2">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
|
||||||
|
<Button click={() => ($state = '/login')}>
|
||||||
|
<LogIn />
|
||||||
|
<p>Login</p>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button click={() => ($state = '/register')}>
|
||||||
|
<UserPlus />
|
||||||
|
<p>Register</p>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if $state === '/login'}
|
||||||
|
<LoginForm callback={() => ($state = '/landing')} />
|
||||||
|
{:else if $state === '/register'}
|
||||||
|
<RegisterForm callback={() => ($state = '/landing')} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
0
src/routes/(landing)/privacy/+page.svelte
Normal file
0
src/routes/(landing)/privacy/+page.svelte
Normal file
0
src/routes/(landing)/terms/+page.svelte
Normal file
0
src/routes/(landing)/terms/+page.svelte
Normal file
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
import { goBack } from '$lib/';
|
import { goBack } from '$lib/';
|
||||||
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Inputs/Button.svelte';
|
||||||
import Link from '$lib/components/Link.svelte';
|
import Link from '$lib/components/Inputs/Link.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-center items-center h-screen">
|
<div class="flex justify-center items-center h-screen">
|
||||||
|
@ -16,11 +16,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<Button click={goBack}>
|
<Button click={goBack}>
|
||||||
<Undo />
|
|
||||||
<p>Go Back</p>
|
<p>Go Back</p>
|
||||||
</Button>
|
</Button>
|
||||||
<Link style="button" href="/">
|
<Link style="button" href="/">
|
||||||
<Home />
|
|
||||||
<p>Go Home</p>
|
<p>Go Home</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
6
src/routes/+layout.server.js
Normal file
6
src/routes/+layout.server.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/** @type {import('@sveltejs/kit').ServerLoad} */
|
||||||
|
export const load = async ({ locals }) => {
|
||||||
|
return {
|
||||||
|
user: locals.user
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,16 +2,36 @@
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
import { Toaster } from 'svelte-sonner';
|
import { Toaster } from 'svelte-sonner';
|
||||||
|
import { CircleAlert, TriangleAlert, Info, Check, Loader } from 'lucide-svelte';
|
||||||
import { darkMode } from '$lib/stores.js';
|
import { user } from '$lib/stores';
|
||||||
import ThemeHandler from '$lib/components/ThemeHandler.svelte';
|
import ThemeHandler from '$lib/components/ThemeHandler.svelte';
|
||||||
import PageMeta from '$lib/components/PageMeta.svelte';
|
import PageMeta from '$lib/components/PageMeta.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
user.set(data?.user);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageMeta title="cirro's file uploader" />
|
<PageMeta title="cirro's file uploader" />
|
||||||
<ThemeHandler />
|
<ThemeHandler />
|
||||||
<Toaster theme={$darkMode ? 'dark' : 'light'} />
|
<Toaster
|
||||||
|
position={'bottom-center'}
|
||||||
|
toastOptions={{
|
||||||
|
classes: {
|
||||||
|
toast: '!bg-crust !fill-text !border-crust',
|
||||||
|
title: '!text-text',
|
||||||
|
description: '!text-subtext0',
|
||||||
|
actionButton: 'bg-zinc-400',
|
||||||
|
cancelButton: 'bg-orange-400',
|
||||||
|
closeButton: 'bg-lime-400'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Loader size="20" class="animate-spin text-text" slot="loading-icon" />
|
||||||
|
<Check size="20" class="text-text" slot="success-icon" />
|
||||||
|
<CircleAlert size="20" class="text-text" slot="error-icon" />
|
||||||
|
<Info size="20" class="text-text" slot="info-icon" />
|
||||||
|
<TriangleAlert size="20" class="text-text" slot="warning-icon" />
|
||||||
|
</Toaster>
|
||||||
|
|
||||||
<div class="container">
|
<slot />
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
<script>
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import { LogIn, UserPlus } from 'lucide-svelte';
|
|
||||||
|
|
||||||
import ThemeSwitcher from '$lib/components/ThemeSwitcher.svelte';
|
|
||||||
import Link from '$lib/components/Link.svelte';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
|
||||||
|
|
||||||
function fuckYou() {
|
|
||||||
toast.error('Not Implemented');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex justify-center items-center h-screen">
|
|
||||||
<div class="flex flex-col space-y-1.5">
|
|
||||||
<div>
|
|
||||||
<div class="transition-colors fill-black dark:fill-white">
|
|
||||||
<Logo />
|
|
||||||
</div>
|
|
||||||
<p>Currently hosting <strong>0</strong> files.</p>
|
|
||||||
<p>Elon musk <strong>found dead</strong> in a <strong>pool</strong></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<ThemeSwitcher />
|
|
||||||
|
|
||||||
<Link style="button" href="/login">
|
|
||||||
<LogIn />
|
|
||||||
<p>Login</p>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link style="button" href="/register">
|
|
||||||
<UserPlus />
|
|
||||||
<p>Register</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<Button click={fuckYou}>
|
|
||||||
<p class="w-full text-center">Keycloak Login</p>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { COOKIE } from '$lib/config';
|
||||||
|
import { findUser, createSession } from '$lib/server/database';
|
||||||
|
import { email } from '$lib/server/validator';
|
||||||
|
import { verifyHash } from '$lib/server/crypto';
|
||||||
|
|
||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export async function POST(event) {
|
||||||
|
const { request, cookies } = event;
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (!body?.email || !email(body?.email))
|
||||||
|
return json({ error: 'Invalid email.' }, { status: 400 });
|
||||||
|
|
||||||
|
if (!body?.password || body?.password.length > 128 || body?.password.length < 6)
|
||||||
|
return json({ error: 'Invalid password.' }, { status: 400 });
|
||||||
|
|
||||||
|
const user = await findUser({ email: body?.email });
|
||||||
|
if (!user) return json({ error: 'User record not found.' }, { status: 401 });
|
||||||
|
|
||||||
|
if (!(await verifyHash(user.password, body?.password)))
|
||||||
|
return json({ error: 'User record not found.' }, { status: 401 });
|
||||||
|
|
||||||
|
const session = await createSession(user.id);
|
||||||
|
|
||||||
|
cookies.set(COOKIE, session.id, { path: '/' });
|
||||||
|
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
}
|
17
src/routes/api/auth/logout/+server.js
Normal file
17
src/routes/api/auth/logout/+server.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { getSession, deleteSession } from '$lib/server/database';
|
||||||
|
import { COOKIE } from '$lib/config';
|
||||||
|
|
||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export async function GET({ cookies }) {
|
||||||
|
const session = await getSession(cookies.get(COOKIE));
|
||||||
|
if (!session) {
|
||||||
|
cookies.delete(COOKIE, { path: '/' });
|
||||||
|
return redirect(302, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteSession(session.id);
|
||||||
|
cookies.delete(COOKIE, { path: '/' });
|
||||||
|
|
||||||
|
redirect(302, '/');
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { COOKIE } from '$lib/config';
|
||||||
|
import { createUser, createSession, findUser } from '$lib/server/database';
|
||||||
|
import { email } from '$lib/server/validator';
|
||||||
|
|
||||||
|
export async function POST(event) {
|
||||||
|
const { request, cookies } = event;
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (!body?.username || body?.username.length > 16 || body?.username.length < 3)
|
||||||
|
return json({ error: 'Invalid username.' }, { status: 400 });
|
||||||
|
|
||||||
|
if (!body?.email || !email(body?.email))
|
||||||
|
return json({ error: 'Invalid email.' }, { status: 400 });
|
||||||
|
|
||||||
|
if (!body?.password || body?.password.length > 128 || body?.password.length < 6)
|
||||||
|
return json({ error: 'Invalid password.' }, { status: 400 });
|
||||||
|
|
||||||
|
const usernameTaken = !!(await findUser({ username: body?.username }));
|
||||||
|
if (usernameTaken) return json({ error: 'That username is taken.' }, { status: 400 });
|
||||||
|
|
||||||
|
const emailUsed = !!(await findUser({ email: body?.email }));
|
||||||
|
if (emailUsed)
|
||||||
|
return json({ error: 'That email has been used too many times.' }, { status: 400 });
|
||||||
|
|
||||||
|
const user = await createUser(body?.username, body?.email, body?.password).catch((e) => {});
|
||||||
|
if (!user) return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
|
||||||
|
const session = await createSession(user.id);
|
||||||
|
if (!session) return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
|
||||||
|
cookies.set(COOKIE, session.id, { path: '/' });
|
||||||
|
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
}
|
14
src/routes/api/statistics/+server.js
Normal file
14
src/routes/api/statistics/+server.js
Normal file
|
@ -0,0 +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: await prisma.user.count({}),
|
||||||
|
files: await prisma.upload.count({}),
|
||||||
|
storage: bytesToHumanReadable(get(USAGE))
|
||||||
|
});
|
||||||
|
}
|
44
src/routes/api/upload/+server.ts
Normal file
44
src/routes/api/upload/+server.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (file.size / 1048576 >= user.maxUploadMB)
|
||||||
|
return error(413, { status: 413, message: 'Content Too Large' });
|
||||||
|
|
||||||
|
let id = generateId(undefined, 10);
|
||||||
|
let internalName = `${Date.now()}-${file.name}`;
|
||||||
|
|
||||||
|
const object = await minio
|
||||||
|
.putObject(
|
||||||
|
BUCKET,
|
||||||
|
`${user.id}/${internalName}`,
|
||||||
|
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, internalName, file.size);
|
||||||
|
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}`);
|
||||||
|
};
|
8
src/routes/api/user/+server.js
Normal file
8
src/routes/api/user/+server.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export async function GET(event) {
|
||||||
|
const { request, cookies, locals } = event;
|
||||||
|
|
||||||
|
return json(locals?.user);
|
||||||
|
}
|
43
src/routes/download/[id]/+server.ts
Normal file
43
src/routes/download/[id]/+server.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { getUpload } from '$lib/server/database.js';
|
||||||
|
import minio, { BUCKET } from '$lib/server/minio';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const GET = async ({ params, locals }) => {
|
||||||
|
let id: any = params.id.split('.');
|
||||||
|
if (id.length > 1) id.pop();
|
||||||
|
id = id.join('');
|
||||||
|
|
||||||
|
const file = await getUpload(id);
|
||||||
|
if (!file) throw error(404, { status: 404, message: 'File Not Found' });
|
||||||
|
|
||||||
|
if (!file.public && locals?.user?.id !== file.uploader.id)
|
||||||
|
throw error(403, { status: 403, message: 'Forbidden' });
|
||||||
|
|
||||||
|
const object = await minio.getObject(BUCKET, `${file.uploader.id}/${file.internalName}`);
|
||||||
|
const metadata = await minio.statObject(BUCKET, `${file.uploader.id}/${file.internalName}`);
|
||||||
|
|
||||||
|
const ac = new AbortController();
|
||||||
|
ac.signal.onabort = () => object.destroy;
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
object.on('data', (chunk) => {
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
});
|
||||||
|
object.on('end', () => {
|
||||||
|
controller.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
43
src/routes/file/[id]/+page.server.ts
Normal file
43
src/routes/file/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { getSettings, getUpload } from '$lib/server/database';
|
||||||
|
import minio, { BUCKET } from '$lib/server/minio';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load({ params, locals }) {
|
||||||
|
const file = await getUpload(params.id);
|
||||||
|
if (!file) throw error(404, { status: 404, message: 'File Not Found' });
|
||||||
|
|
||||||
|
if (!file.public && locals?.user?.id !== file.uploader.id)
|
||||||
|
throw error(403, { status: 403, message: 'Forbidden' });
|
||||||
|
|
||||||
|
const settings = await getSettings(file.uploader.id);
|
||||||
|
if (!settings) throw error(500, { status: 500, message: 'Internal Server Error' });
|
||||||
|
|
||||||
|
const metadata = await minio.statObject(BUCKET, `${file.uploader.id}/${file.internalName}`);
|
||||||
|
|
||||||
|
function formatString(input: string) {
|
||||||
|
if (file && metadata)
|
||||||
|
return input
|
||||||
|
.replaceAll('{{file}}', file.fileName)
|
||||||
|
.replaceAll('{{username}}', file.uploader.username)
|
||||||
|
.replaceAll('{{time}}', file.uploaded.toUTCString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
fileName: file.fileName,
|
||||||
|
uploaded: file.uploaded,
|
||||||
|
size: metadata.size,
|
||||||
|
type: metadata.metaData['content-type']
|
||||||
|
},
|
||||||
|
uploader: {
|
||||||
|
username: file.uploader.username
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: formatString(settings.embedTitle),
|
||||||
|
description: formatString(settings.embedDescription),
|
||||||
|
color: '#' + settings.embedColor.toString(16),
|
||||||
|
large: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
56
src/routes/file/[id]/+page.svelte
Normal file
56
src/routes/file/[id]/+page.svelte
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { bytesToHumanReadable } from '$lib';
|
||||||
|
import mime from 'mime';
|
||||||
|
import Link from '$lib/components/Inputs/Link.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const ext = `.${mime.getExtension(data.file.type)}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.file.fileName}</title>
|
||||||
|
<meta property="og:title" content={data.settings.title} />
|
||||||
|
<meta property="og:description" content={data.settings.description} />
|
||||||
|
<meta property="og:url" content="{$page.url.origin}/file/{data.file.id}" />
|
||||||
|
<meta property="og:site_name" content="cirro's file uploader" />
|
||||||
|
<meta name="theme-color" content={data.settings.color} />
|
||||||
|
{#if data.file.type.includes('video')}
|
||||||
|
<meta property="og:type" content="video.other" />
|
||||||
|
<meta property="og:video:url" content="{$page.url.origin}/download/{data.file.id}{ext}" />
|
||||||
|
{:else if data.file.type.includes('image')}
|
||||||
|
{#if data.settings.large}
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
{/if}
|
||||||
|
<meta property="og:image" content="{$page.url.origin}/download/{data.file.id}{ext}" />
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<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 mx-auto rounded-lg shadow-lg w-fit bg-crust">
|
||||||
|
{#if data.file.type.includes('video')}
|
||||||
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
|
<video class="h-[36rem]" src="/download/{data.file.id}{ext}" controls></video>
|
||||||
|
{:else if data.file.type.includes('image')}
|
||||||
|
<img class="h-[36rem]" src="/download/{data.file.id}{ext}" alt={data.file.id} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Link style="button" href="/download/{data.file.id}{ext}">
|
||||||
|
<p class="w-full font-bold text-center">
|
||||||
|
Download ({bytesToHumanReadable(data.file.size)})
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,36 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Mail, SquareAsterisk, LogIn, Undo } from 'lucide-svelte';
|
|
||||||
import { goBack } from '$lib/';
|
|
||||||
|
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
|
||||||
import FormInput from '$lib/components/FormInput.svelte';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex justify-center items-center h-screen">
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<div class="transition-colors fill-black dark:fill-white">
|
|
||||||
<Logo />
|
|
||||||
</div>
|
|
||||||
<form action="">
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<FormInput type={'email'} name={'email'} id={'email'} placeholder={'user@example.com'}>
|
|
||||||
<Mail />
|
|
||||||
</FormInput>
|
|
||||||
<FormInput type={'password'} name={'password'} id={'password'} placeholder={'•'.repeat(16)}>
|
|
||||||
<SquareAsterisk />
|
|
||||||
</FormInput>
|
|
||||||
<div class="flex place-content-between">
|
|
||||||
<Button click={goBack}>
|
|
||||||
<Undo />
|
|
||||||
<p>Go Back</p>
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
<LogIn />
|
|
||||||
<p>Login</p>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,47 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Mail, SquareAsterisk, LogIn, Undo, User, UserPlus } from 'lucide-svelte';
|
|
||||||
import { goBack } from '$lib/';
|
|
||||||
|
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
|
||||||
import FormInput from '$lib/components/FormInput.svelte';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex justify-center items-center h-screen">
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<div class="transition-colors fill-black dark:fill-white">
|
|
||||||
<Logo />
|
|
||||||
</div>
|
|
||||||
<form action="">
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<FormInput type={'username'} name={'username'} id={'username'} placeholder={'Username'}>
|
|
||||||
<User />
|
|
||||||
</FormInput>
|
|
||||||
<FormInput type={'email'} name={'email'} id={'email'} placeholder={'user@example.com'}>
|
|
||||||
<Mail />
|
|
||||||
</FormInput>
|
|
||||||
<FormInput type={'password'} name={'password'} id={'password'} placeholder={'•'.repeat(16)}>
|
|
||||||
<SquareAsterisk />
|
|
||||||
</FormInput>
|
|
||||||
<FormInput
|
|
||||||
type={'password'}
|
|
||||||
name={'cpassword'}
|
|
||||||
id={'cpassword'}
|
|
||||||
placeholder={'•'.repeat(16)}
|
|
||||||
>
|
|
||||||
<SquareAsterisk />
|
|
||||||
</FormInput>
|
|
||||||
<div class="flex place-content-between">
|
|
||||||
<Button click={goBack}>
|
|
||||||
<Undo />
|
|
||||||
<p>Go Back</p>
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
<UserPlus />
|
|
||||||
<p>Register</p>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -6,5 +6,9 @@ export default {
|
||||||
extend: {},
|
extend: {},
|
||||||
container: { center: true }
|
container: { center: true }
|
||||||
},
|
},
|
||||||
plugins: []
|
plugins: [
|
||||||
|
require('@catppuccin/tailwindcss')({
|
||||||
|
defaultFlavour: 'latte'
|
||||||
|
})
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue