containerisation and source restructure
This commit is contained in:
parent
4f01a74602
commit
45eea8a484
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -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
|
||||
},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
data/data.sqlite
Normal file
BIN
data/data.sqlite
Normal file
Binary file not shown.
BIN
data/db.sqlite
BIN
data/db.sqlite
Binary file not shown.
58
docker/Dockerfile
Normal file
58
docker/Dockerfile
Normal 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
BIN
docker/default.sqlite
Normal file
Binary file not shown.
15
docker/entrypoint.sh
Normal file
15
docker/entrypoint.sh
Normal 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
46
docker/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<h3>Hello World</h3>
|
||||
</template>
|
||||
@ -1,6 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import HelloWorld from '../HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld', () => {})
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export default defineConfig({
|
||||
port: 8081,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080',
|
||||
'/api': 'http://localhost:7000',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -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)),
|
||||
},
|
||||
}),
|
||||
)
|
||||
1
simple_chat_api/__init__.py
Normal file
1
simple_chat_api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from simple_chat_api.main import main
|
||||
4
simple_chat_api/__main__.py
Normal file
4
simple_chat_api/__main__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from simple_chat_api.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -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
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
@ -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)
|
||||
11
simple_chat_api/requirements.txt
Normal file
11
simple_chat_api/requirements.txt
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user