1use serde::{Deserialize, Serialize};
11
12pub fn validate_token_name(name: &str) -> Result<(), &'static str> {
14 let name = name.trim();
15 if name.is_empty() {
16 Err("请输入令牌名称,不能只包含空白字符")
17 } else if name.chars().count() > 64 {
18 Err("令牌名称不能超过 64 个字符")
19 } else {
20 Ok(())
21 }
22}
23
24#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
33#[serde(rename_all = "lowercase")]
34pub enum TokenScope {
35 Read,
36 Write,
37 Admin,
38}
39
40impl TokenScope {
41 pub fn as_str(self) -> &'static str {
43 match self {
44 TokenScope::Read => "read",
45 TokenScope::Write => "write",
46 TokenScope::Admin => "admin",
47 }
48 }
49
50 pub fn from_db(s: &str) -> Option<Self> {
52 match s {
53 "read" => Some(TokenScope::Read),
54 "write" => Some(TokenScope::Write),
55 "admin" => Some(TokenScope::Admin),
56 _ => None,
57 }
58 }
59
60 fn rank(self) -> u8 {
62 match self {
63 TokenScope::Read => 1,
64 TokenScope::Write => 2,
65 TokenScope::Admin => 3,
66 }
67 }
68
69 pub fn grants(self, required: TokenScope) -> bool {
71 self.rank() >= required.rank()
72 }
73}
74
75impl PartialOrd for TokenScope {
76 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
77 Some(self.cmp(other))
78 }
79}
80
81impl Ord for TokenScope {
82 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
83 self.rank().cmp(&other.rank())
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct McpToken {
90 pub id: String,
91 pub user_id: i32,
92 pub name: String,
93 pub scope: TokenScope,
94 pub notes: NoteGrant,
95 #[serde(skip)]
97 pub token_enc: String,
98 #[serde(skip)]
100 pub token_hash: String,
101 pub created_at: chrono::DateTime<chrono::Utc>,
102 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
103 pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
104 pub revoked_at: Option<chrono::DateTime<chrono::Utc>>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct McpTokenSummary {
110 pub id: String,
111 pub name: String,
112 pub scope: TokenScope,
113 #[serde(default)]
114 pub notes: NoteGrant,
115 pub created_at: chrono::DateTime<chrono::Utc>,
116 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
117 pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
118 pub revoked_at: Option<chrono::DateTime<chrono::Utc>>,
119}
120
121impl From<McpToken> for McpTokenSummary {
122 fn from(t: McpToken) -> Self {
123 Self {
124 id: t.id,
125 name: t.name,
126 scope: t.scope,
127 notes: t.notes,
128 created_at: t.created_at,
129 expires_at: t.expires_at,
130 last_used_at: t.last_used_at,
131 revoked_at: t.revoked_at,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
138pub struct NoteGrant {
139 pub read: bool,
140 pub write: bool,
141 pub notebook_ids: Option<Vec<i32>>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146pub struct CreateTokenResponse {
147 #[serde(flatten)]
148 pub summary: McpTokenSummary,
149 pub plaintext: String,
151}
152
153#[cfg(test)]
154mod tests {
155 #[test]
156 fn token_name_requires_non_whitespace_and_at_most_64_characters() {
157 for name in ["", " \t\n", "\u{3000}"] {
158 assert!(super::validate_token_name(name).is_err());
159 }
160 for name in [" claude-code-macbook ".to_string(), "名".repeat(64)] {
161 assert!(super::validate_token_name(&name).is_ok());
162 }
163 assert!(super::validate_token_name(&"名".repeat(65)).is_err());
164 assert!(super::validate_token_name(&"a".repeat(65)).is_err());
165 }
166
167 use super::*;
168
169 #[test]
170 fn scope_ranking_grants_chain() {
171 assert!(TokenScope::Read.grants(TokenScope::Read));
173 assert!(!TokenScope::Read.grants(TokenScope::Write));
174 assert!(!TokenScope::Read.grants(TokenScope::Admin));
175 assert!(TokenScope::Write.grants(TokenScope::Read));
177 assert!(TokenScope::Write.grants(TokenScope::Write));
178 assert!(!TokenScope::Write.grants(TokenScope::Admin));
179 assert!(TokenScope::Admin.grants(TokenScope::Read));
181 assert!(TokenScope::Admin.grants(TokenScope::Write));
182 assert!(TokenScope::Admin.grants(TokenScope::Admin));
183 }
184
185 #[test]
186 fn scope_ord_total_order() {
187 assert!(TokenScope::Read < TokenScope::Write);
188 assert!(TokenScope::Write < TokenScope::Admin);
189 assert!(TokenScope::Admin >= TokenScope::Read);
190 let mut v = [TokenScope::Admin, TokenScope::Read, TokenScope::Write];
192 v.sort();
193 assert_eq!(v, [TokenScope::Read, TokenScope::Write, TokenScope::Admin]);
194 }
195
196 #[test]
197 fn scope_db_roundtrip() {
198 for s in [TokenScope::Read, TokenScope::Write, TokenScope::Admin] {
199 assert_eq!(TokenScope::from_db(s.as_str()), Some(s));
200 }
201 assert_eq!(TokenScope::from_db("root"), None);
202 assert_eq!(TokenScope::from_db(""), None);
203 }
204
205 #[test]
206 fn scope_serde_lowercase() {
207 let s = serde_json::to_string(&TokenScope::Admin).unwrap();
208 assert_eq!(s, "\"admin\"");
209 let v: TokenScope = serde_json::from_str("\"read\"").unwrap();
210 assert_eq!(v, TokenScope::Read);
211 }
212}