feat(dimentorin): materials module (CRUD konten mentoring, public+protected routes)

- app_materials table: mentor_id, title, slug, category, description, content
- domain/application/infrastructure pola articles: repo postgres, service, DTO ZodValidate
- routes: GET /materials (public, published only), GET /materials/{id|slug|categories}, POST/PUT/DELETE (auth, author-only)
- verified e2e: create 3 materi sebagai mentor, list, slug, categories
This commit is contained in:
asepharyana
2026-08-05 17:01:59 +07:00
parent b4a9972ad7
commit ac8177d87e
19 changed files with 801 additions and 3 deletions
+2
View File
@@ -1,9 +1,11 @@
pub mod articles;
pub mod materials;
pub mod mentors;
pub mod payments;
pub mod sessions;
pub use articles::{articles_protected_routes, articles_public_routes};
pub use materials::{materials_protected_routes, materials_public_routes};
pub use mentors::{mentors_protected_routes, mentors_public_routes};
pub use payments::payments_protected_routes;
pub use sessions::{sessions_protected_routes, sessions_public_routes};
@@ -0,0 +1,135 @@
use paginator_utils::PaginatorResponse;
use chrono::Utc;
use uuid::Uuid;
use super::super::domain::{
CreateMaterialCommand, MaterialEntity, MaterialListItem,
MaterialRepository, MaterialService, UpdateMaterialCommand,
};
use crate::materials::domain::{MaterialRepository as _};
use imphnen_utils::AppError;
pub struct MaterialServiceImpl {
repo: Box<dyn MaterialRepository>,
}
impl MaterialServiceImpl {
pub fn new(repo: Box<dyn MaterialRepository>) -> Self {
Self { repo }
}
fn slugify(title: &str) -> String {
let slug: String = title
.to_lowercase()
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c
} else if c.is_whitespace() {
'-'
} else {
'-'
}
})
.collect();
slug.trim_matches('-').to_string()
}
}
#[async_trait::async_trait]
impl MaterialService for MaterialServiceImpl {
async fn list_materials(
&self,
page: u64,
per_page: u64,
category: Option<String>,
published_only: bool,
) -> Result<PaginatorResponse<MaterialListItem>, AppError> {
let res = self
.repo
.find_all_paginated(page, per_page, category, published_only)
.await?;
Ok(PaginatorResponse {
data: res
.data
.into_iter()
.map(|e| MaterialListItem::from_entity(&e))
.collect(),
meta: res.meta,
})
}
async fn get_material_by_id(&self, id: Uuid) -> Result<MaterialEntity, AppError> {
self.repo.find_by_id(id).await
}
async fn get_material_by_slug(&self, slug: &str) -> Result<MaterialEntity, AppError> {
self.repo.find_by_slug(slug).await
}
async fn list_categories(&self) -> Result<Vec<String>, AppError> {
self.repo.find_categories().await
}
async fn create_material(
&self,
cmd: CreateMaterialCommand,
) -> Result<MaterialListItem, AppError> {
let now = Utc::now();
let entity = MaterialEntity {
id: Uuid::new_v4(),
mentor_id: cmd.mentor_id,
title: cmd.title.clone(),
slug: format!("{}-{}", Self::slugify(&cmd.title), Uuid::new_v4().to_string()[..8].to_string()),
category: cmd.category,
description: cmd.description,
content: cmd.content,
cover_url: cmd.cover_url,
is_published: true,
created_at: now,
updated_at: now,
};
self.repo.create(entity.clone()).await?;
Ok(MaterialListItem::from_entity(&entity))
}
async fn update_material(
&self,
id: Uuid,
actor_id: Uuid,
cmd: UpdateMaterialCommand,
) -> Result<MaterialListItem, AppError> {
let existing = self.repo.find_by_id(id).await?;
if existing.mentor_id != actor_id {
return Err(AppError::ForbiddenError(
"Only the author can update this material".into(),
));
}
// do not change mentor on update
let entity = MaterialEntity {
id,
mentor_id: existing.mentor_id,
title: cmd.title.unwrap_or(existing.title),
slug: existing.slug,
category: cmd.category.unwrap_or(existing.category),
description: cmd.description.unwrap_or(existing.description),
content: cmd.content.unwrap_or(existing.content),
cover_url: cmd.cover_url.or(existing.cover_url),
is_published: cmd.is_published.unwrap_or(existing.is_published),
created_at: existing.created_at,
updated_at: Utc::now(),
};
self.repo.update(id, entity.clone()).await?;
Ok(MaterialListItem::from_entity(&entity))
}
async fn delete_material(&self, id: Uuid, actor_id: Uuid) -> Result<(), AppError> {
let existing = self.repo.find_by_id(id).await?;
if existing.mentor_id != actor_id {
return Err(AppError::ForbiddenError(
"Only the author can delete this material".into(),
));
}
self.repo.delete(id).await
}
}
@@ -0,0 +1,3 @@
pub mod material_service;
pub use material_service::MaterialServiceImpl;
@@ -0,0 +1,17 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct MaterialEntity {
pub id: Uuid,
pub mentor_id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub description: String,
pub content: String,
pub cover_url: Option<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,55 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct CreateMaterialCommand {
pub mentor_id: Uuid,
pub title: String,
pub category: String,
pub description: String,
pub content: String,
pub cover_url: Option<String>,
}
#[derive(Clone, Debug)]
pub struct UpdateMaterialCommand {
pub title: Option<String>,
pub category: Option<String>,
pub description: Option<String>,
pub content: Option<String>,
pub cover_url: Option<String>,
pub is_published: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct MaterialListItem {
pub id: Uuid,
pub mentor_id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub description: String,
pub cover_url: Option<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl MaterialListItem {
pub fn from_entity(e: &MaterialEntity) -> Self {
Self {
id: e.id,
mentor_id: e.mentor_id,
title: e.title.clone(),
slug: e.slug.clone(),
category: e.category.clone(),
description: e.description.clone(),
cover_url: e.cover_url.clone(),
is_published: e.is_published,
created_at: e.created_at,
updated_at: e.updated_at,
}
}
}
pub use crate::materials::domain::material::MaterialEntity;
@@ -0,0 +1,11 @@
pub mod material;
pub mod material_types;
pub mod repository;
pub mod service;
pub use material::MaterialEntity;
pub use material_types::{
CreateMaterialCommand, MaterialListItem, UpdateMaterialCommand,
};
pub use repository::MaterialRepository;
pub use service::MaterialService;
@@ -0,0 +1,23 @@
use async_trait::async_trait;
use paginator_utils::PaginatorResponse;
use uuid::Uuid;
use super::material::MaterialEntity;
use imphnen_utils::AppError;
#[async_trait]
pub trait MaterialRepository: Send + Sync {
async fn find_all_paginated(
&self,
page: u64,
per_page: u64,
category: Option<String>,
published_only: bool,
) -> Result<PaginatorResponse<MaterialEntity>, AppError>;
async fn find_by_id(&self, id: Uuid) -> Result<MaterialEntity, AppError>;
async fn find_by_slug(&self, slug: &str) -> Result<MaterialEntity, AppError>;
async fn find_categories(&self) -> Result<Vec<String>, AppError>;
async fn create(&self, entity: MaterialEntity) -> Result<Uuid, AppError>;
async fn update(&self, id: Uuid, entity: MaterialEntity) -> Result<(), AppError>;
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
}
@@ -0,0 +1,34 @@
use async_trait::async_trait;
use paginator_utils::PaginatorResponse;
use uuid::Uuid;
use super::material::MaterialEntity;
use super::material_types::{
CreateMaterialCommand, MaterialListItem, UpdateMaterialCommand,
};
use imphnen_utils::AppError;
#[async_trait]
pub trait MaterialService: Send + Sync {
async fn list_materials(
&self,
page: u64,
per_page: u64,
category: Option<String>,
published_only: bool,
) -> Result<PaginatorResponse<MaterialListItem>, AppError>;
async fn get_material_by_id(&self, id: Uuid) -> Result<MaterialEntity, AppError>;
async fn get_material_by_slug(&self, slug: &str) -> Result<MaterialEntity, AppError>;
async fn list_categories(&self) -> Result<Vec<String>, AppError>;
async fn create_material(
&self,
cmd: CreateMaterialCommand,
) -> Result<MaterialListItem, AppError>;
async fn update_material(
&self,
id: Uuid,
actor_id: Uuid,
cmd: UpdateMaterialCommand,
) -> Result<MaterialListItem, AppError>;
async fn delete_material(&self, id: Uuid, actor_id: Uuid) -> Result<(), AppError>;
}
@@ -0,0 +1,137 @@
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
use zod_rs::prelude::*;
use crate::materials::domain::{MaterialEntity, MaterialListItem};
use imphnen_libs::ZodValidate;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateMaterialRequest {
#[zod(min_length(3), max_length(200))]
pub title: String,
#[zod(min_length(1), max_length(100))]
pub category: String,
#[zod(min_length(3), max_length(500))]
pub description: String,
#[zod(min_length(10))]
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cover_url: Option<String>,
}
impl ZodValidate for CreateMaterialRequest {
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
Self::validate_and_parse(value).map_err(|e| e.to_string())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateMaterialRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cover_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_published: Option<bool>,
}
impl ZodValidate for UpdateMaterialRequest {
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
Self::validate_and_parse(value).map_err(|e| e.to_string())
}
}
#[derive(Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MaterialResponse {
pub id: Uuid,
pub mentor_id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub description: String,
pub content: String,
pub cover_url: Option<String>,
pub is_published: bool,
pub created_at: String,
pub updated_at: String,
}
impl MaterialResponse {
pub fn from_entity(e: &MaterialEntity) -> Self {
Self {
id: e.id,
mentor_id: e.mentor_id,
title: e.title.clone(),
slug: e.slug.clone(),
category: e.category.clone(),
description: e.description.clone(),
content: e.content.clone(),
cover_url: e.cover_url.clone(),
is_published: e.is_published,
created_at: e.created_at.to_rfc3339(),
updated_at: e.updated_at.to_rfc3339(),
}
}
}
#[derive(Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MaterialListItemResponse {
pub id: Uuid,
pub mentor_id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub description: String,
pub cover_url: Option<String>,
pub is_published: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<&MaterialEntity> for MaterialListItemResponse {
fn from(e: &MaterialEntity) -> Self {
Self {
id: e.id,
mentor_id: e.mentor_id,
title: e.title.clone(),
slug: e.slug.clone(),
category: e.category.clone(),
description: e.description.clone(),
cover_url: e.cover_url.clone(),
is_published: e.is_published,
created_at: e.created_at.to_rfc3339(),
updated_at: e.updated_at.to_rfc3339(),
}
}
}
impl From<&MaterialListItem> for MaterialListItemResponse {
fn from(e: &MaterialListItem) -> Self {
Self {
id: e.id,
mentor_id: e.mentor_id,
title: e.title.clone(),
slug: e.slug.clone(),
category: e.category.clone(),
description: e.description.clone(),
cover_url: e.cover_url.clone(),
is_published: e.is_published,
created_at: e.created_at.to_rfc3339(),
updated_at: e.updated_at.to_rfc3339(),
}
}
}
pub type MaterialListResponse = PaginatorResponse<MaterialListItemResponse>;
@@ -0,0 +1,130 @@
use super::dto::{
CreateMaterialRequest, MaterialListItemResponse, MaterialResponse,
UpdateMaterialRequest,
};
use crate::materials::domain::{
CreateMaterialCommand, MaterialService, UpdateMaterialCommand,
};
use axum::{
Extension, extract::{Path, Query},
http::{HeaderMap, header::AUTHORIZATION},
response::IntoResponse,
};
use imphnen_libs::{ValidatedJson, decode_access_token};
use imphnen_utils::{ApiSuccess, AppError};
use paginator_utils::PaginatorResponse;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
fn extract_user_id(headers: &HeaderMap) -> Result<Uuid, AppError> {
let token = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
let claims = decode_access_token(token)
.map_err(|_| AppError::AuthenticationError("Token tidak valid".to_string()))?;
Uuid::parse_str(&claims.claims.user_id)
.map_err(|_| AppError::AuthenticationError("Token tidak valid".to_string()))
}
pub async fn get_materials_list(
Extension(service): Extension<Arc<dyn MaterialService>>,
Query(params): Query<HashMap<String, String>>,
) -> Result<impl IntoResponse, AppError> {
let page: u64 = params.get("page").and_then(|p| p.parse().ok()).unwrap_or(1);
let per_page: u64 = params
.get("per_page")
.and_then(|p| p.parse().ok())
.unwrap_or(10);
let category = params.get("category").cloned().filter(|c| !c.is_empty());
// public listing always shows published only
let res = service
.list_materials(page, per_page, category, true)
.await?;
let data: Vec<MaterialListItemResponse> =
res.data.iter().map(|e| e.into()).collect();
Ok(ApiSuccess(PaginatorResponse {
data,
meta: res.meta,
}))
}
pub async fn get_material_categories(
Extension(service): Extension<Arc<dyn MaterialService>>,
) -> Result<impl IntoResponse, AppError> {
let categories = service.list_categories().await?;
Ok(ApiSuccess(categories))
}
pub async fn get_material_by_slug(
Extension(service): Extension<Arc<dyn MaterialService>>,
Path(slug): Path<String>,
) -> Result<impl IntoResponse, AppError> {
let material = service.get_material_by_slug(&slug).await?;
if !material.is_published {
return Err(AppError::NotFoundError("Material not found".into()));
}
Ok(ApiSuccess(MaterialResponse::from_entity(&material)))
}
pub async fn get_material_by_id(
Extension(service): Extension<Arc<dyn MaterialService>>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
let material = service.get_material_by_id(id).await?;
if !material.is_published {
return Err(AppError::NotFoundError("Material not found".into()));
}
Ok(ApiSuccess(MaterialResponse::from_entity(&material)))
}
pub async fn post_create_material(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn MaterialService>>,
ValidatedJson(body): ValidatedJson<CreateMaterialRequest>,
) -> Result<impl IntoResponse, AppError> {
let user_id = extract_user_id(&headers)?;
let cmd = CreateMaterialCommand {
mentor_id: user_id,
title: body.title,
category: body.category,
description: body.description,
content: body.content,
cover_url: body.cover_url,
};
let material = service.create_material(cmd).await?;
Ok(ApiSuccess(MaterialListItemResponse::from(&material)))
}
pub async fn put_update_material(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn MaterialService>>,
Path(id): Path<Uuid>,
ValidatedJson(body): ValidatedJson<UpdateMaterialRequest>,
) -> Result<impl IntoResponse, AppError> {
let user_id = extract_user_id(&headers)?;
let cmd = UpdateMaterialCommand {
title: body.title,
category: body.category,
description: body.description,
content: body.content,
cover_url: body.cover_url,
is_published: body.is_published,
};
let material = service.update_material(id, user_id, cmd).await?;
Ok(ApiSuccess(MaterialListItemResponse::from(&material)))
}
pub async fn delete_material(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn MaterialService>>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
let user_id = extract_user_id(&headers)?;
service.delete_material(id, user_id).await?;
Ok(ApiSuccess(serde_json::json!({
"message": format!("Material {} deleted", id)
})))
}
@@ -0,0 +1,3 @@
pub mod dto;
pub mod handlers;
pub mod routes;
@@ -0,0 +1,41 @@
use super::handlers::{
delete_material, get_material_by_id, get_material_by_slug, get_material_categories,
get_materials_list, post_create_material, put_update_material,
};
use crate::materials::application::MaterialServiceImpl;
use crate::materials::domain::MaterialService;
use crate::materials::infrastructure::persistence::PostgresMaterialRepository;
use axum::{
Extension, Router, routing::{delete, get, post, put},
};
use imphnen_libs::AppState;
use sea_orm::DatabaseConnection;
use std::sync::Arc;
fn build_service(db: DatabaseConnection) -> Arc<dyn MaterialService> {
let repo = Box::new(PostgresMaterialRepository::new(db));
Arc::new(MaterialServiceImpl::new(repo))
}
pub fn materials_public_routes(db: DatabaseConnection) -> Router {
let service = build_service(db);
Router::new()
.route("/materials", get(get_materials_list))
.route("/materials/categories", get(get_material_categories))
.route("/materials/slug/{slug}", get(get_material_by_slug))
.route("/materials/{id}", get(get_material_by_id))
.layer(Extension(service))
}
pub fn materials_protected_routes(
db: DatabaseConnection,
state: Arc<AppState>,
) -> Router {
let service = build_service(db);
Router::new()
.route("/materials", post(post_create_material))
.route("/materials/{id}", put(put_update_material))
.route("/materials/{id}", delete(delete_material))
.layer(Extension(service))
.layer(Extension((*state).clone()))
}
@@ -0,0 +1,2 @@
pub mod http;
pub mod persistence;
@@ -0,0 +1,3 @@
pub mod postgres_material_repository;
pub use postgres_material_repository::PostgresMaterialRepository;
@@ -0,0 +1,152 @@
use async_trait::async_trait;
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
use sea_orm::{
ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait,
ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set,
};
use uuid::Uuid;
use crate::materials::domain::{MaterialEntity, MaterialRepository};
use imphnen_entities::seaorm::common::materials::{
ActiveModel, Column, Entity, Model,
};
use imphnen_utils::AppError;
#[derive(Clone)]
pub struct PostgresMaterialRepository {
db: DatabaseConnection,
}
impl PostgresMaterialRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
fn to_entity(model: Model) -> MaterialEntity {
MaterialEntity {
id: model.id,
mentor_id: model.mentor_id,
title: model.title,
slug: model.slug,
category: model.category,
description: model.description,
content: model.content,
cover_url: model.cover_url,
is_published: model.is_published,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}
#[async_trait]
impl MaterialRepository for PostgresMaterialRepository {
async fn find_all_paginated(
&self,
page: u64,
per_page: u64,
category: Option<String>,
published_only: bool,
) -> Result<PaginatorResponse<MaterialEntity>, AppError> {
let mut query = Entity::find();
if published_only {
query = query.filter(Column::IsPublished.eq(true));
}
if let Some(cat) = category.filter(|c| !c.is_empty()) {
query = query.filter(Column::Category.eq(cat));
}
query = query.order_by_desc(Column::CreatedAt);
let paginator = query.paginate(&self.db, per_page);
let total = paginator
.num_items()
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let items = paginator
.fetch_page(page.saturating_sub(1))
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let data = items.into_iter().map(Self::to_entity).collect();
let meta =
PaginatorResponseMeta::new(page as u32, per_page as u32, total as u32);
Ok(PaginatorResponse { data, meta })
}
async fn find_by_id(&self, id: Uuid) -> Result<MaterialEntity, AppError> {
let model = Entity::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| AppError::NotFoundError("Material not found".into()))?;
Ok(Self::to_entity(model))
}
async fn find_by_slug(&self, slug: &str) -> Result<MaterialEntity, AppError> {
let model = Entity::find()
.filter(Column::Slug.eq(slug))
.one(&self.db)
.await?
.ok_or_else(|| AppError::NotFoundError("Material not found".into()))?;
Ok(Self::to_entity(model))
}
async fn find_categories(&self) -> Result<Vec<String>, AppError> {
let rows: Vec<serde_json::Value> = Entity::find()
.select_only()
.column(Column::Category)
.distinct()
.into_json()
.all(&self.db)
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(rows
.into_iter()
.filter_map(|r| r["category"].as_str().map(|s| s.to_string()))
.collect())
}
async fn create(&self, entity: MaterialEntity) -> Result<Uuid, AppError> {
let model = ActiveModel {
id: Set(entity.id),
mentor_id: Set(entity.mentor_id),
title: Set(entity.title),
slug: Set(entity.slug),
category: Set(entity.category),
description: Set(entity.description),
content: Set(entity.content),
cover_url: Set(entity.cover_url),
is_published: Set(entity.is_published),
created_at: Set(entity.created_at),
updated_at: Set(entity.updated_at),
};
model.insert(&self.db).await?;
Ok(entity.id)
}
async fn update(&self, id: Uuid, entity: MaterialEntity) -> Result<(), AppError> {
let model = ActiveModel {
id: Set(id),
mentor_id: Set(entity.mentor_id),
title: Set(entity.title),
slug: Set(entity.slug),
category: Set(entity.category),
description: Set(entity.description),
content: Set(entity.content),
cover_url: Set(entity.cover_url),
is_published: Set(entity.is_published),
created_at: Set(entity.created_at),
updated_at: Set(entity.updated_at),
};
model.update(&self.db).await?;
Ok(())
}
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
let model = Entity::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| AppError::NotFoundError("Material not found".into()))?;
model.delete(&self.db).await?;
Ok(())
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod application;
pub mod domain;
pub mod infrastructure;
pub use infrastructure::http::routes::{
materials_protected_routes, materials_public_routes,
};
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "app_materials")]
pub struct Model {
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
pub id: Uuid,
#[sea_orm(indexed)]
pub mentor_id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub description: String,
pub content: String,
#[sea_orm(nullable)]
pub cover_url: Option<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
@@ -3,6 +3,7 @@ pub mod audit_log;
pub mod enum_impls;
pub mod enums;
pub mod events;
pub mod materials;
pub mod otp_cache;
pub mod payments;
pub mod rate_limit;
+8 -3
View File
@@ -6,9 +6,9 @@ use imphnen_cms::{
roadmap_public_routes, testimonials_protected_routes, testimonials_public_routes,
};
use imphnen_dimentorin::{
articles_protected_routes, articles_public_routes, mentors_protected_routes,
mentors_public_routes, payments_protected_routes, sessions_protected_routes,
sessions_public_routes,
articles_protected_routes, articles_public_routes, materials_protected_routes,
materials_public_routes, mentors_protected_routes, mentors_public_routes,
payments_protected_routes, sessions_protected_routes, sessions_public_routes,
};
use imphnen_gacha::gacha_router;
use imphnen_hackathon::hackathon_router;
@@ -76,6 +76,7 @@ pub async fn gateway_service(postgres_clients: PostgresClients) -> Router {
.merge(mentors_public_routes(db.clone(), Arc::clone(&state_arc)))
.merge(sessions_public_routes(db.clone()))
.merge(articles_public_routes(db.clone()))
.merge(materials_public_routes(db.clone()))
.merge(
Router::new()
.merge(mentors_protected_routes(db.clone(), Arc::clone(&state_arc)))
@@ -85,6 +86,10 @@ pub async fn gateway_service(postgres_clients: PostgresClients) -> Router {
db.clone(),
Arc::clone(&state_arc),
))
.merge(materials_protected_routes(
db.clone(),
Arc::clone(&state_arc),
))
.layer(from_fn(auth_middleware)),
);