Compare commits

...
10 Commits
Author SHA1 Message Date
maulanasdqnandClaude Opus 4.6 b68e362a02 fix: extract user_id (UUID) from JWT instead of email in session handlers
Nix Build & Deploy / deploy (push) Canceled after 0s
Nix Build & Deploy / build (push) Canceled after 0s
All session handlers were passing the user's email to service methods
that expected a UUID, causing "Invalid user ID" errors. Now extracts
user_id from JWT claims instead.

Fixed: get_my_sessions, post_book_session, put_update_session_status,
post_submit_feedback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:00:32 +07:00
maulanasdqnandClaude Opus 4.6 db44c5a51f feat: add roadmap CRUD module to CMS
New endpoints under /v1/landing/cms:
- GET /roadmap — public, paginated list
- GET /roadmap/detail/{id} — public, detail
- POST /roadmap/vote/{id} — public, increment votes
- POST /roadmap/create — protected (Administrator)
- PATCH /roadmap/update/{id} — protected (Administrator)
- DELETE /roadmap/delete/{id} — protected (Administrator)

Table: roadmap_items (id, title, description, status, votes, is_deleted, created_at, updated_at)
Status values: upcoming, in_progress, completed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:42:15 +07:00
maulanasdqnandClaude Opus 4.6 6570bbf752 fix: add all frontend origins to default CORS allowed list
Added backoffice, hackathon, qr, and infra subdomains to the default
CORS_ALLOWED_ORIGINS. Previously only gacha, imphnen.dev, and
dimentorin were allowed, causing Network Error on other apps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:04:48 +07:00
maulanasdqnandClaude Opus 4.6 5667a0d608 feat: add include query param and sessions to unified /me endpoint
GET /v1/iam/users/me now supports ?include=hackathon,qr,mentor,sessions
to control which module profiles are returned. Omitting the param
returns all modules. Also added dimentorin sessions data.

Examples:
  /v1/iam/users/me                        → all modules
  /v1/iam/users/me?include=hackathon      → only hackathon profile
  /v1/iam/users/me?include=mentor,sessions → mentor + sessions only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:28:55 +07:00
maulanasdqnandClaude Opus 4.6 a4bbc73c7e feat: unify /me endpoint to aggregate all module profiles
GET /v1/iam/users/me now returns hackathon, QR, and mentor profiles
alongside the core IAM user data. Module-specific profiles are
included as optional fields when the user exists in those modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:47:27 +07:00
maulanasdqnandClaude Opus 4.6 729335014f fix: use correct Swagger security scheme name across all modules
Changed "bearer_auth" to "Bearer" in utoipa security annotations
for QR and Hackathon handlers to match the registered SecurityAddon
scheme name. Fixes Swagger UI not sending Authorization header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:11:01 +07:00
maulanasdqnandClaude Opus 4.6 852e9652ee fix: correct middleware layer ordering in QR routes
Same issue as hackathon routes - qr_auth_middleware needs Arc<PgPool>
from Extension but was outermost layer. Swapped order so pool is
available when middleware runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:32:39 +07:00
maulanasdqnandClaude Opus 4.6 f225ee8969 fix: correct middleware layer ordering in hackathon routes
The hackathon_auth_middleware requires Arc<PgPool> from Extension,
but was applied as the outermost layer (running before Extension(pool)
was injected). Swapped layer order so pool Extension is outermost,
making it available when the auth middleware runs.

