@@ -340,6 +340,63 @@ pub fn extract_token_from_header(auth_header: &str, headers: &HeaderMap) -> Resu
340340 ) )
341341}
342342
343+ // ═══════════════════════════════════════════════════════════════════════════════
344+ // ADMIN AUTHENTICATION
345+ // ═══════════════════════════════════════════════════════════════════════════════
346+
347+ /// Admin JWT claims structure
348+ #[ derive( Debug , Serialize , Deserialize ) ]
349+ pub struct AdminClaims {
350+ /// Subject (admin user ID)
351+ pub sub : String ,
352+ /// Expiration time (required for admin tokens)
353+ pub exp : Option < i64 > ,
354+ /// Issued at
355+ pub iat : Option < i64 > ,
356+ /// Scope (must include "admin" or "*")
357+ #[ serde( default ) ]
358+ pub scope : String ,
359+ }
360+
361+ impl AdminClaims {
362+ /// Check if this token has admin privileges
363+ pub fn is_valid_admin ( & self ) -> bool {
364+ self . scope
365+ . split_whitespace ( )
366+ . any ( |s| s == "admin" || s == "*" )
367+ }
368+ }
369+
370+ /// Validate an admin JWT token and extract claims
371+ pub fn validate_admin_token ( token : & str , secret : & str ) -> Result < AdminClaims , ApiError > {
372+ let key = DecodingKey :: from_secret ( secret. as_bytes ( ) ) ;
373+ let mut validation = Validation :: new ( Algorithm :: HS256 ) ;
374+ validation. validate_exp = true ; // Admin tokens MUST have valid expiration
375+ validation. leeway = 60 ; // 1 minute leeway for clock skew
376+
377+ let claims = decode :: < AdminClaims > ( token, & key, & validation)
378+ . map ( |data| data. claims )
379+ . map_err ( |e| {
380+ tracing:: debug!( "Admin token validation failed: {}" , e) ;
381+ ApiError :: s3 ( S3ErrorCode :: InvalidToken , "Invalid or expired admin token" )
382+ } ) ?;
383+
384+ // Verify admin scope
385+ if !claims. is_valid_admin ( ) {
386+ tracing:: warn!(
387+ admin_id = %claims. sub,
388+ scope = %claims. scope,
389+ "Admin token missing required 'admin' scope"
390+ ) ;
391+ return Err ( ApiError :: s3 (
392+ S3ErrorCode :: AccessDenied ,
393+ "Token does not have admin privileges" ,
394+ ) ) ;
395+ }
396+
397+ Ok ( claims)
398+ }
399+
343400/// Hash a user ID for storage (privacy)
344401pub fn hash_user_id ( user_id : & str ) -> String {
345402 use fula_crypto:: hashing:: hash;
0 commit comments