fixes
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.13-slim
|
FROM python:3.14-slim
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
|
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
|||||||
62
backend/tests/test_routers.py
Normal file
62
backend/tests/test_routers.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_router_list_marks_global_ssh_key_usage(monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path / 'routers.db'}")
|
||||||
|
monkeypatch.setenv("DATA_DIR", str(tmp_path / 'data'))
|
||||||
|
monkeypatch.setenv("SECRET_KEY", "test-secret")
|
||||||
|
monkeypatch.setenv("DEFAULT_ADMIN_USERNAME", "admin")
|
||||||
|
monkeypatch.setenv("DEFAULT_ADMIN_PASSWORD", "admin")
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
login_response = client.post("/api/auth/login", data={"username": "admin", "password": "admin"})
|
||||||
|
token = login_response.json()["access_token"]
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
settings_response = client.put(
|
||||||
|
"/api/settings",
|
||||||
|
json={
|
||||||
|
"backup_retention_days": 7,
|
||||||
|
"log_retention_days": 7,
|
||||||
|
"export_cron": "",
|
||||||
|
"binary_cron": "",
|
||||||
|
"retention_cron": "",
|
||||||
|
"enable_auto_export": False,
|
||||||
|
"connection_test_interval_minutes": 0,
|
||||||
|
"global_ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END OPENSSH PRIVATE KEY-----",
|
||||||
|
"pushover_token": None,
|
||||||
|
"pushover_userkey": None,
|
||||||
|
"notify_failures_only": True,
|
||||||
|
"smtp_host": None,
|
||||||
|
"smtp_port": 587,
|
||||||
|
"smtp_login": None,
|
||||||
|
"smtp_password": None,
|
||||||
|
"smtp_notifications_enabled": False,
|
||||||
|
"recipient_email": None,
|
||||||
|
"clear_global_ssh_key": False
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert settings_response.status_code == 200
|
||||||
|
|
||||||
|
create_response = client.post(
|
||||||
|
"/api/routers",
|
||||||
|
json={
|
||||||
|
"name": "edge01",
|
||||||
|
"host": "10.0.0.1",
|
||||||
|
"port": 22,
|
||||||
|
"ssh_user": "admin",
|
||||||
|
"ssh_password": None,
|
||||||
|
"ssh_key": None
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert create_response.status_code == 200
|
||||||
|
|
||||||
|
list_response = client.get("/api/routers", headers=headers)
|
||||||
|
assert list_response.status_code == 200
|
||||||
|
payload = list_response.json()
|
||||||
|
assert payload[0]["uses_global_ssh_key"] is True
|
||||||
|
assert payload[0]["has_effective_ssh_key"] is True
|
||||||
@@ -1,13 +1,3 @@
|
|||||||
upstream backend_upstream {
|
|
||||||
server backend:8000;
|
|
||||||
keepalive 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
map $uri $static_file {
|
|
||||||
~*\.(?:css|js|mjs|png|jpg|jpeg|gif|svg|ico|webp|woff2?)$ 1;
|
|
||||||
default 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
@@ -18,71 +8,7 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
#gzip on;
|
|
||||||
#gzip_comp_level 5;
|
|
||||||
#gzip_min_length 1024;
|
|
||||||
#gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;
|
|
||||||
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
|
|
||||||
location ^~ /assets/ {
|
|
||||||
expires 7d;
|
|
||||||
add_header Cache-Control "public, max-age=604800, immutable" always;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /favicon.ico {
|
|
||||||
expires 7d;
|
|
||||||
add_header Cache-Control "public, max-age=604800" always;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /index.html {
|
|
||||||
add_header Cache-Control "no-store, no-cache" always;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /api/ {
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
|
||||||
add_header Pragma "no-cache" always;
|
|
||||||
proxy_pass http://backend_upstream/api/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /docs {
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
|
||||||
proxy_pass http://backend_upstream/docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /openapi.json {
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
|
||||||
proxy_pass http://backend_upstream/openapi.json;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /redoc {
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
|
||||||
proxy_pass http://backend_upstream/redoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
if ($static_file) {
|
|
||||||
add_header Cache-Control "max-age=600" always;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_header Cache-Control "no-store, no-cache" always;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export class LoginPageComponent {
|
|||||||
error = '';
|
error = '';
|
||||||
submitting = false;
|
submitting = false;
|
||||||
readonly form = this.fb.nonNullable.group({
|
readonly form = this.fb.nonNullable.group({
|
||||||
username: ['admin', Validators.required],
|
username: ['', Validators.required],
|
||||||
password: ['admin', Validators.required]
|
password: ['', Validators.required]
|
||||||
});
|
});
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ body.dark-theme .topbar {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar__lang-picker::after,
|
|
||||||
.auth-toolbar__select-wrap::after {
|
.auth-toolbar__select-wrap::after {
|
||||||
content: '\e902';
|
content: '\e902';
|
||||||
font-family: 'primeicons';
|
font-family: 'primeicons';
|
||||||
|
|||||||
@@ -2438,16 +2438,6 @@ body.dark-theme .api-connection-banner{
|
|||||||
min-width: 132px;
|
min-width: 132px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar__lang-picker::after{
|
|
||||||
content: "▾";
|
|
||||||
position: absolute;
|
|
||||||
right: 0.8rem;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
pointer-events: none;
|
|
||||||
color: var(--text-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar__lang-select option{
|
.topbar__lang-select option{
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
@@ -3377,17 +3367,6 @@ body.dark-theme .p-confirm-dialog .p-confirm-dialog-icon{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2026-04 layout fixes: auth centering and storage header spacing */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.storage-browser__header{
|
.storage-browser__header{
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
4
reverse-proxy/Dockerfile
Normal file
4
reverse-proxy/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:mainline
|
||||||
|
COPY reverse-proxy/default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
65
reverse-proxy/default.conf
Normal file
65
reverse-proxy/default.conf
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
upstream frontend_upstream {
|
||||||
|
server frontend:80;
|
||||||
|
keepalive 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream backend_upstream {
|
||||||
|
server backend:8000;
|
||||||
|
keepalive 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
map $uri $static_file {
|
||||||
|
~*\.(?:css|js|mjs|png|jpg|jpeg|gif|svg|ico|webp|woff2?)$ 1;
|
||||||
|
default 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
proxy_pass http://backend_upstream/api/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /docs {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
proxy_pass http://backend_upstream/docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /openapi.json {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
proxy_pass http://backend_upstream/openapi.json;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /redoc {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
proxy_pass http://backend_upstream/redoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend_upstream;
|
||||||
|
|
||||||
|
if ($static_file) {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_header Cache-Control "no-store, no-cache" always;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user