airlab_lib/model/
user.rs

1use crate::ctx::Ctx;
2use crate::model::ModelManager;
3use crate::model::Result;
4use crate::model::base::{self, DbBmc};
5use crate::pwd::{self, ContentToHash};
6use modql::field::{Fields, HasFields};
7use modql::filter::{FilterNodes, ListOptions, OpValsInt64, OpValsString};
8use sea_query::{Expr, Iden, PostgresQueryBuilder, Query, SimpleExpr};
9use sea_query_binder::SqlxBinder;
10use serde::{Deserialize, Serialize};
11use sqlx::FromRow;
12use sqlx::postgres::PgRow;
13use uuid::Uuid;
14
15#[derive(Clone, Fields, FromRow, Debug, Serialize, Deserialize, Default)]
16pub struct User {
17    pub id: i32,
18    pub username: String,
19    pub email: Option<String>,
20    pub name: Option<String>,
21    #[serde(rename = "isActive")]
22    pub is_active: bool,
23    #[serde(rename = "isAdmin")]
24    pub is_admin: bool,
25    #[serde(rename = "updatedAt")]
26    pub updated_at: chrono::DateTime<chrono::Utc>,
27    #[serde(rename = "createdAt")]
28    pub created_at: chrono::DateTime<chrono::Utc>,
29}
30
31impl UserBmc {
32    #[must_use]
33    pub fn get_create_sql(drop_table: bool) -> String {
34        let table = Self::TABLE;
35        format!(
36            r##"{}
37create table if not exists "{table}" (
38  id integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1000) PRIMARY KEY,
39
40  username varchar(128) NOT NULL UNIQUE,
41
42  email character varying,
43  name character varying,
44  password character varying,
45  is_active boolean DEFAULT false NOT NULL,
46  is_admin boolean DEFAULT false NOT NULL,
47  meta jsonb,
48  created_at timestamp with time zone DEFAULT now() NOT NULL,
49  updated_at timestamp with time zone DEFAULT now() NOT NULL,
50
51  -- Auth
52  pwd varchar(256),
53  reset_token varchar(256),
54  pwd_salt uuid NOT NULL DEFAULT gen_random_uuid(),
55  token_salt uuid NOT NULL DEFAULT gen_random_uuid()
56);
57ALTER TABLE ONLY "user"
58  ADD CONSTRAINT "UQ_user_email" UNIQUE (email);
59CREATE INDEX "IDX_user_email" ON "user" USING btree (email);
60CREATE INDEX "IDX_user_is_active" ON "user" USING btree (is_active);
61        "##,
62            if drop_table {
63                format!("drop table if exists {table};")
64            } else {
65                String::new()
66            }
67        )
68    }
69}
70#[derive(Fields, Default, Deserialize, Debug)]
71pub struct UserForCreate {
72    pub username: Option<String>,
73    pub pwd_clear: Option<String>,
74    pub email: String,
75    pub name: Option<String>,
76}
77
78#[derive(Fields, Default, Serialize, Deserialize, Debug, Clone)]
79pub struct MinUser {
80    pub email: String,
81    pub id: i32,
82    pub name: String,
83}
84
85#[derive(Fields, Default, Deserialize, Debug)]
86pub struct UserForUpdate {
87    pub email: Option<String>,
88    pub name: Option<String>,
89    pub reset_token: Option<String>,
90    pub is_admin: Option<bool>,
91    pub is_active: Option<bool>,
92}
93
94#[derive(Fields)]
95pub struct UserForInsert {
96    pub username: String,
97}
98
99#[derive(Clone, FromRow, Fields, Debug)]
100pub struct UserForLogin {
101    pub id: i32,
102    pub username: String,
103
104    pub pwd: Option<String>,
105    pub pwd_salt: Uuid,
106    pub token_salt: Uuid,
107}
108
109#[derive(Clone, FromRow, Fields, Debug)]
110pub struct UserForAuth {
111    pub id: i32,
112    pub username: String,
113
114    // -- token info
115    pub token_salt: Uuid,
116}
117
118pub trait UserBy: HasFields + for<'r> FromRow<'r, PgRow> + Unpin + Send {}
119
120impl UserBy for User {}
121impl UserBy for UserForLogin {}
122impl UserBy for UserForAuth {}
123
124#[derive(Iden)]
125enum UserIden {
126    Id,
127    Username,
128    ResetToken,
129    Pwd,
130}
131
132#[derive(FilterNodes, Deserialize, Default, Debug)]
133pub struct UserFilter {
134    id: Option<OpValsInt64>,
135
136    name: Option<OpValsString>,
137}
138
139pub struct UserBmc;
140
141impl DbBmc for UserBmc {
142    const TABLE: &'static str = "user";
143}
144
145impl UserBmc {
146    pub async fn get<E>(ctx: &Ctx, mm: &ModelManager, id: i32) -> Result<E>
147    where
148        E: UserBy,
149    {
150        base::get::<Self, _>(ctx, mm, id).await
151    }
152
153    pub async fn list(
154        ctx: &Ctx,
155        mm: &ModelManager,
156        filters: Option<Vec<UserFilter>>,
157        list_options: Option<ListOptions>,
158    ) -> Result<Vec<User>> {
159        base::list::<Self, _, _>(ctx, mm, filters, list_options).await
160    }
161
162    pub async fn first_by_username<E>(
163        _ctx: &Ctx,
164        mm: &ModelManager,
165        username: &str,
166    ) -> Result<Option<E>>
167    where
168        E: UserBy,
169    {
170        let db = mm.db();
171
172        let mut query = Query::select();
173        query
174            .from(Self::table_ref())
175            .columns(E::field_idens())
176            .and_where(Expr::col(UserIden::Username).eq(username));
177
178        let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
179        let user = sqlx::query_as_with::<_, E, _>(&sql, values)
180            .fetch_optional(db)
181            .await?;
182
183        Ok(user)
184    }
185
186    pub async fn first_by_token<E>(_ctx: &Ctx, mm: &ModelManager, token: &str) -> Result<Option<E>>
187    where
188        E: UserBy,
189    {
190        let db = mm.db();
191
192        let mut query = Query::select();
193        query
194            .from(Self::table_ref())
195            .columns(E::field_idens())
196            .and_where(Expr::col(UserIden::ResetToken).eq(token));
197
198        let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
199        let user = sqlx::query_as_with::<_, E, _>(&sql, values)
200            .fetch_optional(db)
201            .await?;
202
203        Ok(user)
204    }
205
206    pub async fn create(ctx: &Ctx, mm: &ModelManager, user_c: UserForCreate) -> Result<i32> {
207        base::create::<Self, _>(ctx, mm, user_c).await
208    }
209
210    pub async fn update(
211        ctx: &Ctx,
212        mm: &ModelManager,
213        id: i32,
214        group_u: UserForUpdate,
215    ) -> Result<()> {
216        base::update::<Self, _>(ctx, mm, id, group_u).await
217    }
218
219    pub async fn update_pwd(ctx: &Ctx, mm: &ModelManager, id: i32, pwd_clear: &str) -> Result<()> {
220        let db = mm.db();
221
222        let user: UserForLogin = Self::get(ctx, mm, id).await?;
223        let pwd = pwd::hash_pwd(&ContentToHash {
224            content: pwd_clear.to_string(),
225            salt: user.pwd_salt,
226        })?;
227
228        let mut query = Query::update();
229        query
230            .table(Self::table_ref())
231            .value(UserIden::Pwd, SimpleExpr::from(pwd))
232            .and_where(Expr::col(UserIden::Id).eq(id));
233
234        let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
235        let _count = sqlx::query_with(&sql, values)
236            .execute(db)
237            .await?
238            .rows_affected();
239
240        Ok(())
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use anyhow::{Context, Result};
248
249    #[ignore]
250    #[tokio::test]
251    async fn test_first_ok_demo1() -> Result<()> {
252        let mm = ModelManager::new().await?;
253        let ctx = Ctx::root_ctx();
254        let fx_username = "demo1@uzh.ch";
255
256        let user: User = UserBmc::first_by_username(&ctx, &mm, fx_username)
257            .await?
258            .context("Should have user 'demo1'")?;
259
260        assert_eq!(user.username, fx_username);
261
262        Ok(())
263    }
264}