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:
co-authored by
Claude Sonnet 4.6
parent
2ae43b3bcc
commit
331a4a4e88
@@ -1,153 +0,0 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
// ============================================================
|
||||
// Request DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[zod(min_length(3), max_length(200))]
|
||||
pub topic: String,
|
||||
#[zod(max_length(1000))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub scheduled_at: String,
|
||||
#[zod(min(15.0), max(240.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
#[zod(max_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for BookSessionRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[zod(min_length(1), max_length(50))]
|
||||
pub status: String,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for UpdateSessionStatusRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[zod(min_length(10), max_length(2000))]
|
||||
pub feedback: String,
|
||||
#[zod(min(1.0), max(5.0), int)]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for SessionFeedbackRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Response DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use request::{
|
||||
BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto,
|
||||
};
|
||||
pub use response::{
|
||||
AvailabilitySlotDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionDetailDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::sessions::domain::{
|
||||
BookSessionCommand, SessionFeedbackCommand, UpdateSessionStatusCommand,
|
||||
};
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[zod(min_length(3), max_length(200))]
|
||||
pub topic: String,
|
||||
#[zod(max_length(1000))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub scheduled_at: String,
|
||||
#[zod(min(15.0), max(240.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
#[zod(max_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for BookSessionRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BookSessionRequestDto> for BookSessionCommand {
|
||||
fn from(dto: BookSessionRequestDto) -> Self {
|
||||
Self {
|
||||
topic: dto.topic,
|
||||
description: dto.description,
|
||||
scheduled_at: dto.scheduled_at,
|
||||
duration_minutes: dto.duration_minutes,
|
||||
session_type: dto.session_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[zod(min_length(1), max_length(50))]
|
||||
pub status: String,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for UpdateSessionStatusRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateSessionStatusRequestDto> for UpdateSessionStatusCommand {
|
||||
fn from(dto: UpdateSessionStatusRequestDto) -> Self {
|
||||
Self {
|
||||
status: dto.status,
|
||||
meeting_link: dto.meeting_link,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[zod(min_length(10), max_length(2000))]
|
||||
pub feedback: String,
|
||||
#[zod(min(1.0), max(5.0), int)]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for SessionFeedbackRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SessionFeedbackRequestDto> for SessionFeedbackCommand {
|
||||
fn from(dto: SessionFeedbackRequestDto) -> Self {
|
||||
Self {
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use crate::sessions::domain::{
|
||||
AvailabilitySlot, BookedSession, MentorAvailability, SessionDetail,
|
||||
SessionFeedbackResult, SessionList, SessionListItem, UpdatedSessionStatus,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl From<BookedSession> for BookSessionResponseDto {
|
||||
fn from(s: BookedSession) -> Self {
|
||||
Self {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
topic: s.topic,
|
||||
description: s.description,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
created_at: s.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl From<SessionListItem> for SessionListItemDto {
|
||||
fn from(s: SessionListItem) -> Self {
|
||||
Self {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
mentee_fullname: s.mentee_fullname,
|
||||
mentee_email: s.mentee_email,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
impl From<SessionList> for SessionListResponseDto {
|
||||
fn from(list: SessionList) -> Self {
|
||||
Self {
|
||||
sessions: list
|
||||
.sessions
|
||||
.into_iter()
|
||||
.map(SessionListItemDto::from)
|
||||
.collect(),
|
||||
total: list.total,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<SessionDetail> for SessionDetailDto {
|
||||
fn from(d: SessionDetail) -> Self {
|
||||
Self {
|
||||
id: d.id,
|
||||
mentor_id: d.mentor_id,
|
||||
mentor_fullname: d.mentor_fullname,
|
||||
mentee_id: d.mentee_id,
|
||||
mentee_fullname: d.mentee_fullname,
|
||||
topic: d.topic,
|
||||
description: d.description,
|
||||
scheduled_at: d.scheduled_at,
|
||||
duration_minutes: d.duration_minutes,
|
||||
meeting_link: d.meeting_link,
|
||||
session_type: d.session_type,
|
||||
status: d.status,
|
||||
feedback: d.feedback,
|
||||
rating: d.rating,
|
||||
feedback_submitted_at: d.feedback_submitted_at,
|
||||
created_at: d.created_at,
|
||||
updated_at: d.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
impl From<AvailabilitySlot> for AvailabilitySlotDto {
|
||||
fn from(s: AvailabilitySlot) -> Self {
|
||||
Self {
|
||||
date: s.date,
|
||||
time: s.time,
|
||||
available: s.available,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<MentorAvailability> for MentorAvailabilityDto {
|
||||
fn from(a: MentorAvailability) -> Self {
|
||||
Self {
|
||||
mentor_id: a.mentor_id,
|
||||
availability_commitment: a.availability_commitment,
|
||||
preferred_formats: a.preferred_formats,
|
||||
slots: a.slots.into_iter().map(AvailabilitySlotDto::from).collect(),
|
||||
booked_dates: a.booked_dates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<UpdatedSessionStatus> for UpdateSessionStatusResponseDto {
|
||||
fn from(u: UpdatedSessionStatus) -> Self {
|
||||
Self {
|
||||
id: u.id,
|
||||
status: u.status,
|
||||
meeting_link: u.meeting_link,
|
||||
updated_at: u.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
|
||||
impl From<SessionFeedbackResult> for SessionFeedbackResponseDto {
|
||||
fn from(r: SessionFeedbackResult) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
feedback: r.feedback,
|
||||
rating: r.rating,
|
||||
submitted_at: r.submitted_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use imphnen_libs::ValidatedJson;
|
||||
use imphnen_utils::{ApiSuccess, extract_email};
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::domain::SessionService;
|
||||
use super::dto::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||
UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SessionStatusFilter {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[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 = service.book_session(mentor_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[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 = 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 = service.get_mentor_availability(mentor_id).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 = service.update_session_status(session_id, user_email, dto).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 = service.submit_feedback(session_id, user_email, dto).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 = service.get_user_sessions(user_email, filter.status).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,38 +1,44 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{get, post, put},
|
||||
Extension, Router,
|
||||
use super::handlers::{
|
||||
get_mentor_availability, get_mentor_sessions, get_my_sessions, post_book_session,
|
||||
post_submit_feedback, put_update_session_status,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use imphnen_libs::AppState;
|
||||
use crate::sessions::application::SessionServiceImpl;
|
||||
use crate::sessions::domain::SessionService;
|
||||
use crate::sessions::infrastructure::persistence::PostgresSessionRepository;
|
||||
use super::handlers::{
|
||||
get_mentor_availability, get_mentor_sessions, get_my_sessions, post_book_session,
|
||||
post_submit_feedback, put_update_session_status,
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
routing::{get, post, put},
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn build_service(db: DatabaseConnection) -> Arc<dyn SessionService> {
|
||||
let repo = Arc::new(PostgresSessionRepository::new(db));
|
||||
Arc::new(SessionServiceImpl::new(repo))
|
||||
let repo = Arc::new(PostgresSessionRepository::new(db));
|
||||
Arc::new(SessionServiceImpl::new(repo))
|
||||
}
|
||||
|
||||
pub fn sessions_public_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||
.layer(Extension(service))
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn sessions_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||
.route("/sessions/update/{id}/status", put(put_update_session_status))
|
||||
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension((*state).clone()))
|
||||
pub fn sessions_protected_routes(
|
||||
db: DatabaseConnection,
|
||||
state: Arc<AppState>,
|
||||
) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||
.route(
|
||||
"/sessions/update/{id}/status",
|
||||
put(put_update_session_status),
|
||||
)
|
||||
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension((*state).clone()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user