Compare commits

...

4 Commits

Author SHA1 Message Date
b44177ce47 updated tests 2025-11-13 09:32:13 +01:00
819b2b7c55 added project overview 2025-10-30 11:15:23 +01:00
b6a29b1842 users can remove themselves 2025-10-02 11:26:47 +02:00
842ad3ff44 tast tow 2025-10-02 10:48:30 +02:00
17 changed files with 306 additions and 11 deletions

View File

@ -11,7 +11,8 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix",
"python.testing.unittestArgs": ["-s", "simple_chat_api/tests", "-p", "*.py"],
"python.testing.unittestArgs": ["-s", ".", "-p", "*.py"],
"python.testing.pytestEnabled": false,
"python.testing.cwd": "${workspaceFolder}",
"python.testing.unittestEnabled": true
}

View File

@ -4,7 +4,9 @@ This is a simple one-room chat application with user management, developed as a
**Not recommended for production use.**
A test protocol can be found here: [simple_chat_testprotokoll.pdf](simple_chat_testprotokoll.pdf)
A test protocol can be found here: [simple_chat_testprotokoll.pdf](docs/simple_chat_testprotokoll.pdf)
A Project overview can be found here: [Perplexity overview](docs/project_overview.md)
## Running with Docker

77
docs/project_overview.md Normal file
View File

