apeters 1d204f26b8 pre-Korves.Net
Signed-off-by: apeters <apeters@korves.net>
2025-05-21 08:05:07 +00:00

266 lines
9.3 KiB
Python

import components.objects
from components.models.objects import model_classes, uuid4
from components.utils import batch, ensure_list
from components.web.utils import *
blueprint = Blueprint("objects", __name__, url_prefix="/objects")
@blueprint.context_processor
async def load_context():
context = dict()
context["system_fields"] = {
object_type: system_fields
for object_type, system_fields in model_classes["system_fields"].items()
}
context["unique_fields"] = {
object_type: unique_fields
for object_type, unique_fields in model_classes["unique_fields"].items()
}
context["schemas"] = {
object_type: v.model_json_schema()
for object_type, v in model_classes["forms"].items()
}
return context
@blueprint.before_request
async def objects_before_request():
if "object_type" in request.view_args:
if request.view_args["object_type"] not in model_classes["types"]:
if "Hx-Request" in request.headers:
return trigger_notification(
level="error",
response_code=204,
title="Object type error",
message="Object type is unknown",
)
else:
return (f"<h1>Object type error</h1><p>Object type is unknown</p>", 409)
@blueprint.route("/<object_type>/<object_id>")
@acl("user")
@formoptions(["emailusers", "domains", "keypairs", "users"])
async def get_object(object_type: str, object_id: str):
try:
object_data = await components.objects.get(
object_id=object_id, object_type=object_type
)
if not object_data:
return (f"<h1>Object not found</h1><p>Object is unknown</p>", 404)
"""
Inject form options not provided by user permission.
A user will not be able to access detailed information about objects
with inherited permission nor will they be able to remove assignments.
"""
if object_type == "addresses":
object_domain = {
"name": object_data.details.assigned_domain.name,
"value": object_data.details.assigned_domain.id,
}
if object_domain not in request.form_options["domains"]:
object_domain["name"] = f"{object_data.details.assigned_domain.name} ⚠️"
request.form_options["domains"].append(object_domain)
for object_emailuser in object_data.details.assigned_emailusers:
if object_emailuser:
u = {
"name": object_emailuser.name,
"value": object_emailuser.id,
}
if u not in request.form_options["emailusers"]:
u["name"] = f"{object_emailuser.name} ⚠️"
request.form_options["emailusers"].append(u)
elif object_type == "domains":
object_keypair_injections = []
for attr in ["assigned_dkim_keypair", "assigned_arc_keypair"]:
object_keypair_details = getattr(object_data.details, attr)
if hasattr(object_keypair_details, "id"):
object_keypair = {
"name": object_keypair_details.name,
"value": object_keypair_details.id,
}
if (
object_keypair not in request.form_options["keypairs"]
and object_keypair not in object_keypair_injections
):
object_keypair_injections.append(object_keypair)
for keypair in object_keypair_injections:
keypair["name"] = keypair["name"] + " ⚠️"
request.form_options["keypairs"].append(keypair)
except ValidationError as e:
return validation_error(e.errors())
except ValueError as e:
name, message = e.args
return validation_error([{"loc": [name], "msg": message}])
return await render_or_json(
"objects/object.html", request.headers, object=object_data
)
@blueprint.route("/<object_type>")
@blueprint.route("/<object_type>/search", methods=["POST"])
@acl("user")
@formoptions(["domains"])
async def get_objects(object_type: str):
try:
(
search_model,
page,
page_size,
sort_attr,
sort_reverse,
filters,
) = table_search_helper(
request.form_parsed, object_type, default_sort_attr="name"
)
except ValidationError as e:
return validation_error(e.errors())
if request.method == "POST":
try:
match_any = {
"key_name": search_model.q,
"domain": search_model.q,
"local_part": search_model.q,
"username": search_model.q,
"assigned_domain": search_model.q,
}
match_all = (
{"assigned_users": [session["id"]]}
if not "system" in session["acl"]
else {}
)
matched_objects = await components.objects.search(
object_type=object_type,
match_any=match_any,
fully_resolve=True,
match_all=filters | match_all,
)
except ValidationError as e:
return validation_error(e.errors())
object_pages = [
m
for m in batch(
sorted(
matched_objects,
key=lambda x: getattr(x, sort_attr),
reverse=sort_reverse,
),
page_size,
)
]
try:
object_pages[page - 1]
except IndexError:
page = len(object_pages)
objects = object_pages[page - 1] if page else object_pages
return await render_template(
"objects/includes/objects/table_body.html",
data={
"objects": objects,
"page_size": page_size,
"page": page,
"pages": len(object_pages),
"elements": len(matched_objects),
},
)
else:
return await render_template(
"objects/objects.html", data={"object_type": object_type}
)
@blueprint.route("/<object_type>", methods=["POST"])
@acl("user")
async def create_object(object_type: str):
try:
async with ClusterLock(object_type, current_app):
object_id = await components.objects.create(
object_type=object_type, data=request.form_parsed
)
except ValidationError as e:
return validation_error(e.errors())
except ValueError as e:
name, message = e.args
return validation_error([{"loc": [name], "msg": message}])
return trigger_notification(
level="success",
response_code=204,
title="Object created",
message=f"Object {object_id} created",
)
@blueprint.route("/<object_type>/delete", methods=["POST"])
@blueprint.route("/<object_type>/<object_id>", methods=["DELETE"])
@acl("user")
async def delete_object(object_type: str, object_id: str | None = None):
if request.method == "POST":
object_id = request.form_parsed.get("id")
try:
object_ids = ensure_list(object_id)
async with ClusterLock(object_type, current_app):
deleted_objects = await components.objects.delete(
object_id=object_ids, object_type=object_type
)
except ValidationError as e:
return validation_error(e.errors())
except ValueError as e:
name, message = e.args
return validation_error([{"loc": [name], "msg": message}])
return trigger_notification(
level="success",
response_code=204,
title="Object removed",
message=f"{len(deleted_objects)} object{'s' if len(deleted_objects) > 1 else ''} removed",
)
@blueprint.route("/<object_type>/patch", methods=["POST"])
@blueprint.route("/<object_type>/<object_id>", methods=["PATCH"])
@acl("user")
async def patch_object(object_type: str, object_id: str | None = None):
if request.method == "POST":
object_id = request.form_parsed.get("id")
try:
async with ClusterLock(object_type, current_app):
patched_objects = await components.objects.patch(
object_id=object_id, object_type=object_type, data=request.form_parsed
)
except ValidationError as e:
return validation_error(e.errors())
except ValueError as e:
name, message = e.args
return validation_error([{"loc": [name], "msg": message}])
await ws_htmx(
"_user",
"beforeend",
f'<div hx-trigger="load once" hx-sync="#object-details:drop" hx-target="#object-details" hx-select="#object-details" hx-select-oob="#object-name" hx-swap="outerHTML" hx-get="/objects/{object_type}/{object_id}"></div>',
f"/objects/{object_type}/{object_id}",
exclude_self=True,
)
return trigger_notification(
level="success" if len(patched_objects) > 0 else "warning",
response_code=204,
title="Patch completed",
message=f"{len(patched_objects)} object{'s' if (len(patched_objects) > 1 or len(patched_objects) == 0) else ''} modified",
)