From 1f0a50255dc262f0990f16b087838a2c448a1965 Mon Sep 17 00:00:00 2001 From: Soham Sen Date: Mon, 9 Oct 2023 02:25:44 +0530 Subject: [PATCH] Add user authentication support --- .env.example | 12 ++ README.md | 11 ++ package.json | 3 + src/lib/crypto.ts | 18 +- src/lib/server/auth.ts | 40 ++++ src/lib/server/email/base.ts | 39 ++++ src/lib/server/email/verify.ts | 23 +++ .../20231008132503_add_user/migration.sql | 22 +++ .../migration.sql | 16 ++ .../20231008151119_set_ondelete/migration.sql | 5 + .../20231008193745_use_nanoid/migration.sql | 36 ++++ .../migration.sql | 2 + src/lib/server/prisma/schema.prisma | 22 +++ src/lib/server/services.ts | 7 +- src/lib/types.ts | 15 ++ src/routes/(auth)/+layout.svelte | 50 +++++ src/routes/(auth)/login/+page.server.ts | 58 ++++++ src/routes/(auth)/login/+page.svelte | 48 +++++ src/routes/(auth)/logout/+page.server.ts | 14 ++ src/routes/(auth)/register/+page.server.ts | 113 +++++++++++ src/routes/(auth)/register/+page.svelte | 83 ++++++++ src/routes/(auth)/validate/+page.server.ts | 20 ++ src/routes/+page.server.ts | 6 + src/routes/+page.svelte | 20 +- src/routes/[key]/+page.server.ts | 7 +- src/routes/[key]/+page.svelte | 23 ++- src/routes/[key]/edit/+page.server.ts | 20 ++ src/routes/[key]/edit/+page.svelte | 186 ++++++++++++++++++ src/routes/api/paste/+server.ts | 54 ++++- yarn.lock | 22 +++ 30 files changed, 980 insertions(+), 15 deletions(-) create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/email/base.ts create mode 100644 src/lib/server/email/verify.ts create mode 100644 src/lib/server/prisma/migrations/20231008132503_add_user/migration.sql create mode 100644 src/lib/server/prisma/migrations/20231008143438_add_authtoken/migration.sql create mode 100644 src/lib/server/prisma/migrations/20231008151119_set_ondelete/migration.sql create mode 100644 src/lib/server/prisma/migrations/20231008193745_use_nanoid/migration.sql create mode 100644 src/lib/server/prisma/migrations/20231008201146_add_verified_to_user/migration.sql create mode 100644 src/routes/(auth)/+layout.svelte create mode 100644 src/routes/(auth)/login/+page.server.ts create mode 100644 src/routes/(auth)/login/+page.svelte create mode 100644 src/routes/(auth)/logout/+page.server.ts create mode 100644 src/routes/(auth)/register/+page.server.ts create mode 100644 src/routes/(auth)/register/+page.svelte create mode 100644 src/routes/(auth)/validate/+page.server.ts create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/[key]/edit/+page.server.ts create mode 100644 src/routes/[key]/edit/+page.svelte diff --git a/.env.example b/.env.example index b1f36f0..472572e 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,13 @@ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" +SALT="this is a very insecure salt, change it" + +MAIL_ENABLED=false +MAIL_SERVER="smtp.gmail.com" +MAIL_PORT=465 +MAIL_USE_SSL=true +MAIL_USERNAME="" +MAIL_PASSWORD="" +MAIL_FROM='"YABin" ' + +PUBLIC_REGISRATION_ENABLED=true +PUBLIC_URL="http://localhost:5173" diff --git a/README.md b/README.md index dd4230c..67344b1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Well, cause no pastebin I could find had ALL of the following features: - View raw pastes. Normally, encrypted pastebins do not have this. With this site, you can either get the Base64-encoded encrypted paste, or decrypt it on the server side (even with the password) and get the raw paste. - Keyboard shortcuts! - And of course, being fully open-source and easily self-hostable. + - **NEW** Ability to edit pastes after creation, and a dashboard for viewing all your pastes. - **Comes with a CLI tool to create and read pastes from the command line!** - **It can even be run on edge servers and in serverless environments!** @@ -50,6 +51,16 @@ See [cli/README.md](cli/README.md) for detailed instructions and library usage. Right now, my instance is using PostgreSQL on Vercel. However, it can be run using any SQL DB such as SQLite or MySQL. To use other backends, please update the provider in [schema.prisma](src/lib/server/prisma/schema.prisma) +### .env Configuration + +`DATABASE_URL` needs to point to a running SQL database. It uses PostgreSQL by default, but can be changed to MySQL or SQLite by modifying the provider in [schema.prisma](src/lib/server/prisma/schema.prisma). + +Remember to modify `SALT` to something secure if you plan on using user accounts. + +You can disable or enable public registration by modifying the `PUBLIC_REGISRATION_ENABLED` variable to `true` or `false`. + +By default, if no e-mail services are configured, all user accounts will be marked as validated. To enable e-mail validation, please configure the `MAIL_*` variables. + #### Locally ```bash diff --git a/package.json b/package.json index 3b225a0..a7ec700 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@sveltejs/adapter-node": "^1.2.4", "@sveltejs/kit": "^1.5.0", "@types/node-cron": "^3.0.7", + "@types/nodemailer": "^6.4.11", "@types/prismjs": "^1.26.0", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -41,7 +42,9 @@ "dependencies": { "@prisma/client": "^4.15.0", "base64-js": "^1.5.1", + "nanoid": "^5.0.1", "node-cron": "^3.0.2", + "nodemailer": "^6.9.5", "prism-themes": "^1.9.0", "prismjs": "^1.29.0", "sanitize-html": "^2.10.0" diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 40a59bb..031c3f1 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,6 +1,6 @@ import base64 from 'base64-js'; -export async function encrypt(plaintext: string) { +export async function encrypt(plaintext: string, keyStr: string | undefined = undefined) { const encoder = new TextEncoder(); const iv: Uint8Array = crypto.getRandomValues(new Uint8Array(12)); @@ -8,8 +8,13 @@ export async function encrypt(plaintext: string) { const alg = { name: 'AES-GCM', iv, length: 256 }; - const key = (await crypto.subtle.generateKey(alg, true, ['encrypt'])) as CryptoKey; - const keyStr = base64.fromByteArray(new Uint8Array(await crypto.subtle.exportKey('raw', key))); + let key: CryptoKey; + if (!keyStr) { + key = (await crypto.subtle.generateKey(alg, true, ['encrypt'])) as CryptoKey; + keyStr = base64.fromByteArray(new Uint8Array(await crypto.subtle.exportKey('raw', key))); + } else { + key = await crypto.subtle.importKey('raw', base64.toByteArray(keyStr), alg, false, ['encrypt']); + } const enc = await crypto.subtle.encrypt(alg, key, encoder.encode(plaintext)); const encStr = base64.fromByteArray(new Uint8Array(enc)); @@ -92,3 +97,10 @@ export async function decryptWithPassword(ciphertext: string, iv: string, passwo const dec = await crypto.subtle.decrypt(alg, key, base64.toByteArray(ciphertext)); return decoder.decode(dec); } + +export async function hashPassword(password: string, salt: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(password + salt); + const hash = await crypto.subtle.digest('SHA-512', data); + return base64.fromByteArray(new Uint8Array(hash)); +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..2eb893d --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,40 @@ +import { SALT } from '$env/static/private'; +import { hashPassword } from '$lib/crypto'; +import prisma from '@db'; +import type { Cookies } from '@sveltejs/kit'; + +export const getUserIdFromCookie = async (cookies: Cookies) => { + const token = cookies.get('token'); + if (!token) return null; + + const authToken = await prisma.authToken.findFirst({ + where: { token, expiresAt: { gt: new Date() } }, + include: { user: { select: { id: true, verified: true } } } + }); + if (!authToken) return null; + if (!authToken.user.verified) return null; + + return authToken.user.id; +}; + +export const generateVerificationHash = async (userId: string) => { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) throw new Error('User not found'); + + const hash = await hashPassword(`${user.email}${user.id}${user.password}${user.verified}`, SALT); + return hash; +}; + +export const validateVerificationHash = async (userId: string, hash: string) => { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return false; + + const newHash = await hashPassword( + `${user.email}${user.id}${user.password}${user.verified}`, + SALT + ); + if (newHash !== hash) return false; + + await prisma.user.update({ where: { id: userId }, data: { verified: true } }); + return true; +}; diff --git a/src/lib/server/email/base.ts b/src/lib/server/email/base.ts new file mode 100644 index 0000000..7d032ad --- /dev/null +++ b/src/lib/server/email/base.ts @@ -0,0 +1,39 @@ +import nodemailer from 'nodemailer'; +import { + MAIL_ENABLED, + MAIL_SERVER, + MAIL_PASSWORD, + MAIL_PORT, + MAIL_USERNAME, + MAIL_USE_SSL, + MAIL_FROM +} from '$env/static/private'; + +export async function sendEmail(to: string, subject: string, content: string) { + if (MAIL_ENABLED !== 'true') { + return false; + } + + const transporter = nodemailer.createTransport({ + host: MAIL_SERVER, + port: Number(MAIL_PORT), + secure: MAIL_USE_SSL === 'true', + auth: { + user: MAIL_USERNAME, + pass: MAIL_PASSWORD + } + }); + + const info = await transporter.sendMail({ + from: MAIL_FROM, + to, + subject, + text: content + }); + + if (info.accepted.length === 0) { + return false; + } + + return true; +} diff --git a/src/lib/server/email/verify.ts b/src/lib/server/email/verify.ts new file mode 100644 index 0000000..44a5a7a --- /dev/null +++ b/src/lib/server/email/verify.ts @@ -0,0 +1,23 @@ +import { PUBLIC_URL } from '$env/static/public'; +import prisma from '@db'; +import { generateVerificationHash } from '../auth'; +import { sendEmail } from './base'; + +export const sendVerificationEmail = async (userId: string) => { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return false; + + const hash = await generateVerificationHash(userId); + + const verifyUrl = `${PUBLIC_URL}/validate?hash=${encodeURIComponent( + hash + )}&userId=${encodeURIComponent(userId)}`; + + const content = `To verify your email, please click the following link: ${verifyUrl}`; + const subject = 'YABin: Verify your email'; + + const sent = await sendEmail(user.email, subject, content); + if (!sent) return false; + + return true; +}; diff --git a/src/lib/server/prisma/migrations/20231008132503_add_user/migration.sql b/src/lib/server/prisma/migrations/20231008132503_add_user/migration.sql new file mode 100644 index 0000000..39c3a75 --- /dev/null +++ b/src/lib/server/prisma/migrations/20231008132503_add_user/migration.sql @@ -0,0 +1,22 @@ +-- AlterTable +ALTER TABLE "Paste" ADD COLUMN "ownerId" BIGINT; + +-- CreateTable +CREATE TABLE "User" ( + "id" BIGSERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Paste" ADD CONSTRAINT "Paste_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/lib/server/prisma/migrations/20231008143438_add_authtoken/migration.sql b/src/lib/server/prisma/migrations/20231008143438_add_authtoken/migration.sql new file mode 100644 index 0000000..14e0f9b --- /dev/null +++ b/src/lib/server/prisma/migrations/20231008143438_add_authtoken/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "AuthToken" ( + "id" BIGSERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMP(3) NOT NULL, + "token" TEXT NOT NULL, + "userId" BIGINT NOT NULL, + + CONSTRAINT "AuthToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AuthToken_token_key" ON "AuthToken"("token"); + +-- AddForeignKey +ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/lib/server/prisma/migrations/20231008151119_set_ondelete/migration.sql b/src/lib/server/prisma/migrations/20231008151119_set_ondelete/migration.sql new file mode 100644 index 0000000..ef4968f --- /dev/null +++ b/src/lib/server/prisma/migrations/20231008151119_set_ondelete/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "AuthToken" DROP CONSTRAINT "AuthToken_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/lib/server/prisma/migrations/20231008193745_use_nanoid/migration.sql b/src/lib/server/prisma/migrations/20231008193745_use_nanoid/migration.sql new file mode 100644 index 0000000..e605b83 --- /dev/null +++ b/src/lib/server/prisma/migrations/20231008193745_use_nanoid/migration.sql @@ -0,0 +1,36 @@ +/* + Warnings: + + - The primary key for the `AuthToken` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "AuthToken" DROP CONSTRAINT "AuthToken_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Paste" DROP CONSTRAINT "Paste_ownerId_fkey"; + +-- AlterTable +ALTER TABLE "AuthToken" DROP CONSTRAINT "AuthToken_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ALTER COLUMN "userId" SET DATA TYPE TEXT, +ADD CONSTRAINT "AuthToken_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "AuthToken_id_seq"; + +-- AlterTable +ALTER TABLE "Paste" ALTER COLUMN "ownerId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "User" DROP CONSTRAINT "User_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "User_id_seq"; + +-- AddForeignKey +ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Paste" ADD CONSTRAINT "Paste_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/lib/server/prisma/migrations/20231008201146_add_verified_to_user/migration.sql b/src/lib/server/prisma/migrations/20231008201146_add_verified_to_user/migration.sql new file mode 100644 index 0000000..b6012fc --- /dev/null +++ b/src/lib/server/prisma/migrations/20231008201146_add_verified_to_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "verified" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/lib/server/prisma/schema.prisma b/src/lib/server/prisma/schema.prisma index eeaba9b..9d9e6f8 100644 --- a/src/lib/server/prisma/schema.prisma +++ b/src/lib/server/prisma/schema.prisma @@ -10,6 +10,26 @@ datasource db { url = env("DATABASE_URL") } +model User { + id String @id @default(nanoid(16)) + username String @unique + email String @unique + password String + name String + verified Boolean @default(false) + pastes Paste[] + AuthToken AuthToken[] +} + +model AuthToken { + id String @id @default(nanoid(16)) + createdAt DateTime @default(now()) + expiresAt DateTime + token String @unique + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + model Paste { id BigInt @id @default(autoincrement()) createdAt DateTime @default(now()) @@ -22,6 +42,8 @@ model Paste { expiresAt DateTime? expiresCount Int? readCount Int @default(0) + ownerId String? + owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) @@index([key]) } diff --git a/src/lib/server/services.ts b/src/lib/server/services.ts index 816c00c..883ff2c 100644 --- a/src/lib/server/services.ts +++ b/src/lib/server/services.ts @@ -11,7 +11,8 @@ export async function getPaste(key: string) { initVector: true, language: true, expiresCount: true, - readCount: true + readCount: true, + ownerId: true } }); @@ -28,9 +29,9 @@ export async function getPaste(key: string) { throw error(404, 'Not found'); } - const { content, encrypted, passwordProtected, initVector, language } = data; + const { content, encrypted, passwordProtected, initVector, language, ownerId } = data; - return { key, content, encrypted, passwordProtected, initVector, language }; + return { key, content, encrypted, passwordProtected, initVector, language, ownerId }; } export async function deleteExpiredPastes() { diff --git a/src/lib/types.ts b/src/lib/types.ts index 0bcd6ed..8040292 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,6 +12,13 @@ export interface Paste { initVector?: string; } +export interface PastePatch { + key: string; + content: string; + encrypted?: boolean; + initVector?: string; +} + export interface PasteCreateResponse { success: boolean; data?: { @@ -22,3 +29,11 @@ export interface PasteCreateResponse { code: number; }; } + +export interface PastePatchResponse { + success: boolean; + data?: { + key: string; + }; + error?: string; +} diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte new file mode 100644 index 0000000..32adb0c --- /dev/null +++ b/src/routes/(auth)/+layout.svelte @@ -0,0 +1,50 @@ + + +
+
+
+

