Skip to main content

tibba_model/
user.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    Error, JsonSnafu, Model, ModelListParams, Schema, SchemaAllowEdit, SchemaOption,
17    SchemaOptionValue, SchemaType, SchemaView, SqlxSnafu, Status, format_datetime,
18    new_schema_options,
19};
20use serde::{Deserialize, Serialize};
21use snafu::ResultExt;
22use sqlx::FromRow;
23use sqlx::types::Json;
24use sqlx::{Pool, Postgres, QueryBuilder};
25use std::collections::HashMap;
26use time::PrimitiveDateTime;
27type Result<T> = std::result::Result<T, Error>;
28
29pub const ROLE_ADMIN: &str = "admin";
30pub const ROLE_SUPER_ADMIN: &str = "su";
31
32#[derive(FromRow)]
33struct UserSchema {
34    id: i64,
35    status: i16,
36    created: PrimitiveDateTime,
37    modified: PrimitiveDateTime,
38    account: String,
39    password: String,
40    nickname: Option<String>,
41    phone: Option<String>,
42    roles: Option<Json<Vec<String>>>,
43    groups: Option<Json<Vec<String>>>,
44    remark: Option<String>,
45    email: Option<String>,
46    avatar: Option<String>,
47    last_login_at: Option<PrimitiveDateTime>,
48}
49
50#[derive(Deserialize, Serialize)]
51pub struct User {
52    pub id: i64,
53    pub status: i16,
54    pub created: String,
55    pub modified: String,
56    pub account: String,
57    #[serde(skip_serializing)]
58    pub password: String,
59    pub nickname: Option<String>,
60    pub phone: Option<String>,
61    pub roles: Option<Vec<String>>,
62    pub groups: Option<Vec<String>>,
63    pub remark: Option<String>,
64    pub email: Option<String>,
65    pub avatar: Option<String>,
66    pub last_login_at: Option<String>,
67}
68
69impl From<UserSchema> for User {
70    fn from(user: UserSchema) -> Self {
71        Self {
72            id: user.id,
73            status: user.status,
74            created: format_datetime(user.created),
75            modified: format_datetime(user.modified),
76            account: user.account,
77            password: user.password,
78            nickname: user.nickname,
79            phone: user.phone,
80            roles: user.roles.map(|roles| roles.0),
81            groups: user.groups.map(|groups| groups.0),
82            remark: user.remark,
83            email: user.email,
84            avatar: user.avatar,
85            last_login_at: user.last_login_at.map(format_datetime),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Deserialize, Default)]
91pub struct UserUpdateParams {
92    pub nickname: Option<String>,
93    pub phone: Option<String>,
94    pub email: Option<String>,
95    pub avatar: Option<String>,
96    pub roles: Option<Vec<String>>,
97    pub groups: Option<Vec<String>>,
98    pub status: Option<i16>,
99}
100
101pub struct UserModel {}
102
103impl Model for UserModel {
104    type Output = User;
105    fn new() -> Self {
106        Self {}
107    }
108    fn keyword(&self) -> String {
109        "account".to_string()
110    }
111    async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
112        SchemaView {
113            schemas: vec![
114                Schema::new_id(),
115                Schema {
116                    name: "account".to_string(),
117                    category: SchemaType::String,
118                    read_only: true,
119                    required: true,
120                    identity: true,
121                    ..Default::default()
122                },
123                Schema::new_status(),
124                Schema {
125                    name: "nickname".to_string(),
126                    category: SchemaType::String,
127                    ..Default::default()
128                },
129                Schema {
130                    name: "phone".to_string(),
131                    category: SchemaType::String,
132                    ..Default::default()
133                },
134                Schema {
135                    name: "roles".to_string(),
136                    category: SchemaType::Strings,
137                    options: Some(new_schema_options(&[ROLE_ADMIN, ROLE_SUPER_ADMIN])),
138                    ..Default::default()
139                },
140                Schema {
141                    name: "groups".to_string(),
142                    category: SchemaType::Strings,
143                    options: Some(new_schema_options(&["it", "marketing"])),
144                    ..Default::default()
145                },
146                Schema {
147                    name: "last_login_at".to_string(),
148                    category: SchemaType::Date,
149                    read_only: true,
150                    ..Default::default()
151                },
152                Schema::new_created(),
153                Schema::new_modified(),
154            ],
155            allow_edit: SchemaAllowEdit {
156                owner: true,
157                roles: vec![ROLE_SUPER_ADMIN.to_string()],
158                ..Default::default()
159            },
160            ..Default::default()
161        }
162    }
163    async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
164        let result = sqlx::query_as::<_, UserSchema>(
165            r#"SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL"#,
166        )
167        .bind(id as i64)
168        .fetch_optional(pool)
169        .await
170        .context(SqlxSnafu)?;
171
172        Ok(result.map(|user| user.into()))
173    }
174    async fn delete_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<()> {
175        sqlx::query(
176            r#"UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL"#
177        )
178            .bind(id as i64)
179            .execute(pool)
180            .await
181            .context(SqlxSnafu)?;
182        Ok(())
183    }
184    async fn update_by_id(
185        &self,
186        pool: &Pool<Postgres>,
187        id: u64,
188        data: serde_json::Value,
189    ) -> Result<()> {
190        let params: UserUpdateParams = serde_json::from_value(data).context(JsonSnafu)?;
191        let _ = sqlx::query(
192            r#"
193            UPDATE users SET
194                email = COALESCE($1, email),
195                avatar = COALESCE($2, avatar),
196                roles = COALESCE($3, roles),
197                groups = COALESCE($4, groups),
198                status = COALESCE($5, status),
199                nickname = COALESCE($6, nickname),
200                phone = COALESCE($7, phone)
201            WHERE id = $8 AND deleted_at IS NULL
202            "#,
203        )
204        .bind(params.email.as_deref())
205        .bind(params.avatar.as_deref())
206        .bind(params.roles.map(Json))
207        .bind(params.groups.map(Json))
208        .bind(params.status)
209        .bind(params.nickname.as_deref())
210        .bind(params.phone.as_deref())
211        .bind(id as i64)
212        .execute(pool)
213        .await
214        .context(SqlxSnafu)?;
215
216        Ok(())
217    }
218    fn push_filter_conditions<'args>(
219        &self,
220        qb: &mut QueryBuilder<'args, Postgres>,
221        filters: &HashMap<String, String>,
222    ) -> Result<()> {
223        if let Some(status) = filters.get("status").and_then(|s| s.parse::<i16>().ok()) {
224            qb.push(" AND status = ");
225            qb.push_bind(status);
226        }
227        if let Some(role) = filters.get("role") {
228            qb.push(" AND roles @> ");
229            qb.push_bind(Json(vec![role.clone()]));
230            qb.push("::jsonb");
231        }
232        if let Some(group) = filters.get("group") {
233            qb.push(" AND groups @> ");
234            qb.push_bind(Json(vec![group.clone()]));
235            qb.push("::jsonb");
236        }
237        Ok(())
238    }
239
240    async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
241        let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM users");
242        self.push_conditions(&mut qb, params)?;
243        let count = qb
244            .build_query_scalar::<i64>()
245            .fetch_one(pool)
246            .await
247            .context(SqlxSnafu)?;
248        Ok(count)
249    }
250
251    async fn list(
252        &self,
253        pool: &Pool<Postgres>,
254        params: &ModelListParams,
255    ) -> Result<Vec<Self::Output>> {
256        let mut qb = QueryBuilder::new("SELECT * FROM users");
257        self.push_conditions(&mut qb, params)?;
258        params.push_pagination(&mut qb);
259        let result = qb
260            .build_query_as::<UserSchema>()
261            .fetch_all(pool)
262            .await
263            .context(SqlxSnafu)?;
264        Ok(result.into_iter().map(|u| u.into()).collect())
265    }
266    async fn search_options(
267        &self,
268        pool: &Pool<Postgres>,
269        keyword: Option<String>,
270    ) -> Result<Vec<SchemaOption>> {
271        let params = ModelListParams {
272            keyword,
273            limit: 20,
274            page: 1,
275            ..Default::default()
276        };
277        let users = self.list(pool, &params).await?;
278        Ok(users
279            .into_iter()
280            .map(|u| SchemaOption {
281                label: u.account,
282                value: SchemaOptionValue::String(u.id.to_string()),
283            })
284            .collect())
285    }
286}
287
288impl UserModel {
289    pub async fn register(
290        &self,
291        pool: &Pool<Postgres>,
292        account: &str,
293        password: &str,
294    ) -> Result<u64> {
295        // Get current time for created_at and updated_at
296
297        // Insert user and return the last insert ID
298        let row: (i64,) = sqlx::query_as(
299            r#"
300            INSERT INTO users (
301                status, account, password
302            ) VALUES (
303                $1, $2, $3
304            ) RETURNING id
305            "#,
306        )
307        .bind(Status::Enabled as i16)
308        .bind(account)
309        .bind(password)
310        .fetch_one(pool)
311        .await
312        .context(SqlxSnafu)?;
313
314        Ok(row.0 as u64)
315    }
316
317    pub async fn get_by_account(
318        &self,
319        pool: &Pool<Postgres>,
320        account: &str,
321    ) -> Result<Option<User>> {
322        let result = sqlx::query_as::<_, UserSchema>(
323            r#"SELECT * FROM users WHERE account = $1 AND deleted_at IS NULL"#,
324        )
325        .bind(account)
326        .fetch_optional(pool)
327        .await
328        .context(SqlxSnafu)?;
329
330        Ok(result.map(|user| user.into()))
331    }
332
333    pub async fn update_by_account(
334        &self,
335        pool: &Pool<Postgres>,
336        account: &str,
337        params: UserUpdateParams,
338    ) -> Result<()> {
339        let _ = sqlx::query(
340            r#"
341            UPDATE users SET
342                email = COALESCE($1, email),
343                avatar = COALESCE($2, avatar),
344                nickname = COALESCE($3, nickname),
345                phone = COALESCE($4, phone)
346            WHERE account = $5 AND deleted_at IS NULL
347            "#,
348        )
349        .bind(params.email.as_deref())
350        .bind(params.avatar.as_deref())
351        .bind(params.nickname.as_deref())
352        .bind(params.phone.as_deref())
353        .bind(account)
354        .execute(pool)
355        .await
356        .context(SqlxSnafu)?;
357        Ok(())
358    }
359
360    /// 登录成功后更新 last_login_at 为当前时间。
361    pub async fn update_last_login_at(&self, pool: &Pool<Postgres>, account: &str) -> Result<()> {
362        sqlx::query(
363            r#"UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE account = $1 AND deleted_at IS NULL"#,
364        )
365        .bind(account)
366        .execute(pool)
367        .await
368        .context(SqlxSnafu)?;
369        Ok(())
370    }
371}