@ -0,0 +1,77 @@
<img src="https://r2cdn.perplexity.ai/pplx-full-logo-primary-dark%402x.png" style="height:64px;margin-right:32px"/>
# Erstelle einen Überblick über die verwendeten Technologien mit jeweils kurzer Beschreibung anhand einschlägiger Beispiele zu dem Projekt unter der URL git+https://git.urmel.duckdns.org/Kyattsukuro/simplechat und gib die jeweiligen Webseiten zu den Dokumentationen an.
Das Projekt "simplechat" verwendet folgende Haupttechnologien, die jeweils eine spezifische Rolle im Chat-System innehaben:
Python:
- Wird für das Backend (API und Serverlogik) genutzt.
- Ermöglicht die Verarbeitung von Chatnachrichten und Nutzerverwaltung.
- Beispiel aus dem Projekt: Backend-Code in simple_chat_api, basierend auf Python-Frameworks wie FastAPI oder ähnlichem, um HTTP- und WebSocket-Kommunikation zu realisieren.
- Dokumentation: https://www.python.org/doc/
Vue.js:
- Frontend-Framework für die Benutzeroberfläche.
- Stellt reaktive Komponenten dar, um Nachrichten anzuzeigen und Nutzereingaben dynamisch zu verarbeiten.
- Beispiel: frontend-Verzeichnis mit Vue-Komponenten, z.B. Chatfenster und Nutzerliste.
- Dokumentation: https://vuejs.org/guide/introduction.html
TypeScript:
- Statisch typisierte Erweiterung von JavaScript, verwendet im Frontend zur besseren Codequalität und Wartbarkeit.
- Wird zusammen mit Vue.js genutzt, um Typensicherheit zu gewährleisten.
- Beispiel: TypeScript-Dateien im frontend-Teil, die Logik und Datenmodelle definieren.
- Dokumentation: https://www.typescriptlang.org/docs/
Docker (inkl. Docker Compose):
- Containerisierung der Anwendung für einfache Bereitstellung und Umgebungskonsistenz.
- docker/compose.yml definiert Dienste und deren Abhängigkeiten (Backend, Frontend).
- Ermöglicht das Starten kompletter Entwicklungs- oder Produktionsumgebungen mit einem Befehl.
- Dokumentation: https://docs.docker.com/compose/
CSS:
- Für das Styling der Benutzeroberfläche.
- Ermöglicht das Layout und visuelle Erscheinungsbild des Chat-Frontends.
- Beispiel: CSS-Dateien im frontend-Bereich, die die Chat-Komponenten formatieren.
- Dokumentation: https://developer.mozilla.org/de/docs/Web/CSS
Übersichtstabelle der Technologien:
| Technologie | Verwendung | Kurzbeschreibung | Dokumentation |
| :------------- | :----------------------- | :--------------------------------------------------------- | :-------------------------------------------- |
| Python | Backend-Programmierung | Serverlogik, Nachrichtenverarbeitung, Nutzerverwaltung | https://www.python.org/doc/ |
| Vue.js | Frontend UI | Reaktive Benutzeroberflächen, Datenbindung | https://vuejs.org/guide/introduction.html |
| TypeScript | Frontend-Entwicklung | Statische Typisierung von JavaScript, bessere Codequalität | https://www.typescriptlang.org/docs/ |
| Docker Compose | Container-Orchestrierung | Einfache Verwaltung von Backend- und Frontend-Diensten | https://docs.docker.com/compose/ |
| CSS | Frontend Styling | Gestaltung des Layouts und der Optik | https://developer.mozilla.org/de/docs/Web/CSS |
Diese Technologien bilden zusammen die Grundlage des einfachen Chat-Systems, das sowohl serverseitige Verarbeitung als auch eine moderne, reaktive Web-Oberfläche bietet. Das Projekt ist als Schulprojekt konzipiert und deswegen nicht produktionsreif, zeigt aber exemplarisch den Einsatz von Python für das Backend, Vue.js und TypeScript für das Frontend sowie Docker für die Containerisierung.[^1]
<span style="display:none">[^10][^11][^12][^13][^14][^15][^16][^17][^18][^19][^2][^20][^21][^3][^4][^5][^6][^7][^8][^9]</span>
<div align="center"></div>
[^1]: https://git.urmel.duckdns.org/Kyattsukuro/simplechat
[^2]: https://simplechat365.com
[^3]: https://github.com/microsoft/simplechat
[^4]: https://github.com/RichDaly/Simple_Chat_Project
[^5]: https://simplechat365.com/how-it-works/
[^6]: https://arkanis.de/weblog/2010-09-05-simple-chat-the-details/
[^7]: https://www.reddit.com/r/node/comments/1gwerxw/best_tech_stack_for_a_chat_app_with_ai_python_vs/
[^8]: https://masteringbackend.com/posts/build-a-real-time-chat-app-with-vuejs-socket-io-and-nodejs
[^9]: https://dev.to/vsfarooqkhan/how-to-build-a-real-time-chat-application-using-typescript-and-socketio-32k5
[^10]: https://iemafzalhassan.hashnode.dev/containerize-and-orchestrate-building-a-real-timethree-tier-chat-app-with-docker-and-kubernetes
[^11]: https://www.scaler.com/topics/chat-interface-project-css/
[^12]: https://simplechat.pro
[^13]: https://www.geeksforgeeks.org/python/simple-chat-room-using-python/
[^14]: https://javascript.plainenglish.io/build-a-real-time-chat-app-with-vue-js-your-step-by-step-guide-to-success-e0cc72cbd326
[^15]: https://www.reddit.com/r/rust/comments/10skc78/building_deploying_a_web_chat_app_with_react/
[^16]: https://github.com/adrianhuber17/chat-app
[^17]: https://www.youtube.com/watch?v=_sxoqRIbW0c
[^18]: https://www.prodwaregroup.com/our-solutions/simplechat/
[^19]: https://python.plainenglish.io/building-a-real-time-chat-app-backend-with-python-and-fastapi-83af81ecec70
[^20]: https://sendbird.com/de/blog/build-a-vue-chat-app
[^21]: https://github.com/assistant-ui/assistant-ui

BIN
docs/unittest_sample.sqlite Normal file

Binary file not shown.

View File

@ -43,7 +43,7 @@ onMounted(() => {
msg.requestMessages()
msgTimer.value = setInterval(() => {
msg.requestMessages(msg.lastMsg()?.timestamp)
}, 3000) // 3s
}, 1000) // 1s
})
// Clean up polling

View File