YABin

+ + + + +
+
+ + +
diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts new file mode 100644 index 0000000..4218826 --- /dev/null +++ b/src/routes/(auth)/login/+page.server.ts @@ -0,0 +1,58 @@ +import type { Actions } from './$types'; +import { fail, redirect } from '@sveltejs/kit'; +import prisma from '@db'; +import { hashPassword } from '$lib/crypto'; +import { nanoid } from 'nanoid'; +import { SALT } from '$env/static/private'; + +export const actions: Actions = { + default: async ({ cookies, request }) => { + const data = await request.formData(); + + const usernameOrEmail = data.get('username-email'); + const password = data.get('password'); + + if (!usernameOrEmail || !password) { + return fail(400, { success: false, errors: ['All fields are required'] }); + } + + const hashedPassword = await hashPassword(password.toString(), SALT); + const user = await prisma.user.findFirst({ + where: { + OR: [ + { username: usernameOrEmail.toString(), password: hashedPassword }, + { email: usernameOrEmail.toString(), password: hashedPassword } + ] + } + }); + + if (!user) { + return fail(400, { success: false, errors: ['Invalid username or password'] }); + } + + if (!user.verified) { + return fail(401, { success: false, errors: ['Account not verified'] }); + } + + await prisma.authToken.deleteMany({ where: { expiresAt: { lte: new Date() } } }); + + const authToken = await prisma.authToken.create({ + data: { + user: { connect: { id: user.id } }, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days + token: nanoid(32) + } + }); + + cookies.set('token', authToken.token, { + path: '/', + maxAge: 60 * 60 * 24 * 30, // 30 days + secure: true, + httpOnly: true, + sameSite: 'strict' + }); + + throw redirect(303, '/'); + } +}; diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte new file mode 100644 index 0000000..9c36273 --- /dev/null +++ b/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,48 @@ + + +
+

User Login

+
+ {#if form?.errors} +
    + {#each form.errors as error} +
  • {error}
  • + {/each} +
+ {/if} + {#if form?.success} +
Success, redirecting...
+ {/if} + +
+
+ + +
+
+ + +
+ +
+ + Don't have an account? Register. + + + +
+ +
+ Forgot password? + Click here. +
+
+
+
diff --git a/src/routes/(auth)/logout/+page.server.ts b/src/routes/(auth)/logout/+page.server.ts new file mode 100644 index 0000000..638c546 --- /dev/null +++ b/src/routes/(auth)/logout/+page.server.ts @@ -0,0 +1,14 @@ +import { redirect, type Actions, type RequestHandler } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies }) => { + cookies.delete('token'); + throw redirect(303, '/'); +}; + +export const actions: Actions = { + default({ cookies }) { + cookies.delete('token'); + throw redirect(303, '/'); + } +}; diff --git a/src/routes/(auth)/register/+page.server.ts b/src/routes/(auth)/register/+page.server.ts new file mode 100644 index 0000000..1c8b655 --- /dev/null +++ b/src/routes/(auth)/register/+page.server.ts @@ -0,0 +1,113 @@ +import type { Actions } from './$types'; +import { fail, redirect } from '@sveltejs/kit'; +import prisma from '@db'; +import { hashPassword } from '$lib/crypto'; +import { nanoid } from 'nanoid'; +import { MAIL_ENABLED, SALT } from '$env/static/private'; +import { PUBLIC_REGISRATION_ENABLED } from '$env/static/public'; +import { sendVerificationEmail } from '$lib/server/email/verify'; + +const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g; + +export const actions: Actions = { + default: async ({ cookies, request }) => { + if (PUBLIC_REGISRATION_ENABLED !== 'true') { + return fail(404, { success: false, errors: ['Not found'] }); + } + + const data = await request.formData(); + + const name = data.get('name'); + const username = data.get('username'); + const email = data.get('email'); + const password = data.get('password'); + const cnfPassword = data.get('confirm-password'); + + const errors: string[] = []; + + if (!name || !username || !email || !password || !cnfPassword) { + errors.push('All fields are required'); + } + + if (email && !emailRegex.test(email?.toString())) { + errors.push('Invalid email address'); + } + + if (password && password.toString().length < 8) { + errors.push('Password must be at least 8 characters long'); + } + + if (password && password !== cnfPassword) { + errors.push('Passwords do not match'); + } + + if (name && name.toString().length > 50) { + errors.push('Name is too long'); + } + + if (username && username.toString().length > 50) { + errors.push('Username is too long'); + } + + if (username && email) { + const existingCount = await prisma.user.count({ + where: { + OR: [{ username: username.toString() }, { email: email.toString() }] + } + }); + if (existingCount > 0) { + errors.push('Username or email already exists'); + } + } + + if (errors.length > 0) { + return fail(400, { success: false, errors }); + } + + if (name && username && email && password) { + const user = await prisma.user.create({ + data: { + name: name.toString(), + username: username.toString(), + email: email.toString(), + password: await hashPassword(password.toString(), SALT), + verified: false + } + }); + + if (MAIL_ENABLED === 'true') { + const sentVerificationEmail = await sendVerificationEmail(user.id); + if (sentVerificationEmail) { + return { success: true, message: 'Please check your e-mail for verification link' }; + } + } + + await prisma.user.update({ where: { id: user.id }, data: { verified: true } }); + + const authToken = await prisma.authToken.create({ + data: { + user: { + connect: { + id: user.id + } + }, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days + token: nanoid(32) + } + }); + + cookies.set('token', authToken.token, { + path: '/', + maxAge: 60 * 60 * 24 * 30, // 30 days + secure: true, + httpOnly: true, + sameSite: 'strict' + }); + + throw redirect(303, '/'); + } + + return { success: false, errors: ['Unknown error'] }; + } +}; diff --git a/src/routes/(auth)/register/+page.svelte b/src/routes/(auth)/register/+page.svelte new file mode 100644 index 0000000..ea8a784 --- /dev/null +++ b/src/routes/(auth)/register/+page.svelte @@ -0,0 +1,83 @@ + + +
+

New User Registration

+
+ {#if form?.errors} +
    + {#each form.errors as error} +
  • {error}
  • + {/each} +
+ {/if} + {#if form?.success} +
{form.message}
+ {/if} + + {#if PUBLIC_REGISRATION_ENABLED == 'true'} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + Already have an account? Login. + + + +
+
+ {:else} +

Registration has been disabled.

+ {/if} +
+
diff --git a/src/routes/(auth)/validate/+page.server.ts b/src/routes/(auth)/validate/+page.server.ts new file mode 100644 index 0000000..396cceb --- /dev/null +++ b/src/routes/(auth)/validate/+page.server.ts @@ -0,0 +1,20 @@ +import { redirect, error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { validateVerificationHash } from '$lib/server/auth'; + +export const load: PageServerLoad = async ({ url }) => { + const userId = url.searchParams.get('userId'); + const hash = url.searchParams.get('hash'); + + if (!userId || !hash) { + throw error(404, 'Not found'); + } + + const isValid = await validateVerificationHash(decodeURIComponent(userId), decodeURIComponent(hash)); + + if (!isValid) { + throw error(404, 'Not found'); + } + + throw redirect(303, '/login'); +}; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..b5527f0 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,6 @@ +import { getUserIdFromCookie } from '$lib/server/auth'; + +export async function load({ cookies }) { + const userId = await getUserIdFromCookie(cookies); + return { loggedIn: !!userId }; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 29d6c5f..9187fca 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,6 +6,10 @@ import Select from 'svelte-select'; import { encrypt, encryptWithPassword } from '$lib/crypto'; import Hamburger from '$lib/components/Hamburger.svelte'; + import { PUBLIC_REGISRATION_ENABLED } from '$env/static/public'; + import type { PageData } from './$types'; + + export let data: PageData; const initialConfig: PasteConfig = { language: 'plaintext', @@ -209,7 +213,7 @@ Save -
+
@@ -222,6 +226,19 @@
+ {#if PUBLIC_REGISRATION_ENABLED == 'true'} +
+ {#if data.loggedIn} +
+ +
+ {:else} + Login + Register + {/if} +
+ {/if} + + +
+ {:else} +