From bff37eab52e41a612d8e01b3af7ebb639a21f71f Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Sun, 3 Aug 2025 21:24:12 +0200 Subject: [PATCH] added rudermentery comunication --- .vscode/launch.json | 10 +- backend_py/__pycache__/auth.cpython-312.pyc | Bin 0 -> 6370 bytes backend_py/__pycache__/config.cpython-312.pyc | Bin 0 -> 343 bytes .../__pycache__/db_handler.cpython-312.pyc | Bin 2578 -> 4424 bytes backend_py/__pycache__/main.cpython-312.pyc | Bin 0 -> 1722 bytes .../__pycache__/messages.cpython-312.pyc | Bin 0 -> 2493 bytes backend_py/__pycache__/utils.cpython-312.pyc | Bin 0 -> 996 bytes backend_py/auth.py | 117 +++++++++++++++++ backend_py/config.py | 5 + backend_py/db_handler.py | 37 +++++- backend_py/main.py | 119 ++---------------- backend_py/messages.py | 37 ++++++ backend_py/utils.py | 17 +++ data/db.sqlite | Bin 12288 -> 16384 bytes frontend/src/composable/auth.ts | 35 +----- frontend/src/composable/message.ts | 57 +++++++++ frontend/src/composable/utils.ts | 8 ++ frontend/src/main.ts | 2 +- frontend/src/router/index.ts | 7 +- frontend/src/views/Chat.vue | 47 ++++++- frontend/src/views/User.vue | 64 ++++++++++ frontend/vite.config.ts | 7 ++ shell.nix | 4 + 23 files changed, 424 insertions(+), 149 deletions(-) create mode 100644 backend_py/__pycache__/auth.cpython-312.pyc create mode 100644 backend_py/__pycache__/config.cpython-312.pyc create mode 100644 backend_py/__pycache__/main.cpython-312.pyc create mode 100644 backend_py/__pycache__/messages.cpython-312.pyc create mode 100644 backend_py/__pycache__/utils.cpython-312.pyc create mode 100644 backend_py/auth.py create mode 100644 backend_py/config.py create mode 100644 backend_py/messages.py create mode 100644 backend_py/utils.py create mode 100644 frontend/src/composable/message.ts create mode 100644 frontend/src/composable/utils.ts create mode 100644 frontend/src/views/User.vue diff --git a/.vscode/launch.json b/.vscode/launch.json index 96d5521..f19048b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,9 +8,17 @@ "name": "Python Debugger: Current File", "type": "debugpy", "request": "launch", - "program": "${cwd}/backend_py/main.py", + "program": "${file}", "console": "integratedTerminal" }, + { + "name": "Python Debugger: Main", + "type": "debugpy", + "request": "launch", + "program": "${cwd}/backend_py/main.py", + "console": "integratedTerminal", + "justMyCode": true + }, { "type": "node", "request": "launch", diff --git a/backend_py/__pycache__/auth.cpython-312.pyc b/backend_py/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d7a195957154208975225b54f5d4b551178aa8b GIT binary patch literal 6370 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 L-bXUL@+JR2lI2HY literal 0 HcmV?d00001 diff --git a/backend_py/__pycache__/config.cpython-312.pyc b/backend_py/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c8d4e893c37b0eda11ce321c48e08bb2d227fcb GIT binary patch literal 343 zcmX@j%ge<81c|fzGv)y4#~=<2Fhd!iHGqui3@HpLj5!Rsj8Tk?3@J=eOiT=^j490N zjB8j{GeT5GF;}u`vb_XxG#PL4I2TnGlsM<-m84dbR2jLc`5L7rscMB7rQ~Hq6;_nT zl@yy5=9d)Wl(@x~lngQgXcl{Maz<)yYO$Xt%Pszb#Ny(d%p|>Jh|xtLci!Ui3J-}7 zc6APN4Y|dWkyxA&50zfY@EPQwUq<>F`MIh3#XurIFIC?;KLyCi%q_@CP0mOx(N9WD z&Q8rsi7%+s2Wm{qOxG)@yv1RYo1apelWJGQ12h8U%VHTI@qw91Vp9MB literal 0 HcmV?d00001 diff --git a/backend_py/__pycache__/db_handler.cpython-312.pyc b/backend_py/__pycache__/db_handler.cpython-312.pyc index ed90643b052f980e473078bc7b42a91a7507d478..5f795fe21612d4fa4ed0fb7ac9b59a6de2c8540f 100644 GIT binary patch literal 4424 zcmc&%T}&L;6~41GvpdT!>>4&;XTe!8NxF5suE7{t#%>dXvDHA+Vq<$-X*BE%urRZW z?+hhNH!6vE067wlQWYNhKqO1Wva2|1rTVcCeQ1L`lZhxUyaORfCWbMEZS z!iuVW>y`H0d(S=hXU=!dckg~yTU$e*y!Pv*@tYDMf5nbl#ByWz=g?RtDp5I|7+jj; zu~6w005UT7>6jifo1qO;mjkc<+FIM|YodW~BpFeW@TMMO(OL!rtOE zNb@S}9h#z9L!kwEQY#E5gSI5gImOUq*{+dgBd<>D*bd9`t!YIskJQStrHtsB)1`jc zkOG{<7Eogg0@^nwl!m5H5`ZD1D;1E=Fv(&Sk=dX15`Zw5c7Co00Kc7(`?Cb8sK*y zt%W1lwaT=W&*mVQ3@v93Cd0H24<11gMR5oP^KSrJwlAxe9ZmCj!wzKf5KB4B77$K5 zXk}psOEISC3pkHCp$S_@T!HJEXF;GFkAfS6kvoCatB+!v!3%S5R2*3syEA+{H`ia$ zL>uqSd>DE6inA&%xX-l+KbGA&5PMy#@uNP~HxGHM^7AAus)EL=qUKlqv+zlyf*OFa zfO?4OgD_SDGyH03UQA1B82YvFlbEg!EQ4x2(?+x!X9ZX*q=oT3Fe?hPY772kgB|J} zxtP!8w2YO9OoO1Brh2BF%3weS0u8hwcK1J69`9DnFv-X zpj#vpd-p3FxNCF7M=T6%Rt~m(;GM)JF*uR8JIuj2sVc4|M+zkA8-xTGMzT3IDN+1b z+wTyTExdsg1p#R($USyVna>nmr9$US%(Rh48ct7++3NO_z)^16Ol9X7D{WAVh zd{a8H-PHB7;#%zE_`UdMQ`bgQH<*UErSO*2Qj%I8OE0lF%y{^~vUqNW=m<2HNe~g( zI}TbbDyIU20&k)%6!g?FVtH7#6I#a+%jF893Kh(5g6#}>o4-kBxTH8}i;8MwbF)3| z9V6|hI@+&fuFK$)=ckKH2PUj}+ z4BCJq24c3g*Ku`%t)9T#PAEEDIBFrGy`41y1KyStRh3!V*9$I>i@HjmCuP2csPYD<7eiA;kUf=b3_{_nl(*x zg9j(xFJR6xf&_JSnJ9y^S6@j%7mx`g#i~-Vns7O3k42H2{01T3d@lhjCT;=^k$c== z(oeq$L$-KpTBFWwWcx?6x}{MDnk|lIsc8Y7qraJIe@2XH76WO2Mz~ryitF<2NW)Tb zvA7j!Ek#=Iwr@mU-HN12k<lGWC>Ed@#rZOhBBP%()Quz1?m0TstULu{W{!_uXl~PyAU#g*i;pF zZw`6lpgH)Asw(>;Nf=_+pg_?@^e76JGYM!V1MFdB^FW!lfR85@3iKRW^??Z#N>7yRUkAl;#Zw@3y^o zA8d0TSGxuRm?0Wl4u25dinf)aZFjpjqQ|$Q9i?c;Z@Pcm^GVO|PM13SH=>sp#O+XY zJ=DA%JF@)d2XEeO+G_16wRSwzO0DNMVm%9e+mYB(VX<)gC+pIS40U)0uvll`Tvzqi z!sbnj(i3n1_BL>ck?|z58SO>!9T2vl!;8al(`8h?fT9rv`)h&E28tpMKXV?$JiH$S zF|lIq5~!Yr1+jZ4%!!w|m7yJi_3G8N-j9B`%i3T2CGjW@K()rR>S5}w6~y>j=MF3%e`r4H{iMK}f9PHR{*6xuo}t<~ARa3jv1`m*x}s;s zHKX9Xg6k@ZRGu0XL1oY0%3m$#Z&t|rW3HJL>6E^X;#CyLR*qYk3--IkmU^83&KKw) z*x}7<;#7DpaoiWA{Y!H8OOh&))EA`nFC_F;plKs;WKMYI7r7S~PCp}1J*x|Fi3Rl; Rf$E>JSGf370)l9 zyLt;4umCRqt61^x07GCWmF)ipuoKr^@eI&VbfPxGv+Nqv)A;a&yeELudNOr*LeP3O z!A6|6MTcVrAQPuXOvt1HKfA(-$xIrDO$M1U96pvYa5$dObKy&Rd=!rvv8)v~F2xf1 zn32MyGi%v$EJkpa#9|Y5JZEQh>t%wbpTQJ(-_TwhxHoirsCa7r>J#+b^VIXO@M7Ph z+Pf(CuJJcSwHhwO3!^32y@#lbv{6Dg6Qq&3j{xy8LpE@ztpcB-8Kg%Azk3Zq@d)wJ zQDF-_KtH>>ktJ#Jv&-lLY$Of*!e$=K+ZG6r4fKP!4K~qVVpQU~aY(NtAN)jBd2mBOMmJcP8e08Jv> znX$$3JSLX&2wNOU5;JGJbi=TvcqW}r<}|@6*H#QXqUTe&nDbGpbu$67gBILDy^XnM zKLgfYwQ;sEQ&>@hOKR}J_P1(iMeSHpJC@b($2R}`*~R_i%WdRF-+RS7+c(pn5 z?ZxxU%I-gme@XGbQ-Z5*;O#1RTBTpy3x_lqt<+64o}0JcmPV4P946(b?Pg4+W{3FO z;;-^H7^Dp~kxAZSO(soTp_xMq1KVZh2-(TEJ2*H~-OIgA9R7~Goo_jC%Ish;4ORst z28!kyVCK7m#I36e6i>q9nN`5;{J_KG_s^|6KJhuqTp+5|(JS85`-03b=X13G&u7b# zbjHZ1a4$K?;`n&YgY=iTCG<@K^2WFB)+w|ux#5u;2i5^IKYaD@B>WCII#vG$2l)cB diff --git a/backend_py/__pycache__/main.cpython-312.pyc b/backend_py/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86eedc1a1351ad09e028ffa30d6ddcd3a7d33d5d GIT binary patch literal 1722 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 str | None: + token = request.get_cookie("oauth2") + if not token: + return None + + try: + decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"], options={"verify_sub": False}) + curent_time = time.time() + if decoded.get("exp", float("inf")) + decoded.get("iat", float("inf")) < curent_time: + return None + return decoded["sub"]["user"] + except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as e: + print(f"Token error: {e}") + return None + + +@app.route("/", method=["GET"]) +def get_user(): + username = username_by_token(request) + if not username: + response.status = 401 + response.content_type = 'application/json' + return dumps({"error": "Unauthorized"}) + + return dumps({"name": username}) + +@app.route("/add", method=["POST"]) +@admin_guard() +def add_user(user): + data = read_keys_from_request(["new_user", "new_password", "new_admin"]) + role = "admin" if data.get("new_admin", False) else "user" + response.content_type = 'application/json' + try: + request.db_connector.add_user(data["new_user"], hash_context.hash(data["new_password"]), role) + response.status = 200 + return dumps({"message": "User created successfully"}) + except ValueError as e: + response.status = 400 + return dumps({"error": str(e)}) + + diff --git a/backend_py/config.py b/backend_py/config.py new file mode 100644 index 0000000..b0a8547 --- /dev/null +++ b/backend_py/config.py @@ -0,0 +1,5 @@ +from passlib.context import CryptContext + + +JWT_SECRET = "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq" +hash_context = CryptContext(schemes=["bcrypt"]) \ No newline at end of file diff --git a/backend_py/db_handler.py b/backend_py/db_handler.py index def35b5..1b5f502 100644 --- a/backend_py/db_handler.py +++ b/backend_py/db_handler.py @@ -2,7 +2,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy import Column, Integer, String from dataclasses import dataclass - +import time Base = declarative_base() @@ -14,6 +14,16 @@ class User(Base): hash = Column(String) role = Column(String) +class Message(Base): + __tablename__ = 'messages' + + id = Column(Integer, primary_key=True, autoincrement=True) + room = Column(String) + content = Column(String) + user = Column(String) + timestamp = Column(Integer) + + class DbConnector: def __init__(self, db_url: str): self.engine = create_engine(db_url) @@ -22,8 +32,10 @@ class DbConnector: self._create_defaults() def _create_defaults(self): - self.add_user(name="admin", hash="$2b$12$IcUr5w7pIFaXaGVFP5yVV.b.sIYjDbETR3l2PKgWO4nkrHU.1HmFa", role="admin") - + try: + self.add_user(name="admin", hash="$2b$12$IcUr5w7pIFaXaGVFP5yVV.b.sIYjDbETR3l2PKgWO4nkrHU.1HmFa", role="admin") + except ValueError: + print("Default admin user already exists") def get_user(self, name: str) -> User | None: return self.session.query(User).filter(User.name==name).first() @@ -31,6 +43,23 @@ class DbConnector: def add_user(self, name: str, hash: str, role: str = "user"): if self.get_user(name): raise ValueError("User already exists") - new_user = User(name, hash, role) + new_user = User() + new_user.name = name + new_user.hash = hash + new_user.role = role self.session.add(new_user) 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) + self.session.commit() + self.session.refresh(new_msg) # Refresh to get the auto-incremented ID + return new_msg + + + def get_messages_from_room(self, room: str, since: int| None = None) -> list[Message]: + query = self.session.query(Message).filter(Message.room == room) + if since is not None: + query = query.filter(Message.timestamp >= since) + return query.all() diff --git a/backend_py/main.py b/backend_py/main.py index 0b6f5ba..68d131b 100644 --- a/backend_py/main.py +++ b/backend_py/main.py @@ -1,121 +1,18 @@ -from bottle import response, request, Bottle -from json import dumps, loads -import time +from bottle import request, Bottle from db_handler import DbConnector -from functools import wraps -import jwt -from passlib.context import CryptContext +import auth +import messages + import bcrypt # Needet because of: https://github.com/pyca/bcrypt/issues/684 if not hasattr(bcrypt, '__about__'): bcrypt.__about__ = type('about', (object,), {'__version__': bcrypt.__version__}) -def user_guard(reyection_msg: str = "Requires authentication", allow_anonymous: bool = False): - def user_guard_decorator(fn: callable): - @wraps(fn) - async def wrapper(*args, **kwargs): - username = username_by_token(request) - if not username and not allow_anonymous: - response.status = 401 - return dumps({"error": reyection_msg}) - if username: - user = request.db_connector.get_user(username) - return await fn(user, *args, **kwargs) - return wrapper - return user_guard_decorator - -def admin_guard(reyection_msg: str = "Requires admin priveledges"): - def admin_guard_decorator(fn: callable): - @wraps(fn) - @user_guard(reyection_msg) - async def wrapper(user, *args, **kwargs): - if user.role != "admin": - response.status = 401 - return dumps({"error": reyection_msg}) - return await fn(user, *args, **kwargs) - return wrapper - return admin_guard_decorator - -def read_keys_from_request(keys: None|dict = None): - result = {} - try: - body = request.body.read() - data = loads(body.decode('utf-8')) - except: - return result - if keys: - missing_keys = [key for key in keys if key not in data] - if missing_keys: - raise ValueError(f"Missing required keys: {', '.join(missing_keys)}") - data = {key: data[key] for key in keys} - return data - - -hash_context = CryptContext(schemes=["bcrypt"]) - app = Bottle() -@app.route("/auth/token", method=["POST"]) -def token(): - body = request.body.read() - try: - data = loads(body.decode('utf-8')) - username = data.get("user") - password = data.get("password") - except: - response.status = 400 - return dumps({"error": "Invalid JSON format"}) - - user = request.db_connector.get_user(username) - if not user or not hash_context.verify(password, user.hash): - response.status = 401 - return dumps({"error": "Invalid username or password"}) - - jwt_content = { - "sub": { - "user": user.name, - }, - "iat": time.time(), - "exp": 60 * 20 - } - token = jwt.encode(jwt_content, "secret", algorithm="HS256") - response.set_cookie("oauth2", token, max_age=60*20, path='/') - response.status = 200 - return dumps(jwt_content) - -def username_by_token(request) -> str | None: - token = request.get_cookie("oauth2") - if not token: - return None - - try: - decoded = jwt.decode(token, "secret", algorithms=["HS256"]) - curent_time = time.time() - if decoded.get("exp", float("inf")) + decoded.get("iat", float("inf")) < curent_time: - return None - return decoded["sub"]["user"] - except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): - return None - -@app.route("/user", method=["GET"]) -def get_user(): - username = username_by_token(request) - if not username: - response.status = 401 - return dumps({"error": "Unauthorized"}) - - return dumps({"name": username}) - -@app.route("/user_add", method=["POST"]) -@admin_guard() -def add_user(user): - data = read_keys_from_request(["new_user", "new_password"]) - request.db_connector.add_user(data["new_user"], hash_context.genhash) - - def initialize_app(): db = DbConnector("sqlite:///./data/db.sqlite") @@ -128,4 +25,10 @@ def initialize_app(): if __name__ == "__main__": initialize_app() - app.run(host='localhost', port=8080, debug=True) \ No newline at end of file + + app.mount('/user', auth.app) + app.mount('/messages', messages.app) + + root_app = Bottle() + root_app.mount('/api', app) + root_app.run(host='localhost', port=8080, debug=True) \ No newline at end of file diff --git a/backend_py/messages.py b/backend_py/messages.py new file mode 100644 index 0000000..9fa1e1f --- /dev/null +++ b/backend_py/messages.py @@ -0,0 +1,37 @@ + +from bottle import Bottle, request, response +from json import dumps, loads + +from db_handler import User, Message +from auth import user_guard +from utils import read_keys_from_request +app = Bottle() + +def serialize_message(messages: list[Message]) -> str: + return dumps({getattr(msg, "id"): + {key: getattr(msg, key) + for key in msg.__dict__.keys() if key[0] != '_' and key != 'id'} + for msg in messages}) + +@app.route('/', method=['POST']) +@user_guard() +def recive_msg(user: User, room: str): + msg = read_keys_from_request(keys=["content"]) + if not msg: + 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('/', method=['GET']) +@user_guard() +def return_msgs(user: User, room: str): + since = request.query.get('since', None) + if since: + try: + since = int(since) + except ValueError: + response.status = 400 + return {"error": "Invalid 'since' parameter"} + messages = request.db_connector.get_messages_from_room(room, since) + return serialize_message(messages) \ No newline at end of file diff --git a/backend_py/utils.py b/backend_py/utils.py new file mode 100644 index 0000000..7c3043f --- /dev/null +++ b/backend_py/utils.py @@ -0,0 +1,17 @@ +from json import loads +from bottle import request + + +def read_keys_from_request(keys: None|dict = None): + result = {} + try: + body = request.body.read() + data = loads(body.decode('utf-8')) + except: + return result + if keys: + missing_keys = [key for key in keys if key not in data] + if missing_keys: + raise ValueError(f"Missing required keys: {', '.join(missing_keys)}") + data = {key: data[key] for key in keys} + return data \ No newline at end of file diff --git a/data/db.sqlite b/data/db.sqlite index 945c2d12deb9f5c63355d8701ae2c7a5ee0ef09c..214de2cc8cea273aeebfb728d661ad85246a11f5 100644 GIT binary patch delta 440 zcmZojXlP)ZAT21$z`(!)#4x}#QO8(VltHh_ftUXW12f-p2L3?)vwX`p7Mk+aH>xwU zi_6L~HYt`QCgr5&rWO|`rl%IeXcp%nSH}=ng%C$4A6EsKsDcI;XJ(3mr(cMxyK9hw zpMQvgU#O3djsh2FQGR}|LYQNavxj34h@YIFSCX1nf-F>8oLU6qn}F1mWC9H?Nz5&P znFtaL2=erG42o3nc8yff$V}1X(yU@+7Z(?2Yz&^9#qZBjl%JD2c`bi92NQoP1OI0J z)Xjndk^G@POsvw4tfIyFxvBYisVYWEDuzZXE=hio$(jB+$zD-W|eg%$cu6j-` zenuW)QKeB)?!m<#zA0{IiTb8qM#1it;U-?isUT;1GqQ>|vWidMFYhRrn39{B#{~>j uCjQq9{NMRsZx&Rz&u`1kEX@eDjfsOx?Ze|_;qzD?+ diff --git a/frontend/src/composable/auth.ts b/frontend/src/composable/auth.ts index 2a435f3..9b0a69c 100644 --- a/frontend/src/composable/auth.ts +++ b/frontend/src/composable/auth.ts @@ -1,37 +1,11 @@ import { API_URL } from '@/main' +import { getJsonOrError } from '@/composable/utils' export interface User { user: string + role: string } -function getJsonOrError(response: Response) { - if (!response.ok) { - return response.json().then((data) => { - throw new Error(data.error || 'Request failed') - }) - } - return response.json() -} - -/* -const getUser = async (token: string | undefined) => { - if (!token) { - throw new Error('No token provided') - } - return fetch(`${API_URL}/user`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }).then(async (response) => { - let data = await getJsonOrError(response) - return { - name: data.name, - } as User - }) -} -*/ - const readToken = () => { const token = document.cookie.split('; ').find((row) => row.startsWith('oauth2=')) if (token) { @@ -57,7 +31,7 @@ export const getSessionFromJWT = (): User => { } export const requestToken = async (user: string, password: string): Promise => { - return fetch(`${API_URL}/auth/token`, { + return fetch(`${API_URL}/user/token`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -70,7 +44,8 @@ export const requestToken = async (user: string, password: string): Promise { let data = await getJsonOrError(response) return { - name: data.sub.user, + user: data.sub.user as string, + role: data.sub.role as string, } as User }) } diff --git a/frontend/src/composable/message.ts b/frontend/src/composable/message.ts new file mode 100644 index 0000000..942b9f3 --- /dev/null +++ b/frontend/src/composable/message.ts @@ -0,0 +1,57 @@ +import { getJsonOrError } from '@/composable/utils' +import { API_URL } from '@/main' +import type { Ref } from 'vue' +import { ref } from 'vue' + +interface Message { + id: number + user: string + content: string + timestamp: string +} + +export const messageHandler = (room: string = 'general') => { + let messages: Ref> = ref({}) + + function requestMessages(since: string | undefined): Promise> { + let query = since ? `?since=${since}` : '' + + return fetch(`${API_URL}/messages/${room}${query}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }).then(async (response) => { + let data = await getJsonOrError(response) + messages.value = { ...messages.value, ...(data as Record) } + return messages.value + }) + } + + function sendMessage(message: string): Promise> { + return fetch(`${API_URL}/messages/${room}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content: message }), + }).then(async (response) => { + let data = await getJsonOrError(response) + messages.value = { ...messages.value, ...(data as Record) } + return messages.value + }) + } + + return { + requestMessages, + sendMessage, + messages, + lastMsg: () => { + const highestKey = Math.max(...Object.keys(messages).map(Number)) + if (highestKey === -Infinity) { + return undefined + } + return messages.value[highestKey] + }, + } +} diff --git a/frontend/src/composable/utils.ts b/frontend/src/composable/utils.ts new file mode 100644 index 0000000..bd2dca1 --- /dev/null +++ b/frontend/src/composable/utils.ts @@ -0,0 +1,8 @@ +export function getJsonOrError(response: Response) { + if (!response.ok) { + return response.json().then((data) => { + throw new Error(data.error || 'Request failed') + }) + } + return response.json() +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 1d21f46..3e144cc 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -6,7 +6,7 @@ import router from './router' const app = createApp(App) -export const API_URL = 'http://localhost:8080' +export const API_URL = document.location.origin + '/api' app.use(router) app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index a094090..4126e12 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -10,11 +10,16 @@ const router = createRouter({ component: HomeView, }, { - path: '/chat', + path: '/', name: 'chat', component: () => import('../views/Chat.vue'), meta: { requiresAuth: true }, }, + { + path: '/user', + name: 'user', + component: () => import('../views/User.vue'), + }, { path: '/*', name: 'any', diff --git a/frontend/src/views/Chat.vue b/frontend/src/views/Chat.vue index d08fff2..6c92695 100644 --- a/frontend/src/views/Chat.vue +++ b/frontend/src/views/Chat.vue @@ -1,15 +1,54 @@ - diff --git a/frontend/src/views/User.vue b/frontend/src/views/User.vue new file mode 100644 index 0000000..ac10c77 --- /dev/null +++ b/frontend/src/views/User.vue @@ -0,0 +1,64 @@ + + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d6b83b7..23bfba2 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -14,4 +14,11 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, + server: { + port: 8081, + host: '0.0.0.0', + proxy: { + '/api': 'http://localhost:8080', + }, + }, }) diff --git a/shell.nix b/shell.nix index 2fbd88f..7413c22 100644 --- a/shell.nix +++ b/shell.nix @@ -10,8 +10,12 @@ pkgs.mkShell { sqlite ]; + shellHook = '' + source .venv/bin/activate + ''; exitHook = '' echo "closing env.." + ''; }