From b72ecc55e97f373ab7ef8037b1a62781999b92f1 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Thu, 18 Sep 2025 10:43:34 +0200 Subject: [PATCH] feat: added passwort change endpoint; finishing touches --- README.md | 38 +++- docker/Dockerfile | 4 +- docker/compose.yml | 18 ++ docker/default.sqlite | Bin 16384 -> 0 bytes frontend/src/assets/main.css | 8 +- frontend/src/components/Header.vue | 7 +- frontend/src/composable/settings.ts | 23 +++ frontend/src/router/index.ts | 4 +- frontend/src/views/User.vue | 34 +++- simple_chat_api/config.py | 3 +- simple_chat_api/db_handler.py | 7 + simple_chat_api/endpoints/auth.py | 16 ++ simple_chat_api/main.py | 5 +- simple_chat_api/tests/auth_entpoints.py | 233 ++++++++++++++++++++++-- simple_chat_testprotokoll.pdf | Bin 0 -> 32213 bytes 15 files changed, 363 insertions(+), 37 deletions(-) create mode 100644 docker/compose.yml delete mode 100644 docker/default.sqlite create mode 100644 simple_chat_testprotokoll.pdf diff --git a/README.md b/README.md index 477da20..23e91b2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ -Run tests: -`python -m unittest discover -s simple_chat_api/tests -p "*.py"` +# Simple Chat + +This is a simple one-room chat application with user management, developed as a school project for study purposes. + +**Not recommended for production use.** + +A test protocol can be found here: [simple_chat_testprotokoll.pdf](simple_chat_testprotokoll.pdf) + +## Running with Docker + +```sh +docker compose -f docker/compose.yml up +``` + +### Running in Development Mode + +With Docker: + +```sh +BUILD_TARGET=dev docker compose -f docker/compose.yml +``` + +Then, install dependencies for the frontend and start the development server: + +```sh +cd frontend +npm install +npm run dev +``` + +For the backend, install dependencies and run the server: + +```sh +pip install -r simple_chat_api/requirements.txt +python -m simple_chat_api +``` diff --git a/docker/Dockerfile b/docker/Dockerfile index 4b0e88a..1caab7f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -25,8 +25,6 @@ RUN rm /etc/nginx/nginx.conf RUN ln -s /simple-chat/docker/nginx.conf /etc/nginx/nginx.conf RUN mkdir -p /var/nginx -COPY docker/default.sqlite /simple-chat/data/data.sqlite - # Dev stage FROM base AS dev ARG ROOT_PASSWD @@ -53,6 +51,6 @@ RUN ssh-keygen -A && \ CMD ["bash", "/entrypoint.sh"] # Production stage -FROM base AS production +FROM base AS prod # Run entrypoint CMD ["bash", "/entrypoint.sh"] diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..1745eab --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,18 @@ +services: + simple_chat: + build: + context: ../ + dockerfile: docker/Dockerfile + target: ${BUILD_TARGET:-prod} + container_name: simple_chat + ports: + - "8080:8080" + environment: + - ROOT_PASSWD=adminpass + - JWT_SECRET=change_me + volumes: + - data:/simple-chat/data + restart: unless-stopped + +volumes: + data: diff --git a/docker/default.sqlite b/docker/default.sqlite deleted file mode 100644 index 299dd8b2cd436563128112140bb71c43702e71ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI($!^*}7zc2hB@0#JN>SxQ6?Gy-Y9du`uz?U&l|qOC7tCUogj}Kl3^;fJ+iblh z*S<&l06q2@daQbDk3II-9y@kIm8fcOm0IZ=VLp!y{~7&!nn6@61t(E{Y;cAs9{_(NTxxU0i8<~tQ!e?u`9<~=FUKmY_l00ck)1V8`; zKmY_l00cnbcLJA>gUjsZCVjQ#;+C!&s^j3E>MUv~t%#|bh-#^$f`}GZ(N1Jl>maFE z6SJa%ilrJV)(eFvD6(o>mVp{6C7nwtUcYUbu4=lsFO415zTI~|>|DxN$vW{pSaN z8QDuAWA6r**yk)wX=X>AIU}8{?9cJowfz3MKjwMVFF^wBt?#Kda)j4kQOsLLV;-%4 zAg9nk00ck)1V8`;KmY_l00ck)1VG?#3gp7!jmz+ZvEJS0Tia27`&39iHMsaBT60Hc zOgIqaS~opb(n|fbkeuvwXZ_v#WTWDrs+dn+y3#nu%|c&Ia-9iBnHV z-B<^o&58xt&RQH-7LPKeNj$z^GL?p`OeJYj8@-5CE4gHnzfKDJNdwKeN$MXo^1Pjh zT4lMHof`FOXPR%Qt*Jaa<)X26!W6~Hs4=c}8n69YZs3 zvZQw1j!*Z8QU;&mY$H>S&l?S{#W_;*;W)AFJor~s-4E6&e div { + @apply overflow-scroll max-h-full 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 index 62fd3b0..91d69a5 100644 --- a/frontend/src/components/Header.vue +++ b/frontend/src/components/Header.vue @@ -10,11 +10,8 @@ import router from '@/router' >

