//! JWT claims structure — mirrors proto `TokenClaims` for local verification. //! //! Used as the deserialization target for `jsonwebtoken::decode`. //! Field names match standard JWT claim names (`sub`, `iss`, `exp`, etc.). use std::collections::HashMap; use serde::{Deserialize, Serialize}; /// Parsed JWT payload, matching the proto `TokenClaims` shape. /// /// Deserialized by `jsonwebtoken` during HS256 verification. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenClaims { /// Subject — the user UUID. pub sub: String, /// Issuer — expected to be `"appks"`. pub iss: String, /// Issued-at (unix seconds). pub iat: i64, /// Expiration (unix seconds). pub exp: i64, /// Unique token ID (used for revocation tracking via `jti`). pub jti: String, /// Space-separated scopes, e.g. `"im:read im:write"`. pub scope: String, /// Extensible metadata (workspace_id, role, etc.). #[serde(default)] pub extra: HashMap, } impl TokenClaims { /// Check whether this token carries a specific scope. pub fn has_scope(&self, scope: &str) -> bool { self.scope.split_whitespace().any(|s| s == scope) } /// Convert from the proto-generated `TokenClaims` (RPC verify response). pub fn from_proto(proto: crate::pb::core::TokenClaims) -> Self { Self { sub: proto.sub, iss: proto.iss, iat: proto.iat, exp: proto.exp, jti: proto.jti, scope: proto.scope, extra: proto.extra, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_has_scope() { let claims = TokenClaims { sub: "user-1".into(), iss: "appks".into(), iat: 0, exp: 9999999999, jti: "tok-1".into(), scope: "im:read im:write admin".into(), extra: HashMap::new(), }; assert!(claims.has_scope("im:read")); assert!(claims.has_scope("admin")); assert!(!claims.has_scope("im:delete")); } #[test] fn test_deserialize_from_json() { let json = r#"{ "sub": "user-1", "iss": "appks", "iat": 1000, "exp": 2000, "jti": "tok-1", "scope": "im:read", "extra": {"workspace_id": "ws-1"} }"#; let claims: TokenClaims = serde_json::from_str(json).unwrap(); assert_eq!(claims.sub, "user-1"); assert_eq!(claims.extra.get("workspace_id").unwrap(), "ws-1"); } }