@ -5,6 +5,7 @@ import { type User, primaryUser } from '@/composable/auth.ts'
import { deleteUser, addUser, changePassword } from '@/composable/settings'
import UserInfo from '@/components/UserInfo.vue'
import router from '@/router'
const new_user_name = ref('')
const new_user_passwd = ref('')
@ -50,6 +51,20 @@ const onChangePassword = async () => {
<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>
<button
@click="
() =>
deleteUser(primaryUser.getSessionFromJWT()).then(() => {
primaryUser.removeToken()
router.push('login')
})
"
>
Delete Profile
</button>
</div>
</div>
<div class="boxed w-80">

View File

@ -1 +1,2 @@
from simple_chat_api.main import main
from simple_chat_api.main import main
from simple_chat_api.db_handler.db_handler import DbConnector

View File

View File

@ -34,6 +34,8 @@ class DbConnector:
self.session = sessionmaker(bind=self.engine)()
self._create_defaults()
def close(self):
self.session.close()
def _create_optional_default_user(self):
for user in optional_default_user:

View File

@ -0,0 +1,71 @@
from simple_chat_api.db_handler.db_handler import DbConnector
from simple_chat_api.config import hash_context
class UserManager:
"""
Class that handles authentication with database backend
"""
def __init__(self, db_con: DbConnector):
self.db_con = db_con
def create_user(self, username: str, password: str) -> bool:
"""
Create a new user in the database
:param username: logon name
:param password: password to save
:return: True if user was created
"""
try:
self.db_con.add_user(username, password)
except:
return False
return True
def authenticate(self, username: str, password: str) -> bool:
"""
Authenticate against the database
:param username: username to check
:param password: password to check
:return: True if authenticated
"""
user = self.db_con.get_user(username)
if not user or not hash_context.verify(password, user.hash):
return False
return True
def delete_user(self, username: str) -> bool:
"""
Delete a user from the database
:param username: username to delete
:return: True on success
"""
try:
self.db_con.delete_user(username)
except:
return False
return True
def change_user_password(self, username: str, password: str) -> bool:
"""
Change the password for a user
:param username: username to change
:param password: new password
:return: True on success
"""
try:
self.db_con.change_password(username, hash_context.hash(password))
except:
return False
return True
def check_user_exists(self, username: str) -> bool:
"""
Check if a user exists in the database
:param username: logon name to look for
:return: True if user exists
"""
if self.db_con.get_user(username):
return True
return False

View File

@ -127,9 +127,16 @@ def change_password(user):
return dumps({"error": str(e)})
@app.route("/delete/<deletion_target>", method=["POST"])
@admin_guard()
def delete_user(_, deletion_target: str):
@user_guard()
def delete_user(user, deletion_target: str):
response.content_type = 'application/json'
is_admin = admin_guard()(lambda _: True)() == True
# Note: we could just use user.role == "admin" but we
# want to follow potential requirement changes in the decorator
if user.name != deletion_target and not is_admin:
response.status = 401
return dumps({"error": "Not permited"})
try:
request.db_connector.delete_user(deletion_target)
response.status = 200

View File

@ -2,7 +2,7 @@
from bottle import Bottle, request, response
from json import dumps, loads
from simple_chat_api.db_handler import User, Message
from simple_chat_api.db_handler.db_handler import User, Message
from simple_chat_api.endpoints.auth import user_guard
from simple_chat_api.utils import read_keys_from_request
app = Bottle()
@ -21,6 +21,7 @@ def recive_msg(user: User, room: str):
response.status = 400
return {"error": "Missing 'content' in request body"}
new_msg = request.db_connector.add_msg_to_room(room, msg["content"], user.name)
return serialize_message([new_msg])
@app.route('/<room>', method=['GET'])

View File

@ -1,6 +1,6 @@
from bottle import request, Bottle
from simple_chat_api.db_handler import DbConnector
from simple_chat_api.db_handler.db_handler import DbConnector
import simple_chat_api.endpoints.auth as auth
import simple_chat_api.endpoints.messages as messages

View File

View File

@ -160,7 +160,7 @@ class TestAuthEndpoints(unittest.TestCase):
else:
self.assertEqual(response.status_code, 400, f"Non-admin user {user} should not create users; {response.text}")
def test_5_delete_user(self):
def test_6_delete_user(self):
if self.userSessions == {}:
self.skipTest("No user sessions available. Run test_get_token first.")
@ -175,10 +175,14 @@ class TestAuthEndpoints(unittest.TestCase):
response = session.post(f"{API_URL}/user/delete/nonexistent")
self.assertEqual(response.status_code, 400, f"Deleting non-existent user should fail; {response.text}")
else:
response = session.post(f"{API_URL}/user/delete/max")
response = session.post(f"{API_URL}/user/delete/ina")
self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not delete users; {response.text}")
response = session.post(f"{API_URL}/user/delete/{user}")
self.assertEqual(response.status_code, 200, f"Non-admin user {user} should be able to delete self; {response.text}")
def test_6_change_password(self):
def test_5_change_password(self):
if self.userSessions == {}:
self.skipTest("No user sessions available. Run test_get_token first.")

View File

@ -0,0 +1,114 @@
import unittest
import os
import shutil
from simple_chat_api.db_handler.db_handler import DbConnector
from simple_chat_api.db_handler.user_manager import UserManager
DB_PATH = "./data/TEST.sqlite"
EXISTING_USER = ("admin", "admin")
class TestUserManagmentWrapper(unittest.TestCase):
def setUp(self):
shutil.copyfile("docs/unittest_sample.sqlite", DB_PATH)
db = DbConnector(f"sqlite:///{DB_PATH}")
self.user_mgr = UserManager(db)
def tearDown(self):
self.user_mgr.db_con.close()
os.remove(DB_PATH)
def test_create_user(self):
users = [("someone", "pass"), ("someone", "fail")]
for user in users:
with self.subTest(user):
ret = self.user_mgr.create_user(*user)
if user[1] == "fail":
self.assertFalse(ret, f"user {user} was created but should not")
else:
self.assertTrue(ret, f"user {user} faild to be created")
def test_authenticate(self):
test_cases = [
{
"username": EXISTING_USER[0],
"password": EXISTING_USER[1],
"expected": True,
"msg": f"Existing user {EXISTING_USER} could not be authenticated"
},
{
"username": EXISTING_USER[0],
"password": "unknown_pass",
"expected": False,
"msg": f"Existing user {EXISTING_USER[0]} could be authenticated with wrong password"
},
{
"username": "unknown_user",
"password": "unknown_pass",
"expected": False,
"msg": "An unknown user was authenticated"
},
]
for case in test_cases:
with self.subTest(username=case["username"], password=case["password"]):
result = self.user_mgr.authenticate(case["username"], case["password"])
self.assertEqual(result, case["expected"], case["msg"])
def test_delete_user(self):
test_cases = [
{
"username": EXISTING_USER[0],
"expected": True,
"msg": f"The existing user {EXISTING_USER[0]} could not be removed"
},
{
"username": "unknown_user",
"expected": False,
"msg": "an unknown user could be deleted"
},
]
for case in test_cases:
with self.subTest(username=case["username"]):
result = self.user_mgr.delete_user(case["username"])
self.assertEqual(result, case["expected"], case["msg"])
def test_change_password(self):
test_cases = [
{
"username": EXISTING_USER[0],
"new_password": "new_pass",
"expected": True,
"msg": f"Could not change password for existing user {EXISTING_USER[0]}"
},
{
"username": "unknown_user",
"new_password": "new_pass",
"expected": False,
"msg": "The password for an unknown user was changed"
},
]
for case in test_cases:
with self.subTest(username=case["username"]):
result = self.user_mgr.change_user_password(case["username"], case["new_password"])
self.assertEqual(result, case["expected"], case["msg"])
def test_check_user_exists(self):
test_cases = [
{
"username": EXISTING_USER[0],
"expected": True,
"msg": f"The existing user {EXISTING_USER[0]} could not be found"
},
{
"username": "unknown_user",
"expected": False,
"msg": "An unknown user was found"
},
]
for case in test_cases:
with self.subTest(username=case["username"]):
result = self.user_mgr.check_user_exists(case["username"])
self.assertEqual(result, case["expected"], case["msg"])