Simple Chat

-
diff --git a/frontend/src/composable/settings.ts b/frontend/src/composable/settings.ts index 2bae06f..3128c18 100644 --- a/frontend/src/composable/settings.ts +++ b/frontend/src/composable/settings.ts @@ -21,6 +21,29 @@ export const deleteUser = async (user: User): Promise => { }) } +export const changePassword = async (oldPassword: string, newPassword: string): Promise => { + const response = await fetch(`${API_URL}/user/changePassword`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + old_password: oldPassword, + new_password: newPassword, + }), + }) + if (response.ok) { + const data = await response.json() + return data.message + } else { + const errorData = await response.json().catch(() => null) + throw new Error( + 'Failed to change password. ' + (errorData && errorData.error ? errorData.error : ''), + ) + } +} + export const addUser = async ( username: string, password: string, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e132129..a8806ac 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -16,8 +16,8 @@ const router = createRouter({ meta: { requiresAuth: true }, }, { - path: '/admin', - name: 'admin', + path: '/settings', + name: 'settings', component: () => import('../views/User.vue'), }, { diff --git a/frontend/src/views/User.vue b/frontend/src/views/User.vue index bdff190..8542bab 100644 --- a/frontend/src/views/User.vue +++ b/frontend/src/views/User.vue @@ -2,7 +2,7 @@ import { ref } from 'vue' import { API_URL } from '@/main.ts' import { type User, primaryUser } from '@/composable/auth.ts' -import { deleteUser, addUser } from '@/composable/settings' +import { deleteUser, addUser, changePassword } from '@/composable/settings' import UserInfo from '@/components/UserInfo.vue' @@ -11,6 +11,9 @@ const new_user_passwd = ref('') const new_admin = ref(false) const userCreationMsg = ref({ message: '', type: 'info' }) const userDeletionMsg = ref({ message: '', type: 'info' }) +const changePassMsg = ref({ message: '', type: 'info' }) + +const changePassField = ref({ old: '', new: '' }) const onNewUserCreation = async () => { addUser(new_user_name.value, new_user_passwd.value, new_admin.value) @@ -25,6 +28,19 @@ const onNewUserCreation = async () => { console.error(error) }) } + +const onChangePassword = async () => { + changePassword(changePassField.value.old, changePassField.value.new) + .then((data: string) => { + changePassMsg.value = { message: data, type: 'success' } + changePassField.value.old = '' + changePassField.value.new = '' + }) + .catch((error: any) => { + changePassMsg.value = { message: `${error}`, type: 'error' } + console.error(error) + }) +} diff --git a/simple_chat_api/config.py b/simple_chat_api/config.py index 3d70061..163a891 100644 --- a/simple_chat_api/config.py +++ b/simple_chat_api/config.py @@ -2,7 +2,8 @@ from passlib.context import CryptContext import os -JWT_SECRET = "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq" +JWT_SECRET = os.environ.get("JWT_SECRET", "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq") +debug = os.environ.get("DEV", "False").lower() in ("true", "1", "t") hash_context = CryptContext(schemes=["bcrypt"]) db_url = os.environ.get("DATABASE_URL", "sqlite:///./data/data.sqlite") use_optional_default_user = os.environ.get("CREATE_OPTIONAL_DEFAULT_USER", "False").lower() in ("true", "1", "t") diff --git a/simple_chat_api/db_handler.py b/simple_chat_api/db_handler.py index a8d8c2b..3d023dc 100644 --- a/simple_chat_api/db_handler.py +++ b/simple_chat_api/db_handler.py @@ -71,6 +71,13 @@ class DbConnector: self.session.delete(user) self.session.commit() + def change_password(self, name: str, new_hash: str): + user = self.get_user(name) + if not user: + raise ValueError("User does not exist") + user.hash = new_hash + self.session.commit() + def add_msg_to_room(self, room: str, msg: str, user: str): new_msg = Message(room=room, content=msg, user=user, timestamp=int(time.time())) self.session.add(new_msg) diff --git a/simple_chat_api/endpoints/auth.py b/simple_chat_api/endpoints/auth.py index 6122bd3..a712446 100644 --- a/simple_chat_api/endpoints/auth.py +++ b/simple_chat_api/endpoints/auth.py @@ -110,6 +110,22 @@ def add_user(user): response.status = 400 return dumps({"error": str(e)}) +@app.route("/changePassword", method=["POST"]) +@user_guard() +def change_password(user): + data = read_keys_from_request(["new_password", "old_password"]) + response.content_type = 'application/json' + try: + if not hash_context.verify(data["old_password"], user.hash): + response.status = 400 + return dumps({"error": "Old password is incorrect"}) + request.db_connector.change_password(user.name, hash_context.hash(data["new_password"])) + response.status = 200 + return dumps({"message": "Password changed successfully"}) + except ValueError as e: + response.status = 400 + return dumps({"error": str(e)}) + @app.route("/delete/", method=["POST"]) @admin_guard() def delete_user(_, deletion_target: str): diff --git a/simple_chat_api/main.py b/simple_chat_api/main.py index c3677c5..da885a9 100644 --- a/simple_chat_api/main.py +++ b/simple_chat_api/main.py @@ -4,6 +4,7 @@ from simple_chat_api.db_handler import DbConnector import simple_chat_api.endpoints.auth as auth import simple_chat_api.endpoints.messages as messages +import simple_chat_api.config as config import bcrypt # Needet because of: https://github.com/pyca/bcrypt/issues/684 @@ -14,7 +15,7 @@ app = Bottle() def initialize_app(): - db = DbConnector("sqlite:///./data/data.sqlite") + db = DbConnector(config.db_url) @app.hook('before_request') def attach_resources(): @@ -31,4 +32,4 @@ def main(): root_app = Bottle() root_app.mount('/api', app) - root_app.run(host='localhost', port=7000, debug=True) \ No newline at end of file + root_app.run(host='localhost', port=7000, debug=config.debug) \ No newline at end of file diff --git a/simple_chat_api/tests/auth_entpoints.py b/simple_chat_api/tests/auth_entpoints.py index b160a4a..5184ac4 100644 --- a/simple_chat_api/tests/auth_entpoints.py +++ b/simple_chat_api/tests/auth_entpoints.py @@ -5,9 +5,17 @@ import time import os import signal import requests +import unittest server_process = None API_URL = "http://localhost:7000/api" +DB_PATH = "./data/TEST.sqlite" + +users = { + "admin": "admin", + "max": "12345" + } + def setUpModule(): """Start the API server in a separate process before running tests""" def run_server(): @@ -15,7 +23,7 @@ def setUpModule(): # Needet to get python3 dir env = os.environ.copy() env.update({ - "DATABASE_URL": "sqlite:///./data/TEST.sqlite", + "DATABASE_URL": f"sqlite:///{DB_PATH}", "CREATE_OPTIONAL_DEFAULT_USER": "true" }) @@ -40,9 +48,20 @@ def tearDownModule(): if server_process: server_process.send_signal(signal.SIGTERM) server_process.wait() + os.remove(DB_PATH) + +def build_session(user: str, password: str) -> requests.Session: + session = requests.Session() + response = session.post(f"{API_URL}/user/token", json={ + "user": user, + "password": password + }) + if response.status_code != 200: + raise ValueError(f"Failed to get token for user {user}; {response.text}") + return [session, response.json()] + class TestServer(unittest.TestCase): - def test_online(self): """Test if the server is running""" response = requests.get(API_URL) @@ -50,25 +69,19 @@ class TestServer(unittest.TestCase): class TestAuthEndpoints(unittest.TestCase): + userSessions = {} def __init__(self, methodName = "runTest"): super().__init__(methodName) - self.users = { - "admin": "admin", - "max": "12345" - } - self.userSessions = {} - def test_get_token(self): + def test_1_get_token(self): """Test the /token endpoint""" - for user,password in self.users.items(): + for user,password in users.items(): with self.subTest(user=user): - self.userSessions[user] = requests.Session() - response = self.userSessions[user].post(f"{API_URL}/user/token", json={ - "user": user, - "password": password - }) - self.assertEqual(response.status_code, 200, f"Failed to get token for user {user}; {response.text}") - data = response.json() + try: + session, data = build_session(user, password) + self.userSessions[user] = session + except ValueError as e: + self.fail(str(e)) excepted = { "sub": { "user": user, @@ -76,3 +89,191 @@ class TestAuthEndpoints(unittest.TestCase): }, } self.assertEqual(data["sub"], excepted["sub"], f"Token content mismatch for user {user}") + + def test_2_get_user(self): + if self.userSessions == {}: + self.skipTest("No user sessions available. Run test_get_token first.") + for user,session in self.userSessions.items(): + with self.subTest(user=user): + response = session.get(f"{API_URL}/user") + self.assertEqual(response.status_code, 200, f"Failed to get user info for {user}; {response.text}") + data = response.json() + excepted = { + "name": user, + } + self.assertEqual(data, excepted, f"User info mismatch for {user}") + + def test_3_get_all(self): + if self.userSessions == {}: + self.skipTest("No user sessions available. Run test_get_token first.") + + for user,session in self.userSessions.items(): + with self.subTest(user=user): + response = session.get(f"{API_URL}/user/getAll") + if user == "admin": + self.assertEqual(response.status_code, 200, f"Failed to get all users for admin; {response.text}") + data = response.json() + excepted = [ + {"name": "admin", "role": "admin"}, + {"name": "max", "role": "user"} + ] + self.assertTrue(all(item in data for item in excepted), f"User list mismatch for admin") + else: + self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not access /getAll; {response.text}") + + def test_4_create_user(self): + if self.userSessions == {}: + self.skipTest("No user sessions available. Run test_get_token first.") + + new_users = [{ + "new_user": "newuser", + "new_password": "newpass", + "new_admin": False + }, + { + "new_user": "admin2", + "new_password": "adminpass", + "new_admin": True + }, + { + "new_user": "invalid/user", + "new_password": "pass", + "new_admin": False + } + ] + + for user,session in self.userSessions.items(): + with self.subTest(user=user): + if user == "admin": + for new_user in new_users: + if "invalid" in new_user["new_user"]: + response = session.post(f"{API_URL}/user/add", json=new_user) + self.assertEqual(response.status_code, 400, f"Creating user with invalid name should fail; {response.text}") + continue + response = session.post(f"{API_URL}/user/add", json=new_user) + self.assertEqual(response.status_code, 200, f"Admin failed to create new user; {response.text}") + data = response.json() + self.assertIn("created successfully", data["message"], f"Created user info mismatch") + + response = session.post(f"{API_URL}/user/add", json=new_user) + self.assertEqual(response.status_code, 400, f"Creating duplicate user should fail; {response.text}") + else: + self.assertEqual(response.status_code, 400, f"Non-admin user {user} should not create users; {response.text}") + + def test_5_delete_user(self): + if self.userSessions == {}: + self.skipTest("No user sessions available. Run test_get_token first.") + + for user,session in self.userSessions.items(): + with self.subTest(user=user): + if user == "admin": + response = session.post(f"{API_URL}/user/delete/kim") + self.assertEqual(response.status_code, 200, f"Admin failed to delete user kim; {response.text}") + data = response.json() + self.assertIn("deleted successfully", data["message"], f"Deleted user info mismatch") + + 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") + self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not delete users; {response.text}") + + def test_6_change_password(self): + if self.userSessions == {}: + self.skipTest("No user sessions available. Run test_get_token first.") + + for user,session in self.userSessions.items(): + with self.subTest(user=user): + pyload = { + "new_password": "newadminpass" + } + response = session.post(f"{API_URL}/user/changePassword/", json=pyload) + self.assertEqual(response.status_code, 200, f"Admin failed to change password; {response.text}") + + +class TestMessage(unittest.TestCase): + last_msg = None + userSessions = {} + messages = [ + {"content": "Admin here, managing things."}, + {"content": "Hello, this is max!"}, + ] + @classmethod + def setUpClass(cls): + for user, password in users.items(): + cls.userSessions[user], _ = build_session(user, password) + + def test_1_post_message(self): + if not self.userSessions: + self.skipTest("No user sessions available. Run test_get_token first.") + + + + for i, (user, session) in enumerate(self.userSessions.items()): + with self.subTest(user=user): + response = session.post(f"{API_URL}/messages/general", json=self.messages[i]) + self.assertEqual( + response.status_code, + 200, + f"User {user} failed to post message; {response.text}" + ) + + clear_json = response.json() + + msg_id, data = list(clear_json.items())[-1] + + TestMessage.last_msg = { + "message_id": msg_id, + "timestamp": data["timestamp"], + } + + self.assertIn( + self.messages[i]["content"], + data["content"], + f"Posted message response missing content for user {user}" + ) + time.sleep(1) # Ensure different timestamps + + def test_2_get_messages_since(self): + if not self.userSessions or self.last_msg is None: + self.skipTest("No user sessions available or no messages posted. Run previous tests first.") + + for user, session in self.userSessions.items(): + with self.subTest(user=user): + response = session.get(f"{API_URL}/messages/general?since={self.last_msg['timestamp']}") + self.assertEqual( + response.status_code, + 200, + f"User {user} failed to get messages since timestamp; {response.text}" + ) + + first_key = list(response.json().keys())[0] + + self.assertEqual( + first_key, + self.last_msg["message_id"], # match full message dict + f"User {user} did not receive the expected message" + ) + + def test_3_get_all_messages(self): + if not self.userSessions or self.last_msg is None: + self.skipTest("No user sessions available or no messages posted. Run previous tests first.") + + for user, session in self.userSessions.items(): + with self.subTest(user=user): + response = session.get(f"{API_URL}/messages/general") + self.assertEqual( + response.status_code, + 200, + f"User {user} failed to get all messages; {response.text}" + ) + + messages_data = response.json() + message_contents = [msg["content"] for _, msg in messages_data.items()] + + expected_contents = [msg["content"] for msg in self.messages] + for content in expected_contents: + self.assertIn(content, message_contents, f"Message with content '{content}' not found") + + self.assertEqual(len(messages_data), len(self.messages), "Number of messages doesn't match expected") + \ No newline at end of file diff --git a/simple_chat_testprotokoll.pdf b/simple_chat_testprotokoll.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5f10385aa4261f52abad6331615b285e635303b3 GIT binary patch literal 32213 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 literal 0 HcmV?d00001