1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum UserRole {
12 Admin,
14 Blocked,
16}
17
18impl UserRole {
19 #[cfg(feature = "server")]
21 #[allow(clippy::should_implement_trait)]
23 pub fn from_str(s: &str) -> Option<Self> {
24 match s {
25 "admin" => Some(UserRole::Admin),
26 "blocked" => Some(UserRole::Blocked),
27 _ => None,
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct SessionUser {
35 pub id: i32,
37 pub username: String,
39 pub email: String,
41 pub display_name: Option<String>,
43 pub avatar_url: Option<String>,
45 pub role: UserRole,
47 #[cfg_attr(
49 target_arch = "wasm32",
50 serde(deserialize_with = "crate::models::rfc3339::deserialize")
51 )]
52 pub created_at: DateTime<Utc>,
53 pub session_generation: i32,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct PublicUser {
60 pub id: i32,
62 pub username: String,
64 pub email: String,
66 pub display_name: Option<String>,
68 pub avatar_url: Option<String>,
70 pub role: UserRole,
72 #[cfg_attr(
74 target_arch = "wasm32",
75 serde(deserialize_with = "crate::models::rfc3339::deserialize")
76 )]
77 pub created_at: DateTime<Utc>,
78}
79
80impl From<SessionUser> for PublicUser {
81 fn from(u: SessionUser) -> Self {
83 PublicUser {
84 id: u.id,
85 username: u.username,
86 email: u.email,
87 display_name: u.display_name,
88 avatar_url: u.avatar_url,
89 role: u.role,
90 created_at: u.created_at,
91 }
92 }
93}
94
95impl PublicUser {
96 pub fn display_label(&self) -> &str {
98 self.display_name.as_deref().unwrap_or(&self.username)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use chrono::{TimeZone, Utc};
106
107 fn sample_user() -> SessionUser {
108 SessionUser {
109 id: 1,
110 username: "admin".to_string(),
111 email: "admin@test.com".to_string(),
112 display_name: None,
113 avatar_url: None,
114 role: UserRole::Admin,
115 created_at: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
116 session_generation: 0,
117 }
118 }
119
120 #[test]
121 #[cfg(feature = "server")]
122 fn user_role_from_str() {
123 assert_eq!(UserRole::from_str("admin"), Some(UserRole::Admin));
124 assert_eq!(UserRole::from_str("blocked"), Some(UserRole::Blocked));
125 assert_eq!(UserRole::from_str("unknown"), None);
126 assert_eq!(UserRole::from_str(""), None);
127 }
128
129 #[test]
130 fn user_role_serde_roundtrip() {
131 let json = serde_json::to_string(&UserRole::Admin).unwrap();
132 assert_eq!(
133 serde_json::from_str::<UserRole>(&json).unwrap(),
134 UserRole::Admin
135 );
136 }
137
138 #[test]
139 fn session_user_to_public_user_conversion() {
140 let session = sample_user();
143 let public: PublicUser = session.clone().into();
144 assert_eq!(public.id, session.id);
145 assert_eq!(public.username, session.username);
146 assert_eq!(public.email, session.email);
147 assert_eq!(public.role, session.role);
148 assert_eq!(public.created_at, session.created_at);
149 }
150
151 #[test]
152 fn public_user_excludes_session_generation() {
153 let session = sample_user();
155 let public: PublicUser = session.into();
156 let json = serde_json::to_string(&public).unwrap();
157 assert!(!json.contains("session_generation"));
158 }
159
160 #[test]
161 fn display_label_falls_back_to_username() {
162 let session = sample_user();
163 let mut public: PublicUser = session.into();
164 assert_eq!(public.display_label(), "admin");
166 public.display_name = Some("站长".to_string());
168 assert_eq!(public.display_label(), "站长");
169 }
170}