Add admin users actions and profile actions

This commit is contained in:
Mustafa Gezen 2023-03-06 05:49:48 +01:00
parent 76dc39fc6b
commit 13c430c2aa
19 changed files with 601 additions and 42 deletions

View File

@ -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"],
)

View File

@ -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');

View File

@ -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",

View File

@ -1,2 +1,3 @@
ADMIN = "admin"
ELEVATED = "elevated"
POSSIBLE_ROLES = [ADMIN, ELEVATED]

View 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)

View 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",
}
}
)

View File

@ -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")

View File

@ -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();
});

View File

@ -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>

View 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 %}

View 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 %}

View 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 %}

View File

@ -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>

View 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 %}

View File

@ -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:

View File

@ -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",
],
)

View File

@ -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",

View File

@ -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: ""

View File

@ -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: {}