yap #1
20 changed files with 315 additions and 31 deletions
|
@ -24,8 +24,11 @@
|
|||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.11.0",
|
||||
"argon2": "^0.40.1",
|
||||
"lucide-svelte": "^0.358.0",
|
||||
"minio": "^7.1.3",
|
||||
"svelte-sonner": "^0.3.19"
|
||||
"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;
|
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"
|
|
@ -14,8 +14,19 @@ model User {
|
|||
password String
|
||||
createdAt DateTime @default(now())
|
||||
lastSeen DateTime @default(now())
|
||||
uploads Upload[]
|
||||
settings UserSettings?
|
||||
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 {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
import { getSession } from '$lib/server/database';
|
||||
import { COOKIE } from '$lib/config';
|
||||
|
||||
const PUBLIC_RESOURCES = [
|
||||
'/',
|
||||
|
@ -11,10 +13,17 @@ const PUBLIC_RESOURCES = [
|
|||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
if (!PUBLIC_RESOURCES.includes(event.route.id)) {
|
||||
console.log(event.route.id);
|
||||
return redirect(303, '/');
|
||||
}
|
||||
const { cookies, locals } = event;
|
||||
const session = await getSession(cookies.get(COOKIE));
|
||||
const user = session?.user;
|
||||
|
||||
locals.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
};
|
||||
|
||||
if (!PUBLIC_RESOURCES.includes(event.route.id) && !user) return redirect(303, '/');
|
||||
|
||||
return await resolve(event);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { blur } 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/FormInput.svelte';
|
||||
|
@ -17,10 +18,31 @@
|
|||
async function login() {
|
||||
disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
toast('Failed to login.');
|
||||
disabled = false;
|
||||
}, 5_000);
|
||||
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>
|
||||
|
||||
|
@ -36,7 +58,7 @@
|
|||
name={'email'}
|
||||
id={'email'}
|
||||
placeholder={'user@example.com'}
|
||||
bind={email}
|
||||
bind:value={email}
|
||||
required={true}
|
||||
>
|
||||
<Mail />
|
||||
|
@ -46,7 +68,7 @@
|
|||
name={'password'}
|
||||
id={'password'}
|
||||
placeholder={'•'.repeat(16)}
|
||||
bind={password}
|
||||
bind:value={password}
|
||||
required={true}
|
||||
>
|
||||
<SquareAsterisk />
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { blur } from 'svelte/transition';
|
||||
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/FormInput.svelte';
|
||||
|
@ -45,15 +46,13 @@
|
|||
}).catch((_) => toast.error(_.message));
|
||||
const body = await response.json().catch((_) => toast.error(_.message));
|
||||
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
toast.error(body?.error || 'Client Error', { id });
|
||||
if (!body?.success) {
|
||||
toast.error(body?.error || 'Unexpected Error', { id });
|
||||
return (disabled = false);
|
||||
}
|
||||
|
||||
if (response.status >= 500) {
|
||||
toast.error(body?.error || 'Server Error', { id });
|
||||
return (disabled = false);
|
||||
}
|
||||
toast.success('Welcome, ' + body.data.username, { id });
|
||||
goto('/dashboard');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const COOKIE = '.FILE-UPLOADER-SESSION';
|
||||
|
||||
export const MAIL_WHITELIST = ['gmail.com', 'outlook.com', 'madhouselabs.net', 'dfuser.xyz'];
|
||||
|
|
9
src/lib/server/crypto.js
Normal file
9
src/lib/server/crypto.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { hash, verify } from 'argon2';
|
||||
|
||||
export async function createHash(input) {
|
||||
return await hash(input);
|
||||
}
|
||||
|
||||
export async function verifyHash(hash, input) {
|
||||
return await verify(hash, input);
|
||||
}
|
|
@ -1,4 +1,63 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { createHash } from './crypto';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
export default prisma;
|
||||
|
||||
export async function createUser(username, email, password) {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: await createHash(password)
|
||||
}
|
||||
});
|
||||
|
||||
const settings = await prisma.userSettings.create({
|
||||
data: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createSession(userId, needsMfa = false) {
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
id: randomBytes(64).toString('base64'),
|
||||
userId,
|
||||
authorized: !needsMfa,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 12)
|
||||
}
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function findUser({ email, username }) {
|
||||
if (!email || !username) return false;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
username
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getSession(id) {
|
||||
if (!id) return false;
|
||||
|
||||
const session = await prisma.session.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
|
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 +1,4 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const darkMode = writable();
|
||||
export const user = writable();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// /** @type {import('@sveltejs/kit').Load} */
|
||||
// export const load = async ({ locals }) => {
|
||||
// return {
|
||||
// session: await locals.getSession()
|
||||
// };
|
||||
// };
|
||||
/** @type {import('@sveltejs/kit').Load} */
|
||||
export const load = async ({ locals }) => {
|
||||
return {
|
||||
user: locals.user
|
||||
};
|
||||
};
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
|
||||
import { darkMode } from '$lib/stores.js';
|
||||
import { darkMode, user } from '$lib/stores.js';
|
||||
import ThemeHandler from '$lib/components/ThemeHandler.svelte';
|
||||
import PageMeta from '$lib/components/PageMeta.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
user.set(data?.user);
|
||||
</script>
|
||||
|
||||
<PageMeta title="cirro's file uploader" />
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import ThemeSwitcher from '$lib/components/ThemeSwitcher.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
|
||||
import LoginForm from '$lib/components/LoginForm.svelte';
|
||||
import RegisterForm from '$lib/components/RegisterForm.svelte';
|
||||
|
||||
|
|
|
@ -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, locals } = 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 }
|
||||
);
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { COOKIE } from '$lib/config';
|
||||
import { createUser, createSession } from '$lib/server/database';
|
||||
import { email } from '$lib/server/validator';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function POST(event) {
|
||||
|
@ -6,10 +9,28 @@ export async function POST(event) {
|
|||
const body = await request.json();
|
||||
|
||||
if (!body?.username || body?.username.length > 16 || body?.username.length < 3)
|
||||
return json({ error: 'Invalid username' }, { status: 400 });
|
||||
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 });
|
||||
return json({ error: 'Invalid password.' }, { status: 400 });
|
||||
|
||||
return json({ error: 'Not Implemented' }, { status: 500 });
|
||||
const user = await createUser(body?.username, body?.email, body?.password);
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
|
4
src/routes/api/user/+server.js
Normal file
4
src/routes/api/user/+server.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET(event) {
|
||||
const { request, cookies, locals } = event;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores';
|
||||
</script>
|
||||
|
||||
<p>
|
||||
<tt class="block whitespace-pre-wrap">
|
||||
{JSON.stringify($page.data?.session, null, 4)}
|
||||
{JSON.stringify($user)}
|
||||
</tt>
|
||||
</p>
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -195,6 +195,11 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@phc/format@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4"
|
||||
integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
|
||||
|
@ -205,6 +210,11 @@
|
|||
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz"
|
||||
integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==
|
||||
|
||||
"@prisma/client@5.11.0":
|
||||
version "5.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.11.0.tgz#d8e55fab85163415b2245fb408b9106f83c8106d"
|
||||
integrity sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==
|
||||
|
||||
"@prisma/debug@5.11.0":
|
||||
version "5.11.0"
|
||||
resolved "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz"
|
||||
|
@ -459,6 +469,15 @@ arg@^5.0.2:
|
|||
resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
|
||||
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
|
||||
|
||||
argon2@^0.40.1:
|
||||
version "0.40.1"
|
||||
resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.40.1.tgz#52e059d2606938b05c97e3702ee2c689993f8f9e"
|
||||
integrity sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==
|
||||
dependencies:
|
||||
"@phc/format" "^1.0.0"
|
||||
node-addon-api "^7.1.0"
|
||||
node-gyp-build "^4.8.0"
|
||||
|
||||
aria-query@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz"
|
||||
|
@ -1226,6 +1245,16 @@ nanoid@^3.3.7:
|
|||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
node-addon-api@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.0.tgz#71f609369379c08e251c558527a107107b5e0fdb"
|
||||
integrity sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==
|
||||
|
||||
node-gyp-build@^4.8.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd"
|
||||
integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==
|
||||
|
||||
node-releases@^2.0.14:
|
||||
version "2.0.14"
|
||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz"
|
||||
|
@ -1728,6 +1757,11 @@ util@^0.12.3:
|
|||
is-typed-array "^1.1.3"
|
||||
which-typed-array "^1.1.2"
|
||||
|
||||
validator@^13.11.0:
|
||||
version "13.11.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b"
|
||||
integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==
|
||||
|
||||
vite@^5.0.3:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz"
|
||||
|
|
Loading…
Reference in a new issue