add camera edit and add functionality

This commit is contained in:
2025-08-21 14:58:00 +02:00
parent a0eddece86
commit aab0e0b780
3 changed files with 391 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
use crate::{language::language, page::Page, AppState};
use axum::{
extract::State,
extract::{Path, Query, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
routing::{get, post},
@@ -12,13 +12,33 @@ use axum_extra::extract::{
};
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());
@@ -94,6 +114,11 @@ async fn protected_page(
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";
@@ -103,10 +128,306 @@ async fn protected_page(
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))
}

View File

@@ -143,6 +143,12 @@ async fn game(
};
let Some(camera) = backend.get_camera(&uuid).await else {
// Check if user is admin
if cookies.get("admin_session").is_some() {
// Redirect to camera add form with pre-filled UUID
return axum::response::Redirect::to(&format!("/admin/cameras/add?uuid={}", uuid))
.into_response();
}
return not_found(lang_cookies, headers).await.into_response();
};

View File

@@ -25,6 +25,69 @@ impl Backend {
}
}
pub(crate) async fn create_camera(&self, uuid: &Uuid, name: &str, desc: Option<&str>) -> Result<(), sqlx::Error> {
let uuid_str = uuid.to_string();
match self {
Backend::Sqlite(db) => {
sqlx::query!(
"INSERT INTO camera (uuid, name, desc) VALUES (?, ?, ?)",
uuid_str,
name,
desc
)
.execute(db)
.await?;
Ok(())
}
}
}
pub(crate) async fn update_camera(&self, uuid: &Uuid, name: &str, desc: Option<&str>) -> Result<bool, sqlx::Error> {
let uuid_str = uuid.to_string();
match self {
Backend::Sqlite(db) => {
let result = sqlx::query!(
"UPDATE camera SET name = ?, desc = ? WHERE uuid = ?",
name,
desc,
uuid_str
)
.execute(db)
.await?;
Ok(result.rows_affected() > 0)
}
}
}
pub(crate) async fn delete_camera(&self, uuid: &Uuid) -> Result<bool, sqlx::Error> {
let uuid_str = uuid.to_string();
match self {
Backend::Sqlite(db) => {
let result = sqlx::query!(
"DELETE FROM camera WHERE uuid = ?",
uuid_str
)
.execute(db)
.await?;
Ok(result.rows_affected() > 0)
}
}
}
pub(crate) async fn get_all_cameras(&self) -> Vec<Camera> {
match self {
Backend::Sqlite(db) => {
sqlx::query_as!(
Camera,
"SELECT uuid, desc, name FROM camera ORDER BY name"
)
.fetch_all(db)
.await
.unwrap_or_default()
}
}
}
pub(crate) async fn amount_total_cameras(&self) -> i64 {
match self {
Backend::Sqlite(db) => {