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 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}