feat: v0.3.0 — standardize codebase, centralize infra, merge QR into CMS

- Enforce axum best practices across all 13 workspace crates
  (max 200 LOC/file, no comments, no unwrap, clean architecture)
- Fix domain→infrastructure dependency inversions in imphnen-iam and imphnen-dimentorin
- Extract imphnen-storage (MinIO) and imphnen-email (Lettre) as standalone crates
- Centralize all config in ENV struct: CDN_URL, CORS_ALLOWED_ORIGINS
- Centralize SMTP through imphnen-email; remove dead HackathonConfig
- Centralize database: QR crate now shares main DB pool (single DATABASE_URL)
- Rename QR users table to qr_users to avoid collision with main users table
- Merge imphnen-qr into imphnen-cms/src/qr (13 crates, down from 14)
- Restructure imphnen-hackathon flat modules into clean architecture
- Remove all stale env vars from .env.example (SurrealDB, QR_JWT, Hackathon infra)
- Fix Dockerfile to include all current workspace crates
- Bump all crate versions 0.2.0 → 0.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-02 22:29:08 +07:00
co-authored by Claude Sonnet 4.6
parent 2ae43b3bcc
commit 331a4a4e88
442 changed files with 22226 additions and 18700 deletions
@@ -0,0 +1,9 @@
pub mod mutation_handlers;
pub mod query_handlers;
pub use mutation_handlers::{
post_book_session, post_submit_feedback, put_update_session_status,
};
pub use query_handlers::{
get_mentor_availability, get_mentor_sessions, get_my_sessions,
};
@@ -0,0 +1,112 @@
use super::super::dto::{
BookSessionRequestDto, BookSessionResponseDto, SessionFeedbackRequestDto,
SessionFeedbackResponseDto, UpdateSessionStatusRequestDto,
UpdateSessionStatusResponseDto,
};
use crate::sessions::domain::SessionService;
use axum::{
extract::{Extension, Path},
http::HeaderMap,
response::IntoResponse,
};
use imphnen_libs::ValidatedJson;
use imphnen_utils::AppError;
use imphnen_utils::{ApiSuccess, extract_email};
use std::sync::Arc;
#[utoipa::path(
post,
path = "/v1/mentors/{id}/sessions/create",
tag = "sessions",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Mentor ID"),
),
request_body = BookSessionRequestDto,
responses(
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
(status = 400, description = "Invalid request"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn post_book_session(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn SessionService>>,
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 resp = BookSessionResponseDto::from(
service
.book_session(mentor_id, user_email, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
}
#[utoipa::path(
put,
path = "/v1/sessions/update/{id}/status",
tag = "sessions",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Session ID"),
),
request_body = UpdateSessionStatusRequestDto,
responses(
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
(status = 400, description = "Invalid request"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Session not found"),
)
)]
pub async fn put_update_session_status(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn SessionService>>,
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 resp = UpdateSessionStatusResponseDto::from(
service
.update_session_status(session_id, user_email, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
}
#[utoipa::path(
post,
path = "/v1/sessions/{id}/feedback/create",
tag = "sessions",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Session ID"),
),
request_body = SessionFeedbackRequestDto,
responses(
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
(status = 400, description = "Invalid request or session not completed"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Session not found"),
)
)]
pub async fn post_submit_feedback(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn SessionService>>,
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 resp = SessionFeedbackResponseDto::from(
service
.submit_feedback(session_id, user_email, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
}
@@ -0,0 +1,94 @@
use super::super::dto::{MentorAvailabilityDto, SessionListResponseDto};
use crate::sessions::domain::SessionService;
use axum::{
extract::{Extension, Path, Query},
http::HeaderMap,
response::IntoResponse,
};
use imphnen_utils::AppError;
use imphnen_utils::{ApiSuccess, extract_email};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Deserialize)]
pub struct SessionStatusFilter {
pub status: Option<String>,
}
#[utoipa::path(
get,
path = "/v1/mentors/{id}/sessions",
tag = "sessions",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Mentor ID"),
("status" = Option<String>, Query, description = "Filter by status"),
),
responses(
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn get_mentor_sessions(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn SessionService>>,
Path(mentor_id): Path<String>,
Query(filter): Query<SessionStatusFilter>,
) -> Result<impl IntoResponse, AppError> {
let _user_email = extract_email(&headers)
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
let resp = SessionListResponseDto::from(
service
.get_mentor_sessions(mentor_id, filter.status)
.await?,
);
Ok(ApiSuccess(resp))
}
#[utoipa::path(
get,
path = "/v1/mentors/{id}/availability",
tag = "sessions",
params(
("id" = String, Path, description = "Mentor ID"),
),
responses(
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn get_mentor_availability(
Extension(service): Extension<Arc<dyn SessionService>>,
Path(mentor_id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
let resp =
MentorAvailabilityDto::from(service.get_mentor_availability(mentor_id).await?);
Ok(ApiSuccess(resp))
}
#[utoipa::path(
get,
path = "/v1/users/me/sessions",
tag = "sessions",
security(("Bearer" = [])),
params(
("status" = Option<String>, Query, description = "Filter by status"),
),
responses(
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
(status = 401, description = "Unauthorized"),
)
)]
pub async fn get_my_sessions(
headers: HeaderMap,
Extension(service): Extension<Arc<dyn SessionService>>,
Query(filter): Query<SessionStatusFilter>,
) -> Result<impl IntoResponse, AppError> {
let user_email = extract_email(&headers)
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
let resp = SessionListResponseDto::from(
service.get_user_sessions(user_email, filter.status).await?,
);
Ok(ApiSuccess(resp))
}