summaryrefslogtreecommitdiff
path: root/src/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'src/handlers')
-rw-r--r--src/handlers/admin.rs143
-rw-r--r--src/handlers/frontend_builder.rs121
-rw-r--r--src/handlers/mod.rs53
3 files changed, 0 insertions, 317 deletions
diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs
deleted file mode 100644
index b0863af..0000000
--- a/src/handlers/admin.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use std::collections::HashMap;
-use std::io::{BufWriter, Write};
-
-use anyhow::Context;
-use axum::body::Bytes;
-use axum::extract::{Multipart, Path, Request, State};
-use axum::http::{HeaderMap, StatusCode};
-use axum::middleware::Next;
-use axum::response::{IntoResponse, Response};
-use axum::{Extension, Json};
-use rand::Rng;
-use subtle::ConstantTimeEq;
-
-use crate::db::product::get_all_products;
-use crate::db::story::{format_filepath, Story};
-use crate::db::ProductDatabaseHandler;
-use crate::error::AppError;
-
-use super::{ApiContext, AppState};
-
-pub fn generate_token() -> String {
- const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
- abcdefghijklmnopqrstuvwxyz\
- 0123456789)(*&^%$#@!~";
- let mut rng = rand::thread_rng();
- let token: String = (0..32)
- .map(|_| {
- let idx = rng.gen_range(0..CHARSET.len());
- CHARSET[idx] as char
- })
- .collect();
- println!("New Token generated: {}", token);
- token
-}
-
-pub async fn admin_middleware(
- State(state): State<AppState>,
- headers: HeaderMap,
- request: Request,
- next: Next,
-) -> Result<Response, StatusCode> {
- let mut expected_token = state.admin_token.lock().await;
-
- let token = headers
- .get("X-Admin-Token")
- .and_then(|header| header.to_str().ok())
- .ok_or(StatusCode::UNAUTHORIZED)?;
-
- if expected_token
- .as_bytes()
- .ct_ne(token.as_bytes())
- .unwrap_u8()
- == 1
- {
- eprintln!("Warning: Unauthorized access attempt with invalid token");
- *expected_token = generate_token();
- return Err(StatusCode::UNAUTHORIZED);
- }
- *expected_token = generate_token();
-
- Ok(next.run(request).await)
-}
-
-pub async fn admin_healthcheck() -> impl IntoResponse {
- StatusCode::OK
-}
-
-pub async fn upload_story(
- ctx: Extension<ApiContext>,
- mut multipart: Multipart,
-) -> Result<impl IntoResponse, AppError> {
- let mut form: HashMap<String, Bytes> = HashMap::new();
- while let Some(field) = multipart.next_field().await? {
- if let Some(label) = field.name() {
- form.insert(label.to_string(), field.bytes().await?);
- }
- }
-
- let name = if let Some(name_value) = form.get("name") {
- std::str::from_utf8(name_value).with_context(|| "Failed to parse name")?
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let description = if let Some(description_value) = form.get("description") {
- std::str::from_utf8(description_value).with_context(|| "Failed to parse description")?
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let language = if let Some(language_value) = form.get("language") {
- String::from_utf8(language_value.to_vec()).with_context(|| "Failed to parse language")?
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let pdf = if let Some(pdf_value) = form.get("pdf") {
- let filename = format_filepath(name, "pdf");
- let file = std::fs::File::create_new(filename)?;
- BufWriter::new(file).write_all(pdf_value)?;
- pdf_value.to_vec()
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let mut tags: Vec<&str> = Vec::new();
- if let Some(tags_value) = form.get("tag") {
- let tagstring = std::str::from_utf8(tags_value)?;
- for tag in tagstring.split(",") {
- tags.push(tag.trim());
- }
- }
-
- let story_template = Story {
- id: 0,
- pid: 0,
- language,
- pdf,
- };
-
- let story = story_template
- .post(&ctx.pool, name, description, tags)
- .await?;
- Ok((StatusCode::CREATED, Json(story)).into_response())
-}
-
-pub async fn delete_story(
- ctx: Extension<ApiContext>,
- Path(story_id): Path<i64>,
-) -> Result<impl IntoResponse, AppError> {
- Story::delete(&ctx.pool, story_id).await?;
- Ok((StatusCode::NO_CONTENT).into_response())
-}
-
-pub async fn get_stories(ctx: Extension<ApiContext>) -> Result<impl IntoResponse, AppError> {
- let stories = Story::get_all(&ctx.pool).await?;
- Ok((StatusCode::OK, Json(stories)).into_response())
-}
-
-pub async fn get_products(ctx: Extension<ApiContext>) -> Result<impl IntoResponse, AppError> {
- let products = get_all_products(&ctx.pool).await?;
- Ok((StatusCode::OK, Json(products)).into_response())
-}
diff --git a/src/handlers/frontend_builder.rs b/src/handlers/frontend_builder.rs
deleted file mode 100644
index dd49854..0000000
--- a/src/handlers/frontend_builder.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use axum::response::IntoResponse;
-use axum::Extension;
-use chrono::DateTime;
-use maud::{html, Markup, DOCTYPE};
-
-use crate::db::product::{get_all_specificproducts, ProductMarker, SpecificProduct};
-use crate::db::ProductDatabaseHandler;
-use crate::error::AppError;
-
-pub async fn home() -> Result<impl IntoResponse, AppError> {
- Ok(html!((page("Home", Markup::default()))))
-}
-
-fn page(page_title: &str, content: Markup) -> Markup {
- html!(
- (head(page_title))
- body class="bg-[#eae3d2] text-[#333] dark:bg-[#333] dark:text-[#eae3d2] font-mono text-base" {
- (header())
- (navbar())
- (content)
- }
- )
-}
-
-fn header() -> Markup {
- html!(
- header {
- h1 class="underline decoration-[#601B69] dark:decoration-[#e1b1e7] text-center tracking-widest font-serif p-3 text-xl" {"Vithuran Vishnuthas"}
- }
- )
-}
-
-fn navbar() -> Markup {
- html!({
- navbar class="p-3 text-sm flex flex-row flex-wrap content-center justify-around decoration-[#eae3d2] tracking-wide" {
- a class="hover:underline" href="/" { "Home" }
- a class="hover:underline" href="/stories" { "Stories" }
- a class="hover:underline" href="/" { "Tools" }
- a class="hover:underline" href="/" { "Games" }
- }
- })
-}
-
-fn cards<'a, T: PageBuilder<'a> + ProductMarker>(
- products: Vec<SpecificProduct<T>>,
- category: &str,
-) -> Markup {
- html!(
- div class="m-12" {
- h1 class="p-6 font-serif text-xl tracking-wider underline decoration-[#601B69] dark:decoration-[#E1B1E7]" { (category) }
- div class="grid grid-cols-2 gap-8" {
- @for product in products {
- (card(product))
- }
- }
- }
- )
-}
-
-fn card<'a, T: PageBuilder<'a> + ProductMarker>(specific_product: SpecificProduct<T>) -> Markup {
- let date = |i: i64| -> String {
- if let Some(datetime) = DateTime::from_timestamp(i / 1_000, 0) {
- return format!("{}", datetime.format("%Y/%m/%d")).to_string();
- }
- "Unknown".to_string()
- };
- let upload_date = date(specific_product.product.uploaddate);
- let update_date = date(specific_product.product.updatedate);
- html!(
- div class="p-9 shadow" {
- h2 class="text-[#601B69] dark:text-[#E1B1E7]" {
- (specific_product.product.name)
- }
- div {
- (specific_product.product.description)
- }
- div {
- (specific_product.detail.product_specific_card_content(&specific_product))
- }
- div class="flex flex-row space-x-5 mt-9 text-xs" {
- span {"Upload: " (upload_date)}
- @if upload_date != update_date {
- span {"Update: " (update_date)}
- }
- }
- }
- )
-}
-
-fn head(page_title: &str) -> Markup {
- html! {
- head {
- (DOCTYPE)
- meta charset="utf-8";
- link rel="stylesheet" type="text/css" href="/static/styles.css";
- title { (page_title) }
- }
- }
-}
-
-pub async fn build_page<'a, T: Sized + ProductMarker + PageBuilder<'a> + ProductDatabaseHandler>(
- ctx: Extension<super::ApiContext>,
-) -> Result<impl IntoResponse, AppError> {
- Ok(html!(
- (page(
- T::page_title(),
- cards(
- get_all_specificproducts::<T>(&ctx.pool).await?,
- T::page_title()
- )
- ))
- ))
-}
-
-pub trait PageBuilder<'a> {
- fn page_title() -> &'a str;
- fn product_specific_card_content<T: PageBuilder<'a> + ProductMarker>(
- &self,
- specific_product: &SpecificProduct<T>,
- ) -> Markup;
-}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
deleted file mode 100644
index 3235f24..0000000
--- a/src/handlers/mod.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-use std::sync::Arc;
-use tokio::sync::Mutex;
-
-use axum::extract::DefaultBodyLimit;
-use axum::middleware::{self};
-use axum::routing::{delete, get, post};
-use axum::Router;
-use sqlx::SqlitePool;
-use tower_http::add_extension::AddExtensionLayer;
-use tower_http::services::ServeDir;
-
-use admin::{admin_healthcheck, admin_middleware, delete_story, generate_token, upload_story};
-use frontend_builder::home;
-
-use crate::db::story::Story;
-
-use self::admin::{get_products, get_stories};
-use self::frontend_builder::build_page;
-
-mod admin;
-pub mod frontend_builder;
-
-#[derive(Debug, Clone)]
-pub struct ApiContext {
- pool: SqlitePool,
-}
-
-#[derive(Clone)]
-struct AppState {
- admin_token: Arc<Mutex<String>>,
-}
-
-pub async fn router(pool: SqlitePool) -> Router {
- let state = AppState {
- admin_token: Arc::new(Mutex::new(generate_token())),
- };
-
- Router::new()
- .route("/admin/story/:story_id", delete(delete_story))
- .route("/admin/story", post(upload_story).get(get_stories))
- .route("/admin/product", get(get_products))
- .route("/admin", get(admin_healthcheck))
- .layer(middleware::from_fn_with_state(
- state.clone(),
- admin_middleware,
- ))
- .with_state(state)
- .route("/", get(home))
- .route("/stories", get(build_page::<Story>))
- .nest_service("/static", ServeDir::new("static"))
- .layer(DefaultBodyLimit::max(1074000000))
- .layer(AddExtensionLayer::new(ApiContext { pool }))
-}