add camera edit and add functionality
This commit is contained in:
323
src/admin.rs
323
src/admin.rs
@@ -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))
|
||||
}
|
||||
|
@@ -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();
|
||||
};
|
||||
|
||||
|
@@ -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) => {
|
||||
|
Reference in New Issue
Block a user