koprogo_api/infrastructure/web/handlers/
auth_handlers.rs1use crate::application::dto::{
2 LoginRequest, LoginResponse, RefreshTokenRequest, RegisterRequest, SwitchRoleRequest,
3 UserResponse,
4};
5use crate::application::error::AppError;
6use crate::infrastructure::web::{
7 build_clearing_cookie, build_refresh_cookie, AppState, AuthenticatedUser, REFRESH_COOKIE_NAME,
8};
9use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
10use serde::Serialize;
11use validator::Validate;
12
13#[derive(Serialize)]
19struct AuthBody {
20 token: String,
21 user: UserResponse,
22}
23
24impl From<LoginResponse> for AuthBody {
25 fn from(r: LoginResponse) -> Self {
26 Self {
27 token: r.token,
28 user: r.user,
29 }
30 }
31}
32
33fn auth_response_with_cookie(resp: LoginResponse) -> HttpResponse {
35 let cookie = build_refresh_cookie(&resp.refresh_token);
36 HttpResponse::Ok().cookie(cookie).json(AuthBody::from(resp))
37}
38
39#[utoipa::path(
40 post,
41 path = "/auth/login",
42 tag = "Auth",
43 summary = "Login",
44 request_body = LoginRequest,
45 responses(
46 (status = 201, description = "Resource created successfully"),
47 (status = 400, description = "Bad Request"),
48 (status = 404, description = "Not Found"),
49 (status = 500, description = "Internal Server Error"),
50 ),
51)]
52#[post("/auth/login")]
53pub async fn login(
54 data: web::Data<AppState>,
55 request: web::Json<LoginRequest>,
56) -> Result<HttpResponse, AppError> {
57 request
58 .validate()
59 .map_err(|errors| AppError::Validation(errors.to_string()))?;
60
61 let response = data.auth_use_cases.login(request.into_inner()).await?;
62 Ok(auth_response_with_cookie(response))
63}
64
65#[utoipa::path(
66 post,
67 path = "/auth/register",
68 tag = "Auth",
69 summary = "Register",
70 request_body = RegisterRequest,
71 responses(
72 (status = 201, description = "Resource created successfully"),
73 (status = 400, description = "Bad Request"),
74 (status = 404, description = "Not Found"),
75 (status = 500, description = "Internal Server Error"),
76 ),
77)]
78#[post("/auth/register")]
79pub async fn register(
80 data: web::Data<AppState>,
81 req: HttpRequest,
82 request: web::Json<RegisterRequest>,
83) -> impl Responder {
84 if let Err(errors) = request.validate() {
86 return HttpResponse::BadRequest().json(serde_json::json!({
87 "error": "Validation failed",
88 "details": errors.to_string()
89 }));
90 }
91
92 let deja_authentifie = req
113 .headers()
114 .get(actix_web::http::header::AUTHORIZATION)
115 .is_some()
116 || req.cookie(REFRESH_COOKIE_NAME).is_some();
117
118 match data.auth_use_cases.register(request.into_inner()).await {
119 Ok(response) => {
120 if deja_authentifie {
121 HttpResponse::Created().json(AuthBody::from(response))
123 } else {
124 let cookie = build_refresh_cookie(&response.refresh_token);
125 HttpResponse::Created()
126 .cookie(cookie)
127 .json(AuthBody::from(response))
128 }
129 }
130 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
131 "error": e
132 })),
133 }
134}
135
136#[utoipa::path(
137 get,
138 path = "/auth/me",
139 tag = "Auth",
140 summary = "Get Current User",
141 responses(
142 (status = 200, description = "Success"),
143 (status = 400, description = "Bad Request"),
144 (status = 404, description = "Not Found"),
145 (status = 500, description = "Internal Server Error"),
146 ),
147)]
148#[get("/auth/me")]
149pub async fn get_current_user(
150 data: web::Data<AppState>,
151 req: HttpRequest,
152) -> Result<HttpResponse, AppError> {
153 let auth_header = req
154 .headers()
155 .get("Authorization")
156 .ok_or(AppError::Unauthorized)?
157 .to_str()
158 .map_err(|_| AppError::Validation("invalid authorization header".to_string()))?;
159
160 let token = auth_header.trim_start_matches("Bearer ").trim();
161
162 let claims = data.auth_use_cases.verify_token(token)?;
163
164 let user_id = uuid::Uuid::parse_str(&claims.sub)
165 .map_err(|e| AppError::Validation(format!("invalid user id in token: {}", e)))?;
166
167 let user = data.auth_use_cases.get_user_by_id(user_id).await?;
168 Ok(HttpResponse::Ok().json(user))
169}
170
171#[utoipa::path(
172 post,
173 path = "/auth/refresh",
174 tag = "Auth",
175 summary = "Refresh Token",
176 description = "Le refresh token est lu depuis le cookie HttpOnly \
177 `koprogo_refresh` (WP-FE1) — aucun corps de requête. \
178 La réponse rote le cookie et ne contient pas de \
179 refresh_token.",
180 responses(
181 (status = 200, description = "Access token rafraîchi"),
182 (status = 401, description = "Cookie refresh absent, expiré ou révoqué"),
183 (status = 500, description = "Internal Server Error"),
184 ),
185)]
186#[post("/auth/refresh")]
187pub async fn refresh_token(
188 data: web::Data<AppState>,
189 req: HttpRequest,
190) -> Result<HttpResponse, AppError> {
191 let refresh_token = req
194 .cookie(REFRESH_COOKIE_NAME)
195 .map(|c| c.value().to_owned())
196 .filter(|v| !v.is_empty())
197 .ok_or(AppError::Unauthorized)?;
198
199 let response = data
200 .auth_use_cases
201 .refresh_token(RefreshTokenRequest { refresh_token })
202 .await?;
203 Ok(auth_response_with_cookie(response))
205}
206
207#[utoipa::path(
208 post,
209 path = "/auth/switch-role",
210 tag = "Auth",
211 summary = "Switch Role",
212 request_body = SwitchRoleRequest,
213 responses(
214 (status = 201, description = "Resource created successfully"),
215 (status = 400, description = "Bad Request"),
216 (status = 404, description = "Not Found"),
217 (status = 500, description = "Internal Server Error"),
218 ),
219)]
220#[post("/auth/switch-role")]
221pub async fn switch_role(
222 data: web::Data<AppState>,
223 user: AuthenticatedUser,
224 request: web::Json<SwitchRoleRequest>,
225) -> impl Responder {
226 let payload = request.into_inner();
227
228 match data
229 .auth_use_cases
230 .switch_active_role(user.user_id, payload.role_id)
231 .await
232 {
233 Ok(response) => auth_response_with_cookie(response),
234 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({ "error": e })),
235 }
236}
237
238#[utoipa::path(
239 post,
240 path = "/auth/logout",
241 tag = "Auth",
242 summary = "Logout",
243 description = "Révoque tous les refresh tokens de l'utilisateur \
244 (déconnexion serveur) et expire le cookie HttpOnly \
245 `koprogo_refresh` (WP-FE1).",
246 responses(
247 (status = 200, description = "Déconnecté, cookie expiré"),
248 (status = 401, description = "Access token absent ou invalide"),
249 (status = 500, description = "Internal Server Error"),
250 ),
251)]
252#[post("/auth/logout")]
253pub async fn logout(
254 data: web::Data<AppState>,
255 user: AuthenticatedUser,
256) -> Result<HttpResponse, AppError> {
257 data.auth_use_cases
261 .revoke_all_refresh_tokens(user.user_id)
262 .await
263 .map_err(AppError::from)?;
264
265 Ok(HttpResponse::Ok()
266 .cookie(build_clearing_cookie())
267 .json(serde_json::json!({ "message": "logged out" })))
268}