Compare commits
4 Commits
2025d26a5c
...
b44177ce47
| Author | SHA1 | Date | |
|---|---|---|---|
| b44177ce47 | |||
| 819b2b7c55 | |||
| b6a29b1842 | |||
| 842ad3ff44 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -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
|
||||
}
|
||||
|
||||
@ -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
77
docs/project_overview.md
Normal 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
BIN
docs/unittest_sample.sqlite
Normal file
Binary file not shown.
@ -43,7 +43,7 @@ onMounted(() => {
|
||||
msg.requestMessages()
|
||||
msgTimer.value = setInterval(() => {
|
||||
msg.requestMessages(msg.lastMsg()?.timestamp)
|
||||
}, 3000) // 3s
|
||||
}, 1000) // 1s
|
||||
})
|
||||
|
||||
// Clean up polling
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
0
simple_chat_api/db_handler/__init__.py
Normal file
0
simple_chat_api/db_handler/__init__.py
Normal 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:
|
||||
71
simple_chat_api/db_handler/user_manager.py
Normal file
71
simple_chat_api/db_handler/user_manager.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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
|
||||
|
||||
0
simple_chat_api/tests/__init__.py
Normal file
0
simple_chat_api/tests/__init__.py
Normal 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.")
|
||||
|
||||
|
||||
114
simple_chat_api/tests/user_manager_unittest.py
Normal file
114
simple_chat_api/tests/user_manager_unittest.py
Normal 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"])
|
||||
Loading…
x
Reference in New Issue
Block a user