use std::collections::HashMap; use actix_web::{HttpResponse, web}; use serde::Serialize; use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::error::AppError; use crate::service::AppService; use crate::session::Session; /// Response payload for `POST /auth/ws-token`. #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct WsTokenResponse { /// Short-lived JWT prefixed with "Bearer " for use in the Socket.IO CONNECT auth packet. pub token: String, /// Unix timestamp (seconds) when the token expires. pub expires_at: i64, } #[utoipa::path( post, path = "/api/v1/auth/ws-token", tag = "Auth", operation_id = "authWsToken", summary = "Issue a short-lived WebSocket token", description = "Issue a short-lived JWT (30 minutes) scoped to IM WebSocket access. \ The token is signed by the appks signing key and can be verified by imks either \ locally (via cached signing keys) or via RPC. The returned token should be passed \ as `{ token: }` in the Socket.IO CONNECT auth packet. Requires an \ authenticated session.", responses( (status = 200, description = "Token issued successfully.", body = ApiResponse), (status = 401, description = "The current session is unauthenticated or the login state has expired.", body = ApiErrorResponse), (status = 500, description = "Token issuance or Redis write failed.", body = ApiErrorResponse) ) )] pub async fn handle( service: web::Data, session: Session, ) -> Result { let user_uid = session.user().ok_or(AppError::Unauthorized)?; let issued = service .internal_auth .issue_token( &user_uid.to_string(), 1800, // 30-minute TTL (frontend refreshes every 25 min) vec!["im:read".into(), "im:write".into()], HashMap::new(), ) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new(WsTokenResponse { token: format!("Bearer {}", issued.access_token), expires_at: issued.expires_at, }))) }