mirror of
https://github.com/resf/distro-tools.git
synced 2024-12-03 10:16:26 +00:00
Add admin users actions and profile actions
This commit is contained in:
parent
76dc39fc6b
commit
13c430c2aa
@ -0,0 +1,9 @@
|
||||
load("@aspect_rules_py//py:defs.bzl", "py_library")
|
||||
|
||||
py_library(
|
||||
name = "apollo_lib",
|
||||
srcs = ["publishing_tools/apollo_tree.py"],
|
||||
imports = [".."],
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = ["@pypi_aiohttp//:pkg"],
|
||||
)
|
@ -29,21 +29,21 @@ values
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/NFV/aarch64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/NFV/aarch64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/NFV/source/tree/repodata/repomd.xml', 'NFV'),
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/CRB/aarch64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/CRB/aarch64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/CRB/source/tree/repodata/repomd.xml', 'CRB'),
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/aarch64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/aarch64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/source/tree/repodata/repomd.xml', 'ResilientStorage'),
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/SAP/x86_64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/x86_64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/source/tree/repodata/repomd.xml', 'SAP'),
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/x86_64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/x86_64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/source/tree/repodata/repomd.xml', 'SAPHANA'),
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/SAP/aarch64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/aarch64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/source/tree/repodata/repomd.xml', 'SAP'),
|
||||
(5, true, 'aarch64', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/aarch64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/aarch64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/source/tree/repodata/repomd.xml', 'SAPHANA'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/BaseOS/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/BaseOS/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/BaseOS/source/tree/repodata/repomd.xml', 'BaseOS'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/AppStream/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/AppStream/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/AppStream/source/tree/repodata/repomd.xml', 'AppStream'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/HighAvailability/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/HighAvailability/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/HighAvailability/source/tree/repodata/repomd.xml', 'HighAvailability'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/NFV/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/NFV/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/NFV/source/tree/repodata/repomd.xml', 'NFV'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/CRB/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/CRB/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/CRB/source/tree/repodata/repomd.xml', 'CRB'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/source/tree/repodata/repomd.xml', 'ResilientStorage'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/SAP/x86_64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/x86_64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/source/tree/repodata/repomd.xml', 'SAP'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/x86_64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/x86_64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/source/tree/repodata/repomd.xml', 'SAPHANA'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/SAP/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/source/tree/repodata/repomd.xml', 'SAP'),
|
||||
(6, true, 's390x', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/s390x/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/s390x/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/source/tree/repodata/repomd.xml', 'SAPHANA'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/BaseOS/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/BaseOS/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/BaseOS/source/tree/repodata/repomd.xml', 'BaseOS'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/AppStream/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/AppStream/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/AppStream/source/tree/repodata/repomd.xml', 'AppStream'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/HighAvailability/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/HighAvailability/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/HighAvailability/source/tree/repodata/repomd.xml', 'HighAvailability'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/NFV/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/NFV/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/NFV/source/tree/repodata/repomd.xml', 'NFV'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/CRB/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/CRB/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/CRB/source/tree/repodata/repomd.xml', 'CRB'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/ResilientStorage/source/tree/repodata/repomd.xml', 'ResilientStorage'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/SAP/x86_64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/x86_64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/source/tree/repodata/repomd.xml', 'SAP'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/x86_64/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/x86_64/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/source/tree/repodata/repomd.xml', 'SAPHANA');
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/SAP/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAP/source/tree/repodata/repomd.xml', 'SAP'),
|
||||
(7, true, 'ppc64le', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/ppc64le/os/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/ppc64le/debug/tree/repodata/repomd.xml', 'http://dl.rockylinux.org/pub/rocky/9/SAPHANA/source/tree/repodata/repomd.xml', 'SAPHANA');
|
||||
|
@ -6,6 +6,7 @@ py_library(
|
||||
srcs = [
|
||||
"roles.py",
|
||||
"routes/admin_index.py",
|
||||
"routes/admin_users.py",
|
||||
"routes/advisories.py",
|
||||
"routes/api_advisories.py",
|
||||
"routes/api_compat.py",
|
||||
@ -14,6 +15,7 @@ py_library(
|
||||
"routes/api_updateinfo.py",
|
||||
"routes/login.py",
|
||||
"routes/logout.py",
|
||||
"routes/profile.py",
|
||||
"routes/red_hat_advisories.py",
|
||||
"routes/statistics.py",
|
||||
"server.py",
|
||||
|
@ -1,2 +1,3 @@
|
||||
ADMIN = "admin"
|
||||
ELEVATED = "elevated"
|
||||
POSSIBLE_ROLES = [ADMIN, ELEVATED]
|
||||
|
234
apollo/server/routes/admin_users.py
Normal file
234
apollo/server/routes/admin_users.py
Normal file
@ -0,0 +1,234 @@
|
||||
import secrets
|
||||
from math import ceil
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi_pagination import Params
|
||||
from fastapi_pagination.ext.tortoise import paginate
|
||||
|
||||
from apollo.db import User
|
||||
from apollo.server import roles
|
||||
from apollo.server.utils import templates, pwd_context
|
||||
|
||||
router = APIRouter(tags=["non-api"])
|
||||
|
||||
|
||||
def validate_user(request: Request, user: User):
|
||||
if user.role not in roles.POSSIBLE_ROLES:
|
||||
return templates.TemplateResponse(
|
||||
"admin_user_new.jinja", {
|
||||
"request": request,
|
||||
"should_hide_form": True,
|
||||
"error": f"Invalid role {user.role}",
|
||||
}
|
||||
)
|
||||
|
||||
if not user.name or len(user.name) < 2:
|
||||
return templates.TemplateResponse(
|
||||
"admin_user_new.jinja", {
|
||||
"request": request,
|
||||
"should_hide_form": True,
|
||||
"error": "Name is too short",
|
||||
}
|
||||
)
|
||||
|
||||
if not user.email or len(user.email) < 3 or "@" not in user.email:
|
||||
return templates.TemplateResponse(
|
||||
"admin_user_new.jinja", {
|
||||
"request": request,
|
||||
"should_hide_form": True,
|
||||
"error": "Invalid email",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def admin_users(request: Request, params: Params = Depends()):
|
||||
params.size = 50
|
||||
users = await paginate(
|
||||
User.all().order_by("created_at"),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin_users.jinja", {
|
||||
"request": request,
|
||||
"users": users,
|
||||
"users_pages": ceil(users.total / users.size),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/new", response_class=HTMLResponse)
|
||||
async def admin_user_new(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"admin_user_new.jinja", {
|
||||
"request": request,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/new", response_class=HTMLResponse)
|
||||
async def admin_user_new_post(
|
||||
request: Request,
|
||||
name: str = Form(default=None),
|
||||
email: str = Form(default=None),
|
||||
role: str = Form(default=None),
|
||||
):
|
||||
user = User(name=name, email=email, role=role)
|
||||
validation = validate_user(request, user)
|
||||
if validation:
|
||||
return validation
|
||||
|
||||
random_password = secrets.token_urlsafe(16)
|
||||
user.password = pwd_context.hash(random_password)
|
||||
|
||||
await user.save()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin_user_new.jinja", {
|
||||
"request": request,
|
||||
"should_hide_form": True,
|
||||
"gen_password": random_password,
|
||||
"email": email,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_class=HTMLResponse)
|
||||
async def admin_user(request: Request, user_id: int):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if user is None:
|
||||
return templates.TemplateResponse(
|
||||
"error.jinja", {
|
||||
"request": request,
|
||||
"message": f"User with id {user_id} not found",
|
||||
}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin_user.jinja", {
|
||||
"request": request,
|
||||
"user": user,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{user_id}", response_class=HTMLResponse)
|
||||
async def admin_user_post(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
name: str = Form(default=None),
|
||||
email: str = Form(default=None),
|
||||
role: str = Form(default=None),
|
||||
):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if user is None:
|
||||
return templates.TemplateResponse(
|
||||
"error.jinja", {
|
||||
"request": request,
|
||||
"message": f"User with id {user_id} not found",
|
||||
}
|
||||
)
|
||||
|
||||
user.name = name
|
||||
user.email = email
|
||||
user.role = role
|
||||
|
||||
validation = validate_user(request, user)
|
||||
if validation:
|
||||
return validation
|
||||
|
||||
await user.save()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin_user.jinja", {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"title": "Successfully updated user",
|
||||
"kind": "success",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{user_id}/password", response_class=HTMLResponse)
|
||||
async def admin_user_password_post(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
new_password: str = Form(default=None),
|
||||
confirm_password: str = Form(default=None),
|
||||
):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if user is None:
|
||||
return templates.TemplateResponse(
|
||||
"error.jinja", {
|
||||
"request": request,
|
||||
"message": f"User with id {user_id} not found",
|
||||
}
|
||||
)
|
||||
|
||||
if new_password != confirm_password:
|
||||
return templates.TemplateResponse(
|
||||
"admin_user.jinja", {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"title": "Passwords do not match",
|
||||
"kind": "error",
|
||||
}
|
||||
)
|
||||
|
||||
if not new_password or len(new_password) < 8:
|
||||
return templates.TemplateResponse(
|
||||
"admin_user.jinja", {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"title": "Password is too short",
|
||||
"kind": "error",
|
||||
}
|
||||
)
|
||||
|
||||
user.password = pwd_context.hash(new_password)
|
||||
await user.save()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin_user.jinja", {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"title": "Successfully updated password",
|
||||
"kind": "success",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{user_id}/delete", response_class=HTMLResponse)
|
||||
async def admin_user_delete(request: Request, user_id: int):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if user is None:
|
||||
return templates.TemplateResponse(
|
||||
"error.jinja", {
|
||||
"request": request,
|
||||
"message": f"User with id {user_id} not found",
|
||||
}
|
||||
)
|
||||
|
||||
# Cannot delete yourself
|
||||
if user.id == request.state.user.id:
|
||||
return templates.TemplateResponse(
|
||||
"error.jinja", {
|
||||
"request": request,
|
||||
"message": "Cannot delete yourself",
|
||||
}
|
||||
)
|
||||
|
||||
# Cannot delete admins
|
||||
if user.role == "admin":
|
||||
return templates.TemplateResponse(
|
||||
"error.jinja", {
|
||||
"request": request,
|
||||
"message": "Cannot delete admin users",
|
||||
}
|
||||
)
|
||||
|
||||
await user.delete()
|
||||
|
||||
return RedirectResponse("/admin/users", status_code=302)
|
83
apollo/server/routes/profile.py
Normal file
83
apollo/server/routes/profile.py
Normal file
@ -0,0 +1,83 @@
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from apollo.server.utils import templates, pwd_context
|
||||
|
||||
router = APIRouter(tags=["non-api"])
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def profile(request: Request):
|
||||
return templates.TemplateResponse("profile.jinja", {
|
||||
"request": request,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/", response_class=HTMLResponse)
|
||||
async def profile_post(
|
||||
request: Request,
|
||||
current_password: str = Form(default=None),
|
||||
new_password: str = Form(default=None),
|
||||
confirm_password: str = Form(default=None),
|
||||
):
|
||||
if not current_password or not new_password or not confirm_password:
|
||||
return templates.TemplateResponse(
|
||||
"profile.jinja", {
|
||||
"request": request,
|
||||
"notification":
|
||||
{
|
||||
"kind": "error",
|
||||
"title": "Please fill out all fields",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
actual_current_password = request.state.user.password
|
||||
if not pwd_context.verify(current_password, actual_current_password):
|
||||
return templates.TemplateResponse(
|
||||
"profile.jinja", {
|
||||
"request": request,
|
||||
"notification":
|
||||
{
|
||||
"kind": "error",
|
||||
"title": "Current password is incorrect",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if new_password != confirm_password:
|
||||
return templates.TemplateResponse(
|
||||
"profile.jinja", {
|
||||
"request": request,
|
||||
"notification":
|
||||
{
|
||||
"kind": "error",
|
||||
"title": "New passwords do not match",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if len(new_password) < 8:
|
||||
return templates.TemplateResponse(
|
||||
"profile.jinja", {
|
||||
"request": request,
|
||||
"notification":
|
||||
{
|
||||
"kind": "error",
|
||||
"title": "New password must be at least 8 characters",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
request.state.user.password = pwd_context.hash(new_password)
|
||||
await request.state.user.save()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"profile.jinja", {
|
||||
"request": request,
|
||||
"notification": {
|
||||
"kind": "success",
|
||||
"title": "Password updated",
|
||||
}
|
||||
}
|
||||
)
|
@ -14,7 +14,9 @@ from apollo.server.routes.advisories import router as advisories_router
|
||||
from apollo.server.routes.statistics import router as statistics_router
|
||||
from apollo.server.routes.login import router as login_router
|
||||
from apollo.server.routes.logout import router as logout_router
|
||||
from apollo.server.routes.profile import router as profile_router
|
||||
from apollo.server.routes.admin_index import router as admin_index_router
|
||||
from apollo.server.routes.admin_users import router as admin_users_router
|
||||
from apollo.server.routes.red_hat_advisories import router as red_hat_advisories_router
|
||||
from apollo.server.routes.api_advisories import router as api_advisories_router
|
||||
from apollo.server.routes.api_updateinfo import router as api_updateinfo_router
|
||||
@ -22,7 +24,7 @@ from apollo.server.routes.api_red_hat import router as api_red_hat_router
|
||||
from apollo.server.routes.api_compat import router as api_compat_router
|
||||
from apollo.server.routes.api_osv import router as api_osv_router
|
||||
from apollo.server.settings import SECRET_KEY, SettingsMiddleware, get_setting
|
||||
from apollo.server.utils import admin_user_scheme, templates
|
||||
from apollo.server.utils import admin_user_scheme, user_scheme, templates
|
||||
from apollo.db import Settings
|
||||
|
||||
from common.info import Info
|
||||
@ -33,10 +35,14 @@ from common.fastapi import StaticFilesSym, RenderErrorTemplateException
|
||||
app = FastAPI()
|
||||
|
||||
app.mount(
|
||||
"/static", StaticFilesSym(directory="apollo/server/static"), name="static"
|
||||
"/static",
|
||||
StaticFilesSym(directory="apollo/server/static"),
|
||||
name="static",
|
||||
)
|
||||
app.mount(
|
||||
"/assets", StaticFilesSym(directory="apollo/server/assets"), name="assets"
|
||||
"/assets",
|
||||
StaticFilesSym(directory="apollo/server/assets"),
|
||||
name="assets",
|
||||
)
|
||||
|
||||
app.add_middleware(SettingsMiddleware)
|
||||
@ -45,11 +51,21 @@ app.include_router(advisories_router)
|
||||
app.include_router(statistics_router, prefix="/statistics")
|
||||
app.include_router(login_router, prefix="/login")
|
||||
app.include_router(logout_router, prefix="/logout")
|
||||
app.include_router(
|
||||
profile_router,
|
||||
prefix="/profile",
|
||||
dependencies=[Depends(user_scheme)],
|
||||
)
|
||||
app.include_router(
|
||||
admin_index_router,
|
||||
prefix="/admin",
|
||||
dependencies=[Depends(admin_user_scheme)]
|
||||
)
|
||||
app.include_router(
|
||||
admin_users_router,
|
||||
prefix="/admin/users",
|
||||
dependencies=[Depends(admin_user_scheme)]
|
||||
)
|
||||
app.include_router(red_hat_advisories_router, prefix="/red_hat")
|
||||
app.include_router(api_advisories_router, prefix="/api/v3/advisories")
|
||||
app.include_router(api_updateinfo_router, prefix="/api/v3/updateinfo")
|
||||
|
@ -9,22 +9,31 @@ import '@carbon/web-components/es/components/button';
|
||||
import '@carbon/web-components/es/components/notification';
|
||||
import '@carbon/web-components/es/components/tag';
|
||||
import '@carbon/web-components/es/components/list';
|
||||
import '@carbon/web-components/es/components/select';
|
||||
import '@carbon/web-components/es/components/modal';
|
||||
|
||||
function fixForm() {
|
||||
const buttons = document.querySelectorAll('bx-btn');
|
||||
buttons.forEach((button) => {
|
||||
if (!button.getAttribute('form_id')) {
|
||||
return;
|
||||
let form: any = null;
|
||||
if (button.getAttribute('form_id')) {
|
||||
form = document.querySelector('form#' + button.getAttribute('form_id'));
|
||||
}
|
||||
|
||||
const form: any = document.querySelector(
|
||||
'form#' + button.getAttribute('form_id')
|
||||
);
|
||||
if (form) {
|
||||
button.addEventListener('click', () => {
|
||||
// If it has "open_modal" attribute, open the modal
|
||||
const modalId = button.getAttribute('open_modal');
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
if (form) {
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (modalId) {
|
||||
const modal: any = document.querySelector('bx-modal#' + modalId);
|
||||
if (modal) {
|
||||
modal.open = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Also do the same for bx-input and enter key
|
||||
@ -62,6 +71,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// Add "active" if location has prefix, e.g. /admin/ -> /admin
|
||||
// For / only we need to check if the location is exactly /
|
||||
const pathname = window.location.pathname;
|
||||
let currentActive: any = null;
|
||||
document.querySelectorAll('bx-side-nav-link').forEach((el) => {
|
||||
const href = el.getAttribute('href');
|
||||
if (href === '/') {
|
||||
@ -70,6 +80,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
} else if (pathname.startsWith(href || '')) {
|
||||
el.setAttribute('active', '');
|
||||
if (currentActive) {
|
||||
currentActive.removeAttribute('active');
|
||||
}
|
||||
currentActive = el;
|
||||
}
|
||||
});
|
||||
|
||||
@ -90,5 +104,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
}
|
||||
|
||||
// For all bx-select, elements, using the "set_value" attribute, set the value
|
||||
// of the select element to the value of the attribute.
|
||||
document.querySelectorAll('bx-select').forEach((el: any) => {
|
||||
const setValue = el.getAttribute('set_value');
|
||||
if (setValue) {
|
||||
el.value = setValue;
|
||||
}
|
||||
});
|
||||
|
||||
fixForm();
|
||||
});
|
||||
|
@ -16,6 +16,19 @@
|
||||
.bx--container {
|
||||
margin-left: 16rem
|
||||
}
|
||||
|
||||
bx-inline-notification {
|
||||
padding-left: 16rem;
|
||||
}
|
||||
|
||||
#apollo-notification-wrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.top-notification {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bx--with-rail .bx--container {
|
||||
@ -43,9 +56,9 @@
|
||||
{% block content %}
|
||||
<bx-side-nav aria-label="Side navigation" expanded>
|
||||
<bx-side-nav-items>
|
||||
<bx-side-nav-link href="/admin">General</bx-side-nav-link>
|
||||
<bx-side-nav-link href="/admin/users">Users</bx-side-nav-link>
|
||||
<bx-side-nav-link href="/admin/oidc">OIDC</bx-side-nav-link>
|
||||
<bx-side-nav-link href="/admin/">General</bx-side-nav-link>
|
||||
<bx-side-nav-link href="/admin/users/">Users</bx-side-nav-link>
|
||||
<bx-side-nav-link href="/admin/oidc/">OIDC</bx-side-nav-link>
|
||||
</bx-side-nav-items>
|
||||
</bx-side-nav>
|
||||
|
||||
|
60
apollo/server/templates/admin_user.jinja
Normal file
60
apollo/server/templates/admin_user.jinja
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends "admin_layout.jinja" %}
|
||||
|
||||
{% block admin_content %}
|
||||
<h2 style="display:block;margin-bottom:1rem;">Update user</h2>
|
||||
|
||||
<form id="edit_user_form" action="" method="POST">
|
||||
<bx-form-item>
|
||||
<bx-input required name="name" value="{{ user.name }}" form_id="edit_user_form">
|
||||
<span slot="label-text">Name</span>
|
||||
</bx-input>
|
||||
<bx-input required name="email" type="email" value="{{ user.email }}" form_id="edit_user_form">
|
||||
<span slot="label-text">Email</span>
|
||||
</bx-input>
|
||||
<bx-select label-text="Role" name="role" set_value="{{ user.role }}" form_id="edit_user_form">
|
||||
<bx-select-item value="admin">Admin</bx-select-item>
|
||||
<bx-select-item value="elevated">Elevated</bx-select-item>
|
||||
</bx-select>
|
||||
<bx-btn type="submit" style="margin-top:1rem;margin-bottom:1rem;display:block" form_id="edit_user_form">
|
||||
Update user
|
||||
</bx-btn>
|
||||
</bx-form-item>
|
||||
</form>
|
||||
|
||||
<h2 style="display:block;margin-top:2rem;margin-bottom:1rem;">Change password</h2>
|
||||
<form id="change_password_form" action="/admin/users/{{ user.id }}/password" method="POST">
|
||||
<bx-form-item>
|
||||
<bx-input required name="new_password" type="password" form_id="change_password_form">
|
||||
<span slot="label-text">New password</span>
|
||||
</bx-input>
|
||||
<bx-input required name="confirm_password" type="password" form_id="change_password_form">
|
||||
<span slot="label-text">Confirm new password</span>
|
||||
</bx-input>
|
||||
<bx-btn type="submit" style="margin-top:1rem;margin-bottom:1rem;display:block" form_id="change_password_form">
|
||||
Change password
|
||||
</bx-btn>
|
||||
</bx-form-item>
|
||||
</form>
|
||||
|
||||
<h2 style="display:block;margin-top:2rem;margin-bottom:1rem;">Danger zone</h2>
|
||||
|
||||
<bx-modal id="delete-user-modal">
|
||||
<bx-modal-header>
|
||||
<bx-modal-close-button></bx-modal-close-button>
|
||||
<bx-modal-heading>Delete user</bx-modal-heading>
|
||||
</bx-modal-header>
|
||||
<bx-modal-body>
|
||||
<p>Are you sure you want to delete {{ user.name }}?</p>
|
||||
</bx-modal-body>
|
||||
<bx-modal-footer>
|
||||
<bx-modal-footer-button kind="secondary" data-modal-close>Cancel</bx-modal-footer-button>
|
||||
<bx-modal-footer-button kind="danger">Delete</bx-modal-footer-button>
|
||||
</bx-modal-footer>
|
||||
</bx-modal>
|
||||
|
||||
<form id="delete_user_form" action="/admin/users/{{ user.id }}/delete" method="POST">
|
||||
</form>
|
||||
<bx-btn kind="danger" open_modal="delete-user-modal">
|
||||
Delete user
|
||||
</bx-btn>
|
||||
{% endblock %}
|
42
apollo/server/templates/admin_user_new.jinja
Normal file
42
apollo/server/templates/admin_user_new.jinja
Normal file
@ -0,0 +1,42 @@
|
||||
{% extends "admin_layout.jinja" %}
|
||||
|
||||
{% block admin_content %}
|
||||
<h2 style="display:block;margin-bottom:1rem;">Create new user</h2>
|
||||
|
||||
{% if not should_hide_form %}
|
||||
<form id="new_user_form" action="" method="POST">
|
||||
<bx-form-item>
|
||||
<bx-input required name="name" form_id="new_user_form">
|
||||
<span slot="label-text">Name</span>
|
||||
</bx-input>
|
||||
<bx-input required name="email" type="email" form_id="new_user_form">
|
||||
<span slot="label-text">Email</span>
|
||||
</bx-input>
|
||||
<bx-select label-text="Role" name="role" form_id="new_user_form">
|
||||
<bx-select-item value=""></bx-select-item>
|
||||
<bx-select-item value="admin">Admin</bx-select-item>
|
||||
<bx-select-item value="elevated">Elevated</bx-select-item>
|
||||
</bx-select>
|
||||
<p>A random password will be generated on creation</p>
|
||||
<bx-btn type="submit" style="margin-top:1rem;margin-bottom:1rem;display:block" form_id="new_user_form">
|
||||
Create new user
|
||||
</bx-btn>
|
||||
</bx-form-item>
|
||||
</form>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<div style="margin-top:3rem;">
|
||||
<h5 style="color:#fa4d56;border-left:0;margin:0;">
|
||||
{{ error }}
|
||||
</h5>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if gen_password %}
|
||||
<div style="margin-top:3rem;">
|
||||
<h5 style="color:#198038;border-left:0;margin:0;">
|
||||
User {{ email }} successfully created with password {{ gen_password }}
|
||||
</h5>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
39
apollo/server/templates/admin_users.jinja
Normal file
39
apollo/server/templates/admin_users.jinja
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "admin_layout.jinja" %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h2 style="display:block;margin-bottom:1rem;">Users</h2>
|
||||
<bx-btn href="new">Create new user</bx-btn>
|
||||
</div>
|
||||
|
||||
<bx-pagination page-size="{{ users.size }}" start="{{ (users.page-1) * users.size }}" total="{{ users.total }}">
|
||||
<bx-page-sizes-select slot="page-sizes-select">
|
||||
<option value="50">50</option>
|
||||
</bx-page-sizes-select>
|
||||
<bx-pages-select value="{{ users.page - 1 }}" total="{{ users_pages }}"></bx-pages-select>
|
||||
</bx-pagination>
|
||||
<bx-data-table>
|
||||
<bx-table>
|
||||
<bx-table-head>
|
||||
<bx-table-header-row>
|
||||
<bx-table-header-cell>ID</bx-table-header-cell>
|
||||
<bx-table-header-cell>Name</bx-table-header-cell>
|
||||
<bx-table-header-cell>Created at</bx-table-header-cell>
|
||||
<bx-table-header-cell>Email</bx-table-header-cell>
|
||||
<bx-table-header-cell>Role</bx-table-header-cell>
|
||||
</bx-table-header-row>
|
||||
</bx-table-head>
|
||||
<bx-table-body>
|
||||
{% for user in users.items -%}
|
||||
<bx-table-row>
|
||||
<bx-table-cell><a href="/admin/users/{{ user.id }}">{{ user.id }}</a></bx-table-cell>
|
||||
<bx-table-cell>{{ user.name }}</bx-table-cell>
|
||||
<bx-table-cell>{{ user.created_at.date() }}</bx-table-cell>
|
||||
<bx-table-cell>{{ user.email }}</bx-table-cell>
|
||||
<bx-table-cell>{{ user.role }}</bx-table-cell>
|
||||
</bx-table-row>
|
||||
{% endfor %}
|
||||
</bx-table-body>
|
||||
</bx-table>
|
||||
</bx-data-table>
|
||||
{% endblock %}
|
@ -56,6 +56,10 @@
|
||||
.apollo-outer bx-inline-notification~#apollo-notification-wrapper>bx-inline-notification {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#apollo-notification-wrapper {
|
||||
margin: 0 3.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if notification %}
|
||||
@ -111,7 +115,7 @@
|
||||
{% include "light_icon.jinja" %}
|
||||
</bx-header-nav-item>
|
||||
{% if request.session.get("user.name") %}
|
||||
<bx-header-nav-item>{{ request.session.get("user.name") }}</bx-header-nav-item>
|
||||
<bx-header-nav-item href="/profile/">{{ request.session.get("user.name") }}</bx-header-nav-item>
|
||||
<bx-header-nav-item href="/logout/">Logout</bx-header-nav-item>
|
||||
{% else %}
|
||||
<bx-header-nav-item href="/login/">Login</bx-header-nav-item>
|
||||
@ -122,11 +126,12 @@
|
||||
<div class="apollo-outer">
|
||||
{% block outer_content %}{% endblock %}
|
||||
{% if title %}
|
||||
<bx-inline-notification kind="info" title="{{ title }}" hide-close-button>
|
||||
<bx-inline-notification kind="{% if kind %}{{ kind }}{% else %}info{% endif %}" title="{{ title }}"
|
||||
subtitle="{{ subtitle }}" hide-close-button>
|
||||
</bx-inline-notification>
|
||||
{% endif %}
|
||||
{% if notification %}
|
||||
<div id="apollo-notification-wrapper" style="margin:0 3.5rem">
|
||||
<div id="apollo-notification-wrapper">
|
||||
<bx-inline-notification class="top-notification" kind="{{ notification.get('kind', 'none') }}"
|
||||
title="{{ notification['title'] }}" subtitle="{{ notification['subtitle'] }}"
|
||||
hide-close-button></bx-inline-notification>
|
||||
|
24
apollo/server/templates/profile.jinja
Normal file
24
apollo/server/templates/profile.jinja
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "layout.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Profile</h2>
|
||||
|
||||
<div style="width:100%;max-width:450px;margin-top:1rem;">
|
||||
<form id="profile_form" action="" method="POST">
|
||||
<bx-form-item>
|
||||
<bx-input required name="current_password" type="password" form_id="profile_form">
|
||||
<span slot="label-text">Current password</span>
|
||||
</bx-input>
|
||||
<bx-input required name="new_password" type="password" form_id="profile_form">
|
||||
<span slot="label-text">New password</span>
|
||||
</bx-input>
|
||||
<bx-input required name="confirm_password" type="password" form_id="profile_form">
|
||||
<span slot="label-text">Confirm new password</span>
|
||||
</bx-input>
|
||||
<bx-btn type="submit" style="margin-left:auto;display:block" form_id="profile_form">
|
||||
Update password
|
||||
</bx-btn>
|
||||
</bx-form-item>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
@ -42,7 +42,9 @@ async def user_scheme(request: Request, raise_exc=True) -> User:
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return await User.get(id=user_id)
|
||||
user = await User.get(id=user_id)
|
||||
request.state.user = user
|
||||
return user
|
||||
|
||||
|
||||
async def is_admin_user(request: Request) -> bool:
|
||||
|
@ -0,0 +1,12 @@
|
||||
load("@rules_python//python:defs.bzl", "py_test")
|
||||
|
||||
py_test(
|
||||
name = "test_apollo_tree",
|
||||
srcs = ["test_apollo_tree.py"],
|
||||
imports = ["../../.."],
|
||||
deps = [
|
||||
"//apollo:apollo_lib",
|
||||
"//common:common_lib",
|
||||
"@pypi_pytest//:pkg",
|
||||
],
|
||||
)
|
@ -15,6 +15,7 @@ py_library(
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"@pypi_fastapi//:pkg",
|
||||
"@pypi_fastapi_pagination//:pkg",
|
||||
"@pypi_pydantic//:pkg",
|
||||
"@pypi_temporalio//:pkg",
|
||||
"@pypi_tortoise_orm//:pkg",
|
||||
|
@ -8,7 +8,7 @@ image:
|
||||
repository: ghcr.io/resf/apollo-rpmworker
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "15c4ca5"
|
||||
tag: "d157846"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
|
@ -8,7 +8,7 @@ image:
|
||||
repository: ghcr.io/resf/apollo-server
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "b498d3b"
|
||||
tag: "bb5159c"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
@ -71,24 +71,17 @@ ingress:
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
resources:
|
||||
requests:
|
||||
cpu: 300m
|
||||
memory: 256Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
minReplicas: 5
|
||||
maxReplicas: 20
|
||||
targetCPUUtilizationPercentage: 60
|
||||
targetMemoryUtilizationPercentage: 50
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user