containerisation and source restructure

This commit is contained in:
Kyattsukuro 2025-08-21 10:53:50 +02:00
parent 4f01a74602
commit 45eea8a484
28 changed files with 178 additions and 57 deletions

3
.vscode/launch.json vendored
View File

@ -15,7 +15,8 @@
"name": "Python Debugger: Main",
"type": "debugpy",
"request": "launch",
"program": "${cwd}/backend_py/main.py",
"cwd": "${workspaceFolder}",
"module": "simple_chat_api",
"console": "integratedTerminal",
"justMyCode": true
},

BIN
data/data.sqlite Normal file

Binary file not shown.

Binary file not shown.

58
docker/Dockerfile Normal file
View File

@ -0,0 +1,58 @@
FROM debian:trixie-slim AS base
ENV DEBIAN_FRONTEND=noninteractive
# Install sys utils, dependencies and nginx
RUN apt-get update && \
apt-get install -yq nginx npm python3 python3-pip
# Copy files
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN mkdir /simple-chat
COPY . /simple-chat
# Install app dependencies
RUN pip3 install -r /simple-chat/simple_chat_api/requirements.txt --break-system-packages --ignore-installed
RUN npm install --prefix /simple-chat/frontend || true
# Building
# || true to allow failure
RUN npm run build --prefix /simple-chat/frontend || true
# Setup nginx
RUN rm /etc/nginx/nginx.conf
RUN ln -s /simple-chat/docker/nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p /var/nginx
# Dev stage
FROM base AS dev
ARG ROOT_PASSWD
ENV PIP_BREAK_SYSTEM_PACKAGES=1
ENV DEV=true
# additional dependencies for dev
RUN apt-get update && \
apt-get install -y build-essential git openssh-server
# SSH configuration
RUN if [ -n "$ROOT_PASSWD" ]; then echo "root:$ROOT_PASSWD" | chpasswd; else passwd -d root; fi && \
echo "PermitRootLogin yes" > /etc/ssh/sshd_config && \
echo "ListenAddress 0.0.0.0" >> /etc/ssh/sshd_config && \
echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config && \
echo "PermitEmptyPasswords no" >> /etc/ssh/sshd_config && \
echo "Subsystem sftp /usr/lib/ssh/sftp-server" >> /etc/ssh/sshd_config
RUN ssh-keygen -A && \
# echo "sshd: ALL" > /etc/hosts.deny && \
echo "sshd: ALL" > /etc/hosts.allow
# Run entrypoint for dev
CMD ["bash", "/entrypoint.sh"]
# Production stage
FROM base AS production
# Run entrypoint
CMD ["bash", "/entrypoint.sh"]

BIN
docker/default.sqlite Normal file

Binary file not shown.

15
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
if [ "$DEV" ]; then
echo "Running in development mode"
echo "Entering endless loop"
while true; do
sleep 20
done
fi
echo "Running in production mode"
echo "Reachable via port 8080"
nginx -g "daemon off;" &
cd /simple-chat || exit 1
python3 -m simple_chat_api

46
docker/nginx.conf Normal file
View File

@ -0,0 +1,46 @@
# Linked to /etc/nginx/nginx.conf
worker_processes 1;
error_log /var/nginx/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
client_max_body_size 2000M;
gzip on;
server {
listen 8080;
listen [::]:8080;
server_name simple-chat.default.local;
root /simple-chat/frontend/dist;
index index.html;
# Proxy specific API endpoints
location /api {
proxy_pass http://localhost:7000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Handle all other routes - serve SPA for all other paths
location / {
try_files $uri $uri/ /index.html;
}
}
}

View File

@ -1220,13 +1220,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
"integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.15.0",
"@eslint/core": "^0.15.2",
"levn": "^0.4.1"
},
"engines": {
@ -1234,9 +1234,9 @@
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
"integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {

View File

@ -5,10 +5,10 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"build-typecheck": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"build-only": "vite build",
"build": "vite build",
"type-check": "vue-tsc --build",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix",

View File

@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<h3>Hello World</h3>
</template>

View File

@ -1,6 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {})

View File

