From 1cd17647d3437a48f12e94f416b48435b2ba4b4b Mon Sep 17 00:00:00 2001 From: Soham Sen Date: Mon, 11 Sep 2023 11:33:37 +0530 Subject: [PATCH 1/9] Added postinstall for prisma generate --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0338300..79324ca 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", - "format": "prettier --plugin-search-dir . --write ." + "format": "prettier --plugin-search-dir . --write .", + "postinstall": "prisma generate" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", From d014128c8538456386f145d7d3dba1acd4d9986e Mon Sep 17 00:00:00 2001 From: Soham Sen Date: Mon, 11 Sep 2023 12:12:12 +0530 Subject: [PATCH 2/9] Update postinstall command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79324ca..f27cfdb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", "format": "prettier --plugin-search-dir . --write .", - "postinstall": "prisma generate" + "postinstall": "prisma generate --schema=./src/lib/server/prisma/schema.prisma" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", From cb98376e35d986c0d489e4b881a6b7cf805f0f7a Mon Sep 17 00:00:00 2001 From: Soham Sen Date: Mon, 11 Sep 2023 12:17:26 +0530 Subject: [PATCH 3/9] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d45375..dd4230c 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ See [cli/README.md](cli/README.md) for detailed instructions and library usage. **Requirements:** Node.js (tested on 18+, should work with 14+), and a SQL database (tested on PostgreSQL, should work with MySQL and SQLite). -Right now, it is using PostgreSQL (cause I had a server lying around). 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) +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) #### Locally From cded2667bf6eeda900dc29a358ae2a99183580d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 23:52:30 +0000 Subject: [PATCH 4/9] Bump urllib3 from 2.0.3 to 2.0.6 in /cli Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.3 to 2.0.6. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.0.3...2.0.6) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- cli/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/poetry.lock b/cli/poetry.lock index 0d0b1b6..e51abc5 100644 --- a/cli/poetry.lock +++ b/cli/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "black" @@ -290,13 +290,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "urllib3" -version = "2.0.3" +version = "2.0.6" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, - {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, + {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, + {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, ] [package.extras] From dbaf66a19be62307bb7e8ce84b5ce5734c9ba7d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:54:39 +0000 Subject: [PATCH 5/9] Bump certifi from 2023.5.7 to 2023.7.22 in /cli Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] --- cli/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/poetry.lock b/cli/poetry.lock index e51abc5..33ec7c5 100644 --- a/cli/poetry.lock +++ b/cli/poetry.lock @@ -49,13 +49,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] From 6f4af716eebf76eb4f9931dbee5295b0091e88e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:53:39 +0000 Subject: [PATCH 6/9] Bump postcss from 8.4.24 to 8.4.31 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.24 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.24...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f27cfdb..3b225a0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.26.0", - "postcss": "^8.4.24", + "postcss": "^8.4.31", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.8.1", "prisma": "^4.15.0", diff --git a/yarn.lock b/yarn.lock index 78e81d3..6ad2d19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1629,10 +1629,10 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.5: - version "8.4.24" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df" - integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== +postcss@^8.3.11, postcss@^8.4.23, postcss@^8.4.31, postcss@^8.4.5: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From 3b331461d3d7f7019309ae4ae922b8f0ca6f6678 Mon Sep 17 00:00:00 2001 From: Soham Sen Date: Mon, 9 Oct 2023 02:25:44 +0530 Subject: [PATCH 7/9] 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} +