From 4b84c475b8d33fa52e1e44c8ca7cf7bbad9464dd Mon Sep 17 00:00:00 2001 From: bytedream Date: Sun, 27 Aug 2023 23:33:22 +0200 Subject: [PATCH] add admin login --- .env.example | 2 + README.md | 2 + package-lock.json | 88 ++++++++++++++++++++-- package.json | 7 +- src/hooks.server.ts | 21 ++++++ src/lib/components/Input/Input.svelte | 5 +- src/lib/components/Toast/ErrorToast.svelte | 58 ++++++++++++++ src/lib/index.ts | 1 - src/lib/server/database.ts | 78 +++++++++++-------- src/lib/server/session.ts | 11 +++ src/routes/+layout.svelte | 4 +- src/routes/admin/+layout.svelte | 3 + src/routes/admin/login/+layout.svelte | 3 + src/routes/admin/login/+page.svelte | 86 +++++++++++++++++++++ src/routes/admin/login/+server.ts | 47 ++++++++++++ tsconfig.json | 5 +- 16 files changed, 377 insertions(+), 44 deletions(-) create mode 100644 src/lib/components/Toast/ErrorToast.svelte delete mode 100644 src/lib/index.ts create mode 100644 src/lib/server/session.ts create mode 100644 src/routes/admin/+layout.svelte create mode 100644 src/routes/admin/login/+layout.svelte create mode 100644 src/routes/admin/login/+page.svelte create mode 100644 src/routes/admin/login/+server.ts diff --git a/.env.example b/.env.example index facfae9..cea10bd 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ DATABASE_URI=sqlite://./database.db +ADMIN_USER=admin +ADMIN_PASSWORD=admin PUBLIC_START_DATE=2023-12-26T00:00:00+0200 PUBLIC_BASE_PATH= diff --git a/README.md b/README.md index 42c8e8d..0207f9a 100644 --- a/README.md +++ b/README.md @@ -34,5 +34,7 @@ Configurations can be done with env variables | `HOST` | Host the server should listen on | | `PORT` | Port the server should listen on | | `DATABASE_URI` | URI to the database as a connection string. Supported databases are [sqlite](https://www.sqlite.org/index.html) and [mariadb](https://mariadb.org/) | +| `ADMIN_USER` | Name for the root admin user. The admin user won't be available if `ADMIN_USER` or `ADMIN_PASSWORD` is set | +| `ADMIN_PASSWORD` | Password for the root admin user defined via `ADMIN_USER`. The admin user won't be available if `ADMIN_USER` or `ADMIN_PASSWORD` is set | | `PUBLIC_BASE_PATH` | If running the website on a sub-path, set this variable to the path so that assets etc. can find the correct location | | `PUBLIC_START_DATE` | The start date when the event starts | diff --git a/package-lock.json b/package-lock.json index 5d8cf8f..f8f8388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,23 @@ "name": "website", "version": "0.0.1", "dependencies": { + "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "mariadb": "^3.2.0", "sequelize": "^6.32.1", + "sequelize-typescript": "^2.1.5", "sqlite3": "^5.1.6" }, "devDependencies": { "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.20.4", + "@types/bcrypt": "^5.0.0", + "@types/node": "^20.5.6", + "@types/validator": "^13.11.1", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.14", - "daisyui": "^3.5.0", + "daisyui": "^3.6.3", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", @@ -946,6 +951,15 @@ "node": ">= 6" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", @@ -998,9 +1012,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "20.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.8.tgz", - "integrity": "sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==" + "version": "20.5.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz", + "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==" }, "node_modules/@types/pug": { "version": "2.0.6", @@ -1540,6 +1554,24 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1951,9 +1983,9 @@ } }, "node_modules/daisyui": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.5.0.tgz", - "integrity": "sha512-wSaeXwaYdMv4yURv9wj7kKQQN9Jyumfh/skIpZfCNkCb2jLf/so+iNKSM8l4rDN0TRvB5OccMlAvsf2UAtk2gg==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.6.3.tgz", + "integrity": "sha512-VNWogAjx37H8kNYd2E/+r1OXc6dOvJTKlKltqIKAlNMFVfx2BIKPcmnVxaHQLfj2vhv1mYDBjgWj+1enQ+4yZA==", "dev": true, "dependencies": { "colord": "^2.9", @@ -4190,6 +4222,12 @@ "node": ">=8.10.0" } }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "peer": true + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -4436,6 +4474,42 @@ "node": ">= 10.0.0" } }, + "node_modules/sequelize-typescript": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/sequelize-typescript/-/sequelize-typescript-2.1.5.tgz", + "integrity": "sha512-x1CNODct8gJyfZPwEZBU5uVGNwgJI2Fda913ZxD5ZtCSRyTDPBTS/0uXciF+MlCpyqjpmoCAPtudQWzw579bzA==", + "dependencies": { + "glob": "7.2.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "@types/validator": "*", + "reflect-metadata": "*", + "sequelize": ">=6.20.1" + } + }, + "node_modules/sequelize-typescript/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", diff --git a/package.json b/package.json index 2c14343..e795aad 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,13 @@ "devDependencies": { "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.20.4", + "@types/bcrypt": "^5.0.0", + "@types/node": "^20.5.6", + "@types/validator": "^13.11.1", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.14", - "daisyui": "^3.5.0", + "daisyui": "^3.6.3", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", @@ -38,9 +41,11 @@ }, "type": "module", "dependencies": { + "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "mariadb": "^3.2.0", "sequelize": "^6.32.1", + "sequelize-typescript": "^2.1.5", "sqlite3": "^5.1.6" } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 125fd7f..6e4794d 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,4 +1,25 @@ import { sequelize } from '$lib/server/database'; +import type { Handle } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; +import { hasSession } from '$lib/server/session'; // make sure that the database and tables exist await sequelize.sync(); + +export const handle: Handle = async ({ event, resolve }) => { + if ( + event.url.pathname.startsWith(`${env.PUBLIC_BASE_PATH}/admin`) && + event.url.pathname != `${env.PUBLIC_BASE_PATH}/admin/login` + ) { + if (!hasSession(event.cookies.get('session') || '')) { + return new Response(null, { + status: 302, + headers: { + location: `${env.PUBLIC_BASE_PATH}/admin/login` + } + }); + } + } + + return resolve(event); +}; diff --git a/src/lib/components/Input/Input.svelte b/src/lib/components/Input/Input.svelte index 7fb1573..7e4ee9e 100644 --- a/src/lib/components/Input/Input.svelte +++ b/src/lib/components/Input/Input.svelte @@ -31,10 +31,11 @@ {/if} -
+
+ import { IconOutline } from 'svelte-heros-v2'; + import { fly } from 'svelte/transition'; + import { onDestroy } from 'svelte'; + + export let timeout = 2000; + export let show = false; + + export function reset() { + progressValue = 1; + } + + let progressValue = 100; + let intervalClear: ReturnType | undefined; + + function startTimout() { + intervalClear = setInterval(() => { + if (++progressValue > 100) { + clearInterval(intervalClear); + show = false; + progressValue = 100; + } + }, timeout / 100); + } + + $: if (show) { + progressValue = 0; + startTimout(); + } + + onDestroy(() => clearInterval(intervalClear)); + + +{#if show && progressValue !== 0} +
{ + clearInterval(intervalClear); + progressValue = 1; + }} + on:mouseleave={startTimout} + role="alert" + > +
+
+ + Nutzername oder Passwort falsch +
+ +
+
+{/if} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 9fc386c..71e601f 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -1,37 +1,53 @@ -import { DataTypes, Sequelize } from 'sequelize'; +import { DataTypes } from 'sequelize'; import { env } from '$env/dynamic/private'; import { building, dev } from '$app/environment'; +import * as bcrypt from 'bcrypt'; +import { BeforeCreate, BeforeUpdate, Column, Model, Sequelize, Table } from 'sequelize-typescript'; + +@Table({ modelName: 'user' }) +export class User extends Model { + @Column({ type: DataTypes.STRING, allowNull: false }) + declare firstname: string; + @Column({ type: DataTypes.STRING, allowNull: false }) + declare lastname: string; + @Column({ type: DataTypes.DATE, allowNull: false }) + declare birthday: Date; + @Column({ type: DataTypes.STRING }) + declare telephone: string; + @Column({ type: DataTypes.STRING, allowNull: false }) + declare username: string; + @Column({ type: DataTypes.ENUM('java', 'bedrock', 'cracked'), allowNull: false }) + declare playertype: 'java' | 'bedrock' | 'cracked'; + @Column({ type: DataTypes.STRING }) + declare password: string; + @Column({ type: DataTypes.UUIDV4 }) + declare uuid: string; +} + +@Table({ modelName: 'admin' }) +export class Admin extends Model { + @Column({ type: DataTypes.STRING, allowNull: false }) + declare username: string; + @Column({ type: DataTypes.STRING, allowNull: false }) + declare password: string; + @Column({ type: DataTypes.BIGINT, allowNull: false }) + declare permissions: number; + + @BeforeCreate + @BeforeUpdate + static hashPassword(instance: Admin) { + if (instance.password != null) { + instance.username = bcrypt.hashSync(instance.password, 10); + } + } + + validatePassword(password: string): boolean { + return bcrypt.compareSync(password, this.password); + } +} export const sequelize = new Sequelize(building ? 'sqlite::memory:' : env.DATABASE_URI, { // only log sql queries in dev mode - logging: dev ? console.log : false -}); - -export const User = sequelize.define('user', { - firstname: { - type: DataTypes.STRING, - allowNull: false - }, - lastname: { - type: DataTypes.STRING, - allowNull: false - }, - birthday: { - type: DataTypes.DATE, - allowNull: false - }, - telephone: DataTypes.STRING, - username: { - type: DataTypes.STRING, - allowNull: false - }, - playertype: { - type: DataTypes.ENUM('java', 'bedrock', 'cracked'), - allowNull: false - }, - password: DataTypes.TEXT, - uuid: { - type: DataTypes.UUIDV4, - allowNull: false - } + logging: dev ? console.log : false, + models: [User, Admin] }); diff --git a/src/lib/server/session.ts b/src/lib/server/session.ts new file mode 100644 index 0000000..f032e7f --- /dev/null +++ b/src/lib/server/session.ts @@ -0,0 +1,11 @@ +const sessions: string[] = []; + +export function addSession(): string { + const session = 'AAA'; + sessions.push(session); + return session; +} + +export function hasSession(session: string): boolean { + return sessions.find((v) => v == session) != undefined; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5fa8163..9035f9c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -80,7 +80,9 @@ />
- +
+ +