@ -25,6 +25,15 @@ export const messageHandler = (room: string = 'general') => {
},
}).then(async (response) => {
let data = await getJsonOrError(response)
let latest_new_msg = Math.max(...Object.keys(data).map(Number), -Infinity)
if (latest_new_msg === -Infinity) {
throw new Error('Invalid server response')
}
if (messages.value[latest_new_msg]) {
// If the latest message is already in the cache, return the cached messages
return messages.value
}
messages.value = { ...messages.value, ...(data as Record<number, Message>) }
return messages.value
})
@ -49,7 +58,7 @@ export const messageHandler = (room: string = 'general') => {
sendMessage,
messages,
lastMsg: () => {
const highestKey = Math.max(...Object.keys(messages).map(Number))
const highestKey: number = Math.max(...Object.keys(messages.value).map(Number), -Infinity)
if (highestKey === -Infinity) {
return undefined
}
@ -59,7 +68,7 @@ export const messageHandler = (room: string = 'general') => {
const keys = Object.keys(messages.value).map(Number)
const index = keys.indexOf(Number(id))
if (index > 0) {
console.log('Previous message found:', messages.value[keys[index - 1]])
//console.log('Previous message found:', messages.value[keys[index - 1]])
return messages.value[keys[index - 1]]
}
return undefined

View File

@ -42,7 +42,6 @@ onMounted(() => {
document.addEventListener('keypress', handleKeyPress)
msg.requestMessages()
msgTimer.value = setInterval(() => {
console.log('Polling for messages...')
msg.requestMessages(msg.lastMsg()?.timestamp)
}, 3000) // 3s
})
@ -57,7 +56,7 @@ onBeforeUnmount(() => {
<template>
<main>
<div class="grid grid-cols-1 grid-rows-10 boxed h-[75vh] max-w-128">
<div class="grid grid-cols-1 grid-rows-10 boxed h-[75vh] max-w-128 min-w-100">
<h1 class="border-b-2">Chat</h1>
<div class="row-span-8 flex flex-col overflow-scroll" ref="scrollContainer">
<div v-if="msg" v-for="(message, id) in msg.messages.value" :key="message.id" class="mb-4">

View File

@ -3,6 +3,7 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {

View File

@ -5,10 +5,10 @@
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",

View File

@ -19,7 +19,7 @@ export default defineConfig({
port: 8081,
host: '0.0.0.0',
proxy: {
'/api': 'http://localhost:8080',
'/api': 'http://localhost:7000',
},
},
})

View File

@ -1,14 +0,0 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)

View File

@ -0,0 +1 @@
from simple_chat_api.main import main

View File

@ -0,0 +1,4 @@
from simple_chat_api.main import main
if __name__ == "__main__":
main()

View File

@ -3,6 +3,7 @@ from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy import Column, Integer, String
from dataclasses import dataclass
import time
import regex
Base = declarative_base()
@ -34,8 +35,8 @@ class DbConnector:
def _create_defaults(self):
try:
self.add_user(name="admin", hash="$2b$12$IcUr5w7pIFaXaGVFP5yVV.b.sIYjDbETR3l2PKgWO4nkrHU.1HmFa", role="admin")
except ValueError:
print("Default admin user already exists")
except ValueError as e:
print(f"Default admin user already exists: {e}")
def get_user(self, name: str) -> User | dict[User] | None:
if not name:
@ -45,6 +46,8 @@ class DbConnector:
def add_user(self, name: str, hash: str, role: str = "user"):
if self.get_user(name):
raise ValueError("User already exists")
if regex.match(r'^[^<>:;,?"*|/]+$', name) is None:
raise ValueError("Invalid username")
new_user = User()
new_user.name = name
new_user.hash = hash

View File

@ -1,8 +1,6 @@
from functools import wraps
from bottle import Bottle, request, response
from config import JWT_SECRET
from config import hash_context
from simple_chat_api.config import JWT_SECRET, hash_context
import jwt
from bottle import request, response
@ -11,7 +9,7 @@ from bottle import request, response
import time
from json import dumps, loads
from utils import read_keys_from_request
from simple_chat_api.utils import read_keys_from_request
app = Bottle()

View File

@ -2,9 +2,9 @@
from bottle import Bottle, request, response
from json import dumps, loads
from db_handler import User, Message
from endpoints.auth import user_guard
from utils import read_keys_from_request
from simple_chat_api.db_handler import User, Message
from simple_chat_api.endpoints.auth import user_guard
from simple_chat_api.utils import read_keys_from_request
app = Bottle()
def serialize_message(messages: list[Message]) -> str:

View File

@ -1,9 +1,9 @@
from bottle import request, Bottle
from db_handler import DbConnector
from simple_chat_api.db_handler import DbConnector
import endpoints.auth as auth
import endpoints.messages as messages
import simple_chat_api.endpoints.auth as auth
import simple_chat_api.endpoints.messages as messages
import bcrypt
# Needet because of: https://github.com/pyca/bcrypt/issues/684
@ -14,7 +14,7 @@ app = Bottle()
def initialize_app():
db = DbConnector("sqlite:///./data/db.sqlite")
db = DbConnector("sqlite:///./data/data.sqlite")
@app.hook('before_request')
def attach_resources():
@ -23,7 +23,7 @@ def initialize_app():
return app
if __name__ == "__main__":
def main():
initialize_app()
app.mount('/user', auth.app)
@ -31,4 +31,4 @@ if __name__ == "__main__":
root_app = Bottle()
root_app.mount('/api', app)
root_app.run(host='localhost', port=8080, debug=True)
root_app.run(host='localhost', port=7000, debug=True)

View File

@ -0,0 +1,11 @@
bcrypt==4.3.0
bottle==0.13.4
cffi==1.17.1
cryptography==45.0.4
greenlet==3.2.3
passlib==1.7.4
pycparser==2.22
PyJWT==2.10.1
regex==2025.7.34
SQLAlchemy==2.0.41
typing_extensions==4.14.0