Skip to main content

zino_model/user/
mod.rs

1//! The `user` model and related services.
2
3use 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/// The `user` model.
32#[derive(
33    Debug, Clone, Default, Serialize, Deserialize, DecodeRow, Entity, Schema, ModelAccessor,
34)]
35#[serde(default)]
36#[schema(auto_rename)]
37pub struct User {
38    // Basic fields.
39    #[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    // Info fields.
58    #[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>, // tag.id, tag.namespace = "*:user"
81
82    // Security.
83    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    // Extensions.
93    extra: Map,
94
95    // Revisions.
96    #[cfg(feature = "owner-id")]
97    #[schema(reference = "User")]
98    owner_id: Option<Uuid>, // user.id
99    #[cfg(feature = "maintainer-id")]
100    #[schema(reference = "User")]
101    maintainer_id: Option<Uuid>, // user.id
102    #[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    /// Sets the `access_key_id`.
208    #[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    /// Sets the `roles` field.
214    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    /// Returns the `union_id` field.
229    #[inline]
230    pub fn union_id(&self) -> &str {
231        &self.union_id
232    }
233
234    /// Returns the `access_key_id` field.
235    #[inline]
236    pub fn access_key_id(&self) -> &str {
237        self.access_key_id.as_str()
238    }
239
240    /// Returns the `roles` field.
241    #[inline]
242    pub fn roles(&self) -> &[String] {
243        self.roles.as_slice()
244    }
245
246    /// Returns a session for the user.
247    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}