1use std::borrow::Cow;
4use std::fmt;
5
6use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::ids::{AdminServiceTokenId, CreatorApiTokenId, HumanId};
10use super::pioneer_codes::PioneerCodeInput;
11use super::urls::GithubSignInAuthorizationUrl;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct GithubUserIdError;
16
17impl fmt::Display for GithubUserIdError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 f.write_str("github_user_id must be a positive integer")
21 }
22}
23
24impl std::error::Error for GithubUserIdError {}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct GithubUserId(i64);
29
30impl GithubUserId {
31 pub fn try_new(value: i64) -> Result<Self, GithubUserIdError> {
33 if value <= 0 {
34 return Err(GithubUserIdError);
35 }
36 Ok(Self(value))
37 }
38
39 pub fn as_i64(self) -> i64 {
41 self.0
42 }
43}
44
45impl fmt::Display for GithubUserId {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{}", self.0)
49 }
50}
51
52impl Serialize for GithubUserId {
53 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
55 where
56 S: Serializer,
57 {
58 serializer.serialize_i64(self.0)
59 }
60}
61
62impl<'de> Deserialize<'de> for GithubUserId {
63 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
65 where
66 D: Deserializer<'de>,
67 {
68 let value = i64::deserialize(deserializer)?;
69 Self::try_new(value).map_err(serde::de::Error::custom)
70 }
71}
72
73impl JsonSchema for GithubUserId {
74 fn inline_schema() -> bool {
76 true
77 }
78
79 fn schema_name() -> Cow<'static, str> {
81 "GithubUserId".into()
82 }
83
84 fn json_schema(_: &mut SchemaGenerator) -> Schema {
86 json_schema!({
87 "type": "integer",
88 "minimum": 1
89 })
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
95#[serde(rename_all = "snake_case")]
96pub enum HumanRole {
97 Creator,
98 Admin,
99}
100
101impl HumanRole {
102 pub fn as_str(self) -> &'static str {
104 match self {
105 Self::Creator => "creator",
106 Self::Admin => "admin",
107 }
108 }
109
110 pub fn from_storage_value(value: &str) -> Option<Self> {
112 match value {
113 "creator" => Some(Self::Creator),
114 "admin" => Some(Self::Admin),
115 _ => None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
122#[serde(rename_all = "snake_case")]
123pub enum HumanStatus {
124 Active,
125 SetupRequired,
126 Disabled,
127 Deleted,
128}
129
130impl HumanStatus {
131 pub fn as_str(self) -> &'static str {
133 match self {
134 Self::Active => "active",
135 Self::SetupRequired => "setup_required",
136 Self::Disabled => "disabled",
137 Self::Deleted => "deleted",
138 }
139 }
140
141 pub fn from_storage_value(value: &str) -> Option<Self> {
143 match value {
144 "active" => Some(Self::Active),
145 "setup_required" => Some(Self::SetupRequired),
146 "disabled" => Some(Self::Disabled),
147 "deleted" => Some(Self::Deleted),
148 _ => None,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Deserialize, garde::Validate, schemars::JsonSchema)]
155#[garde(allow_unvalidated)]
156#[serde(deny_unknown_fields)]
157pub struct GithubSignInLoginRequest {
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub return_to: Option<String>,
160}
161
162#[derive(Debug, Clone, Deserialize, garde::Validate, schemars::JsonSchema)]
164#[garde(allow_unvalidated)]
165#[serde(deny_unknown_fields)]
166pub struct CompleteHumanSetupRequest {
167 pub pioneer_code: PioneerCodeInput,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
172pub struct GithubSignInLoginResponse {
173 pub authorization_url: GithubSignInAuthorizationUrl,
174}
175
176#[derive(Debug, Clone, Deserialize, garde::Validate, schemars::JsonSchema)]
178#[garde(allow_unvalidated)]
179#[serde(deny_unknown_fields)]
180pub struct GithubSignInCallbackRequest {
181 #[garde(custom(crate::validation::trimmed_non_empty))]
182 pub code: String,
183 #[garde(custom(crate::validation::trimmed_non_empty))]
184 pub state: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
189pub struct HumanSessionResponse {
190 pub human_id: HumanId,
191 pub status: HumanStatus,
192 pub github_user_id: GithubUserId,
193 pub github_login: String,
194 pub roles: Vec<HumanRole>,
195 pub csrf_token: String,
196 pub expires_at: String,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
201pub struct CompleteHumanSetupResponse {
202 pub session: HumanSessionResponse,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
207pub struct GithubSignInCallbackResponse {
208 pub session: HumanSessionResponse,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub return_to: Option<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
215pub struct AdminHumanDto {
216 pub human_id: HumanId,
217 pub status: HumanStatus,
218 pub github_user_id: GithubUserId,
219 pub github_login: String,
220 pub roles: Vec<HumanRole>,
221 pub created_at: String,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub disabled_at: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub deleted_at: Option<String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
230pub struct AdminHumanListResponse {
231 pub items: Vec<AdminHumanDto>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
236pub struct AdminHumanRoleResponse {
237 pub human: AdminHumanDto,
238}
239
240#[derive(Debug, Clone, Deserialize, garde::Validate, schemars::JsonSchema)]
242#[garde(allow_unvalidated)]
243#[serde(deny_unknown_fields)]
244pub struct CreateAdminServiceTokenRequest {
245 #[garde(custom(crate::validation::trimmed_non_empty))]
246 pub label: String,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub expires_at: Option<String>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
253pub struct AdminServiceTokenDto {
254 pub id: AdminServiceTokenId,
255 pub label: String,
256 pub status: String,
257 pub created_by_human_id: HumanId,
258 pub created_at: String,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub last_used_at: Option<String>,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub expires_at: Option<String>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub revoked_by_human_id: Option<HumanId>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub revoked_at: Option<String>,
267}
268
269#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
271pub struct AdminServiceTokenCreatedResponse {
272 pub token: String,
273 pub token_record: AdminServiceTokenDto,
274}
275
276impl fmt::Debug for AdminServiceTokenCreatedResponse {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 f.debug_struct("AdminServiceTokenCreatedResponse")
280 .field("token", &"<redacted>")
281 .field("token_record", &self.token_record)
282 .finish()
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
288pub struct AdminServiceTokenListResponse {
289 pub items: Vec<AdminServiceTokenDto>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
294pub struct RevokeAdminServiceTokenResponse {
295 pub token_record: AdminServiceTokenDto,
296}
297
298#[derive(Debug, Clone, Deserialize, garde::Validate, schemars::JsonSchema)]
300#[garde(allow_unvalidated)]
301#[serde(deny_unknown_fields)]
302pub struct CreateCreatorApiTokenRequest {
303 #[garde(custom(crate::validation::trimmed_non_empty))]
304 pub label: String,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub expires_at: Option<String>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
311pub struct CreatorApiTokenDto {
312 pub id: CreatorApiTokenId,
313 pub label: String,
314 pub status: String,
315 pub created_by_human_id: HumanId,
316 pub created_at: String,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub last_used_at: Option<String>,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub expires_at: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub revoked_at: Option<String>,
323}
324
325#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
327pub struct CreatorApiTokenCreatedResponse {
328 pub token: String,
329 pub token_record: CreatorApiTokenDto,
330}
331
332impl fmt::Debug for CreatorApiTokenCreatedResponse {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 f.debug_struct("CreatorApiTokenCreatedResponse")
336 .field("token", &"<redacted>")
337 .field("token_record", &self.token_record)
338 .finish()
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
344pub struct CreatorApiTokenListResponse {
345 pub items: Vec<CreatorApiTokenDto>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
350pub struct RevokeCreatorApiTokenResponse {
351 pub token_record: CreatorApiTokenDto,
352}
353
354#[cfg(test)]
355mod tests {
356 use super::{
357 AdminServiceTokenCreatedResponse, AdminServiceTokenDto, CreatorApiTokenCreatedResponse,
358 CreatorApiTokenDto, GithubUserId,
359 };
360 use crate::models::ids::{AdminServiceTokenId, CreatorApiTokenId, HumanId};
361
362 #[test]
364 fn github_user_ids_are_positive_wire_integers() {
365 let id = GithubUserId::try_new(42).expect("positive id should parse");
366 assert_eq!(id.as_i64(), 42);
367 assert_eq!(
368 serde_json::to_value(id).expect("id should serialize"),
369 serde_json::json!(42)
370 );
371 assert!(GithubUserId::try_new(0).is_err());
372 assert!(serde_json::from_value::<GithubUserId>(serde_json::json!(-1)).is_err());
373 }
374
375 #[test]
377 fn token_creation_debug_output_redacts_raw_tokens() {
378 let human_id = HumanId::generate();
379 let admin = AdminServiceTokenCreatedResponse {
380 token: "agentics_admin_secret".to_string(),
381 token_record: AdminServiceTokenDto {
382 id: AdminServiceTokenId::generate(),
383 label: "admin".to_string(),
384 status: "active".to_string(),
385 created_by_human_id: human_id.clone(),
386 created_at: "2026-06-01T00:00:00Z".to_string(),
387 last_used_at: None,
388 expires_at: None,
389 revoked_by_human_id: None,
390 revoked_at: None,
391 },
392 };
393 let creator = CreatorApiTokenCreatedResponse {
394 token: "agentics_creator_secret".to_string(),
395 token_record: CreatorApiTokenDto {
396 id: CreatorApiTokenId::generate(),
397 label: "creator".to_string(),
398 status: "active".to_string(),
399 created_by_human_id: human_id,
400 created_at: "2026-06-01T00:00:00Z".to_string(),
401 last_used_at: None,
402 expires_at: None,
403 revoked_at: None,
404 },
405 };
406
407 let admin_debug = format!("{admin:?}");
408 let creator_debug = format!("{creator:?}");
409
410 assert!(!admin_debug.contains("agentics_admin_secret"));
411 assert!(!creator_debug.contains("agentics_creator_secret"));
412 assert!(admin_debug.contains("<redacted>"));
413 assert!(creator_debug.contains("<redacted>"));
414 }
415}