1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
12pub enum Scope {
13 Read,
15 Operate,
17 Admin,
19}
20
21impl Scope {
22 pub fn as_str(&self) -> &'static str {
24 match self {
25 Scope::Read => "read",
26 Scope::Operate => "operate",
27 Scope::Admin => "admin",
28 }
29 }
30
31 pub fn parse(s: &str) -> Option<Scope> {
33 match s {
34 "read" => Some(Scope::Read),
35 "operate" => Some(Scope::Operate),
36 "admin" => Some(Scope::Admin),
37 _ => None,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(tag = "auth", rename_all = "snake_case")]
45#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
46pub enum SessionInfo {
47 Disabled,
49 Token {
51 token_name: String,
53 scope: Scope,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 expires_at: Option<String>,
58 },
59}
60
61#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
64pub struct SignInBody {
65 pub token: String,
67}
68
69impl std::fmt::Debug for SignInBody {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("SignInBody")
74 .field("token", &"<redacted>")
75 .finish()
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn scopes_are_ordered() {
85 assert!(Scope::Read < Scope::Operate);
86 assert!(Scope::Operate < Scope::Admin);
87 }
88
89 #[test]
90 fn scope_parse_round_trips_and_rejects_unknown() {
91 for s in [Scope::Read, Scope::Operate, Scope::Admin] {
92 assert_eq!(Scope::parse(s.as_str()), Some(s));
93 }
94 assert_eq!(Scope::parse("Admin"), None);
95 assert_eq!(Scope::parse("write"), None);
96 }
97
98 #[test]
99 fn session_info_wire_shapes() {
100 assert_eq!(
101 serde_json::to_value(SessionInfo::Disabled).unwrap(),
102 serde_json::json!({"auth": "disabled"})
103 );
104 let t = SessionInfo::Token {
105 token_name: "alice".into(),
106 scope: Scope::Admin,
107 expires_at: None,
108 };
109 assert_eq!(
110 serde_json::to_value(&t).unwrap(),
111 serde_json::json!({"auth": "token", "token_name": "alice", "scope": "admin"})
112 );
113 let back: SessionInfo = serde_json::from_value(serde_json::to_value(&t).unwrap()).unwrap();
114 assert_eq!(back, t);
115 }
116
117 #[test]
118 fn sign_in_body_debug_redacts_the_token() {
119 let body = SignInBody {
120 token: "pspo_supersecretvalue".to_string(),
121 };
122 let debug = format!("{body:?}");
123 assert!(!debug.contains("pspo_supersecretvalue"), "{debug}");
124 assert!(debug.contains("<redacted>"), "{debug}");
125 }
126}