auth via DB
This commit is contained in:
+93
@@ -0,0 +1,93 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import HTTPException, Request, status
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
from app.db.database import get_connection
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, password_hash: str) -> bool:
|
||||||
|
if not password_hash:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return pwd_context.verify(plain_password, password_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_username(username: str) -> dict[str, Any] | None:
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
row = con.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, username, display_name, email, password_hash, role, is_active, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE username = ?
|
||||||
|
""",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_id(user_id: int) -> dict[str, Any] | None:
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
row = con.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, username, display_name, email, password_hash, role, is_active, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(username: str, password: str) -> dict[str, Any] | None:
|
||||||
|
user = get_user_by_username(username.strip())
|
||||||
|
|
||||||
|
if not user or not user.get("is_active"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not verify_password(password, user.get("password_hash") or ""):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def current_user(request: Request) -> dict[str, Any] | None:
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = int(user_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
request.session.pop("user_id", None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
user = get_user_by_id(user_id)
|
||||||
|
if not user or not user.get("is_active"):
|
||||||
|
request.session.pop("user_id", None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def require_user(request: Request) -> dict[str, Any]:
|
||||||
|
user = current_user(request)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
headers={"Location": "/portal/login"},
|
||||||
|
)
|
||||||
+12
-1
@@ -2,17 +2,28 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from .routes import apps, backups, deployments, health
|
from .config import read_env_value
|
||||||
|
from .routes import apps, auth, backups, deployments, health
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
app = FastAPI(title="AppFactory Portal")
|
app = FastAPI(title="AppFactory Portal")
|
||||||
|
session_secret = read_env_value("PORTAL_SESSION_SECRET", "") or "dev-only-appfactory-session-secret"
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
SessionMiddleware,
|
||||||
|
secret_key=session_secret,
|
||||||
|
same_site="lax",
|
||||||
|
https_only=False,
|
||||||
|
)
|
||||||
|
|
||||||
static_dir = Path(__file__).parent / "static"
|
static_dir = Path(__file__).parent / "static"
|
||||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||||
|
|
||||||
app.include_router(health.router)
|
app.include_router(health.router)
|
||||||
|
app.include_router(auth.router)
|
||||||
app.include_router(apps.router)
|
app.include_router(apps.router)
|
||||||
app.include_router(backups.router)
|
app.include_router(backups.router)
|
||||||
app.include_router(deployments.router)
|
app.include_router(deployments.router)
|
||||||
|
|||||||
+22
-6
@@ -1,8 +1,9 @@
|
|||||||
import html
|
import html
|
||||||
|
|
||||||
from fastapi import APIRouter, Form
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
from ..auth import require_user
|
||||||
from ..config import (
|
from ..config import (
|
||||||
DEFAULT_APPFACTORY_HOST,
|
DEFAULT_APPFACTORY_HOST,
|
||||||
DEFAULT_GITEA_ORG,
|
DEFAULT_GITEA_ORG,
|
||||||
@@ -20,7 +21,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
def index():
|
def index(request: Request, user=Depends(require_user)):
|
||||||
apps = get_apps()
|
apps = get_apps()
|
||||||
|
|
||||||
gitea_url = read_env_value("GITEA_URL", "")
|
gitea_url = read_env_value("GITEA_URL", "")
|
||||||
@@ -147,11 +148,12 @@ def index():
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/new-app", response_class=HTMLResponse)
|
@router.get("/new-app", response_class=HTMLResponse)
|
||||||
def new_app_form():
|
def new_app_form(request: Request, user=Depends(require_user)):
|
||||||
return page(
|
return page(
|
||||||
"Nová aplikace",
|
"Nová aplikace",
|
||||||
"""
|
"""
|
||||||
@@ -183,11 +185,17 @@ def new_app_form():
|
|||||||
<p><a href="/portal">← Zpět</a></p>
|
<p><a href="/portal">← Zpět</a></p>
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/new-app", response_class=HTMLResponse)
|
@router.post("/new-app", response_class=HTMLResponse)
|
||||||
def create_app(app_id: str = Form(...), app_name: str = Form(...), template: str = Form(...)):
|
def create_app(
|
||||||
|
app_id: str = Form(...),
|
||||||
|
app_name: str = Form(...),
|
||||||
|
template: str = Form(...),
|
||||||
|
user=Depends(require_user),
|
||||||
|
):
|
||||||
if template != "python-fastapi":
|
if template != "python-fastapi":
|
||||||
return HTMLResponse("Nepodporovaná šablona", status_code=400)
|
return HTMLResponse("Nepodporovaná šablona", status_code=400)
|
||||||
|
|
||||||
@@ -207,11 +215,12 @@ def create_app(app_id: str = Form(...), app_name: str = Form(...), template: str
|
|||||||
],
|
],
|
||||||
extra_link=f"/apps/{html.escape(app_id)}/docs",
|
extra_link=f"/apps/{html.escape(app_id)}/docs",
|
||||||
extra_label="Otevřít Swagger",
|
extra_label="Otevřít Swagger",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete-app", response_class=HTMLResponse)
|
@router.post("/delete-app", response_class=HTMLResponse)
|
||||||
def delete_app(app_id: str = Form(...)):
|
def delete_app(app_id: str = Form(...), user=Depends(require_user)):
|
||||||
result = run_command([DELETE_APP_SCRIPT, app_id])
|
result = run_command([DELETE_APP_SCRIPT, app_id])
|
||||||
status = "OK" if result.returncode == 0 else "FAILED"
|
status = "OK" if result.returncode == 0 else "FAILED"
|
||||||
|
|
||||||
@@ -219,11 +228,17 @@ def delete_app(app_id: str = Form(...)):
|
|||||||
title=f"Smazání aplikace: {status}",
|
title=f"Smazání aplikace: {status}",
|
||||||
back_url="/portal",
|
back_url="/portal",
|
||||||
sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
|
sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update-resources", response_class=HTMLResponse)
|
@router.post("/update-resources", response_class=HTMLResponse)
|
||||||
def update_resources(app_id: str = Form(...), memory: str = Form(""), cpus: str = Form("")):
|
def update_resources(
|
||||||
|
app_id: str = Form(...),
|
||||||
|
memory: str = Form(""),
|
||||||
|
cpus: str = Form(""),
|
||||||
|
user=Depends(require_user),
|
||||||
|
):
|
||||||
memory = memory.strip()
|
memory = memory.strip()
|
||||||
cpus = cpus.strip()
|
cpus = cpus.strip()
|
||||||
|
|
||||||
@@ -250,4 +265,5 @@ def update_resources(app_id: str = Form(...), memory: str = Form(""), cpus: str
|
|||||||
("Výstup nasazení", deploy_result.stdout),
|
("Výstup nasazení", deploy_result.stdout),
|
||||||
("Chyba nasazení", deploy_result.stderr),
|
("Chyba nasazení", deploy_result.stderr),
|
||||||
],
|
],
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import html
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
|
from app.auth import authenticate_user, current_user
|
||||||
|
from app.templates.layout import page
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/login", response_class=HTMLResponse)
|
||||||
|
def login_form(request: Request):
|
||||||
|
if current_user(request):
|
||||||
|
return RedirectResponse(url="/portal", status_code=303)
|
||||||
|
|
||||||
|
return _render_login()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_class=HTMLResponse)
|
||||||
|
def login(request: Request, username: str = Form(...), password: str = Form(...)):
|
||||||
|
user = authenticate_user(username, password)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return _render_login("Neplatné přihlašovací údaje.")
|
||||||
|
|
||||||
|
request.session.clear()
|
||||||
|
request.session["user_id"] = user["id"]
|
||||||
|
|
||||||
|
return RedirectResponse(url="/portal", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
def logout(request: Request):
|
||||||
|
request.session.clear()
|
||||||
|
return RedirectResponse(url="/portal/login", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_login(error: str | None = None) -> str:
|
||||||
|
error_html = ""
|
||||||
|
if error:
|
||||||
|
error_html = f'<p class="alert alert-danger">{html.escape(error)}</p>'
|
||||||
|
|
||||||
|
return page(
|
||||||
|
"Přihlášení",
|
||||||
|
f"""
|
||||||
|
<div class="auth-card card">
|
||||||
|
<h2>Přihlášení do portálu</h2>
|
||||||
|
<p class="muted">Přihlaste se interním účtem AppFactory.</p>
|
||||||
|
{error_html}
|
||||||
|
|
||||||
|
<form method="post" action="/portal/login">
|
||||||
|
<p>
|
||||||
|
<label>Uživatelské jméno</label><br>
|
||||||
|
<input name="username" autocomplete="username" required autofocus>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>Heslo</label><br>
|
||||||
|
<input type="password" name="password" autocomplete="current-password" required>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="submit">Přihlásit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import html
|
import html
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Form
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
|
from ..auth import require_user
|
||||||
from ..backups import is_backup_path, list_backups
|
from ..backups import is_backup_path, list_backups
|
||||||
from ..config import BACKUP_SCRIPT
|
from ..config import BACKUP_SCRIPT
|
||||||
from ..shell import run_command
|
from ..shell import run_command
|
||||||
@@ -13,7 +14,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/backups", response_class=HTMLResponse)
|
@router.get("/backups", response_class=HTMLResponse)
|
||||||
def backups_page():
|
def backups_page(request: Request, user=Depends(require_user)):
|
||||||
rows = ""
|
rows = ""
|
||||||
|
|
||||||
for path in list_backups():
|
for path in list_backups():
|
||||||
@@ -86,11 +87,12 @@ def backups_page():
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/backups/create", response_class=HTMLResponse)
|
@router.post("/backups/create", response_class=HTMLResponse)
|
||||||
def create_backup():
|
def create_backup(user=Depends(require_user)):
|
||||||
result = run_command([BACKUP_SCRIPT])
|
result = run_command([BACKUP_SCRIPT])
|
||||||
status = "OK" if result.returncode == 0 else "FAILED"
|
status = "OK" if result.returncode == 0 else "FAILED"
|
||||||
|
|
||||||
@@ -98,11 +100,12 @@ def create_backup():
|
|||||||
title=f"Vytvoření zálohy: {status}",
|
title=f"Vytvoření zálohy: {status}",
|
||||||
back_url="/portal/backups",
|
back_url="/portal/backups",
|
||||||
sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
|
sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/backups/delete")
|
@router.post("/backups/delete")
|
||||||
def delete_backup(backup_path: str = Form(...)):
|
def delete_backup(backup_path: str = Form(...), user=Depends(require_user)):
|
||||||
target = Path(backup_path)
|
target = Path(backup_path)
|
||||||
|
|
||||||
if not is_backup_path(target):
|
if not is_backup_path(target):
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import html
|
import html
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
from app.auth import require_user
|
||||||
from app.db.apps import get_deployment, get_deployments
|
from app.db.apps import get_deployment, get_deployments
|
||||||
from app.templates.layout import page
|
from app.templates.layout import page
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ def render_status_pill(status: str | None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/deployments", response_class=HTMLResponse)
|
@router.get("/deployments", response_class=HTMLResponse)
|
||||||
async def deployments_page():
|
async def deployments_page(request: Request, user=Depends(require_user)):
|
||||||
deployments = get_deployments()
|
deployments = get_deployments()
|
||||||
|
|
||||||
rows = ""
|
rows = ""
|
||||||
@@ -85,12 +86,15 @@ async def deployments_page():
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/deployments/{deployment_id}", response_class=HTMLResponse)
|
@router.get("/deployments/{deployment_id}", response_class=HTMLResponse)
|
||||||
async def deployment_detail_page(
|
async def deployment_detail_page(
|
||||||
deployment_id: int,
|
deployment_id: int,
|
||||||
|
request: Request,
|
||||||
|
user=Depends(require_user),
|
||||||
):
|
):
|
||||||
deployment = get_deployment(deployment_id)
|
deployment = get_deployment(deployment_id)
|
||||||
|
|
||||||
@@ -139,4 +143,5 @@ async def deployment_detail_page(
|
|||||||
<pre>{stderr}</pre>
|
<pre>{stderr}</pre>
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from app.auth import hash_password
|
||||||
|
from app.db.database import get_connection
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Použití: python -m app.scripts.set_admin_password <password>")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
password_hash = hash_password(sys.argv[1])
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
row = con.execute("SELECT id FROM users WHERE username = ?", ("admin",)).fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?,
|
||||||
|
is_active = 1,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE username = ?
|
||||||
|
""",
|
||||||
|
(password_hash, "admin"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (
|
||||||
|
username,
|
||||||
|
display_name,
|
||||||
|
email,
|
||||||
|
password_hash,
|
||||||
|
role,
|
||||||
|
is_active,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
""",
|
||||||
|
("admin", "Admin", "", password_hash, "admin"),
|
||||||
|
)
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
print("Heslo administrátora bylo nastaveno.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -39,6 +39,7 @@ header {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
border-bottom: 5px solid var(--primary);
|
border-bottom: 5px solid var(--primary);
|
||||||
box-shadow: 0 10px 26px rgba(30, 81, 107, 0.16);
|
box-shadow: 0 10px 26px rgba(30, 81, 107, 0.16);
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,43 @@ nav a {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu .muted {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 48px auto 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: var(--danger-bg);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
max-width: 1500px;
|
max-width: 1500px;
|
||||||
|
|||||||
+29
-8
@@ -3,7 +3,31 @@ import html
|
|||||||
from ..config import PORTAL_PREFIX
|
from ..config import PORTAL_PREFIX
|
||||||
|
|
||||||
|
|
||||||
def page(title: str, body: str) -> str:
|
def page(title: str, body: str, user=None) -> str:
|
||||||
|
nav = ""
|
||||||
|
user_panel = ""
|
||||||
|
|
||||||
|
if user:
|
||||||
|
username = html.escape(user.get("username", ""))
|
||||||
|
display_name = html.escape(user.get("display_name") or user.get("username", ""))
|
||||||
|
nav = """
|
||||||
|
<nav>
|
||||||
|
<a href="/portal">Aplikace</a>
|
||||||
|
<a href="/portal/new-app">Nová aplikace</a>
|
||||||
|
<a href="/portal/deployments">Nasazení</a>
|
||||||
|
<a href="/portal/backups">Zálohy</a>
|
||||||
|
</nav>
|
||||||
|
"""
|
||||||
|
user_panel = f"""
|
||||||
|
<div class="user-menu">
|
||||||
|
<span>{display_name}</span>
|
||||||
|
<span class="muted">@{username}</span>
|
||||||
|
<form method="post" action="/portal/logout">
|
||||||
|
<button type="submit" class="btn-secondary">Odhlásit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -17,12 +41,8 @@ def page(title: str, body: str) -> str:
|
|||||||
<img src="{PORTAL_PREFIX}/static/csbot-logo.svg" alt="CSBOT">
|
<img src="{PORTAL_PREFIX}/static/csbot-logo.svg" alt="CSBOT">
|
||||||
<span>AppFactory</span>
|
<span>AppFactory</span>
|
||||||
</a>
|
</a>
|
||||||
<nav>
|
{nav}
|
||||||
<a href="/portal">Aplikace</a>
|
{user_panel}
|
||||||
<a href="/portal/new-app">Nová aplikace</a>
|
|
||||||
<a href="/portal/deployments">Nasazení</a>
|
|
||||||
<a href="/portal/backups">Zálohy</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{body}
|
{body}
|
||||||
@@ -32,7 +52,7 @@ def page(title: str, body: str) -> str:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_result(title, back_url, sections, extra_link=None, extra_label=None):
|
def render_result(title, back_url, sections, extra_link=None, extra_label=None, user=None):
|
||||||
rendered_sections = ""
|
rendered_sections = ""
|
||||||
|
|
||||||
for section_title, content in sections:
|
for section_title, content in sections:
|
||||||
@@ -57,4 +77,5 @@ def render_result(title, back_url, sections, extra_link=None, extra_label=None):
|
|||||||
{rendered_sections}
|
{rendered_sections}
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ fastapi
|
|||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
python-multipart
|
python-multipart
|
||||||
jinja2
|
jinja2
|
||||||
|
passlib[bcrypt]
|
||||||
|
bcrypt<4.1
|
||||||
|
itsdangerous
|
||||||
|
|||||||
Reference in New Issue
Block a user