user/sessions/hatchets/whatnot

This commit is contained in:
cirroskais 2024-04-02 00:08:49 -04:00
parent fd9b6d1588
commit 01fa68881d
No known key found for this signature in database
GPG key ID: 5FC73EBF2678E33D
20 changed files with 315 additions and 31 deletions

View file

@ -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"
}
}

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

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,3 +1,4 @@
import { writable } from 'svelte/store';
export const darkMode = writable();
export const user = writable();

View file

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

View file

@ -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" />

View file

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

View file

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

View file

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

View file

@ -0,0 +1,4 @@
/** @type {import('./$types').RequestHandler} */
export async function GET(event) {
const { request, cookies, locals } = event;
}

View file

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

View file

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