434 lines
14 KiB
Rust
434 lines
14 KiB
Rust
use crate::{language::language, page::Page, AppState};
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::HeaderMap,
|
|
response::{IntoResponse, Redirect, Response},
|
|
routing::{get, post},
|
|
Form, Router,
|
|
};
|
|
use axum_extra::extract::{
|
|
cookie::{Cookie, Expiration},
|
|
CookieJar, PrivateCookieJar,
|
|
};
|
|
use maud::{html, Markup};
|
|
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use time::OffsetDateTime;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Deserialize)]
|
|
struct LoginForm {
|
|
password: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CameraForm {
|
|
uuid: String,
|
|
name: String,
|
|
desc: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DeleteCameraForm {
|
|
uuid: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct EditCameraForm {
|
|
name: String,
|
|
desc: Option<String>,
|
|
}
|
|
|
|
async fn login_page(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
|
let lang = language(&cookies, &headers);
|
|
rust_i18n::set_locale(lang.to_locale());
|
|
|
|
Page::new(lang).content(html! {
|
|
h1 { "Admin Login" }
|
|
form method="POST" action="/admin/login" {
|
|
fieldset {
|
|
label for="password" { "Password:" }
|
|
input
|
|
type="password"
|
|
name="password"
|
|
id="password"
|
|
required;
|
|
input type="submit" value="Login";
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async fn login(
|
|
State(state): State<AppState>,
|
|
private_cookies: PrivateCookieJar,
|
|
Form(form): Form<LoginForm>,
|
|
) -> Response {
|
|
if form.password == state.admin_password {
|
|
// Set secure admin session cookie
|
|
let expiration_date = OffsetDateTime::now_utc() + time::Duration::days(30);
|
|
let mut cookie = Cookie::new("admin_session", "authenticated");
|
|
cookie.set_expires(Expiration::DateTime(expiration_date));
|
|
cookie.set_http_only(true);
|
|
cookie.set_secure(true);
|
|
cookie.set_path("/");
|
|
|
|
let updated_cookies = private_cookies.add(cookie);
|
|
(updated_cookies, Redirect::to("/protected")).into_response()
|
|
} else {
|
|
// Invalid password, redirect back to login
|
|
Redirect::to("/admin/login").into_response()
|
|
}
|
|
}
|
|
|
|
async fn logout(private_cookies: PrivateCookieJar) -> Response {
|
|
// Remove admin session cookie
|
|
let expired_cookie = Cookie::build(("admin_session", ""))
|
|
.expires(Expiration::DateTime(
|
|
OffsetDateTime::now_utc() - time::Duration::days(1),
|
|
))
|
|
.http_only(true)
|
|
.secure(true)
|
|
.path("/")
|
|
.build();
|
|
|
|
let updated_cookies = private_cookies.add(expired_cookie);
|
|
(updated_cookies, Redirect::to("/")).into_response()
|
|
}
|
|
|
|
async fn protected_page(
|
|
private_cookies: PrivateCookieJar,
|
|
cookies: CookieJar,
|
|
headers: HeaderMap,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
let lang = language(&cookies, &headers);
|
|
rust_i18n::set_locale(lang.to_locale());
|
|
|
|
let markup = Page::new(lang).content(html! {
|
|
h1 { "Protected Admin Area" }
|
|
p { "Welcome to the admin area! This is a protected route." }
|
|
p { "Only authenticated administrators can access this page." }
|
|
|
|
h2 { "Camera Management" }
|
|
p { "Manage cameras in the system." }
|
|
a href="/admin/cameras/add" { "Add Camera" }
|
|
" | "
|
|
a href="/admin/cameras" { "Manage Cameras" }
|
|
|
|
form method="POST" action="/admin/logout" {
|
|
input type="submit" value="Logout" class="secondary";
|
|
}
|
|
});
|
|
|
|
markup.into_response()
|
|
}
|
|
|
|
async fn add_camera_page(
|
|
private_cookies: PrivateCookieJar,
|
|
cookies: CookieJar,
|
|
headers: HeaderMap,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
let lang = language(&cookies, &headers);
|
|
rust_i18n::set_locale(lang.to_locale());
|
|
|
|
// Get pre-filled UUID from query params
|
|
let prefilled_uuid = params.get("uuid").unwrap_or(&String::new()).clone();
|
|
|
|
let markup = Page::new(lang).content(html! {
|
|
h1 { "Add Camera" }
|
|
@if !prefilled_uuid.is_empty() {
|
|
p.text-muted { "Auto-detected missing camera with UUID: " strong { (prefilled_uuid) } }
|
|
}
|
|
form method="POST" action="/admin/cameras/add" {
|
|
fieldset {
|
|
label for="uuid" { "Camera UUID:" }
|
|
input
|
|
type="text"
|
|
name="uuid"
|
|
id="uuid"
|
|
placeholder="e.g., 123e4567-e89b-12d3-a456-426614174000"
|
|
value=(prefilled_uuid)
|
|
required;
|
|
|
|
label for="name" { "Camera Name:" }
|
|
input
|
|
type="text"
|
|
name="name"
|
|
id="name"
|
|
placeholder="e.g., Front Entrance Camera"
|
|
required;
|
|
|
|
label for="desc" { "Description (optional):" }
|
|
textarea
|
|
name="desc"
|
|
id="desc"
|
|
placeholder="e.g., Camera monitoring the main entrance" {};
|
|
|
|
input type="submit" value="Add Camera";
|
|
}
|
|
}
|
|
p {
|
|
a href="/protected" { "← Back to Admin Dashboard" }
|
|
}
|
|
});
|
|
|
|
markup.into_response()
|
|
}
|
|
|
|
async fn manage_cameras_page(
|
|
State(state): State<AppState>,
|
|
private_cookies: PrivateCookieJar,
|
|
cookies: CookieJar,
|
|
headers: HeaderMap,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
let lang = language(&cookies, &headers);
|
|
rust_i18n::set_locale(lang.to_locale());
|
|
|
|
let cameras = state.backend.get_all_cameras().await;
|
|
|
|
let markup = Page::new(lang).content(html! {
|
|
h1 { "Manage Cameras" }
|
|
p { "Total cameras: " strong { (cameras.len()) } }
|
|
|
|
@if cameras.is_empty() {
|
|
p.text-muted { "No cameras found in the system." }
|
|
} @else {
|
|
table {
|
|
thead {
|
|
tr {
|
|
th { "UUID" }
|
|
th { "Name" }
|
|
th { "Description" }
|
|
th { "Actions" }
|
|
}
|
|
}
|
|
tbody {
|
|
@for camera in &cameras {
|
|
tr {
|
|
td {
|
|
code { (camera.uuid) }
|
|
}
|
|
td { (camera.name) }
|
|
td {
|
|
@if let Some(desc) = &camera.desc {
|
|
(desc)
|
|
} @else {
|
|
em.text-muted { "No description" }
|
|
}
|
|
}
|
|
td {
|
|
a href=(format!("/admin/cameras/{}/edit", camera.uuid)) class="secondary" style="margin-right: 0.5rem;" { "Edit" }
|
|
form method="POST" action="/admin/cameras/delete" style="display: inline;" {
|
|
input type="hidden" name="uuid" value=(camera.uuid);
|
|
input
|
|
type="submit"
|
|
value="Delete"
|
|
class="secondary"
|
|
onclick="return confirm('Are you sure you want to delete this camera? This will also remove all associated sightings.')";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
p {
|
|
a href="/admin/cameras/add" { "Add New Camera" }
|
|
" | "
|
|
a href="/protected" { "← Back to Admin Dashboard" }
|
|
}
|
|
});
|
|
|
|
markup.into_response()
|
|
}
|
|
|
|
async fn add_camera(
|
|
State(state): State<AppState>,
|
|
private_cookies: PrivateCookieJar,
|
|
Form(form): Form<CameraForm>,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
// Parse UUID
|
|
let uuid = match Uuid::parse_str(&form.uuid) {
|
|
Ok(uuid) => uuid,
|
|
Err(_) => return Redirect::to("/admin/cameras/add?error=invalid_uuid").into_response(),
|
|
};
|
|
|
|
// Check if camera already exists
|
|
if state.backend.get_camera(&uuid).await.is_some() {
|
|
return Redirect::to("/admin/cameras/add?error=already_exists").into_response();
|
|
}
|
|
|
|
// Create the camera
|
|
let desc = if form
|
|
.desc
|
|
.as_ref()
|
|
.map(|s| s.trim().is_empty())
|
|
.unwrap_or(true)
|
|
{
|
|
None
|
|
} else {
|
|
form.desc.as_deref()
|
|
};
|
|
|
|
match state.backend.create_camera(&uuid, &form.name, desc).await {
|
|
Ok(_) => Redirect::to("/admin/cameras?camera_added=1").into_response(),
|
|
Err(_) => Redirect::to("/admin/cameras/add?error=creation_failed").into_response(),
|
|
}
|
|
}
|
|
|
|
async fn edit_camera_page(
|
|
State(state): State<AppState>,
|
|
private_cookies: PrivateCookieJar,
|
|
cookies: CookieJar,
|
|
headers: HeaderMap,
|
|
Path(uuid_str): Path<String>,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
let lang = language(&cookies, &headers);
|
|
rust_i18n::set_locale(lang.to_locale());
|
|
|
|
// Parse UUID
|
|
let uuid = match Uuid::parse_str(&uuid_str) {
|
|
Ok(uuid) => uuid,
|
|
Err(_) => return Redirect::to("/admin/cameras?error=invalid_uuid").into_response(),
|
|
};
|
|
|
|
// Get camera details
|
|
let Some(camera) = state.backend.get_camera(&uuid).await else {
|
|
return Redirect::to("/admin/cameras?error=not_found").into_response();
|
|
};
|
|
|
|
let markup = Page::new(lang).content(html! {
|
|
h1 { "Edit Camera" }
|
|
p.text-muted { "UUID: " code { (camera.uuid) } }
|
|
|
|
form method="POST" action=(format!("/admin/cameras/{}/edit", camera.uuid)) {
|
|
fieldset {
|
|
label for="name" { "Camera Name:" }
|
|
input
|
|
type="text"
|
|
name="name"
|
|
id="name"
|
|
value=(camera.name)
|
|
required;
|
|
|
|
label for="desc" { "Description (optional):" }
|
|
textarea
|
|
name="desc"
|
|
id="desc"
|
|
placeholder="e.g., Camera monitoring the main entrance" {
|
|
@if let Some(desc) = &camera.desc {
|
|
(desc)
|
|
}
|
|
}
|
|
|
|
input type="submit" value="Update Camera";
|
|
}
|
|
}
|
|
p {
|
|
a href="/admin/cameras" { "← Back to Camera List" }
|
|
}
|
|
});
|
|
|
|
markup.into_response()
|
|
}
|
|
|
|
async fn update_camera(
|
|
State(state): State<AppState>,
|
|
private_cookies: PrivateCookieJar,
|
|
Path(uuid_str): Path<String>,
|
|
Form(form): Form<EditCameraForm>,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
// Parse UUID
|
|
let uuid = match Uuid::parse_str(&uuid_str) {
|
|
Ok(uuid) => uuid,
|
|
Err(_) => return Redirect::to("/admin/cameras?error=invalid_uuid").into_response(),
|
|
};
|
|
|
|
// Process description
|
|
let desc = if form
|
|
.desc
|
|
.as_ref()
|
|
.map(|s| s.trim().is_empty())
|
|
.unwrap_or(true)
|
|
{
|
|
None
|
|
} else {
|
|
form.desc.as_deref()
|
|
};
|
|
|
|
match state.backend.update_camera(&uuid, &form.name, desc).await {
|
|
Ok(true) => Redirect::to("/admin/cameras?camera_updated=1").into_response(),
|
|
Ok(false) => Redirect::to("/admin/cameras?error=not_found").into_response(),
|
|
Err(_) => Redirect::to("/admin/cameras?error=update_failed").into_response(),
|
|
}
|
|
}
|
|
|
|
async fn delete_camera(
|
|
State(state): State<AppState>,
|
|
private_cookies: PrivateCookieJar,
|
|
Form(form): Form<DeleteCameraForm>,
|
|
) -> Response {
|
|
// Check if admin is authenticated
|
|
if private_cookies.get("admin_session").is_none() {
|
|
return Redirect::to("/admin/login").into_response();
|
|
}
|
|
|
|
// Parse UUID
|
|
let uuid = match Uuid::parse_str(&form.uuid) {
|
|
Ok(uuid) => uuid,
|
|
Err(_) => return Redirect::to("/admin/cameras?error=invalid_uuid").into_response(),
|
|
};
|
|
|
|
match state.backend.delete_camera(&uuid).await {
|
|
Ok(true) => Redirect::to("/admin/cameras?camera_deleted=1").into_response(),
|
|
Ok(false) => Redirect::to("/admin/cameras?error=not_found").into_response(),
|
|
Err(_) => Redirect::to("/admin/cameras?error=deletion_failed").into_response(),
|
|
}
|
|
}
|
|
|
|
pub fn routes() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/admin/login", get(login_page))
|
|
.route("/admin/login", post(login))
|
|
.route("/admin/logout", post(logout))
|
|
.route("/protected", get(protected_page))
|
|
.route("/admin/cameras", get(manage_cameras_page))
|
|
.route("/admin/cameras/add", get(add_camera_page))
|
|
.route("/admin/cameras/add", post(add_camera))
|
|
.route("/admin/cameras/{uuid}/edit", get(edit_camera_page))
|
|
.route("/admin/cameras/{uuid}/edit", post(update_camera))
|
|
.route("/admin/cameras/delete", post(delete_camera))
|
|
}
|