diff --git a/backend_py/__pycache__/db_handler.cpython-312.pyc b/backend_py/__pycache__/db_handler.cpython-312.pyc index 5f795fe..7e95b6c 100644 Binary files a/backend_py/__pycache__/db_handler.cpython-312.pyc and b/backend_py/__pycache__/db_handler.cpython-312.pyc differ diff --git a/backend_py/endpoints/__pycache__/auth.cpython-312.pyc b/backend_py/endpoints/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..2e8db6f Binary files /dev/null and b/backend_py/endpoints/__pycache__/auth.cpython-312.pyc differ diff --git a/backend_py/endpoints/__pycache__/messages.cpython-312.pyc b/backend_py/endpoints/__pycache__/messages.cpython-312.pyc new file mode 100644 index 0000000..50a0ebb Binary files /dev/null and b/backend_py/endpoints/__pycache__/messages.cpython-312.pyc differ diff --git a/backend_py/auth.py b/backend_py/endpoints/auth.py similarity index 100% rename from backend_py/auth.py rename to backend_py/endpoints/auth.py diff --git a/backend_py/messages.py b/backend_py/endpoints/messages.py similarity index 97% rename from backend_py/messages.py rename to backend_py/endpoints/messages.py index 9fa1e1f..a84d0c6 100644 --- a/backend_py/messages.py +++ b/backend_py/endpoints/messages.py @@ -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() diff --git a/backend_py/main.py b/backend_py/main.py index 68d131b..5fe6f32 100644 --- a/backend_py/main.py +++ b/backend_py/main.py @@ -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 diff --git a/data/db.sqlite b/data/db.sqlite index 214de2c..d5306e0 100644 Binary files a/data/db.sqlite and b/data/db.sqlite differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1d093d..b4c2253 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 37135d5..c5e690a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b07d388..dcdc4c2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,8 +1,9 @@ diff --git a/frontend/src/assets/icons/error.svg b/frontend/src/assets/icons/error.svg new file mode 100644 index 0000000..34c5a0c --- /dev/null +++ b/frontend/src/assets/icons/error.svg @@ -0,0 +1,12 @@ + + + + error + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/icons/info.svg b/frontend/src/assets/icons/info.svg new file mode 100644 index 0000000..1be5d55 --- /dev/null +++ b/frontend/src/assets/icons/info.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/assets/icons/success.svg b/frontend/src/assets/icons/success.svg new file mode 100644 index 0000000..cd99734 --- /dev/null +++ b/frontend/src/assets/icons/success.svg @@ -0,0 +1,12 @@ + + + + success + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index d4b5078..32f4994 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -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; +} diff --git a/frontend/src/components/Header.vue b/frontend/src/components/Header.vue new file mode 100644 index 0000000..62fd3b0 --- /dev/null +++ b/frontend/src/components/Header.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/frontend/src/components/UserInfo.vue b/frontend/src/components/UserInfo.vue new file mode 100644 index 0000000..f614e85 --- /dev/null +++ b/frontend/src/components/UserInfo.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/frontend/src/composable/auth.ts b/frontend/src/composable/auth.ts index 9b0a69c..224d80c 100644 --- a/frontend/src/composable/auth.ts +++ b/frontend/src/composable/auth.ts @@ -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 = 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 => { + 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 => { - 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() diff --git a/frontend/src/composable/message.ts b/frontend/src/composable/message.ts index 942b9f3..b859dac 100644 --- a/frontend/src/composable/message.ts +++ b/frontend/src/composable/message.ts @@ -13,7 +13,9 @@ interface Message { export const messageHandler = (room: string = 'general') => { let messages: Ref> = ref({}) - function requestMessages(since: string | undefined): Promise> { + function requestMessages( + since: string | undefined = undefined, + ): Promise> { 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 + }, } } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4126e12..e132129 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -16,8 +16,8 @@ const router = createRouter({ meta: { requiresAuth: true }, }, { - path: '/user', - name: 'user', + path: '/admin', + name: 'admin', component: () => import('../views/User.vue'), }, { diff --git a/frontend/src/views/Chat.vue b/frontend/src/views/Chat.vue index 6c92695..e9abeb4 100644 --- a/frontend/src/views/Chat.vue +++ b/frontend/src/views/Chat.vue @@ -1,18 +1,46 @@