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",
|
||||
"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",
|
||||
|
||||
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 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()
|
||||
|
||||
@ -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)
|
||||
|
||||
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 { 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<User> => {
|
||||
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<User
|
||||
}).then(async (response) => {
|
||||
let data = await getJsonOrError(response)
|
||||
return {
|
||||
name: data.sub.user,
|
||||
user: data.sub.user as string,
|
||||
role: data.sub.role as string,
|
||||
} 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)
|
||||
|
||||
export const API_URL = 'http://localhost:8080'
|
||||
export const API_URL = document.location.origin + '/api'
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,15 +1,54 @@
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { getSessionFromJWT, type User } from '@/composable/auth'
|
||||
import { messageHandler } from '@/composable/message'
|
||||
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())
|
||||
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>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<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>
|
||||
</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)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 8081,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user