Skip to main content

shaperail_runtime/auth/
tokens.rs

1use actix_web::{web, HttpResponse};
2use serde::{Deserialize, Serialize};
3use shaperail_core::ShaperailError;
4use std::sync::Arc;
5
6use super::jwt::JwtConfig;
7
8/// Request body for issuing a new token pair.
9///
10/// In a real application, this would verify credentials against a database.
11/// For the framework scaffold, the consumer implements the credential check
12/// and calls `issue_tokens` with the verified user info.
13#[derive(Debug, Deserialize)]
14pub struct TokenRequest {
15    /// User ID (pre-verified by the consumer's auth logic).
16    pub user_id: String,
17    /// User role (pre-verified by the consumer's auth logic).
18    pub role: String,
19}
20
21/// Request body for refreshing tokens using a refresh token.
22#[derive(Debug, Deserialize)]
23pub struct RefreshRequest {
24    /// The refresh token to exchange for a new token pair.
25    pub refresh_token: String,
26}
27
28/// Response containing an access + refresh token pair.
29#[derive(Debug, Serialize)]
30pub struct TokenPair {
31    pub access_token: String,
32    pub refresh_token: String,
33    pub token_type: String,
34    pub expires_in: i64,
35}
36
37/// Handler: POST /auth/token — issue a new token pair.
38///
39/// Accepts a `TokenRequest` with pre-verified user credentials.
40/// Returns a `TokenPair` with access and refresh tokens.
41pub async fn handle_issue_token(
42    jwt: web::Data<Arc<JwtConfig>>,
43    body: web::Json<TokenRequest>,
44) -> Result<HttpResponse, ShaperailError> {
45    let access = jwt
46        .encode_access(&body.user_id, &body.role)
47        .map_err(|e| ShaperailError::Internal(format!("Token encoding failed: {e}")))?;
48
49    let refresh = jwt
50        .encode_refresh(&body.user_id, &body.role)
51        .map_err(|e| ShaperailError::Internal(format!("Token encoding failed: {e}")))?;
52
53    let pair = TokenPair {
54        access_token: access,
55        refresh_token: refresh,
56        token_type: "Bearer".to_string(),
57        expires_in: jwt.access_ttl.num_seconds(),
58    };
59
60    Ok(HttpResponse::Ok().json(pair))
61}
62
63/// Handler: POST /auth/refresh — exchange a refresh token for a new token pair.
64///
65/// Validates the refresh token, then issues new access + refresh tokens.
66pub async fn handle_refresh_token(
67    jwt: web::Data<Arc<JwtConfig>>,
68    body: web::Json<RefreshRequest>,
69) -> Result<HttpResponse, ShaperailError> {
70    let claims = jwt
71        .decode(&body.refresh_token)
72        .map_err(|_| ShaperailError::Unauthorized)?;
73
74    if claims.token_type != "refresh" {
75        return Err(ShaperailError::Unauthorized);
76    }
77
78    let access = jwt
79        .encode_access(&claims.sub, &claims.role)
80        .map_err(|e| ShaperailError::Internal(format!("Token encoding failed: {e}")))?;
81
82    let refresh = jwt
83        .encode_refresh(&claims.sub, &claims.role)
84        .map_err(|e| ShaperailError::Internal(format!("Token encoding failed: {e}")))?;
85
86    let pair = TokenPair {
87        access_token: access,
88        refresh_token: refresh,
89        token_type: "Bearer".to_string(),
90        expires_in: jwt.access_ttl.num_seconds(),
91    };
92
93    Ok(HttpResponse::Ok().json(pair))
94}
95
96/// Registers the auth token endpoints on the given Actix `ServiceConfig`.
97pub fn register_auth_routes(cfg: &mut web::ServiceConfig) {
98    cfg.route("/auth/token", web::post().to(handle_issue_token));
99    cfg.route("/auth/refresh", web::post().to(handle_refresh_token));
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn token_request_deserialize() {
108        let json = r#"{"user_id": "u1", "role": "admin"}"#;
109        let req: TokenRequest = serde_json::from_str(json).unwrap();
110        assert_eq!(req.user_id, "u1");
111        assert_eq!(req.role, "admin");
112    }
113
114    #[test]
115    fn refresh_request_deserialize() {
116        let json = r#"{"refresh_token": "some.token.here"}"#;
117        let req: RefreshRequest = serde_json::from_str(json).unwrap();
118        assert_eq!(req.refresh_token, "some.token.here");
119    }
120
121    #[test]
122    fn token_pair_serialize() {
123        let pair = TokenPair {
124            access_token: "at".to_string(),
125            refresh_token: "rt".to_string(),
126            token_type: "Bearer".to_string(),
127            expires_in: 3600,
128        };
129        let json = serde_json::to_value(&pair).unwrap();
130        assert_eq!(json["token_type"], "Bearer");
131        assert_eq!(json["expires_in"], 3600);
132    }
133}