feat: initialize template shell and basic components

This commit is contained in:
2026-05-30 01:09:14 +05:00
commit f42a5f05b2
55 changed files with 13107 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
use askama::Template;
use axum::{
extract::{Form, Path, Query, State},
http::StatusCode,
response::{Html, IntoResponse, Redirect, Response},
};
use mongodb::bson::oid::ObjectId;
use serde::Deserialize;
use crate::common::errors::AppError;
use crate::auth::extractors::AuthenticatedUser;
use crate::developers::models::Developer;
use crate::developers::repository::{DeveloperRepository, MongoDeveloperRepository};
// Wrapper for rendering Askama HTML
struct HtmlTemplate<T>(T);
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => {
tracing::error!("Failed to render template: {:?}", err);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
}
// Askama templates
#[derive(Template)]
#[template(path = "developers/list.html")]
struct DeveloperListTemplate {
username: String,
authenticated: bool,
developers: Vec<Developer>,
}
#[derive(Template)]
#[template(path = "developers/edit.html")]
struct DeveloperEditTemplate {
username: String,
authenticated: bool,
developer: Developer,
}
#[derive(Template)]
#[template(path = "developers/search_results.html")]
struct DeveloperSearchResultsTemplate {
developers: Vec<Developer>,
}
// HANDLERS
pub async fn get_list(
State(dev_repo): State<MongoDeveloperRepository>,
user_opt: Option<AuthenticatedUser>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
return Ok(Redirect::to("/auth/login").into_response());
};
let _ = dev_repo.ensure_seeded(&user.user_id).await;
let developers = dev_repo.find_all_by_user(&user.user_id).await?;
Ok(HtmlTemplate(DeveloperListTemplate {
username: user.username,
authenticated: true,
developers,
})
.into_response())
}
#[derive(Deserialize)]
pub struct CreateDevForm {
pub name: String,
pub email: String,
pub skills: String,
}
pub async fn post_create(
State(dev_repo): State<MongoDeveloperRepository>,
user_opt: Option<AuthenticatedUser>,
Form(payload): Form<CreateDevForm>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
return Ok(Redirect::to("/auth/login").into_response());
};
let name = payload.name.trim();
let email = payload.email.trim();
if name.is_empty() {
return Err(AppError::BadRequest("Developer name cannot be empty".to_string()));
}
let skills: Vec<String> = payload.skills
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
dev_repo.create(&user.user_id, name, email, skills).await?;
Ok(Redirect::to("/developers").into_response())
}
pub async fn get_edit(
State(dev_repo): State<MongoDeveloperRepository>,
user_opt: Option<AuthenticatedUser>,
Path(dev_id_str): Path<String>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
return Ok(Redirect::to("/auth/login").into_response());
};
let dev_id = ObjectId::parse_str(&dev_id_str)
.map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?;
let developer = dev_repo.find_by_id(&dev_id, &user.user_id).await?;
let Some(developer) = developer else {
return Err(AppError::Unauthorized("Developer not found".to_string()));
};
Ok(HtmlTemplate(DeveloperEditTemplate {
username: user.username,
authenticated: true,
developer,
})
.into_response())
}
pub async fn post_update(
State(dev_repo): State<MongoDeveloperRepository>,
user_opt: Option<AuthenticatedUser>,
Path(dev_id_str): Path<String>,
Form(payload): Form<CreateDevForm>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
return Ok(Redirect::to("/auth/login").into_response());
};
let dev_id = ObjectId::parse_str(&dev_id_str)
.map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?;
let name = payload.name.trim();
let email = payload.email.trim();
if name.is_empty() {
return Err(AppError::BadRequest("Developer name cannot be empty".to_string()));
}
let skills: Vec<String> = payload.skills
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
dev_repo.update(&dev_id, &user.user_id, name, email, skills).await?;
Ok(Redirect::to("/developers").into_response())
}
pub async fn post_delete(
State(dev_repo): State<MongoDeveloperRepository>,
user_opt: Option<AuthenticatedUser>,
Path(dev_id_str): Path<String>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
return Ok(Redirect::to("/auth/login").into_response());
};
let dev_id = ObjectId::parse_str(&dev_id_str)
.map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?;
dev_repo.delete(&dev_id, &user.user_id).await?;
Ok(Redirect::to("/developers").into_response())
}
#[derive(Deserialize)]
pub struct SearchQuery {
pub q: String,
}
pub async fn get_search(
State(dev_repo): State<MongoDeveloperRepository>,
user_opt: Option<AuthenticatedUser>,
Query(params): Query<SearchQuery>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
return Ok(StatusCode::UNAUTHORIZED.into_response());
};
let _ = dev_repo.ensure_seeded(&user.user_id).await;
let query_str = params.q.trim();
if query_str.is_empty() {
return Ok(HtmlTemplate(DeveloperSearchResultsTemplate { developers: vec![] }).into_response());
}
let matched_devs = dev_repo.search_by_name(&user.user_id, query_str).await?;
Ok(HtmlTemplate(DeveloperSearchResultsTemplate {
developers: matched_devs,
})
.into_response())
}
+23
View File
@@ -0,0 +1,23 @@
pub mod handlers;
pub mod models;
pub mod repository;
use axum::{
routing::{get, post},
Router,
};
use crate::common::config::Config;
use crate::developers::repository::MongoDeveloperRepository;
pub fn router<S>() -> Router<S>
where
Config: axum::extract::FromRef<S>,
MongoDeveloperRepository: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
.route("/developers", get(handlers::get_list).post(handlers::post_create))
.route("/developers/{id}/edit", get(handlers::get_edit).post(handlers::post_update))
.route("/developers/{id}/delete", post(handlers::post_delete))
.route("/developers/search", get(handlers::get_search))
}
+16
View File
@@ -0,0 +1,16 @@
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Developer {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub user_id: ObjectId,
pub name: String,
pub email: String,
pub skills: Vec<String>,
#[serde_as(as = "mongodb::bson::serde_helpers::datetime::FromChrono04DateTime")]
pub created_at: chrono::DateTime<chrono::Utc>,
}
+123
View File
@@ -0,0 +1,123 @@
use futures::stream::TryStreamExt;
use mongodb::{
bson::{doc, oid::ObjectId},
options::FindOptions,
Database,
};
use crate::common::errors::AppError;
use crate::developers::models::Developer;
#[async_trait::async_trait]
pub trait DeveloperRepository {
async fn find_all_by_user(&self, user_id: &ObjectId) -> Result<Vec<Developer>, AppError>;
async fn find_by_id(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result<Option<Developer>, AppError>;
async fn search_by_name(&self, user_id: &ObjectId, query: &str) -> Result<Vec<Developer>, AppError>;
async fn create(&self, user_id: &ObjectId, name: &str, email: &str, skills: Vec<String>) -> Result<Developer, AppError>;
async fn update(&self, dev_id: &ObjectId, user_id: &ObjectId, name: &str, email: &str, skills: Vec<String>) -> Result<(), AppError>;
async fn delete(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError>;
async fn ensure_seeded(&self, user_id: &ObjectId) -> Result<(), AppError>;
}
#[derive(Clone)]
pub struct MongoDeveloperRepository {
db: Database,
}
impl MongoDeveloperRepository {
pub fn new(db: Database) -> Self {
Self { db }
}
}
#[async_trait::async_trait]
impl DeveloperRepository for MongoDeveloperRepository {
async fn find_all_by_user(&self, user_id: &ObjectId) -> Result<Vec<Developer>, AppError> {
let collection = self.db.collection::<Developer>("developers");
let filter = doc! { "user_id": user_id };
let find_options = FindOptions::builder().sort(doc! { "created_at": -1 }).build();
let mut cursor = collection.find(filter).with_options(find_options).await?;
let mut developers = Vec::new();
while let Some(dev) = cursor.try_next().await? {
developers.push(dev);
}
Ok(developers)
}
async fn find_by_id(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result<Option<Developer>, AppError> {
let collection = self.db.collection::<Developer>("developers");
let filter = doc! { "_id": dev_id, "user_id": user_id };
let dev = collection.find_one(filter).await?;
Ok(dev)
}
async fn search_by_name(&self, user_id: &ObjectId, query: &str) -> Result<Vec<Developer>, AppError> {
let collection = self.db.collection::<Developer>("developers");
// Regex search case-insensitive on developer name
let filter = doc! {
"user_id": user_id,
"name": { "$regex": query, "$options": "i" }
};
let find_options = FindOptions::builder().limit(10).build();
let mut cursor = collection.find(filter).with_options(find_options).await?;
let mut developers = Vec::new();
while let Some(dev) = cursor.try_next().await? {
developers.push(dev);
}
Ok(developers)
}
async fn create(&self, user_id: &ObjectId, name: &str, email: &str, skills: Vec<String>) -> Result<Developer, AppError> {
let collection = self.db.collection::<Developer>("developers");
let new_dev = Developer {
id: None,
user_id: *user_id,
name: name.to_string(),
email: email.to_string(),
skills,
created_at: chrono::Utc::now(),
};
let insert_result = collection.insert_one(new_dev.clone()).await?;
let mut dev = new_dev;
dev.id = Some(insert_result.inserted_id.as_object_id().expect("Inserted ID is ObjectId"));
Ok(dev)
}
async fn update(&self, dev_id: &ObjectId, user_id: &ObjectId, name: &str, email: &str, skills: Vec<String>) -> Result<(), AppError> {
let collection = self.db.collection::<Developer>("developers");
let filter = doc! { "_id": dev_id, "user_id": user_id };
let update = doc! {
"$set": {
"name": name,
"email": email,
"skills": skills
}
};
let result = collection.update_one(filter, update).await?;
if result.matched_count == 0 {
return Err(AppError::Unauthorized("Developer not found or not owned by user".to_string()));
}
Ok(())
}
async fn delete(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError> {
let collection = self.db.collection::<Developer>("developers");
let filter = doc! { "_id": dev_id, "user_id": user_id };
let result = collection.delete_one(filter).await?;
if result.deleted_count == 0 {
return Err(AppError::Unauthorized("Developer not found or not owned by user".to_string()));
}
Ok(())
}
async fn ensure_seeded(&self, user_id: &ObjectId) -> Result<(), AppError> {
let devs = self.find_all_by_user(user_id).await?;
if devs.is_empty() {
let _ = self.create(user_id, "Alice Vance", "alice@example.com", vec!["Rust".to_string(), "Axum".to_string()]).await;
let _ = self.create(user_id, "Bob Carter", "bob@example.com", vec!["Tailwind".to_string(), "JavaScript".to_string()]).await;
let _ = self.create(user_id, "Charlie Smith", "charlie@example.com", vec!["HTML".to_string(), "CSS".to_string()]).await;
}
Ok(())
}
}