added rudermentery comunication
This commit is contained in:
parent
10ee2249ae
commit
bff37eab52
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@ -8,9 +8,17 @@
|
|||||||
"name": "Python Debugger: Current File",
|
"name": "Python Debugger: Current File",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${cwd}/backend_py/main.py",
|
"program": "${file}",
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Python Debugger: Main",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${cwd}/backend_py/main.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|||||||
BIN
backend_py/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend_py/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend_py/__pycache__/config.cpython-312.pyc
Normal file
BIN
backend_py/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend_py/__pycache__/main.cpython-312.pyc
Normal file
BIN
backend_py/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend_py/__pycache__/messages.cpython-312.pyc
Normal file
BIN
backend_py/__pycache__/messages.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend_py/__pycache__/utils.cpython-312.pyc
Normal file
BIN
backend_py/__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
117
backend_py/auth.py
Normal file
117
backend_py/auth.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from bottle import Bottle, request, response
|
||||||
|
from config import JWT_SECRET
|
||||||
|
from config import hash_context
|
||||||
|
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from bottle import request, response
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
from json import dumps, loads
|
||||||
|
|
||||||
|
from utils import read_keys_from_request
|
||||||
|
|
||||||
|
app = Bottle()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def user_guard(reyection_msg: str = "Requires authentication", allow_anonymous: bool = False):
|
||||||
|
def user_guard_decorator(fn: callable):
|
||||||
|
@wraps(fn)
|
||||||
|
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 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)
|
||||||
|
def wrapper(user, *args, **kwargs):
|
||||||
|
if user.role != "admin":
|
||||||
|
response.status = 401
|
||||||
|
return dumps({"error": reyection_msg})
|
||||||
|
return fn(user, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return admin_guard_decorator
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/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,
|
||||||
|
"role": user.role
|
||||||
|
},
|
||||||
|
"iat": time.time(),
|
||||||
|
"exp": time.time() + 60 * 20 # 20 minutes expiration
|
||||||
|
}
|
||||||
|
token = jwt.encode(jwt_content, JWT_SECRET, algorithm="HS256")
|
||||||
|
response.set_cookie("oauth2", token, max_age=60*20, path='/', samesite='lax')
|
||||||
|
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, 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)})
|
||||||
|
|
||||||
|
|
||||||
5
backend_py/config.py
Normal file
5
backend_py/config.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
|
||||||
|
JWT_SECRET = "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq"
|
||||||
|
hash_context = CryptContext(schemes=["bcrypt"])
|
||||||
@ -2,7 +2,7 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import time
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
@ -14,6 +14,16 @@ class User(Base):
|
|||||||
hash = Column(String)
|
hash = Column(String)
|
||||||
role = 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:
|
class DbConnector:
|
||||||
def __init__(self, db_url: str):
|
def __init__(self, db_url: str):
|
||||||
self.engine = create_engine(db_url)
|
self.engine = create_engine(db_url)
|
||||||
@ -22,8 +32,10 @@ class DbConnector:
|
|||||||
self._create_defaults()
|
self._create_defaults()
|
||||||
|
|
||||||
def _create_defaults(self):
|
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:
|
def get_user(self, name: str) -> User | None:
|
||||||
return self.session.query(User).filter(User.name==name).first()
|
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"):
|
def add_user(self, name: str, hash: str, role: str = "user"):
|
||||||
if self.get_user(name):
|
if self.get_user(name):
|
||||||
raise ValueError("User already exists")
|
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.add(new_user)
|
||||||
self.session.commit()
|
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()
|
||||||
|
|||||||
@ -1,121 +1,18 @@
|
|||||||
from bottle import response, request, Bottle
|
from bottle import request, Bottle
|
||||||
from json import dumps, loads
|
|
||||||
import time
|
|
||||||
|
|
||||||
from db_handler import DbConnector
|
from db_handler import DbConnector
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
import jwt
|
import auth
|
||||||
from passlib.context import CryptContext
|
import messages
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
# Needet because of: https://github.com/pyca/bcrypt/issues/684
|
# Needet because of: https://github.com/pyca/bcrypt/issues/684
|
||||||
if not hasattr(bcrypt, '__about__'):
|
if not hasattr(bcrypt, '__about__'):
|
||||||
bcrypt.__about__ = type('about', (object,), {'__version__': bcrypt.__version__})
|
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 = 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():
|
def initialize_app():
|
||||||
db = DbConnector("sqlite:///./data/db.sqlite")
|
db = DbConnector("sqlite:///./data/db.sqlite")
|
||||||
|
|
||||||
@ -128,4 +25,10 @@ def initialize_app():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
initialize_app()
|
initialize_app()
|
||||||
app.run(host='localhost', port=8080, debug=True)
|
|
||||||
|
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)
|
||||||
37
backend_py/messages.py
Normal file
37
backend_py/messages.py
Normal file
@ -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('/<room>', 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('/<room>', 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)
|
||||||
17
backend_py/utils.py
Normal file
17
backend_py/utils.py
Normal file
@ -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
|
||||||
BIN
data/db.sqlite
BIN
data/db.sqlite
Binary file not shown.
@ -1,37 +1,11 @@
|
|||||||
import { API_URL } from '@/main'
|
import { API_URL } from '@/main'
|
||||||
|
import { getJsonOrError } from '@/composable/utils'
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
user: string
|
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 readToken = () => {
|
||||||
const token = document.cookie.split('; ').find((row) => row.startsWith('oauth2='))
|
const token = document.cookie.split('; ').find((row) => row.startsWith('oauth2='))
|
||||||
if (token) {
|
if (token) {
|
||||||
@ -57,7 +31,7 @@ export const getSessionFromJWT = (): User => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const requestToken = async (user: string, password: string): Promise<User> => {
|
export const requestToken = async (user: string, password: string): Promise<User> => {
|
||||||
return fetch(`${API_URL}/auth/token`, {
|
return fetch(`${API_URL}/user/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -70,7 +44,8 @@ export const requestToken = async (user: string, password: string): Promise<User
|
|||||||
}).then(async (response) => {
|
}).then(async (response) => {
|
||||||
let data = await getJsonOrError(response)
|
let data = await getJsonOrError(response)
|
||||||
return {
|
return {
|
||||||
name: data.sub.user,
|
user: data.sub.user as string,
|
||||||
|
role: data.sub.role as string,
|
||||||
} as User
|
} as User
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
57
frontend/src/composable/message.ts
Normal file
57
frontend/src/composable/message.ts
Normal file
@ -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<Record<number, Message>> = ref({})
|
||||||
|
|
||||||
|
function requestMessages(since: string | undefined): Promise<Record<number, Message>> {
|
||||||
|
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<number, Message>) }
|
||||||
|
return messages.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(message: string): Promise<Record<number, Message>> {
|
||||||
|
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<number, Message>) }
|
||||||
|
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]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend/src/composable/utils.ts
Normal file
8
frontend/src/composable/utils.ts
Normal file
@ -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()
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ import router from './router'
|
|||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
export const API_URL = 'http://localhost:8080'
|
export const API_URL = document.location.origin + '/api'
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@ -10,11 +10,16 @@ const router = createRouter({
|
|||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/chat',
|
path: '/',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
component: () => import('../views/Chat.vue'),
|
component: () => import('../views/Chat.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: 'user',
|
||||||
|
component: () => import('../views/User.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/*',
|
path: '/*',
|
||||||
name: 'any',
|
name: 'any',
|
||||||
|
|||||||
@ -1,15 +1,54 @@
|
|||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { getSessionFromJWT, type User } from '@/composable/auth'
|
import { getSessionFromJWT, type User } from '@/composable/auth'
|
||||||
|
import { messageHandler } from '@/composable/message'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { ref, onMounted, type Ref } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'
|
||||||
|
|
||||||
const user: Ref<User> = ref(getSessionFromJWT())
|
const user: Ref<User> = ref(getSessionFromJWT())
|
||||||
console.log('User:', user.value)
|
const msg = messageHandler()
|
||||||
|
|
||||||
|
const newMessage = ref('')
|
||||||
|
|
||||||
|
const msgTimer = ref()
|
||||||
|
// Instantiate polling for messages
|
||||||
|
// ToDo: Try using a WebSocket instead
|
||||||
|
onMounted(() => {
|
||||||
|
msgTimer.value = setInterval(() => {
|
||||||
|
console.log('Polling for messages...')
|
||||||
|
msg.requestMessages(msg.lastMsg()?.timestamp)
|
||||||
|
}, 3000) // 3s
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clean up polling
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(msgTimer.value)
|
||||||
|
msgTimer.value = null
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<h1>Chat</h1>
|
<h1>Chat</h1>
|
||||||
<h3>Hello {{ user.name }}</h3>
|
<div class="max-w-128">
|
||||||
|
<div v-if="msg" v-for="message in msg.messages.value" :key="message.id" class="mb-4">
|
||||||
|
<p>
|
||||||
|
<a>{{ message.user }}</a
|
||||||
|
><a>{{ message.timestamp }}</a
|
||||||
|
>: {{ message.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="newMessage"
|
||||||
|
type="text"
|
||||||
|
placeholder="Type your message here..."
|
||||||
|
class="w-full p-2 border rounded"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="() => msg.sendMessage(newMessage)"
|
||||||
|
class="mt-2 bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
64
frontend/src/views/User.vue
Normal file
64
frontend/src/views/User.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { API_URL } from '@/main.ts'
|
||||||
|
import { type User, getSessionFromJWT } from '@/composable/auth.ts'
|
||||||
|
|
||||||
|
const user = ref<User | undefined>()
|
||||||
|
|
||||||
|
const new_user_name = ref('')
|
||||||
|
const new_user_passwd = ref('')
|
||||||
|
const new_admin = ref(false)
|
||||||
|
const msg = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
user.value = getSessionFromJWT()
|
||||||
|
})
|
||||||
|
|
||||||
|
const onNewUserCreation = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/user/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
new_user: new_user_name.value,
|
||||||
|
new_password: new_user_passwd.value,
|
||||||
|
new_admin: new_admin.value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (response.ok) {
|
||||||
|
msg.value = data.message
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'Failed to create user')
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
msg.value = `Error creating user: ${errorMessage}`
|
||||||
|
console.error('Error creating user:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="user" class="user-profile">
|
||||||
|
<h1>User Profile</h1>
|
||||||
|
<p><strong>Name:</strong> {{ user.user }}</p>
|
||||||
|
<p><strong>Role:</strong> {{ user.role }}</p>
|
||||||
|
|
||||||
|
<div v-if="user.role === 'admin'">
|
||||||
|
<h2>New user</h2>
|
||||||
|
<input v-model="new_user_name" placeholder="Username" />
|
||||||
|
<input v-model="new_user_passwd" type="password" placeholder="Password" />
|
||||||
|
<input v-model="new_admin" type="checkbox" />
|
||||||
|
<button @click="() => onNewUserCreation()">Create User</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p>No user information...</p>
|
||||||
|
</div>
|
||||||
|
<p>{{ msg }}</p>
|
||||||
|
</template>
|
||||||
@ -14,4 +14,11 @@ export default defineConfig({
|
|||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
port: 8081,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user