1use serde::{Deserialize, Serialize};
4use zino_auth::{AccessKeyId, UserSession};
5use zino_core::{
6 Map, Uuid, bail,
7 datetime::DateTime,
8 error::Error,
9 extension::JsonObjectExt,
10 model::{Model, ModelHooks},
11 validation::Validation,
12};
13use zino_derive::{DecodeRow, Entity, ModelAccessor, Schema};
14use zino_orm::ModelHelper;
15
16#[cfg(feature = "tags")]
17use crate::tag::Tag;
18
19mod jwt_auth;
20mod status;
21
22pub use jwt_auth::JwtAuthService;
23pub use status::UserStatus;
24
25#[cfg(feature = "visibility")]
26mod visibility;
27
28#[cfg(feature = "visibility")]
29pub use visibility::UserVisibility;
30
31#[derive(
33 Debug, Clone, Default, Serialize, Deserialize, DecodeRow, Entity, Schema, ModelAccessor,
34)]
35#[serde(default)]
36#[schema(auto_rename)]
37pub struct User {
38 #[schema(read_only)]
40 id: Uuid,
41 #[schema(not_null)]
42 name: String,
43 #[cfg(feature = "namespace")]
44 #[schema(default_value = "User::model_namespace", index_type = "hash")]
45 namespace: String,
46 #[cfg(feature = "visibility")]
47 #[schema(type_name = "String", default_value = "UserVisibility::default")]
48 visibility: UserVisibility,
49 #[schema(
50 type_name = "String",
51 default_value = "UserStatus::default",
52 index_type = "hash"
53 )]
54 status: UserStatus,
55 description: String,
56
57 #[schema(unique)]
59 union_id: String,
60 #[schema(not_null, unique, write_only)]
61 access_key_id: String,
62 #[schema(not_null, unique, write_only)]
63 account: String,
64 #[schema(not_null, write_only)]
65 password: String,
66 nickname: String,
67 #[schema(format = "uri")]
68 avatar: String,
69 #[schema(format = "uri")]
70 website: String,
71 #[schema(format = "email")]
72 email: String,
73 location: String,
74 locale: String,
75 mobile: String,
76 #[schema(snapshot, nonempty, unique_items, index_type = "gin")]
77 roles: Vec<String>,
78 #[cfg(feature = "tags")]
79 #[schema(unique_items, reference = "Tag", index_type = "gin")]
80 tags: Vec<Uuid>, last_login_at: DateTime,
84 #[schema(format = "ip")]
85 last_login_ip: String,
86 current_login_at: DateTime,
87 #[schema(format = "ip")]
88 current_login_ip: String,
89 login_count: u32,
90 failed_login_count: u8,
91
92 extra: Map,
94
95 #[cfg(feature = "owner-id")]
97 #[schema(reference = "User")]
98 owner_id: Option<Uuid>, #[cfg(feature = "maintainer-id")]
100 #[schema(reference = "User")]
101 maintainer_id: Option<Uuid>, #[schema(read_only, default_value = "now", index_type = "btree")]
103 created_at: DateTime,
104 #[schema(default_value = "now", index_type = "btree")]
105 updated_at: DateTime,
106 version: u64,
107 #[cfg(feature = "edition")]
108 edition: u32,
109}
110
111impl Model for User {
112 const MODEL_NAME: &'static str = "user";
113
114 #[inline]
115 fn new() -> Self {
116 Self {
117 id: Uuid::now_v7(),
118 access_key_id: AccessKeyId::new().to_string(),
119 ..Self::default()
120 }
121 }
122
123 fn read_map(&mut self, data: &Map) -> Validation {
124 let mut validation = Validation::new();
125 if let Some(result) = data.parse_uuid("id") {
126 match result {
127 Ok(id) => self.id = id,
128 Err(err) => validation.record_fail("id", err),
129 }
130 }
131 if let Some(name) = data.parse_string("name") {
132 self.name = name.into_owned();
133 }
134 if let Some(union_id) = data.parse_string("union_id") {
135 self.union_id = union_id.into_owned();
136 }
137 if let Some(account) = data.parse_string("account") {
138 self.account = account.into_owned();
139 }
140 if let Some(password) = data.parse_string("password") {
141 match User::encrypt_password(&password) {
142 Ok(password) => self.password = password,
143 Err(err) => validation.record_fail("password", err),
144 }
145 }
146 if let Some(roles) = data.parse_str_array("roles")
147 && let Err(err) = self.set_roles(roles)
148 {
149 validation.record_fail("roles", err);
150 }
151 if self.roles.is_empty() && !validation.contains_key("roles") {
152 validation.record("roles", "should be nonempty");
153 }
154 #[cfg(feature = "tags")]
155 if let Some(result) = data.parse_array("tags") {
156 match result {
157 Ok(tags) => self.tags = tags,
158 Err(err) => validation.record_fail("tags", err),
159 }
160 }
161 #[cfg(feature = "owner-id")]
162 if let Some(result) = data.parse_uuid("owner_id") {
163 match result {
164 Ok(owner_id) => self.owner_id = Some(owner_id),
165 Err(err) => validation.record_fail("owner_id", err),
166 }
167 }
168 #[cfg(feature = "maintainer-id")]
169 if let Some(result) = data.parse_uuid("maintainer_id") {
170 match result {
171 Ok(maintainer_id) => self.maintainer_id = Some(maintainer_id),
172 Err(err) => validation.record_fail("maintainer_id", err),
173 }
174 }
175 validation
176 }
177}
178
179impl ModelHooks for User {
180 type Data = ();
181 #[cfg(feature = "maintainer-id")]
182 type Extension = UserSession<Uuid, String>;
183 #[cfg(not(feature = "maintainer-id"))]
184 type Extension = ();
185
186 #[cfg(feature = "maintainer-id")]
187 #[inline]
188 async fn after_extract(&mut self, session: Self::Extension) -> Result<(), Error> {
189 self.maintainer_id = Some(*session.user_id());
190 Ok(())
191 }
192
193 #[cfg(feature = "maintainer-id")]
194 #[inline]
195 async fn before_validation(
196 data: &mut Map,
197 extension: Option<&Self::Extension>,
198 ) -> Result<(), Error> {
199 if let Some(session) = extension {
200 data.upsert("maintainer_id", session.user_id().to_string());
201 }
202 Ok(())
203 }
204}
205
206impl User {
207 #[inline]
209 pub fn set_access_key_id(&mut self, access_key_id: AccessKeyId) {
210 self.access_key_id = access_key_id.to_string();
211 }
212
213 pub fn set_roles(&mut self, roles: Vec<&str>) -> Result<(), Error> {
215 let num_roles = roles.len();
216 let special_roles = ["superuser", "user", "guest"];
217 for role in &roles {
218 if special_roles.contains(role) && num_roles != 1 {
219 bail!("special role `{}` is exclusive", role);
220 } else if role.is_empty() {
221 bail!("`roles` can not contain empty values");
222 }
223 }
224 self.roles = roles.into_iter().map(|s| s.to_owned()).collect();
225 Ok(())
226 }
227
228 #[inline]
230 pub fn union_id(&self) -> &str {
231 &self.union_id
232 }
233
234 #[inline]
236 pub fn access_key_id(&self) -> &str {
237 self.access_key_id.as_str()
238 }
239
240 #[inline]
242 pub fn roles(&self) -> &[String] {
243 self.roles.as_slice()
244 }
245
246 pub fn user_session(&self) -> UserSession<Uuid, String> {
248 let mut user_session = UserSession::new(self.id, None);
249 user_session.set_access_key_id(self.access_key_id().into());
250 user_session.set_roles(self.roles());
251 user_session
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::User;
258 use zino_core::{Map, extension::JsonObjectExt, model::Model};
259
260 #[test]
261 fn it_checks_user_roles() {
262 let mut alice = User::new();
263 let mut data = Map::new();
264 data.upsert("name", "alice");
265 data.upsert("roles", vec!["admin:user", "auditor"]);
266
267 let validation = alice.read_map(&data);
268 assert!(validation.is_success());
269
270 let user_session = alice.user_session();
271 assert!(user_session.is_admin());
272 assert!(!user_session.is_worker());
273 assert!(user_session.is_auditor());
274 assert!(user_session.has_role("admin:user"));
275 assert!(!user_session.has_role("admin:group"));
276 assert!(user_session.has_role("auditor:log"));
277 assert!(!user_session.has_role("auditor_record"));
278 }
279}