improved ui
This commit is contained in:
parent
bff37eab52
commit
edc9334521
Binary file not shown.
BIN
backend_py/endpoints/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend_py/endpoints/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend_py/endpoints/__pycache__/messages.cpython-312.pyc
Normal file
BIN
backend_py/endpoints/__pycache__/messages.cpython-312.pyc
Normal file
Binary file not shown.
@ -3,7 +3,7 @@ from bottle import Bottle, request, response
|
||||
from json import dumps, loads
|
||||
|
||||
from db_handler import User, Message
|
||||
from auth import user_guard
|
||||
from endpoints.auth import user_guard
|
||||
from utils import read_keys_from_request
|
||||
app = Bottle()
|
||||
|
||||
@ -2,8 +2,8 @@ from bottle import request, Bottle
|
||||
|
||||
from db_handler import DbConnector
|
||||
|
||||
import auth
|
||||
import messages
|
||||
import endpoints.auth as auth
|
||||
import endpoints.messages as messages
|
||||
|
||||
import bcrypt
|
||||
# Needet because of: https://github.com/pyca/bcrypt/issues/684
|
||||
|
||||
BIN
data/db.sqlite
BIN
data/db.sqlite
Binary file not shown.
206
frontend/package-lock.json
generated
206
frontend/package-lock.json
generated
@ -35,6 +35,7 @@
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitest": "^3.1.1",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
@ -2162,6 +2163,16 @@
|
||||
"vite": "^5.2.0 || ^6"
|
||||
}
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node22": {
|
||||
"version": "22.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz",
|
||||
@ -3415,6 +3426,50 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@ -3428,6 +3483,42 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/csso": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||||
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "~2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csso/node_modules/css-tree": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||||
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.28",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csso/node_modules/mdn-data": {
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.4.0.tgz",
|
||||
@ -3563,6 +3654,65 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -5169,6 +5319,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/memorystream": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||
@ -6297,6 +6454,42 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^2.3.1",
|
||||
"css-what": "^6.1.0",
|
||||
"csso": "^5.0.5",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"svgo": "bin/svgo"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/svgo"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
@ -6842,6 +7035,19 @@
|
||||
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-svg-loader": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.0.tgz",
|
||||
"integrity": "sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"svgo": "^3.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.2.13"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitest": "^3.1.1",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
12
frontend/src/assets/icons/error.svg
Normal file
12
frontend/src/assets/icons/error.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>error</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="add" fill="#000000" transform="translate(42.666667, 42.666667)">
|
||||
<path d="M213.333333,3.55271368e-14 C331.136,3.55271368e-14 426.666667,95.5306667 426.666667,213.333333 C426.666667,331.136 331.136,426.666667 213.333333,426.666667 C95.5306667,426.666667 3.55271368e-14,331.136 3.55271368e-14,213.333333 C3.55271368e-14,95.5306667 95.5306667,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,42.6666667 C119.232,42.6666667 42.6666667,119.232 42.6666667,213.333333 C42.6666667,307.434667 119.232,384 213.333333,384 C307.434667,384 384,307.434667 384,213.333333 C384,119.232 307.434667,42.6666667 213.333333,42.6666667 Z M262.250667,134.250667 L292.416,164.416 L243.498667,213.333333 L292.416,262.250667 L262.250667,292.416 L213.333333,243.498667 L164.416,292.416 L134.250667,262.250667 L183.168,213.333333 L134.250667,164.416 L164.416,134.250667 L213.333333,183.168 L262.250667,134.250667 Z" id="error">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
frontend/src/assets/icons/info.svg
Normal file
5
frontend/src/assets/icons/info.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75Z"/>
|
||||
<path d="M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25 12C1.25 6.06294 6.06294 1.25 12 1.25C17.9371 1.25 22.75 6.06294 22.75 12C22.75 17.9371 17.9371 22.75 12 22.75C6.06294 22.75 1.25 17.9371 1.25 12ZM12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 751 B |
12
frontend/src/assets/icons/success.svg
Normal file
12
frontend/src/assets/icons/success.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>success</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="add-copy" fill="#000000" transform="translate(42.666667, 42.666667)">
|
||||
<path d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51296 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.153707,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51296 331.153707,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,384 C119.227947,384 42.6666667,307.43872 42.6666667,213.333333 C42.6666667,119.227947 119.227947,42.6666667 213.333333,42.6666667 C307.43872,42.6666667 384,119.227947 384,213.333333 C384,307.43872 307.438933,384 213.333333,384 Z M293.669333,137.114453 L323.835947,167.281067 L192,299.66912 L112.916693,220.585813 L143.083307,190.4192 L192,239.335893 L293.669333,137.114453 Z" id="Shape">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -1 +1,36 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
h1 {
|
||||
@apply text-3xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl font-semibold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl font-medium;
|
||||
}
|
||||
p {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
@apply w-full p-2 border border-gray-500 bg-teal-200 rounded hover:bg-teal-300 focus:bg-teal-300 focus:outline-none focus:ring-2 focus:ring-teal-500;
|
||||
}
|
||||
input::placeholder {
|
||||
@apply text-gray-500 hover:text-gray-700;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600;
|
||||
}
|
||||
|
||||
main {
|
||||
@apply w-full h-screen flex items-center justify-center;
|
||||
}
|
||||
|
||||
.boxed {
|
||||
@apply space-y-2 bg-blue-200 p-4 rounded-2xl shadow-lg;
|
||||
}
|
||||
|
||||
48
frontend/src/components/Header.vue
Normal file
48
frontend/src/components/Header.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'
|
||||
import { primaryUser, type User } from '@/composable/auth'
|
||||
import router from '@/router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row justify-between items-center bg-blue-200 border-b-2 border-blue-400 p-2"
|
||||
>
|
||||
<div class="subbox">
|
||||
<h3 @click="() => router.push('/')" class="hover:cursor-pointer">Simple Chat</h3>
|
||||
<button
|
||||
v-if="primaryUser.currentUser.value && primaryUser.currentUser.value.role === 'admin'"
|
||||
@click="() => router.push('admin')"
|
||||
>
|
||||
Admin Panel
|
||||
</button>
|
||||
</div>
|
||||
<div class="subbox" v-if="primaryUser.currentUser.value">
|
||||
<p class="h-min">User: {{ primaryUser.currentUser.value.user }}</p>
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
primaryUser.removeToken()
|
||||
router.push('login')
|
||||
}
|
||||
"
|
||||
class="w-min h-min"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference '@/assets/main.css';
|
||||
|
||||
button,
|
||||
input {
|
||||
@apply h-min w-auto;
|
||||
}
|
||||
|
||||
.subbox {
|
||||
@apply flex flex-row space-x-2 items-center;
|
||||
}
|
||||
</style>
|
||||
57
frontend/src/components/UserInfo.vue
Normal file
57
frontend/src/components/UserInfo.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import ErrorSvg from '@/assets/icons/error.svg'
|
||||
import InfoSvg from '@/assets/icons/info.svg' // Add the correct icon
|
||||
import SuccessSvg from '@/assets/icons/success.svg' // Add the correct icon
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
type?: 'error' | 'info' | 'success'
|
||||
}>()
|
||||
|
||||
const type = computed(() => props.type || 'info')
|
||||
const icon = computed(() => {
|
||||
switch (type.value) {
|
||||
case 'error':
|
||||
return ErrorSvg
|
||||
case 'info':
|
||||
return InfoSvg
|
||||
case 'success':
|
||||
return SuccessSvg
|
||||
default:
|
||||
return InfoSvg
|
||||
}
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
switch (type.value) {
|
||||
case 'error':
|
||||
return 'bg-red-100 text-red-800 border-red-400 fill-red-500'
|
||||
case 'info':
|
||||
return 'bg-blue-100 text-blue-800 border-blue-400 fill-blue-500'
|
||||
case 'success':
|
||||
return 'bg-green-100 text-green-800 border-green-400 fill-green-500'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800 border-gray-400 fill-gray-500'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row rounded-2xl border-2 p-2" :class="classes">
|
||||
<div class="h-auto aspect-square border-r-2 flex items-center justify-center mr-1 pr-1">
|
||||
<component :is="icon" class="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<div class="flex mx-auto slot-box">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slot-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,6 @@
|
||||
import { API_URL } from '@/main'
|
||||
import { getJsonOrError } from '@/composable/utils'
|
||||
import { computed, ref, type Ref } from 'vue'
|
||||
|
||||
export interface User {
|
||||
user: string
|
||||
@ -14,38 +15,69 @@ const readToken = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
export const getSessionFromJWT = (): User => {
|
||||
let coockie = readToken()
|
||||
if (!coockie) {
|
||||
throw new Error('No token found in cookies')
|
||||
const userHandler = () => {
|
||||
let curentUser: Ref<User | null> = ref(null)
|
||||
|
||||
const removeToken = () => {
|
||||
document.cookie = 'oauth2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;'
|
||||
curentUser.value = null
|
||||
}
|
||||
let token = coockie.split(' ')[coockie.split(' ').length - 1] // Get the last part of the token
|
||||
try {
|
||||
// Phrase JWT
|
||||
const base64Url = token.split('.')[1] // Get the payload part
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') // Base64 adjustments
|
||||
return JSON.parse(atob(base64)).sub // Decode and parse JSON
|
||||
} catch (_) {
|
||||
throw new Error('Invalid token format')
|
||||
|
||||
const getSessionFromJWT = (): User => {
|
||||
let coockie = readToken()
|
||||
if (!coockie) {
|
||||
throw new Error('No token found in cookies')
|
||||
}
|
||||
let token = coockie.split(' ')[coockie.split(' ').length - 1] // Get the last part of the token
|
||||
try {
|
||||
// Phrase JWT
|
||||
const base64Url = token.split('.')[1] // Get the payload part
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') // Base64 adjustments
|
||||
return JSON.parse(atob(base64)).sub // Decode and parse JSON
|
||||
} catch (_) {
|
||||
throw new Error('Invalid token format')
|
||||
}
|
||||
}
|
||||
|
||||
const requestToken = async (user: string, password: string): Promise<User> => {
|
||||
return fetch(`${API_URL}/user/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // set coockies from responce
|
||||
body: JSON.stringify({
|
||||
user: user,
|
||||
password: password,
|
||||
}),
|
||||
}).then(async (response) => {
|
||||
let data = await getJsonOrError(response)
|
||||
curentUser.value = {
|
||||
user: data.sub.user as string,
|
||||
role: data.sub.role as string,
|
||||
}
|
||||
return curentUser.value
|
||||
})
|
||||
}
|
||||
|
||||
const currentUser = (): User | null => {
|
||||
if (curentUser.value === null) {
|
||||
try {
|
||||
curentUser.value = getSessionFromJWT()
|
||||
} catch (e) {
|
||||
console.error('Error getting session from JWT:', e)
|
||||
curentUser.value = null
|
||||
}
|
||||
}
|
||||
return curentUser.value
|
||||
}
|
||||
|
||||
return {
|
||||
getSessionFromJWT,
|
||||
requestToken,
|
||||
removeToken,
|
||||
currentUser: computed(() => currentUser()),
|
||||
}
|
||||
}
|
||||
|
||||
export const requestToken = async (user: string, password: string): Promise<User> => {
|
||||
return fetch(`${API_URL}/user/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // set coockies from responce
|
||||
body: JSON.stringify({
|
||||
user: user,
|
||||
password: password,
|
||||
}),
|
||||
}).then(async (response) => {
|
||||
let data = await getJsonOrError(response)
|
||||
return {
|
||||
user: data.sub.user as string,
|
||||
role: data.sub.role as string,
|
||||
} as User
|
||||
})
|
||||
}
|
||||
export const primaryUser = userHandler()
|
||||
|
||||
@ -13,7 +13,9 @@ interface Message {
|
||||
export const messageHandler = (room: string = 'general') => {
|
||||
let messages: Ref<Record<number, Message>> = ref({})
|
||||
|
||||
function requestMessages(since: string | undefined): Promise<Record<number, Message>> {
|
||||
function requestMessages(
|
||||
since: string | undefined = undefined,
|
||||
): Promise<Record<number, Message>> {
|
||||
let query = since ? `?since=${since}` : ''
|
||||
|
||||
return fetch(`${API_URL}/messages/${room}${query}`, {
|
||||
@ -53,5 +55,14 @@ export const messageHandler = (room: string = 'general') => {
|
||||
}
|
||||
return messages.value[highestKey]
|
||||
},
|
||||
previousMessage: (id: number): Message | undefined => {
|
||||
const keys = Object.keys(messages.value).map(Number)
|
||||
const index = keys.indexOf(Number(id))
|
||||
if (index > 0) {
|
||||
console.log('Previous message found:', messages.value[keys[index - 1]])
|
||||
return messages.value[keys[index - 1]]
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ const router = createRouter({
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: 'user',
|
||||
path: '/admin',
|
||||
name: 'admin',
|
||||
component: () => import('../views/User.vue'),
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,18 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { getSessionFromJWT, type User } from '@/composable/auth'
|
||||
import { messageHandler } from '@/composable/message'
|
||||
import router from '@/router'
|
||||
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, watch, type Ref } from 'vue'
|
||||
|
||||
const user: Ref<User> = ref(getSessionFromJWT())
|
||||
const msg = messageHandler()
|
||||
|
||||
const newMessage = ref('')
|
||||
|
||||
const msgTimer = ref()
|
||||
const scrollContainer = ref<null | HTMLElement>(null)
|
||||
|
||||
watch(msg.messages, async () => {
|
||||
// To automatically scroll down when new message arrives
|
||||
await nextTick()
|
||||
const el = scrollContainer.value
|
||||
if (!el) return
|
||||
el.scrollTop = el.scrollHeight
|
||||
})
|
||||
|
||||
const onSentMessage = () => {
|
||||
if (newMessage.value.trim() === '') {
|
||||
return
|
||||
}
|
||||
msg
|
||||
.sendMessage(newMessage.value)
|
||||
.then(() => {
|
||||
newMessage.value = ''
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error sending message:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && event.shiftKey === false) {
|
||||
onSentMessage()
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate polling for messages
|
||||
// ToDo: Try using a WebSocket instead
|
||||
// ToDo: Try using a WebSocket instea
|
||||
onMounted(() => {
|
||||
document.addEventListener('keypress', handleKeyPress)
|
||||
msg.requestMessages()
|
||||
msgTimer.value = setInterval(() => {
|
||||
console.log('Polling for messages...')
|
||||
msg.requestMessages(msg.lastMsg()?.timestamp)
|
||||
@ -22,33 +50,58 @@ onMounted(() => {
|
||||
// Clean up polling
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(msgTimer.value)
|
||||
document.removeEventListener('keypress', handleKeyPress)
|
||||
msgTimer.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>Chat</h1>
|
||||
<div class="max-w-128">
|
||||
<div v-if="msg" v-for="message in msg.messages.value" :key="message.id" class="mb-4">
|
||||
<p>
|
||||
<a>{{ message.user }}</a
|
||||
><a>{{ message.timestamp }}</a
|
||||
>: {{ message.content }}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 grid-rows-10 boxed h-[75vh] max-w-128">
|
||||
<h1 class="border-b-2">Chat</h1>
|
||||
<div class="row-span-8 flex flex-col overflow-scroll" ref="scrollContainer">
|
||||
<div v-if="msg" v-for="(message, id) in msg.messages.value" :key="message.id" class="mb-4">
|
||||
<div
|
||||
v-if="
|
||||
!msg.previousMessage(id) ||
|
||||
new Date(Number(message.timestamp) * 1000).toLocaleDateString() !==
|
||||
new Date(Number(msg.previousMessage(id)?.timestamp) * 1000).toLocaleDateString()
|
||||
"
|
||||
class="text-center text-gray-500 my-2"
|
||||
>
|
||||
{{ new Date(Number(message.timestamp) * 1000).toLocaleDateString() }}
|
||||
</div>
|
||||
<p class="whitespace-pre-line">
|
||||
<a class="italic mr-1">{{
|
||||
new Date(Number(message.timestamp) * 1000).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
}}</a>
|
||||
<a class="font-bold">{{ message.user }}</a
|
||||
>: {{ message.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row mt-auto">
|
||||
<textarea
|
||||
v-model="newMessage"
|
||||
placeholder="Type your message here..."
|
||||
class="w-full h-12 min-h-12 p-2 border rounded"
|
||||
/>
|
||||
<button
|
||||
@click="
|
||||
() =>
|
||||
msg.sendMessage(newMessage).then(() => {
|
||||
newMessage = ''
|
||||
})
|
||||
"
|
||||
class="!w-min"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="newMessage"
|
||||
type="text"
|
||||
placeholder="Type your message here..."
|
||||
class="w-full p-2 border rounded"
|
||||
/>
|
||||
<button
|
||||
@click="() => msg.sendMessage(newMessage)"
|
||||
class="mt-2 bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { API_URL } from '@/main.ts'
|
||||
import { requestToken } from '@/composable/auth.ts'
|
||||
import { primaryUser } from '@/composable/auth.ts'
|
||||
import router from '@/router'
|
||||
|
||||
import UserInfo from '@/components/UserInfo.vue'
|
||||
|
||||
const name = ref('')
|
||||
const password = ref('')
|
||||
const msg = ref('')
|
||||
|
||||
const onLogin = () => {
|
||||
requestToken(name.value, password.value)
|
||||
primaryUser
|
||||
.requestToken(name.value, password.value)
|
||||
.then((user) => {
|
||||
console.log('Login successful:', user)
|
||||
router.push({ name: 'chat' })
|
||||
@ -18,15 +21,36 @@ const onLogin = () => {
|
||||
msg.value = error.message || 'Login failed'
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('keypress', handleKeyPress)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keypress', handleKeyPress)
|
||||
})
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
onLogin()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div>
|
||||
<input v-model="name" placeholder="Username" />
|
||||
<input v-model="password" type="password" placeholder="Password" />
|
||||
<button @click="() => onLogin()">Login</button>
|
||||
<p>{{ msg }}</p>
|
||||
<div class="flex flex-col w-80 boxed">
|
||||
<h3>Auth:</h3>
|
||||
<div class="flex flex-col space-y-1 items-center">
|
||||
<input v-model="name" placeholder="Username" />
|
||||
<input v-model="password" type="password" placeholder="Password" />
|
||||
<button @click="() => onLogin()">Login</button>
|
||||
</div>
|
||||
|
||||
<UserInfo type="error" v-if="msg">
|
||||
<template #default>
|
||||
<p v-if="msg">{{ msg }}</p>
|
||||
</template>
|
||||
</UserInfo>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { API_URL } from '@/main.ts'
|
||||
import { type User, getSessionFromJWT } from '@/composable/auth.ts'
|
||||
import { type User, primaryUser } from '@/composable/auth.ts'
|
||||
|
||||
const user = ref<User | undefined>()
|
||||
import UserInfo from '@/components/UserInfo.vue'
|
||||
|
||||
const new_user_name = ref('')
|
||||
const new_user_passwd = ref('')
|
||||
const new_admin = ref(false)
|
||||
const msg = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
user.value = getSessionFromJWT()
|
||||
})
|
||||
const msg = ref({ message: '', type: 'info' })
|
||||
|
||||
const onNewUserCreation = async () => {
|
||||
try {
|
||||
@ -31,34 +27,44 @@ const onNewUserCreation = async () => {
|
||||
|
||||
const data = await response.json()
|
||||
if (response.ok) {
|
||||
msg.value = data.message
|
||||
msg.value = { message: data.message, type: 'success' }
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to create user')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
msg.value = `Error creating user: ${errorMessage}`
|
||||
msg.value = { message: `Error creating user: ${errorMessage}`, type: 'error' }
|
||||
console.error('Error creating user:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="user" class="user-profile">
|
||||
<h1>User Profile</h1>
|
||||
<p><strong>Name:</strong> {{ user.user }}</p>
|
||||
<p><strong>Role:</strong> {{ user.role }}</p>
|
||||
|
||||
<div v-if="user.role === 'admin'">
|
||||
<h2>New user</h2>
|
||||
<input v-model="new_user_name" placeholder="Username" />
|
||||
<input v-model="new_user_passwd" type="password" placeholder="Password" />
|
||||
<input v-model="new_admin" type="checkbox" />
|
||||
<button @click="() => onNewUserCreation()">Create User</button>
|
||||
<main>
|
||||
<div v-if="primaryUser.currentUser.value" class="flex flex-wrap space-x-4 space-y-4">
|
||||
<div class="boxed">
|
||||
<h3>User Profile</h3>
|
||||
<p><a class="font-bold">Name:</a> {{ primaryUser.currentUser.value.user }}</p>
|
||||
<p><a class="font-bold">Role:</a> {{ primaryUser.currentUser.value.role }}</p>
|
||||
</div>
|
||||
<div v-if="primaryUser.currentUser.value.role === 'admin'" class="boxed">
|
||||
<h3>New user</h3>
|
||||
<input v-model="new_user_name" placeholder="Username" />
|
||||
<input v-model="new_user_passwd" type="password" placeholder="Password" />
|
||||
<span class="flex flex-row">
|
||||
<label for="new_admin">Admin:</label>
|
||||
<input v-model="new_admin" id="new_admin" type="checkbox" class="!w-min ml-1" />
|
||||
</span>
|
||||
<button @click="() => onNewUserCreation()">Create User</button>
|
||||
<UserInfo :type="msg.type as any" v-if="msg.message">
|
||||
<template #default>
|
||||
<p>{{ msg.message }}</p>
|
||||
</template>
|
||||
</UserInfo>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>No user information...</p>
|
||||
</div>
|
||||
<p>{{ msg }}</p>
|
||||
<div v-else>
|
||||
<p>No user information...</p>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -5,10 +5,11 @@ import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()],
|
||||
plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss(), svgLoader()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user