Fixes 500 errors on all /v1/hackathon/* authenticated endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:21:30 +07:00
maulanasdqnandClaude Opus 4.6 4ee00f1fe5 fix: resolve permission UUIDs to names in role DTO
The build_role_dto function was using permission UUIDs as both id and
name fields. Now resolves UUIDs to human-readable names via
PermissionsEnum::iter() lookup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:52:50 +07:00
maulanasdqnandClaude Sonnet 4.6 d014a94ea4 feat: add response examples to all hackathon and QR swagger endpoints
Each endpoint now shows a realistic JSON example (data wrapper + version)
so developers can see the exact response shape without calling the API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 02:37:06 +07:00
49 changed files with 1830 additions and 147 deletions
Generated
+1
View File
@@ -1970,6 +1970,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"sqlx",
"strum 0.27.2",
"strum_macros",
"tokio",
+2
View File
@@ -1,7 +1,9 @@
pub mod events;
pub mod roadmap;
pub mod testimonials;
pub mod qr;
pub use events::{events_protected_routes, events_public_routes};
pub use roadmap::{roadmap_protected_routes, roadmap_public_routes};
pub use testimonials::{testimonials_protected_routes, testimonials_public_routes};
pub use qr::qr_router;
@@ -20,12 +20,25 @@ use crate::qr::{
path = "/v1/qr/campaigns",
request_body = CreateCampaignRequest,
responses(
(status = 201, description = "Create a QR campaign"),
(status = 201, description = "Create a QR campaign",
example = json!({
"data": {
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"name": "Imphnen Hackathon 2025",
"url": "https://imphnen.dev/register",
"is_active": false,
"created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"expires_at": "2025-12-31T23:59:59Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Campaigns",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn create_campaign_handler(
Extension(service): Extension<Arc<dyn QrCampaignService>>,
@@ -47,12 +60,27 @@ pub async fn create_campaign_handler(
get,
path = "/v1/qr/campaigns",
responses(
(status = 200, description = "Admin: list all QR campaigns"),
(status = 200, description = "Admin: list all QR campaigns",
example = json!({
"data": [
{
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"name": "Imphnen Hackathon 2025",
"url": "https://imphnen.dev/register",
"is_active": true,
"created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"expires_at": "2025-12-31T23:59:59Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-05T00:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Campaigns",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn list_campaigns_handler(
Extension(service): Extension<Arc<dyn QrCampaignService>>,
@@ -72,12 +100,25 @@ pub async fn list_campaigns_handler(
path = "/v1/qr/campaigns/{id}/activate",
params(("id" = Uuid, Path, description = "Campaign ID")),
responses(
(status = 200, description = "Admin: activate a campaign"),
(status = 200, description = "Admin: activate a campaign (deactivates all others)",
example = json!({
"data": {
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"name": "Imphnen Hackathon 2025",
"url": "https://imphnen.dev/register",
"is_active": true,
"created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"expires_at": "2025-12-31T23:59:59Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-05T10:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Campaigns",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn activate_campaign_handler(
Extension(service): Extension<Arc<dyn QrCampaignService>>,
@@ -98,12 +139,13 @@ pub async fn activate_campaign_handler(
path = "/v1/qr/campaigns/{id}",
params(("id" = Uuid, Path, description = "Campaign ID")),
responses(
(status = 200, description = "Admin: delete a campaign"),
(status = 200, description = "Admin: delete a campaign",
example = json!({"message": "Campaign deleted successfully", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Campaigns",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn delete_campaign_handler(
Extension(service): Extension<Arc<dyn QrCampaignService>>,
@@ -126,11 +168,13 @@ pub async fn delete_campaign_handler(
post,
path = "/v1/qr/campaigns/process-image",
responses(
(status = 200, description = "Process QR code image (multipart/form-data with 'file' field)"),
(status = 200, description = "Process QR code image — send multipart/form-data with field 'file'. Returns PNG image bytes.",
content_type = "image/png"),
(status = 400, description = "No file provided or invalid image"),
(status = 401, description = "Unauthorized")
),
tag = "QR - Campaigns",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn process_image_handler(
Extension(service): Extension<Arc<dyn QrCampaignService>>,
@@ -36,6 +36,6 @@ pub fn qr_campaigns_routes(pool: Arc<PgPool>) -> Router {
.route("/campaigns/{id}", delete(delete_campaign_handler))
.route("/campaigns/process-image", post(process_image_handler))
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(qr_auth_middleware))
.layer(Extension(pool))
}
@@ -19,11 +19,23 @@ use crate::qr::{
get,
path = "/v1/qr/users/me",
responses(
(status = 200, description = "Get my QR user profile"),
(status = 200, description = "Get my QR user profile",
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"name": "Budi Santoso",
"role": "user",
"provider": "google",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "QR - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_me_handler(
Extension(service): Extension<Arc<dyn QrUserService>>,
@@ -38,11 +50,23 @@ pub async fn get_me_handler(
path = "/v1/qr/users/me",
request_body = UpdateProfileRequest,
responses(
(status = 200, description = "Update my QR user profile"),
(status = 200, description = "Update my QR user profile",
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "updated@example.com",
"name": "Budi Santoso Updated",
"role": "user",
"provider": "google",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "QR - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn update_me_handler(
Extension(service): Extension<Arc<dyn QrUserService>>,
@@ -61,12 +85,35 @@ pub async fn update_me_handler(
get,
path = "/v1/qr/users",
responses(
(status = 200, description = "Admin: list all QR users"),
(status = 200, description = "Admin: list all QR users",
example = json!({
"data": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"name": "Budi Santoso",
"role": "user",
"provider": "google",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
{
"id": "4gb96g75-6828-5673-c4gd-3d074g77bgb7",
"email": "admin@example.com",
"name": "Admin User",
"role": "admin",
"provider": "google",
"created_at": "2024-12-01T00:00:00Z",
"updated_at": "2024-12-01T00:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn list_users_handler(
Extension(service): Extension<Arc<dyn QrUserService>>,
@@ -87,12 +134,24 @@ pub async fn list_users_handler(
params(("id" = Uuid, Path, description = "User ID")),
request_body = UpdateRoleRequest,
responses(
(status = 200, description = "Admin: update user role"),
(status = 200, description = "Admin: update user role",
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"name": "Budi Santoso",
"role": "admin",
"provider": "google",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-20T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn update_role_handler(
Extension(service): Extension<Arc<dyn QrUserService>>,
@@ -114,12 +173,13 @@ pub async fn update_role_handler(
path = "/v1/qr/users/{id}",
params(("id" = Uuid, Path, description = "User ID")),
responses(
(status = 200, description = "Admin: delete QR user"),
(status = 200, description = "Admin: delete QR user",
example = json!({"message": "User deleted successfully", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "QR - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn delete_user_handler(
Extension(service): Extension<Arc<dyn QrUserService>>,
@@ -32,6 +32,6 @@ pub fn qr_users_routes(pool: Arc<PgPool>) -> Router {
.route("/users/{id}/role", put(update_role_handler))
.route("/users/{id}", delete(delete_user_handler))
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(qr_auth_middleware))
.layer(Extension(pool))
}
@@ -0,0 +1,3 @@
pub mod roadmap_service;
pub use roadmap_service::RoadmapServiceImpl;
@@ -0,0 +1,47 @@
use crate::roadmap::domain::{RoadmapEntity, RoadmapRepository, RoadmapService};
use async_trait::async_trait;
use imphnen_utils::AppError;
use paginator_rs::PaginationParams;
use paginator_utils::PaginatorResponse;
use std::sync::Arc;
use uuid::Uuid;
pub struct RoadmapServiceImpl {
repo: Arc<dyn RoadmapRepository>,
}
impl RoadmapServiceImpl {
pub fn new(repo: Arc<dyn RoadmapRepository>) -> Self {
Self { repo }
}
}
#[async_trait]
impl RoadmapService for RoadmapServiceImpl {
async fn list(
&self,
params: PaginationParams,
) -> Result<PaginatorResponse<RoadmapEntity>, AppError> {
self.repo.find_all(params).await
}
async fn get(&self, id: Uuid) -> Result<RoadmapEntity, AppError> {
self.repo.find_by_id(id).await
}
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError> {
self.repo.create(entity).await
}
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError> {
self.repo.update(entity).await
}
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
self.repo.delete(id).await
}
async fn vote(&self, id: Uuid) -> Result<(), AppError> {
self.repo.increment_votes(id).await
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod roadmap;
pub mod repository;
pub mod service;
pub use roadmap::RoadmapEntity;
pub use repository::RoadmapRepository;
pub use service::RoadmapService;
@@ -0,0 +1,19 @@
use super::roadmap::RoadmapEntity;
use async_trait::async_trait;
use imphnen_utils::AppError;
use paginator_rs::PaginationParams;
use paginator_utils::PaginatorResponse;
use uuid::Uuid;
#[async_trait]
pub trait RoadmapRepository: Send + Sync {
async fn find_all(
&self,
params: PaginationParams,
) -> Result<PaginatorResponse<RoadmapEntity>, AppError>;
async fn find_by_id(&self, id: Uuid) -> Result<RoadmapEntity, AppError>;
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError>;
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError>;
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
async fn increment_votes(&self, id: Uuid) -> Result<(), AppError>;
}
+14
View File
@@ -0,0 +1,14 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct RoadmapEntity {
pub id: Uuid,
pub title: String,
pub description: String,
pub status: String,
pub votes: i32,
pub is_deleted: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
+19
View File
@@ -0,0 +1,19 @@
use super::roadmap::RoadmapEntity;
use async_trait::async_trait;
use imphnen_utils::AppError;
use paginator_rs::PaginationParams;
use paginator_utils::PaginatorResponse;
use uuid::Uuid;
#[async_trait]
pub trait RoadmapService: Send + Sync {
async fn list(
&self,
params: PaginationParams,
) -> Result<PaginatorResponse<RoadmapEntity>, AppError>;
async fn get(&self, id: Uuid) -> Result<RoadmapEntity, AppError>;
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError>;
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError>;
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
async fn vote(&self, id: Uuid) -> Result<(), AppError>;
}
@@ -0,0 +1,98 @@
use crate::roadmap::domain::roadmap::RoadmapEntity;
use imphnen_libs::ZodValidate;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RoadmapCreateRequestDto {
pub title: String,
pub description: String,
#[schema(example = "upcoming")]
pub status: String,
}
impl ZodValidate for RoadmapCreateRequestDto {
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
}
}
impl From<RoadmapCreateRequestDto> for RoadmapEntity {
fn from(dto: RoadmapCreateRequestDto) -> Self {
RoadmapEntity {
id: Uuid::new_v4(),
title: dto.title,
description: dto.description,
status: dto.status,
votes: 0,
is_deleted: false,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RoadmapUpdateRequestDto {
pub title: String,
pub description: String,
#[schema(example = "upcoming")]
pub status: String,
}
impl ZodValidate for RoadmapUpdateRequestDto {
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RoadmapListItemDto {
pub id: String,
pub title: String,
pub description: String,
pub status: String,
pub votes: i32,
pub is_deleted: bool,
pub created_at: String,
}
impl From<RoadmapEntity> for RoadmapListItemDto {
fn from(e: RoadmapEntity) -> Self {
RoadmapListItemDto {
id: e.id.to_string(),
title: e.title,
description: e.description,
status: e.status,
votes: e.votes,
is_deleted: e.is_deleted,
created_at: e.created_at.to_rfc3339(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RoadmapDetailItemDto {
pub id: String,
pub title: String,
pub description: String,
pub status: String,
pub votes: i32,
pub created_at: String,
pub updated_at: String,
}
impl From<RoadmapEntity> for RoadmapDetailItemDto {
fn from(e: RoadmapEntity) -> Self {
RoadmapDetailItemDto {
id: e.id.to_string(),
title: e.title,
description: e.description,
status: e.status,
votes: e.votes,
created_at: e.created_at.to_rfc3339(),
updated_at: e.updated_at.to_rfc3339(),
}
}
}
@@ -0,0 +1,208 @@
use super::dto::{
RoadmapCreateRequestDto, RoadmapDetailItemDto, RoadmapListItemDto,
RoadmapUpdateRequestDto,
};
use crate::roadmap::domain::RoadmapService;
use axum::{
Extension,
extract::Path,
http::HeaderMap,
response::{IntoResponse, Response},
};
use imphnen_entities::ResponseSuccessDto;
use imphnen_iam::{PermissionsEnum, require_permissions};
use imphnen_libs::{AppState, ValidatedJson};
use imphnen_utils::AppError;
use imphnen_utils::{ApiMessage, ApiPaginated, ApiSuccess};
use paginator_axum::PaginationQuery;
use paginator_utils::PaginatorResponse;
use std::sync::Arc;
use uuid::Uuid;
#[utoipa::path(
get,
path = "/v1/landing/cms/roadmap",
params(
("page" = Option<i64>, Query, description = "Page number"),
("per_page" = Option<i64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search keyword"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Order ASC or DESC"),
),
responses(
(status = 200, description = "[PUBLIC] Get roadmap list")
),
tag = "Roadmap"
)]
pub async fn get_roadmap_list(
Extension(service): Extension<Arc<dyn RoadmapService>>,
PaginationQuery(params): PaginationQuery,
) -> Response {
match service.list(params).await {
Ok(result) => {
let mapped = PaginatorResponse {
data: result
.data
.into_iter()
.map(RoadmapListItemDto::from)
.collect::<Vec<_>>(),
meta: result.meta,
};
ApiPaginated(mapped).into_response()
}
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
.into_response(),
}
}
#[utoipa::path(
get,
path = "/v1/landing/cms/roadmap/detail/{id}",
params(
("id" = String, Path, description = "Roadmap item ID")
),
responses(
(status = 200, description = "[PUBLIC] Get roadmap item by ID", body = ResponseSuccessDto<RoadmapDetailItemDto>)
),
tag = "Roadmap"
)]
pub async fn get_roadmap_by_id(
Extension(service): Extension<Arc<dyn RoadmapService>>,
Path(id): Path<String>,
) -> Response {
let uuid = match Uuid::parse_str(&id) {
Ok(u) => u,
Err(e) => {
return ApiMessage::new(
axum::http::StatusCode::BAD_REQUEST,
format!("Invalid UUID: {e}"),
)
.into_response();
}
};
match service.get(uuid).await {
Ok(item) => ApiSuccess(RoadmapDetailItemDto::from(item)).into_response(),
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string())
.into_response(),
}
}
#[utoipa::path(
post,
security(("Bearer" = [])),
path = "/v1/landing/cms/roadmap/create",
request_body = RoadmapCreateRequestDto,
responses(
(status = 201, description = "[ADMIN] Create new roadmap item")
),
tag = "Roadmap"
)]
pub async fn post_create_roadmap(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn RoadmapService>>,
ValidatedJson(payload): ValidatedJson<RoadmapCreateRequestDto>,
) -> Result<impl IntoResponse, AppError> {
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
let entity = payload.into();
service.create(entity).await?;
Ok(ApiMessage::created("Roadmap item created"))
})
}
#[utoipa::path(
patch,
security(("Bearer" = [])),
path = "/v1/landing/cms/roadmap/update/{id}",
params(
("id" = String, Path, description = "Roadmap item ID")
),
request_body = RoadmapUpdateRequestDto,
responses(
(status = 200, description = "[ADMIN] Update roadmap item")
),
tag = "Roadmap"
)]
pub async fn patch_update_roadmap(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn RoadmapService>>,
Path(id): Path<String>,
ValidatedJson(payload): ValidatedJson<RoadmapUpdateRequestDto>,
) -> Result<impl IntoResponse, AppError> {
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
let uuid = Uuid::parse_str(&id)
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
let existing = service.get(uuid).await?;
let entity = crate::roadmap::domain::RoadmapEntity {
id: existing.id,
title: payload.title,
description: payload.description,
status: payload.status,
votes: existing.votes,
is_deleted: existing.is_deleted,
created_at: existing.created_at,
updated_at: chrono::Utc::now(),
};
service.update(entity).await?;
Ok(ApiMessage::ok("Roadmap item updated"))
})
}
#[utoipa::path(
delete,
security(("Bearer" = [])),
path = "/v1/landing/cms/roadmap/delete/{id}",
params(
("id" = String, Path, description = "Roadmap item ID")
),
responses(
(status = 200, description = "[ADMIN] Soft delete roadmap item")
),
tag = "Roadmap"
)]
pub async fn delete_roadmap(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn RoadmapService>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
let uuid = Uuid::parse_str(&id)
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
service.delete(uuid).await?;
Ok(ApiMessage::ok("Roadmap item deleted"))
})
}
#[utoipa::path(
post,
path = "/v1/landing/cms/roadmap/vote/{id}",
params(
("id" = String, Path, description = "Roadmap item ID")
),
responses(
(status = 200, description = "[PUBLIC] Vote for a roadmap item")
),
tag = "Roadmap"
)]
pub async fn post_vote_roadmap(
Extension(service): Extension<Arc<dyn RoadmapService>>,
Path(id): Path<String>,
) -> Response {
let uuid = match Uuid::parse_str(&id) {
Ok(u) => u,
Err(e) => {
return ApiMessage::new(
axum::http::StatusCode::BAD_REQUEST,
format!("Invalid UUID: {e}"),
)
.into_response();
}
};
match service.vote(uuid).await {
Ok(()) => ApiMessage::ok("Vote recorded").into_response(),
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
.into_response(),
}
}
@@ -0,0 +1,5 @@
pub mod dto;
pub mod handlers;
pub mod routes;
pub use routes::{roadmap_protected_routes, roadmap_public_routes};
@@ -0,0 +1,36 @@
use super::handlers::{
delete_roadmap, get_roadmap_by_id, get_roadmap_list, patch_update_roadmap,
post_create_roadmap, post_vote_roadmap,
};
use crate::roadmap::application::RoadmapServiceImpl;
use crate::roadmap::domain::RoadmapService;
use crate::roadmap::infrastructure::persistence::PostgresRoadmapRepository;
use axum::{
Extension, Router,
routing::{delete, get, patch, post},
};
use sea_orm::DatabaseConnection;
use std::sync::Arc;
fn build_service(db: DatabaseConnection) -> Arc<dyn RoadmapService> {
let repo = Arc::new(PostgresRoadmapRepository::new(db));
Arc::new(RoadmapServiceImpl::new(repo))
}
pub fn roadmap_public_routes(db: DatabaseConnection) -> Router {
let service = build_service(db);
Router::new()
.route("/roadmap", get(get_roadmap_list))
.route("/roadmap/detail/{id}", get(get_roadmap_by_id))
.route("/roadmap/vote/{id}", post(post_vote_roadmap))
.layer(Extension(service))
}
pub fn roadmap_protected_routes(db: DatabaseConnection) -> Router {
let service = build_service(db);
Router::new()
.route("/roadmap/create", post(post_create_roadmap))
.route("/roadmap/update/{id}", patch(patch_update_roadmap))
.route("/roadmap/delete/{id}", delete(delete_roadmap))
.layer(Extension(service))
}
@@ -0,0 +1,2 @@
pub mod http;
pub mod persistence;
@@ -0,0 +1,3 @@
pub mod postgres_roadmap_repository;
pub use postgres_roadmap_repository::PostgresRoadmapRepository;
@@ -0,0 +1,171 @@
use crate::roadmap::domain::{roadmap::RoadmapEntity, repository::RoadmapRepository};
use async_trait::async_trait;
use imphnen_entities::seaorm::common::roadmap_items::{
ActiveModel as RoadmapActiveModel, Column as RoadmapColumn, Entity as RoadmapEntity_,
Model as RoadmapModel,
};
use imphnen_utils::AppError;
use paginator_rs::{PaginationParams, SortDirection};
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
use sea_orm::prelude::*;
use sea_orm::{ActiveValue, Order, PaginatorTrait, QueryOrder};
use std::sync::Arc;
use uuid::Uuid;
fn to_entity(model: RoadmapModel) -> RoadmapEntity {
RoadmapEntity {
id: model.id,
title: model.title,
description: model.description,
status: model.status,
votes: model.votes,
is_deleted: model.is_deleted,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
pub struct PostgresRoadmapRepository {
db: Arc<DatabaseConnection>,
}
impl PostgresRoadmapRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db: Arc::new(db) }
}
}
#[async_trait]
impl RoadmapRepository for PostgresRoadmapRepository {
async fn find_all(
&self,
params: PaginationParams,
) -> Result<PaginatorResponse<RoadmapEntity>, AppError> {
let page = params.page.max(1);
let per_page = params.per_page.clamp(1, 100);
let mut query = RoadmapEntity_::find().filter(RoadmapColumn::IsDeleted.eq(false));
if let Some(ref search) = params.search {
query = query.filter(RoadmapColumn::Title.contains(&search.query));
}
query = match params.sort_by.as_deref() {
Some("title") => match params.sort_direction {
Some(SortDirection::Desc) => query.order_by(RoadmapColumn::Title, Order::Desc),
_ => query.order_by(RoadmapColumn::Title, Order::Asc),
},
Some("votes") => match params.sort_direction {
Some(SortDirection::Asc) => query.order_by(RoadmapColumn::Votes, Order::Asc),
_ => query.order_by(RoadmapColumn::Votes, Order::Desc),
},
_ => match params.sort_direction {
Some(SortDirection::Asc) => {
query.order_by(RoadmapColumn::CreatedAt, Order::Asc)
}
_ => query.order_by(RoadmapColumn::CreatedAt, Order::Desc),
},
};
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
let total = paginator
.num_items()
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let items = paginator
.fetch_page((page - 1) as u64)
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let data = items.into_iter().map(to_entity).collect();
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
Ok(PaginatorResponse { data, meta })
}
async fn find_by_id(&self, id: Uuid) -> Result<RoadmapEntity, AppError> {
let item = RoadmapEntity_::find_by_id(id)
.filter(RoadmapColumn::IsDeleted.eq(false))
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?;
Ok(to_entity(item))
}
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError> {
let active_model = RoadmapActiveModel {
id: ActiveValue::Set(entity.id),
title: ActiveValue::Set(entity.title),
description: ActiveValue::Set(entity.description),
status: ActiveValue::Set(entity.status),
votes: ActiveValue::Set(0),
is_deleted: ActiveValue::Set(false),
created_at: ActiveValue::Set(chrono::Utc::now()),
updated_at: ActiveValue::Set(chrono::Utc::now()),
};
RoadmapEntity_::insert(active_model)
.exec(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(())
}
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError> {
let mut active_model: RoadmapActiveModel = RoadmapEntity_::find_by_id(entity.id)
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?
.into();
active_model.title = ActiveValue::Set(entity.title);
active_model.description = ActiveValue::Set(entity.description);
active_model.status = ActiveValue::Set(entity.status);
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
active_model
.update(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(())
}
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
let mut active_model: RoadmapActiveModel = RoadmapEntity_::find_by_id(id)
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?
.into();
active_model.is_deleted = ActiveValue::Set(true);
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
active_model
.update(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(())
}
async fn increment_votes(&self, id: Uuid) -> Result<(), AppError> {
let item = RoadmapEntity_::find_by_id(id)
.filter(RoadmapColumn::IsDeleted.eq(false))
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?;
let new_votes = item.votes + 1;
let mut active_model: RoadmapActiveModel = item.into();
active_model.votes = ActiveValue::Set(new_votes);
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
active_model
.update(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(())
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod application;
pub mod domain;
pub mod infrastructure;
pub use infrastructure::http::{roadmap_protected_routes, roadmap_public_routes};
@@ -6,14 +6,25 @@ use super::super::dto::{
use crate::sessions::domain::SessionService;
use axum::{
extract::{Extension, Path},
http::HeaderMap,
http::{HeaderMap, header::AUTHORIZATION},
response::IntoResponse,
};
use imphnen_libs::ValidatedJson;
use imphnen_libs::{ValidatedJson, decode_access_token};
use imphnen_utils::AppError;
use imphnen_utils::{ApiSuccess, extract_email};
use imphnen_utils::ApiSuccess;
use std::sync::Arc;
fn extract_user_id(headers: &HeaderMap) -> Result<String, 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()))?;
Ok(claims.claims.user_id)
}
#[utoipa::path(
post,
path = "/v1/dimentorin/mentors/{id}/sessions/create",
@@ -36,11 +47,10 @@ pub async fn post_book_session(
Path(mentor_id): Path<String>,
ValidatedJson(dto): ValidatedJson<BookSessionRequestDto>,
) -> Result<impl IntoResponse, AppError> {
let user_email = extract_email(&headers)
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
let user_id = extract_user_id(&headers)?;
let resp = BookSessionResponseDto::from(
service
.book_session(mentor_id, user_email, dto.into())
.book_session(mentor_id, user_id, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
@@ -68,11 +78,10 @@ pub async fn put_update_session_status(
Path(session_id): Path<String>,
ValidatedJson(dto): ValidatedJson<UpdateSessionStatusRequestDto>,
) -> Result<impl IntoResponse, AppError> {
let user_email = extract_email(&headers)
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
let user_id = extract_user_id(&headers)?;
let resp = UpdateSessionStatusResponseDto::from(
service
.update_session_status(session_id, user_email, dto.into())
.update_session_status(session_id, user_id, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
@@ -101,11 +110,10 @@ pub async fn post_submit_feedback(
Path(session_id): Path<String>,
ValidatedJson(dto): ValidatedJson<SessionFeedbackRequestDto>,
) -> Result<impl IntoResponse, AppError> {
let user_email = extract_email(&headers)
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
let user_id = extract_user_id(&headers)?;
let resp = SessionFeedbackResponseDto::from(
service
.submit_feedback(session_id, user_email, dto.into())
.submit_feedback(session_id, user_id, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
@@ -5,6 +5,7 @@ use axum::{
http::HeaderMap,
response::IntoResponse,
};
use imphnen_libs::decode_access_token;
use imphnen_utils::AppError;
use imphnen_utils::{ApiSuccess, extract_email};
use serde::Deserialize;
@@ -85,10 +86,16 @@ pub async fn get_my_sessions(
Extension(service): Extension<Arc<dyn SessionService>>,
Query(filter): Query<SessionStatusFilter>,
) -> Result<impl IntoResponse, AppError> {
let user_email = extract_email(&headers)
let token = headers
.get(axum::http::header::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()))?;
let user_id = claims.claims.user_id;
let resp = SessionListResponseDto::from(
service.get_user_sessions(user_email, filter.status).await?,
service.get_user_sessions(user_id, filter.status).await?,
);
Ok(ApiSuccess(resp))
}
@@ -3,6 +3,7 @@ pub mod enum_impls;
pub mod enums;
pub mod events;
pub mod rate_limit;
pub mod roadmap_items;
pub mod testimonials;
pub mod types;
pub mod utils;
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "roadmap_items")]
pub struct Model {
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
pub id: Uuid,
#[sea_orm(not_null)]
pub title: String,
#[sea_orm(not_null)]
pub description: String,
#[sea_orm(not_null, default = "'upcoming'")]
pub status: String,
#[sea_orm(not_null, default = "0")]
pub votes: i32,
#[sea_orm(default = "false")]
pub is_deleted: bool,
#[sea_orm(not_null, default = "now()")]
pub created_at: DateTime<Utc>,
#[sea_orm(not_null, default = "now()")]
pub updated_at: DateTime<Utc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
+16
View File
@@ -3,6 +3,11 @@ use imphnen_cms::events::infrastructure::http::dto::{
EventsDetailItemDto, EventsListItemDto,
};
use imphnen_cms::events::infrastructure::http::handlers as events_controller;
use imphnen_cms::roadmap::infrastructure::http::dto::{
RoadmapCreateRequestDto, RoadmapDetailItemDto, RoadmapListItemDto,
RoadmapUpdateRequestDto,
};
use imphnen_cms::roadmap::infrastructure::http::handlers as roadmap_controller;
use imphnen_cms::qr::campaigns::infrastructure::http::dto::CreateCampaignRequest;
use imphnen_cms::qr::campaigns::infrastructure::http::handlers as qr_campaigns_controller;
use imphnen_cms::qr::users::infrastructure::http::dto::{
@@ -137,6 +142,9 @@ use utoipa::OpenApi;
events_controller::get_event_list, events_controller::get_event_by_id,
events_controller::post_create_event, events_controller::patch_update_event,
events_controller::delete_event,
roadmap_controller::get_roadmap_list, roadmap_controller::get_roadmap_by_id,
roadmap_controller::post_create_roadmap, roadmap_controller::patch_update_roadmap,
roadmap_controller::delete_roadmap, roadmap_controller::post_vote_roadmap,
testimonials_controller::get_testimonial_list, testimonials_controller::get_testimonial_by_id,
testimonials_controller::post_create_testimonial, testimonials_controller::patch_update_testimonial,
testimonials_controller::delete_testimonial,
@@ -198,6 +206,11 @@ use utoipa::OpenApi;
RolesListItemDto, RolesDetailItemDto, RolesCreateRequestDto, RolesUpdateRequestDto,
PermissionsCreateRequestDto, PermissionsItemDto,
UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto, UsersCreateRequestDto, FileUploadSchema,
imphnen_iam::users::infrastructure::http::dto::UsersMeResponseDto,
imphnen_iam::users::infrastructure::http::dto::HackathonProfileDto,
imphnen_iam::users::infrastructure::http::dto::QrProfileDto,
imphnen_iam::users::infrastructure::http::dto::MentorProfileDto,
imphnen_iam::users::infrastructure::http::dto::SessionProfileDto,
GachaClaimDetailDto, GachaClaimCreateRequestDto,
GachaCreditDto, GachaCreditAddRequestDto,
GachaItemDto, GachaItemCreateRequestDto,
@@ -209,6 +222,8 @@ use utoipa::OpenApi;
ResponseListSuccessDto<Vec<UsersListItemDto>>, ResponseSuccessDto<UsersDetailItemDto>,
ResponseListSuccessDto<Vec<PermissionsItemDto>>, ResponseSuccessDto<PermissionsItemDto>,
ResponseListSuccessDto<Vec<EventsListItemDto>>, ResponseSuccessDto<EventsDetailItemDto>,
RoadmapCreateRequestDto, RoadmapUpdateRequestDto, RoadmapListItemDto, RoadmapDetailItemDto,
ResponseListSuccessDto<Vec<RoadmapListItemDto>>, ResponseSuccessDto<RoadmapDetailItemDto>,
ResponseListSuccessDto<Vec<TestimonialsListItemDto>>, ResponseSuccessDto<TestimonialsDetailItemDto>,
TestimonialsCreateRequestDto, TestimonialsUpdateRequestDto,
MentorUserRegisterRequestDto, MentorRegisterFromTokenRequestDto, MentorRegisterResponseDto,
@@ -250,6 +265,7 @@ use utoipa::OpenApi;
(name = "Roles", description = "IAM — Role management (/v1/iam/roles)"),
(name = "Permissions", description = "IAM — Permission management (/v1/iam/permissions)"),
(name = "Events", description = "Landing CMS — Event management (/v1/landing/cms/events)"),
(name = "Roadmap", description = "Landing CMS — Roadmap management (/v1/landing/cms/roadmap)"),
(name = "Testimonials", description = "Landing CMS — Testimonial management (/v1/landing/cms/testimonials)"),
(name = "Mentors", description = "Dimentorin — Mentor management (/v1/dimentorin/mentors)"),
(name = "Mentors - Admin", description = "Dimentorin — Mentor admin endpoints (/v1/dimentorin/mentors)"),
+4 -2
View File
@@ -2,8 +2,8 @@ use axum::{
Extension, Router, middleware::from_fn, response::Redirect, routing::get,
};
use imphnen_cms::{
events_protected_routes, events_public_routes, qr_router, testimonials_protected_routes,
testimonials_public_routes,
events_protected_routes, events_public_routes, qr_router, roadmap_protected_routes,
roadmap_public_routes, testimonials_protected_routes, testimonials_public_routes,
};
use imphnen_dimentorin::{
mentors_protected_routes, mentors_public_routes, sessions_protected_routes,
@@ -62,10 +62,12 @@ pub async fn gateway_service(postgres_clients: PostgresClients) -> Router {
let cms_routes = Router::new()
.merge(testimonials_public_routes(db.clone()))
.merge(events_public_routes(db.clone()))
.merge(roadmap_public_routes(db.clone()))
.merge(
Router::new()
.merge(events_protected_routes(db.clone()))
.merge(testimonials_protected_routes(db.clone()))
.merge(roadmap_protected_routes(db.clone()))
.layer(from_fn(auth_middleware)),
);
@@ -17,12 +17,31 @@ use uuid::Uuid;
path = "/v1/hackathon/admin/users",
params(PageQuery),
responses(
(status = 200, description = "Admin: list all users"),
(status = 200, description = "Admin: list all users",
example = json!({
"data": {
"data": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"fullname": "Budi Santoso",
"avatar": null,
"is_active": true,
"is_admin": false,
"created_at": "2025-01-01T00:00:00Z"
}
],
"total": 42,
"page": 1,
"limit": 20
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_list_users(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -45,12 +64,24 @@ pub async fn admin_list_users(
path = "/v1/hackathon/admin/users/{user_id}",
params(("user_id" = Uuid, Path, description = "User ID")),
responses(
(status = 200, description = "Admin: get user by ID"),
(status = 200, description = "Admin: get user by ID",
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"fullname": "Budi Santoso",
"avatar": null,
"is_active": true,
"is_admin": false,
"created_at": "2025-01-01T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 403, description = "Forbidden - admin only"),
(status = 404, description = "User not found")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_get_user(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -66,11 +97,12 @@ pub async fn admin_get_user(
params(("user_id" = Uuid, Path, description = "User ID")),
request_body = SetAdminRequest,
responses(
(status = 200, description = "Admin: set user admin status"),
(status = 200, description = "Admin: set user admin status",
example = json!({"message": "User admin status updated", "version": "0.3.0"})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_set_admin(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -86,11 +118,12 @@ pub async fn admin_set_admin(
path = "/v1/hackathon/admin/users/{user_id}",
params(("user_id" = Uuid, Path, description = "User ID")),
responses(
(status = 200, description = "Admin: delete user"),
(status = 200, description = "Admin: delete user",
example = json!({"message": "User deleted", "version": "0.3.0"})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_delete_user(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -105,11 +138,29 @@ pub async fn admin_delete_user(
path = "/v1/hackathon/admin/teams",
params(PageQuery),
responses(
(status = 200, description = "Admin: list all teams"),
(status = 200, description = "Admin: list all teams",
example = json!({
"data": {
"data": [
{
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers",
"city": "Jakarta",
"visibility": "public",
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-01T00:00:00Z"
}
],
"total": 15,
"page": 1,
"limit": 20
},
"version": "0.3.0"
})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_list_teams(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -132,11 +183,12 @@ pub async fn admin_list_teams(
path = "/v1/hackathon/admin/teams/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Admin: delete team"),
(status = 200, description = "Admin: delete team",
example = json!({"message": "Team deleted", "version": "0.3.0"})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_delete_team(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -151,11 +203,29 @@ pub async fn admin_delete_team(
path = "/v1/hackathon/admin/submissions",
params(PageQuery),
responses(
(status = 200, description = "Admin: list all submissions"),
(status = 200, description = "Admin: list all submissions",
example = json!({
"data": {
"data": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack - Sustainability Monitor",
"status": "submitted",
"submitted_at": "2025-01-20T12:00:00Z",
"created_at": "2025-01-05T00:00:00Z"
}
],
"total": 8,
"page": 1,
"limit": 20
},
"version": "0.3.0"
})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_list_submissions(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -178,11 +248,12 @@ pub async fn admin_list_submissions(
path = "/v1/hackathon/admin/winners",
request_body = SetWinnerRequest,
responses(
(status = 200, description = "Admin: set winner"),
(status = 200, description = "Admin: set winner",
example = json!({"message": "Winner set", "version": "0.3.0"})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_set_winner(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -199,11 +270,12 @@ pub async fn admin_set_winner(
path = "/v1/hackathon/admin/winners/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Admin: remove winner"),
(status = 200, description = "Admin: remove winner",
example = json!({"message": "Winner removed", "version": "0.3.0"})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_remove_winner(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -217,11 +289,23 @@ pub async fn admin_remove_winner(
get,
path = "/v1/hackathon/admin/winners",
responses(
(status = 200, description = "Admin: list winners"),
(status = 200, description = "Admin: list winners",
example = json!({
"data": [
{
"id": "d4e5f6a7-b8c9-0123-defa-234567890123",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"rank": 1,
"prize": "Rp 10.000.000",
"created_at": "2025-02-01T10:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 403, description = "Forbidden - admin only")
),
tag = "Hackathon - Admin",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn admin_list_winners(
Extension(service): Extension<Arc<dyn AdminService>>,
@@ -33,8 +33,7 @@ pub fn hackathon_admin_routes(pool: Arc<PgPool>) -> Router {
)
.route("/admin/winners/{team_id}", delete(admin_remove_winner))
.layer(Extension(service))
.layer(Extension(pool.clone()))
.layer(from_fn(admin_only))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -10,7 +10,24 @@ use uuid::Uuid;
path = "/v1/hackathon/certificates/{user_id}",
params(("user_id" = Uuid, Path, description = "User ID")),
responses(
(status = 200, description = "Get certificate for user"),
(status = 200, description = "Get certificate for user",
body = inline(CertificateResponse),
example = json!({
"data": {
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fullname": "Budi Santoso",
"email": "budi@example.com",
"avatar": "https://cdn.example.com/avatar.png",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"team_name": "Rust Enjoyers",
"is_leader": true,
"project_name": "EcoTrack - Sustainability Monitor",
"submission_status": "confirmed",
"winner_rank": 1,
"winner_prize": "Rp 10.000.000"
},
"version": "0.3.0"
})),
(status = 404, description = "Certificate not found")
),
tag = "Hackathon - Certificates"
@@ -14,11 +14,36 @@ use uuid::Uuid;
path = "/v1/hackathon/chat/teams/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Get team chat messages"),
(status = 200, description = "Get team chat messages",
example = json!({
"data": [
{
"id": "f6a7b8c9-d0e1-2345-fab0-456789012345",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_fullname": "Budi Santoso",
"user_avatar": null,
"message": "Hey team, let's finalize the project name!",
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-01-15T09:00:00Z"
},
{
"id": "a7b8c9d0-e1f2-3456-abc0-567890123456",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "4gb96g75-6828-5673-c4gd-3d074g77bgb7",
"user_fullname": "Dewi Rahayu",
"user_avatar": "https://cdn.example.com/dewi.png",
"message": "I suggest EcoTrack!",
"created_at": "2025-01-15T09:05:00Z",
"updated_at": "2025-01-15T09:05:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Chat",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_team_messages_handler(
Extension(service): Extension<Arc<dyn ChatService>>,
@@ -37,11 +62,25 @@ pub async fn get_team_messages_handler(
params(("team_id" = Uuid, Path, description = "Team ID")),
request_body = SendMessageRequest,
responses(
(status = 200, description = "Send a message to team chat"),
(status = 200, description = "Send a message to team chat",
body = inline(MessageResponse),
example = json!({
"data": {
"id": "b8c9d0e1-f2a3-4567-bcd1-678901234567",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_fullname": "Budi Santoso",
"user_avatar": null,
"message": "Great, let's go with EcoTrack!",
"created_at": "2025-01-15T09:10:00Z",
"updated_at": "2025-01-15T09:10:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Chat",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn send_message_handler(
Extension(service): Extension<Arc<dyn ChatService>>,
@@ -60,12 +99,13 @@ pub async fn send_message_handler(
path = "/v1/hackathon/chat/messages/{message_id}",
params(("message_id" = Uuid, Path, description = "Message ID")),
responses(
(status = 200, description = "Delete a chat message"),
(status = 200, description = "Delete a chat message",
example = json!({"message": "Message deleted", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not message owner")
),
tag = "Hackathon - Chat",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn delete_message_handler(
Extension(service): Extension<Arc<dyn ChatService>>,
@@ -22,6 +22,6 @@ pub fn build_chat_routes(pool: Arc<PgPool>) -> Router {
)
.route("/chat/messages/{message_id}", delete(delete_message_handler))
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -13,11 +13,26 @@ use uuid::Uuid;
get,
path = "/v1/hackathon/invitations/my",
responses(
(status = 200, description = "Get my invitations"),
(status = 200, description = "Get my invitations",
example = json!({
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"team_name": "Rust Enjoyers",
"inviter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"inviter_fullname": "Budi Santoso",
"invitee_email": "newmember@example.com",
"status": "pending",
"created_at": "2025-01-10T00:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Invitations",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_my_invitations_handler(
Extension(service): Extension<Arc<dyn InvitationService>>,
@@ -35,12 +50,13 @@ pub async fn get_my_invitations_handler(
params(("invitation_id" = Uuid, Path, description = "Invitation ID")),
request_body = RespondToInvitationRequest,
responses(
(status = 200, description = "Respond to invitation"),
(status = 200, description = "Respond to invitation",
example = json!({"message": "Invitation accepted", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Invitation not found")
),
tag = "Hackathon - Invitations",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn respond_to_invitation_handler(
Extension(service): Extension<Arc<dyn InvitationService>>,
@@ -65,12 +81,26 @@ pub async fn respond_to_invitation_handler(
params(("team_id" = Uuid, Path, description = "Team ID")),
request_body = CreateInvitationRequest,
responses(
(status = 200, description = "Invite a member to team"),
(status = 200, description = "Invite a member to team",
body = inline(InvitationResponse),
example = json!({
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"team_name": "Rust Enjoyers",
"inviter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"inviter_fullname": "Budi Santoso",
"invitee_email": "newmember@example.com",
"status": "pending",
"created_at": "2025-01-10T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not team leader")
),
tag = "Hackathon - Invitations",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn invite_team_member_handler(
Extension(service): Extension<Arc<dyn InvitationService>>,
@@ -26,6 +26,6 @@ pub fn build_invitation_routes(pool: Arc<PgPool>) -> Router {
post(invite_team_member_handler),
)
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -15,11 +15,26 @@ use uuid::Uuid;
params(("team_id" = Uuid, Path, description = "Team ID")),
request_body = CreateJoinRequestRequest,
responses(
(status = 200, description = "Create a join request"),
(status = 200, description = "Create a join request",
body = inline(JoinRequestResponse),
example = json!({
"data": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_fullname": "Dewi Rahayu",
"user_email": "dewi@example.com",
"user_avatar": null,
"message": "I would love to join your team, I have experience in backend development",
"status": "pending",
"created_at": "2025-01-10T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Join Requests",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn create_join_request_handler(
Extension(service): Extension<Arc<dyn JoinRequestService>>,
@@ -37,11 +52,27 @@ pub async fn create_join_request_handler(
get,
path = "/v1/hackathon/join-requests/my",
responses(
(status = 200, description = "Get my join requests"),
(status = 200, description = "Get my join requests",
example = json!({
"data": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_fullname": "Dewi Rahayu",
"user_email": "dewi@example.com",
"user_avatar": null,
"message": "I would love to join your team",
"status": "pending",
"created_at": "2025-01-10T00:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Join Requests",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_my_join_requests_handler(
Extension(service): Extension<Arc<dyn JoinRequestService>>,
@@ -58,12 +89,28 @@ pub async fn get_my_join_requests_handler(
path = "/v1/hackathon/join-requests/teams/{team_id}/pending",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Get pending join requests for team"),
(status = 200, description = "Get pending join requests for team",
example = json!({
"data": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_fullname": "Dewi Rahayu",
"user_email": "dewi@example.com",
"user_avatar": null,
"message": "I would love to join your team",
"status": "pending",
"created_at": "2025-01-10T00:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not team leader")
),
tag = "Hackathon - Join Requests",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_team_join_requests_handler(
Extension(service): Extension<Arc<dyn JoinRequestService>>,
@@ -84,12 +131,13 @@ pub async fn get_team_join_requests_handler(
params(("request_id" = Uuid, Path, description = "Join request ID")),
request_body = RespondToJoinRequestRequest,
responses(
(status = 200, description = "Respond to join request"),
(status = 200, description = "Respond to join request",
example = json!({"message": "Join request accepted", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not team leader")
),
tag = "Hackathon - Join Requests",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn respond_to_join_request_handler(
Extension(service): Extension<Arc<dyn JoinRequestService>>,
@@ -30,6 +30,6 @@ pub fn build_join_request_routes(pool: Arc<PgPool>) -> Router {
post(respond_to_join_request_handler),
)
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -10,11 +10,18 @@ use std::sync::Arc;
path = "/v1/hackathon/upload",
request_body = UploadRequest,
responses(
(status = 200, description = "Upload a file"),
(status = 200, description = "Upload a file (base64 encoded)",
body = inline(UploadResponse),
example = json!({
"data": {
"url": "https://minio.example.com/uploads/3fa85f64-5717-4562-b3fc-2c963f66afa6/document.pdf"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Storage",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn upload_file_handler(
Extension(service): Extension<Arc<dyn StorageService>>,
@@ -38,11 +45,18 @@ pub async fn upload_file_handler(
path = "/v1/hackathon/upload/avatar",
request_body = UploadRequest,
responses(
(status = 200, description = "Upload avatar image"),
(status = 200, description = "Upload avatar image (base64 encoded)",
body = inline(UploadResponse),
example = json!({
"data": {
"url": "https://minio.example.com/avatars/3fa85f64-5717-4562-b3fc-2c963f66afa6/avatar.png"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Storage",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn upload_avatar_handler(
Extension(service): Extension<Arc<dyn StorageService>>,
@@ -66,11 +80,18 @@ pub async fn upload_avatar_handler(
path = "/v1/hackathon/upload/team",
request_body = UploadRequest,
responses(
(status = 200, description = "Upload team image"),
(status = 200, description = "Upload team logo or banner (base64 encoded)",
body = inline(UploadResponse),
example = json!({
"data": {
"url": "https://minio.example.com/teams/3fa85f64-5717-4562-b3fc-2c963f66afa6/logo.png"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Storage",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn upload_team_handler(
Extension(service): Extension<Arc<dyn StorageService>>,
@@ -94,11 +115,18 @@ pub async fn upload_team_handler(
path = "/v1/hackathon/upload/submission",
request_body = UploadRequest,
responses(
(status = 200, description = "Upload submission file"),
(status = 200, description = "Upload submission screenshot (base64 encoded)",
body = inline(UploadResponse),
example = json!({
"data": {
"url": "https://minio.example.com/submissions/3fa85f64-5717-4562-b3fc-2c963f66afa6/screenshot.png"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Storage",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn upload_submission_handler(
Extension(service): Extension<Arc<dyn StorageService>>,
@@ -18,6 +18,6 @@ pub fn hackathon_storage_routes(
.route("/upload/team", post(upload_team_handler))
.route("/upload/submission", post(upload_submission_handler))
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -6,17 +6,37 @@ use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
use std::sync::Arc;
use uuid::Uuid;
#[utoipa::path(
post,
path = "/v1/hackathon/submissions/teams/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
request_body = CreateSubmissionRequest,
responses(
(status = 200, description = "Create submission for team"),
(status = 200, description = "Create submission for team",
body = inline(SubmissionResponse),
example = json!({
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack - Sustainability Monitor",
"description": "A real-time environmental monitoring platform",
"repository_url": "https://github.com/team/ecotrack",
"demo_url": "https://ecotrack.example.com",
"presentation_url": null,
"screenshots": [],
"status": "draft",
"submitted_at": null,
"submitted_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-05T00:00:00Z",
"updated_at": "2025-01-05T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Submissions",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn create_submission_handler(
Extension(service): Extension<Arc<dyn SubmissionService>>,
@@ -35,12 +55,31 @@ pub async fn create_submission_handler(
path = "/v1/hackathon/submissions/teams/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Get team submission"),
(status = 200, description = "Get team submission",
body = inline(SubmissionResponse),
example = json!({
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack - Sustainability Monitor",
"description": "A real-time environmental monitoring platform",
"repository_url": "https://github.com/team/ecotrack",
"demo_url": "https://ecotrack.example.com",
"presentation_url": "https://slides.example.com/ecotrack",
"screenshots": ["https://cdn.example.com/ss1.png"],
"status": "submitted",
"submitted_at": "2025-01-20T12:00:00Z",
"submitted_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-05T00:00:00Z",
"updated_at": "2025-01-20T12:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Not found")
(status = 404, description = "Submission not found")
),
tag = "Hackathon - Submissions",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_team_submission_handler(
Extension(service): Extension<Arc<dyn SubmissionService>>,
@@ -57,11 +96,30 @@ pub async fn get_team_submission_handler(
params(("submission_id" = Uuid, Path, description = "Submission ID")),
request_body = UpdateSubmissionRequest,
responses(
(status = 200, description = "Update submission"),
(status = 200, description = "Update submission",
body = inline(SubmissionResponse),
example = json!({
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack v2 - Advanced Sustainability Monitor",
"description": "Updated description with more features",
"repository_url": "https://github.com/team/ecotrack",
"demo_url": "https://ecotrack-v2.example.com",
"presentation_url": "https://slides.example.com/ecotrack-v2",
"screenshots": ["https://cdn.example.com/ss1.png", "https://cdn.example.com/ss2.png"],
"status": "draft",
"submitted_at": null,
"submitted_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-05T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Submissions",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn update_submission_handler(
Extension(service): Extension<Arc<dyn SubmissionService>>,
@@ -80,11 +138,30 @@ pub async fn update_submission_handler(
path = "/v1/hackathon/submissions/{submission_id}/submit",
params(("submission_id" = Uuid, Path, description = "Submission ID")),
responses(
(status = 200, description = "Submit project"),
(status = 200, description = "Submit project for review",
body = inline(SubmissionResponse),
example = json!({
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack - Sustainability Monitor",
"description": "A real-time environmental monitoring platform",
"repository_url": "https://github.com/team/ecotrack",
"demo_url": "https://ecotrack.example.com",
"presentation_url": "https://slides.example.com/ecotrack",
"screenshots": ["https://cdn.example.com/ss1.png"],
"status": "submitted",
"submitted_at": "2025-01-20T12:00:00Z",
"submitted_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-05T00:00:00Z",
"updated_at": "2025-01-20T12:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Submissions",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn submit_project_handler(
Extension(service): Extension<Arc<dyn SubmissionService>>,
@@ -100,11 +177,30 @@ pub async fn submit_project_handler(
path = "/v1/hackathon/submissions/{submission_id}/confirm",
params(("submission_id" = Uuid, Path, description = "Submission ID")),
responses(
(status = 200, description = "Confirm submission"),
(status = 200, description = "Confirm submission (admin)",
body = inline(SubmissionResponse),
example = json!({
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack - Sustainability Monitor",
"description": "A real-time environmental monitoring platform",
"repository_url": "https://github.com/team/ecotrack",
"demo_url": "https://ecotrack.example.com",
"presentation_url": "https://slides.example.com/ecotrack",
"screenshots": ["https://cdn.example.com/ss1.png"],
"status": "confirmed",
"submitted_at": "2025-01-20T12:00:00Z",
"submitted_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-05T00:00:00Z",
"updated_at": "2025-01-21T08:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Submissions",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn confirm_submission_handler(
Extension(service): Extension<Arc<dyn SubmissionService>>,
@@ -122,11 +218,30 @@ pub async fn confirm_submission_handler(
path = "/v1/hackathon/submissions/{submission_id}/cancel",
params(("submission_id" = Uuid, Path, description = "Submission ID")),
responses(
(status = 200, description = "Cancel submission"),
(status = 200, description = "Cancel submission",
body = inline(SubmissionResponse),
example = json!({
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"project_name": "EcoTrack - Sustainability Monitor",
"description": "A real-time environmental monitoring platform",
"repository_url": "https://github.com/team/ecotrack",
"demo_url": "https://ecotrack.example.com",
"presentation_url": null,
"screenshots": [],
"status": "cancelled",
"submitted_at": null,
"submitted_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2025-01-05T00:00:00Z",
"updated_at": "2025-01-22T10:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Submissions",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn cancel_submission_handler(
Extension(service): Extension<Arc<dyn SubmissionService>>,
@@ -37,6 +37,6 @@ pub fn hackathon_submissions_routes(pool: Arc<PgPool>) -> Router {
post(cancel_submission_handler),
)
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -18,11 +18,41 @@ use uuid::Uuid;
path = "/v1/hackathon/teams",
request_body = CreateTeamRequest,
responses(
(status = 200, description = "Create a new team"),
(status = 200, description = "Create a new team",
body = inline(TeamResponse),
example = json!({
"data": {
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers",
"description": "A team that loves Rust",
"city": "Jakarta",
"visibility": "public",
"logo": null,
"banner": null,
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"leader": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "leader@example.com",
"fullname": "Budi Santoso",
"avatar": null,
"phone_number": null,
"location": "Jakarta",
"bio": null,
"skills": ["Rust"],
"is_active": true
},
"members": [],
"member_count": 1,
"has_submission": false,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Teams",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn create_team_handler(
Extension(service): Extension<Arc<dyn TeamService>>,
@@ -38,7 +68,57 @@ pub async fn create_team_handler(
path = "/v1/hackathon/teams/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Get team by ID"),
(status = 200, description = "Get team by ID",
body = inline(TeamResponse),
example = json!({
"data": {
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers",
"description": "A team that loves Rust",
"city": "Jakarta",
"visibility": "public",
"logo": null,
"banner": null,
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"leader": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "leader@example.com",
"fullname": "Budi Santoso",
"avatar": null,
"phone_number": null,
"location": "Jakarta",
"bio": null,
"skills": ["Rust"],
"is_active": true
},
"members": [
{
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "leader@example.com",
"fullname": "Budi Santoso",
"avatar": null,
"phone_number": null,
"location": "Jakarta",
"bio": null,
"skills": ["Rust"],
"is_active": true
},
"role": "leader",
"status": "active",
"joined_at": "2025-01-01T00:00:00Z"
}
],
"member_count": 1,
"has_submission": false,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 404, description = "Team not found")
),
tag = "Hackathon - Teams"
@@ -56,7 +136,34 @@ pub async fn get_team_handler(
path = "/v1/hackathon/teams/browse",
params(BrowseTeamsQuery),
responses(
(status = 200, description = "Browse teams with filters")
(status = 200, description = "Browse teams with filters",
body = inline(TeamListResponse),
example = json!({
"data": {
"data": [
{
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers",
"description": "A team that loves Rust",
"city": "Jakarta",
"visibility": "public",
"logo": null,
"banner": null,
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"leader": null,
"members": null,
"member_count": 3,
"has_submission": false,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 10
},
"version": "0.3.0"
}))
),
tag = "Hackathon - Teams"
)]
@@ -80,11 +187,32 @@ pub async fn browse_teams_handler(
get,
path = "/v1/hackathon/teams/my",
responses(
(status = 200, description = "Get my teams"),
(status = 200, description = "Get my teams",
example = json!({
"data": [
{
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers",
"description": "A team that loves Rust",
"city": "Jakarta",
"visibility": "public",
"logo": null,
"banner": null,
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"leader": null,
"members": null,
"member_count": 3,
"has_submission": false,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Teams",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_my_teams_handler(
Extension(service): Extension<Arc<dyn TeamService>>,
@@ -108,12 +236,32 @@ pub async fn get_my_teams_handler(
params(("team_id" = Uuid, Path, description = "Team ID")),
request_body = UpdateTeamRequest,
responses(
(status = 200, description = "Update team"),
(status = 200, description = "Update team",
body = inline(TeamResponse),
example = json!({
"data": {
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers Updated",
"description": "We love Rust and systems programming",
"city": "Bandung",
"visibility": "public",
"logo": "https://cdn.example.com/team-logo.png",
"banner": null,
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"leader": null,
"members": null,
"member_count": 3,
"has_submission": false,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not team leader")
),
tag = "Hackathon - Teams",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn update_team_handler(
Extension(service): Extension<Arc<dyn TeamService>>,
@@ -132,12 +280,13 @@ pub async fn update_team_handler(
path = "/v1/hackathon/teams/{team_id}",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Delete team"),
(status = 200, description = "Delete team",
example = json!({"message": "Team deleted successfully", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not team leader")
),
tag = "Hackathon - Teams",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn delete_team_handler(
Extension(service): Extension<Arc<dyn TeamService>>,
@@ -153,11 +302,12 @@ pub async fn delete_team_handler(
path = "/v1/hackathon/teams/{team_id}/leave",
params(("team_id" = Uuid, Path, description = "Team ID")),
responses(
(status = 200, description = "Leave team"),
(status = 200, description = "Leave team",
example = json!({"message": "Left team successfully", "version": "0.3.0"})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Teams",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn leave_team_handler(
Extension(service): Extension<Arc<dyn TeamService>>,
@@ -176,12 +326,13 @@ pub async fn leave_team_handler(
("member_id" = Uuid, Path, description = "Member user ID")
),
responses(
(status = 200, description = "Remove team member"),
(status = 200, description = "Remove team member",
example = json!({"message": "Member removed successfully", "version": "0.3.0"})),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden")
(status = 403, description = "Forbidden - not team leader")
),
tag = "Hackathon - Teams",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn remove_member_handler(
Extension(service): Extension<Arc<dyn TeamService>>,
@@ -33,8 +33,8 @@ pub fn build_team_routes(pool: Arc<PgPool>) -> Router {
delete(remove_member_handler),
)
.layer(Extension(service))
.layer(Extension(pool.clone()))
.layer(from_fn(hackathon_auth_middleware));
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool.clone()));
Router::new().merge(public).merge(protected)
}
@@ -10,11 +10,28 @@ use uuid::Uuid;
get,
path = "/v1/hackathon/users/me",
responses(
(status = 200, description = "Get my hackathon user profile"),
(status = 200, description = "Get my hackathon user profile",
body = inline(UserResponse),
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"fullname": "Budi Santoso",
"avatar": "https://cdn.example.com/avatar.png",
"phone_number": "+6281234567890",
"location": "Jakarta",
"bio": "Backend developer",
"skills": ["Rust", "Go", "PostgreSQL"],
"is_active": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-10T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn get_me_handler(
Extension(service): Extension<Arc<dyn HackathonUserService>>,
@@ -29,11 +46,28 @@ pub async fn get_me_handler(
path = "/v1/hackathon/users/me",
request_body = UpdateUserRequest,
responses(
(status = 200, description = "Update my hackathon user profile"),
(status = 200, description = "Update my hackathon user profile",
body = inline(UserResponse),
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"fullname": "Budi Santoso",
"avatar": "https://cdn.example.com/avatar.png",
"phone_number": "+6281234567890",
"location": "Surabaya",
"bio": "Backend developer with 5 years of experience",
"skills": ["Rust", "Go", "PostgreSQL", "Redis"],
"is_active": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 401, description = "Unauthorized")
),
tag = "Hackathon - Users",
security(("bearer_auth" = []))
security(("Bearer" = []))
)]
pub async fn update_me_handler(
Extension(service): Extension<Arc<dyn HackathonUserService>>,
@@ -49,7 +83,24 @@ pub async fn update_me_handler(
path = "/v1/hackathon/users/{user_id}",
params(("user_id" = Uuid, Path, description = "User ID")),
responses(
(status = 200, description = "Get hackathon user by ID"),
(status = 200, description = "Get hackathon user by ID",
body = inline(UserResponse),
example = json!({
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "user@example.com",
"fullname": "Budi Santoso",
"avatar": null,
"phone_number": null,
"location": "Bandung",
"bio": null,
"skills": ["JavaScript", "React"],
"is_active": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"version": "0.3.0"
})),
(status = 404, description = "User not found")
),
tag = "Hackathon - Users"
@@ -67,7 +118,20 @@ pub async fn get_user_handler(
path = "/v1/hackathon/users/{user_id}/teams",
params(("user_id" = Uuid, Path, description = "User ID")),
responses(
(status = 200, description = "Get teams for a hackathon user"),
(status = 200, description = "Get teams for a hackathon user",
example = json!({
"data": [
{
"id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"name": "Rust Enjoyers",
"city": "Jakarta",
"visibility": "public",
"leader_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"member_count": 3
}
],
"version": "0.3.0"
})),
(status = 404, description = "User not found")
),
tag = "Hackathon - Users"
@@ -19,6 +19,6 @@ pub fn hackathon_users_routes(pool: Arc<PgPool>) -> Router {
.route("/users/{user_id}", get(get_user_handler))
.route("/users/{user_id}/teams", get(get_user_teams_handler))
.layer(Extension(service))
.layer(Extension(pool))
.layer(from_fn(hackathon_auth_middleware))
.layer(Extension(pool))
}
@@ -8,7 +8,30 @@ use std::sync::Arc;
get,
path = "/v1/hackathon/winners",
responses(
(status = 200, description = "List hackathon winners")
(status = 200, description = "List hackathon winners",
example = json!({
"data": [
{
"id": "d4e5f6a7-b8c9-0123-defa-234567890123",
"team_id": "7c3a1d2e-8f4b-4c5a-9d6e-1f2a3b4c5d6e",
"team_name": "Rust Enjoyers",
"rank": 1,
"prize": "Rp 10.000.000",
"announced_at": "2025-02-01T10:00:00Z",
"created_at": "2025-02-01T10:00:00Z"
},
{
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"team_id": "8d4b2e3f-9a5c-4d6b-0e7f-2a3b4c5d6e7f",
"team_name": "Go Builders",
"rank": 2,
"prize": "Rp 5.000.000",
"announced_at": "2025-02-01T10:00:00Z",
"created_at": "2025-02-01T10:00:00Z"
}
],
"version": "0.3.0"
}))
),
tag = "Hackathon - Winners"
)]
+1
View File
@@ -23,6 +23,7 @@ zod-rs.workspace = true
zod-rs-util.workspace = true
axum-test.workspace = true
sea-orm.workspace = true
sqlx.workspace = true
rand.workspace = true
tokio.workspace = true
chrono.workspace = true
@@ -131,6 +131,65 @@ impl From<&UsersDetailQueryDto> for UsersDetailItemDto {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UsersMeResponseDto {
#[serde(flatten)]
pub user: UsersDetailItemDto,
#[serde(skip_serializing_if = "Option::is_none")]
pub hackathon: Option<HackathonProfileDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub qr: Option<QrProfileDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentor: Option<MentorProfileDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sessions: Option<Vec<SessionProfileDto>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionProfileDto {
pub id: String,
pub topic: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub role: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonProfileDto {
pub is_admin: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skills: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct QrProfileDto {
pub role: String,
pub provider: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MentorProfileDto {
pub mentor_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_company: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub years_of_experience: Option<i32>,
}
impl From<UserListItem> for UsersListItemDto {
fn from(item: UserListItem) -> Self {
Self {
@@ -1,7 +1,15 @@
use super::super::dto::{UsersDetailItemDto, UsersListItemDto};
use super::super::dto::{
HackathonProfileDto, MentorProfileDto, QrProfileDto, SessionProfileDto,
UsersDetailItemDto, UsersListItemDto, UsersMeResponseDto,
};
use crate::require_permissions;
use crate::users::domain::UserService;
use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse};
use axum::{
Extension,
extract::{Path, Query},
http::HeaderMap,
response::IntoResponse,
};
use imphnen_entities::{
PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto,
};
@@ -9,9 +17,18 @@ use imphnen_libs::AppState;
use imphnen_utils::{ApiPaginated, ApiSuccess, AppError};
use paginator_axum::PaginationQuery;
use paginator_utils::PaginatorResponse;
use sea_orm::DatabaseConnection;
use serde::Deserialize;
use std::collections::HashSet;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
pub struct MeQueryParams {
/// Comma-separated list of modules to include: hackathon,qr,mentor,sessions
pub include: Option<String>,
}
#[utoipa::path(
get,
path = "/v1/iam/users",
@@ -82,8 +99,11 @@ pub async fn get_user_by_id(
get,
path = "/v1/iam/users/me",
security(("Bearer" = [])),
params(
("include" = Option<String>, Query, description = "Comma-separated modules to include: hackathon,qr,mentor,sessions. Omit to include all."),
),
responses(
(status = 200, description = "[USER] Get current user", body = ResponseSuccessDto<UsersDetailItemDto>)
(status = 200, description = "[USER] Get current user profile (unified across all modules)", body = ResponseSuccessDto<UsersMeResponseDto>)
),
tag = "Users"
)]
@@ -91,6 +111,7 @@ pub async fn get_user_me(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn UserService>>,
Query(params): Query<MeQueryParams>,
) -> Result<impl IntoResponse, AppError> {
let (claims, _) = crate::permissions_guard(
headers,
@@ -98,9 +119,186 @@ pub async fn get_user_me(
vec![],
)
.await?;
let user = service.get_me(claims.user_id).await?;
let user = service.get_me(claims.user_id.clone()).await?;
if user.is_deleted {
return Err(AppError::NotFoundError("User not found".to_string()));
}
Ok(ApiSuccess(UsersDetailItemDto::from(user)))
let user_dto = UsersDetailItemDto::from(user);
let user_uuid = Uuid::parse_str(&claims.user_id).ok();
let db = &state.postgres_connection.conn;
let includes: Option<HashSet<String>> = params.include.map(|s| {
s.split(',').map(|v| v.trim().to_lowercase()).collect()
});
let should_include = |module: &str| -> bool {
match &includes {
None => true,
Some(set) => set.contains(module),
}
};
let hackathon = if should_include("hackathon") {
fetch_hackathon_profile(db, user_uuid).await
} else {
None
};
let qr = if should_include("qr") {
fetch_qr_profile(db, user_uuid).await
} else {
None
};
let mentor = if should_include("mentor") {
fetch_mentor_profile(db, user_uuid).await
} else {
None
};
let sessions = if should_include("sessions") {
fetch_sessions(db, user_uuid).await
} else {
None
};
Ok(ApiSuccess(UsersMeResponseDto {
user: user_dto,
hackathon,
qr,
mentor,
sessions,
}))
}
async fn fetch_hackathon_profile(
db: &DatabaseConnection,
user_id: Option<Uuid>,
) -> Option<HackathonProfileDto> {
let uid = user_id?;
let pool = db.get_postgres_connection_pool();
sqlx::query_as::<_, HackathonRow>(
"SELECT COALESCE(is_admin, false) as is_admin, phone_number, location, bio, skills FROM hackathon_users WHERE id = $1",
)
.bind(uid)
.fetch_optional(pool)
.await
.ok()?
.map(|h| HackathonProfileDto {
is_admin: h.is_admin,
phone_number: h.phone_number,
location: h.location,
bio: h.bio,
skills: h.skills,
})
}
async fn fetch_qr_profile(
db: &DatabaseConnection,
user_id: Option<Uuid>,
) -> Option<QrProfileDto> {
let uid = user_id?;
let pool = db.get_postgres_connection_pool();
sqlx::query_as::<_, QrRow>("SELECT role, provider FROM qr_users WHERE id = $1")
.bind(uid)
.fetch_optional(pool)
.await
.ok()?
.map(|q| QrProfileDto {
role: q.role,
provider: q.provider,
})
}
async fn fetch_mentor_profile(
db: &DatabaseConnection,
user_id: Option<Uuid>,
) -> Option<MentorProfileDto> {
let uid = user_id?;
let pool = db.get_postgres_connection_pool();
sqlx::query_as::<_, MentorRow>(
r#"SELECT id, status, current_company, "current_role", years_of_experience FROM app_mentors WHERE user_id = $1 AND is_deleted = false"#,
)
.bind(uid)
.fetch_optional(pool)
.await
.ok()?
.map(|m| MentorProfileDto {
mentor_id: m.id.to_string(),
status: m.status,
current_company: m.current_company,
current_role: m.current_role,
years_of_experience: m.years_of_experience,
})
}
async fn fetch_sessions(
db: &DatabaseConnection,
user_id: Option<Uuid>,
) -> Option<Vec<SessionProfileDto>> {
let uid = user_id?;
let pool = db.get_postgres_connection_pool();
let rows = sqlx::query_as::<_, SessionRow>(
r#"SELECT id, topic, description, scheduled_at, duration_minutes,
session_type, status,
CASE WHEN mentor_id = $1 THEN 'mentor' ELSE 'mentee' END as role
FROM sessions
WHERE mentor_id = $1 OR mentee_id = $1
ORDER BY scheduled_at DESC
LIMIT 20"#,
)
.bind(uid)
.fetch_all(pool)
.await
.ok()?;
if rows.is_empty() {
return None;
}
Some(
rows.into_iter()
.map(|s| SessionProfileDto {
id: s.id.to_string(),
topic: s.topic,
description: s.description,
scheduled_at: s.scheduled_at.to_rfc3339(),
duration_minutes: s.duration_minutes,
session_type: s.session_type,
status: s.status,
role: s.role,
})
.collect(),
)
}
#[derive(sqlx::FromRow)]
struct SessionRow {
id: Uuid,
topic: String,
description: Option<String>,
scheduled_at: chrono::DateTime<chrono::Utc>,
duration_minutes: i32,
session_type: String,
status: String,
role: String,
}
#[derive(sqlx::FromRow)]
struct HackathonRow {
is_admin: bool,
phone_number: Option<String>,
location: Option<String>,
bio: Option<String>,
skills: Option<serde_json::Value>,
}
#[derive(sqlx::FromRow)]
struct QrRow {
role: String,
provider: String,
}
#[derive(sqlx::FromRow)]
struct MentorRow {
id: Uuid,
status: Option<String>,
current_company: Option<String>,
current_role: Option<String>,
years_of_experience: Option<i32>,
}
@@ -1,10 +1,11 @@
#![allow(clippy::field_reassign_with_default)]
use crate::users::domain::UserListItem;
use imphnen_entities::{
PermissionsQueryDto, RolesDetailQueryDto, UsersDetailQueryDto,
PermissionsEnum, PermissionsQueryDto, RolesDetailQueryDto, UsersDetailQueryDto,
seaorm::auth::roles::Entity as RolesEntity,
seaorm::auth::users::{Column as UserColumn, Entity as UsersEntity},
};
use strum::IntoEnumIterator;
use imphnen_utils::AppError;
use paginator_rs::{PaginationParams, SortDirection};
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
@@ -12,6 +13,15 @@ use sea_orm::prelude::*;
use sea_orm::{Order, PaginatorTrait, QueryOrder};
use std::sync::Arc;
fn resolve_permission_name(id: &str) -> String {
for perm in PermissionsEnum::iter() {
if perm.id() == id {
return perm.to_string();
}
}
id.to_string()
}
pub fn build_role_dto(
role: Option<imphnen_entities::seaorm::auth::roles::Model>,
) -> RolesDetailQueryDto {
@@ -25,9 +35,10 @@ pub fn build_role_dto(
list
.into_iter()
.map(|p| {
let name = resolve_permission_name(&p);
Some(PermissionsQueryDto {
id: Some(p.clone()),
name: Some(p),
id: Some(p),
name: Some(name),
created_at: None,
updated_at: None,
})
+1 -1
View File
@@ -200,7 +200,7 @@ pub static ENV: Lazy<Env> = Lazy::new(|| {
cdn_url: get_env_with_warning("CDN_URL", "https://cdn.asepharyana.tech"),
cors_allowed_origins: get_env_with_warning(
"CORS_ALLOWED_ORIGINS",
"https://gacha.imphnen.dev,https://imphnen.dev,https://dimentorin.imphnen.dev",
"https://gacha.imphnen.dev,https://imphnen.dev,https://dimentorin.imphnen.dev,https://backoffice.imphnen.dev,https://hackathon.imphnen.dev,https://qr.imphnen.dev,https://infra.imphnen.dev",
)
.split(',')
.map(|s| s.trim().to_string())