Skip to main content

sova_auth/
store.rs

1//! Domain helpers: users, roles, tokens.
2
3use crate::entity::{permission, permission_role, reset_token, role, role_user, user};
4use chrono::{Duration, Utc};
5use sova_core::{Error, Result};
6use sova_db::{DbError, DbHandle};
7use sova_passport::{hash_password, verify_password};
8use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
9use sha2::{Digest, Sha256};
10
11/// Session principal for Fortify (web + API).
12#[derive(Clone, Debug, serde::Serialize)]
13pub struct CurrentUser {
14    pub id: i64,
15    pub email: String,
16    pub name: String,
17    pub avatar_path: Option<String>,
18    pub email_verified: bool,
19    pub two_factor_enabled: bool,
20    pub roles: Vec<String>,
21    pub permissions: Vec<String>,
22}
23
24impl CurrentUser {
25    pub fn has_role(&self, slug: &str) -> bool {
26        self.roles.iter().any(|r| r == slug)
27    }
28
29    pub fn has_permission(&self, slug: &str) -> bool {
30        self.has_role("admin") || self.permissions.iter().any(|p| p == slug)
31    }
32}
33
34pub fn hash_token(raw: &str) -> String {
35    let mut h = Sha256::new();
36    h.update(raw.as_bytes());
37    h.finalize().iter().map(|b| format!("{b:02x}")).collect()
38}
39
40pub fn random_token() -> String {
41    let mut buf = [0u8; 32];
42    let _ = getrandom::getrandom(&mut buf);
43    buf.iter().map(|b| format!("{b:02x}")).collect()
44}
45
46fn db_err(e: sea_orm::DbErr) -> Error {
47    Error::from(DbError::from(e))
48}
49
50pub async fn find_user_by_id(db: &DbHandle, id: i64) -> Result<Option<user::Model>> {
51    user::Entity::find_by_id(id).one(db).await.map_err(db_err)
52}
53
54pub async fn find_user_by_email(db: &DbHandle, email: &str) -> Result<Option<user::Model>> {
55    user::Entity::find()
56        .filter(user::Column::Email.eq(email.trim().to_lowercase()))
57        .one(db)
58        .await
59        .map_err(db_err)
60}
61
62pub async fn load_current_user(db: &DbHandle, id: i64) -> Result<Option<CurrentUser>> {
63    let Some(u) = find_user_by_id(db, id).await? else {
64        return Ok(None);
65    };
66    let (roles, permissions) = load_rbac(db, u.id).await?;
67    Ok(Some(CurrentUser {
68        id: u.id,
69        email: u.email,
70        name: u.name,
71        avatar_path: u.avatar_path,
72        email_verified: u.email_verified_at.is_some(),
73        two_factor_enabled: u.two_factor_confirmed_at.is_some(),
74        roles,
75        permissions,
76    }))
77}
78
79pub async fn load_rbac(db: &DbHandle, user_id: i64) -> Result<(Vec<String>, Vec<String>)> {
80    let links = role_user::Entity::find()
81        .filter(role_user::Column::UserId.eq(user_id))
82        .all(db)
83        .await
84        .map_err(db_err)?;
85    let mut roles = Vec::new();
86    let mut perm_slugs = Vec::new();
87    for link in links {
88        if let Some(r) = role::Entity::find_by_id(link.role_id)
89            .one(db)
90            .await
91            .map_err(db_err)?
92        {
93            roles.push(r.slug.clone());
94            let prs = permission_role::Entity::find()
95                .filter(permission_role::Column::RoleId.eq(r.id))
96                .all(db)
97                .await
98                .map_err(db_err)?;
99            for pr in prs {
100                if let Some(p) = permission::Entity::find_by_id(pr.permission_id)
101                    .one(db)
102                    .await
103                    .map_err(db_err)?
104                {
105                    if !perm_slugs.contains(&p.slug) {
106                        perm_slugs.push(p.slug);
107                    }
108                }
109            }
110        }
111    }
112    Ok((roles, perm_slugs))
113}
114
115pub async fn assign_role(db: &DbHandle, user_id: i64, role_slug: &str) -> Result<()> {
116    let Some(r) = role::Entity::find()
117        .filter(role::Column::Slug.eq(role_slug))
118        .one(db)
119        .await
120        .map_err(db_err)?
121    else {
122        return Err(Error::Internal(format!("role `{role_slug}` missing")));
123    };
124    let exists = role_user::Entity::find()
125        .filter(role_user::Column::UserId.eq(user_id))
126        .filter(role_user::Column::RoleId.eq(r.id))
127        .one(db)
128        .await
129        .map_err(db_err)?;
130    if exists.is_some() {
131        return Ok(());
132    }
133    role_user::ActiveModel {
134        user_id: Set(user_id),
135        role_id: Set(r.id),
136    }
137    .insert(db)
138    .await
139    .map_err(db_err)?;
140    Ok(())
141}
142
143pub async fn register_user(
144    db: &DbHandle,
145    email: &str,
146    name: &str,
147    password: &str,
148) -> Result<user::Model> {
149    let email = email.trim().to_lowercase();
150    if find_user_by_email(db, &email).await?.is_some() {
151        return Err(Error::custom(409, "email already registered"));
152    }
153    if password.len() < 8 {
154        return Err(Error::BadRequest(
155            "password must be at least 8 characters".into(),
156        ));
157    }
158    let model = user::ActiveModel {
159        email: Set(email),
160        name: Set(name.trim().to_string()),
161        password_hash: Set(Some(hash_password(password)?)),
162        created_at: Set(Utc::now()),
163        ..Default::default()
164    };
165    let u = model.insert(db).await.map_err(db_err)?;
166    assign_role(db, u.id, "user").await?;
167    Ok(u)
168}
169
170pub async fn attempt_login(db: &DbHandle, email: &str, password: &str) -> Result<user::Model> {
171    let u = find_user_by_email(db, email)
172        .await?
173        .ok_or(Error::Unauthorized)?;
174    let Some(hash) = u.password_hash.as_deref() else {
175        return Err(Error::Unauthorized);
176    };
177    if !verify_password(password, hash)? {
178        return Err(Error::Unauthorized);
179    }
180    Ok(u)
181}
182
183pub async fn store_reset_token(db: &DbHandle, email: &str, raw: &str) -> Result<()> {
184    let email = email.trim().to_lowercase();
185    // upsert
186    if let Some(existing) = reset_token::Entity::find_by_id(email.clone())
187        .one(db)
188        .await
189        .map_err(db_err)?
190    {
191        let mut am: reset_token::ActiveModel = existing.into();
192        am.token_hash = Set(hash_token(raw));
193        am.created_at = Set(Utc::now());
194        am.update(db).await.map_err(db_err)?;
195    } else {
196        reset_token::ActiveModel {
197            email: Set(email),
198            token_hash: Set(hash_token(raw)),
199            created_at: Set(Utc::now()),
200        }
201        .insert(db)
202        .await
203        .map_err(db_err)?;
204    }
205    Ok(())
206}
207
208pub async fn consume_reset_token(db: &DbHandle, email: &str, raw: &str) -> Result<()> {
209    let email = email.trim().to_lowercase();
210    let row = reset_token::Entity::find_by_id(email.clone())
211        .one(db)
212        .await
213        .map_err(db_err)?
214        .ok_or(Error::BadRequest("invalid or expired reset token".into()))?;
215    if row.token_hash != hash_token(raw) {
216        return Err(Error::BadRequest("invalid or expired reset token".into()));
217    }
218    if row.created_at + Duration::hours(1) < Utc::now() {
219        return Err(Error::BadRequest("invalid or expired reset token".into()));
220    }
221    reset_token::Entity::delete_by_id(email)
222        .exec(db)
223        .await
224        .map_err(db_err)?;
225    Ok(())
226}
227
228pub async fn set_password(db: &DbHandle, user_id: i64, password: &str) -> Result<()> {
229    if password.len() < 8 {
230        return Err(Error::BadRequest(
231            "password must be at least 8 characters".into(),
232        ));
233    }
234    let Some(u) = find_user_by_id(db, user_id).await? else {
235        return Err(Error::NotFound);
236    };
237    let mut am: user::ActiveModel = u.into();
238    am.password_hash = Set(Some(hash_password(password)?));
239    am.update(db).await.map_err(db_err)?;
240    Ok(())
241}
242
243pub async fn mark_email_verified(db: &DbHandle, user_id: i64) -> Result<()> {
244    let Some(u) = find_user_by_id(db, user_id).await? else {
245        return Err(Error::NotFound);
246    };
247    let mut am: user::ActiveModel = u.into();
248    am.email_verified_at = Set(Some(Utc::now()));
249    am.update(db).await.map_err(db_err)?;
250    Ok(())
251}
252
253pub async fn update_profile(
254    db: &DbHandle,
255    user_id: i64,
256    name: &str,
257    email: &str,
258) -> Result<user::Model> {
259    let Some(u) = find_user_by_id(db, user_id).await? else {
260        return Err(Error::NotFound);
261    };
262    let email = email.trim().to_lowercase();
263    if email != u.email && find_user_by_email(db, &email).await?.is_some() {
264        return Err(Error::custom(409, "email already registered"));
265    }
266    let email_changed = email != u.email;
267    let mut am: user::ActiveModel = u.into();
268    am.name = Set(name.trim().to_string());
269    am.email = Set(email);
270    if email_changed {
271        am.email_verified_at = Set(None);
272    }
273    am.update(db).await.map_err(db_err)
274}
275
276pub async fn set_avatar(db: &DbHandle, user_id: i64, path: Option<String>) -> Result<()> {
277    let Some(u) = find_user_by_id(db, user_id).await? else {
278        return Err(Error::NotFound);
279    };
280    let mut am: user::ActiveModel = u.into();
281    am.avatar_path = Set(path);
282    am.update(db).await.map_err(db_err)?;
283    Ok(())
284}
285
286pub async fn enable_2fa_secret(db: &DbHandle, user_id: i64, secret: &str, codes_json: &str) -> Result<()> {
287    let Some(u) = find_user_by_id(db, user_id).await? else {
288        return Err(Error::NotFound);
289    };
290    let mut am: user::ActiveModel = u.into();
291    am.two_factor_secret = Set(Some(secret.to_string()));
292    am.two_factor_recovery_codes = Set(Some(codes_json.to_string()));
293    am.two_factor_confirmed_at = Set(None);
294    am.update(db).await.map_err(db_err)?;
295    Ok(())
296}
297
298pub async fn set_recovery_codes(db: &DbHandle, user_id: i64, codes_json: &str) -> Result<()> {
299    let Some(u) = find_user_by_id(db, user_id).await? else {
300        return Err(Error::NotFound);
301    };
302    let mut am: user::ActiveModel = u.into();
303    am.two_factor_recovery_codes = Set(Some(codes_json.to_string()));
304    am.update(db).await.map_err(db_err)?;
305    Ok(())
306}
307
308pub async fn confirm_2fa(db: &DbHandle, user_id: i64) -> Result<()> {
309    let Some(u) = find_user_by_id(db, user_id).await? else {
310        return Err(Error::NotFound);
311    };
312    let mut am: user::ActiveModel = u.into();
313    am.two_factor_confirmed_at = Set(Some(Utc::now()));
314    am.update(db).await.map_err(db_err)?;
315    Ok(())
316}
317
318pub async fn disable_2fa(db: &DbHandle, user_id: i64) -> Result<()> {
319    let Some(u) = find_user_by_id(db, user_id).await? else {
320        return Err(Error::NotFound);
321    };
322    let mut am: user::ActiveModel = u.into();
323    am.two_factor_secret = Set(None);
324    am.two_factor_recovery_codes = Set(None);
325    am.two_factor_confirmed_at = Set(None);
326    am.update(db).await.map_err(db_err)?;
327    Ok(())
328}
329
330pub async fn list_roles(db: &DbHandle) -> Result<Vec<role::Model>> {
331    role::Entity::find().all(db).await.map_err(db_err)
332}
333
334pub async fn list_permissions(db: &DbHandle) -> Result<Vec<permission::Model>> {
335    permission::Entity::find().all(db).await.map_err(db_err)
336}
337
338pub fn is_system_role_slug(slug: &str) -> bool {
339    matches!(slug, "admin" | "user")
340}
341
342pub async fn find_role(db: &DbHandle, id: i64) -> Result<Option<role::Model>> {
343    role::Entity::find_by_id(id).one(db).await.map_err(db_err)
344}
345
346pub async fn find_permission(db: &DbHandle, id: i64) -> Result<Option<permission::Model>> {
347    permission::Entity::find_by_id(id).one(db).await.map_err(db_err)
348}
349
350pub async fn role_permission_ids(db: &DbHandle, role_id: i64) -> Result<Vec<i64>> {
351    let rows = permission_role::Entity::find()
352        .filter(permission_role::Column::RoleId.eq(role_id))
353        .all(db)
354        .await
355        .map_err(db_err)?;
356    Ok(rows.into_iter().map(|r| r.permission_id).collect())
357}
358
359pub async fn user_role_ids(db: &DbHandle, user_id: i64) -> Result<Vec<i64>> {
360    let rows = role_user::Entity::find()
361        .filter(role_user::Column::UserId.eq(user_id))
362        .all(db)
363        .await
364        .map_err(db_err)?;
365    Ok(rows.into_iter().map(|r| r.role_id).collect())
366}
367
368/// All user ids that have the given role slug (including via direct role_user).
369pub async fn user_ids_with_role(db: &DbHandle, role_slug: &str) -> Result<Vec<i64>> {
370    let Some(r) = role::Entity::find()
371        .filter(role::Column::Slug.eq(role_slug))
372        .one(db)
373        .await
374        .map_err(db_err)?
375    else {
376        return Ok(vec![]);
377    };
378    let rows = role_user::Entity::find()
379        .filter(role_user::Column::RoleId.eq(r.id))
380        .all(db)
381        .await
382        .map_err(db_err)?;
383    Ok(rows.into_iter().map(|r| r.user_id).collect())
384}
385
386/// Users who hold `permission_slug` via any role, plus all `admin` role members.
387pub async fn user_ids_with_permission(db: &DbHandle, permission_slug: &str) -> Result<Vec<i64>> {
388    let mut ids = user_ids_with_role(db, "admin").await?;
389    if let Some(p) = permission::Entity::find()
390        .filter(permission::Column::Slug.eq(permission_slug))
391        .one(db)
392        .await
393        .map_err(db_err)?
394    {
395        let links = permission_role::Entity::find()
396            .filter(permission_role::Column::PermissionId.eq(p.id))
397            .all(db)
398            .await
399            .map_err(db_err)?;
400        for link in links {
401            let rows = role_user::Entity::find()
402                .filter(role_user::Column::RoleId.eq(link.role_id))
403                .all(db)
404                .await
405                .map_err(db_err)?;
406            for row in rows {
407                if !ids.contains(&row.user_id) {
408                    ids.push(row.user_id);
409                }
410            }
411        }
412    }
413    Ok(ids)
414}
415
416pub async fn create_role(db: &DbHandle, name: &str, slug: &str) -> Result<role::Model> {
417    let slug = slug.trim().to_lowercase();
418    if slug.is_empty() || name.trim().is_empty() {
419        return Err(Error::BadRequest("name and slug required".into()));
420    }
421    role::ActiveModel {
422        id: Default::default(),
423        name: Set(name.trim().into()),
424        slug: Set(slug),
425    }
426    .insert(db)
427    .await
428    .map_err(db_err)
429}
430
431pub async fn update_role(
432    db: &DbHandle,
433    id: i64,
434    name: Option<&str>,
435    slug: Option<&str>,
436    allow_system: bool,
437) -> Result<role::Model> {
438    let Some(r) = find_role(db, id).await? else {
439        return Err(Error::NotFound);
440    };
441    if is_system_role_slug(&r.slug)
442        && !allow_system
443        && slug.is_some_and(|s| s.trim().to_lowercase() != r.slug)
444    {
445        return Err(Error::BadRequest("cannot rename system role slug".into()));
446    }
447    let mut am: role::ActiveModel = r.into();
448    if let Some(n) = name {
449        am.name = Set(n.trim().into());
450    }
451    if let Some(s) = slug {
452        am.slug = Set(s.trim().to_lowercase());
453    }
454    am.update(db).await.map_err(db_err)
455}
456
457pub async fn delete_role(db: &DbHandle, id: i64, allow_system: bool) -> Result<()> {
458    let Some(r) = find_role(db, id).await? else {
459        return Err(Error::NotFound);
460    };
461    if is_system_role_slug(&r.slug) && !allow_system {
462        return Err(Error::BadRequest("cannot delete system role".into()));
463    }
464    permission_role::Entity::delete_many()
465        .filter(permission_role::Column::RoleId.eq(id))
466        .exec(db)
467        .await
468        .map_err(db_err)?;
469    role_user::Entity::delete_many()
470        .filter(role_user::Column::RoleId.eq(id))
471        .exec(db)
472        .await
473        .map_err(db_err)?;
474    role::Entity::delete_by_id(id)
475        .exec(db)
476        .await
477        .map_err(db_err)?;
478    Ok(())
479}
480
481pub async fn sync_role_permissions(
482    db: &DbHandle,
483    role_id: i64,
484    permission_ids: &[i64],
485) -> Result<()> {
486    if find_role(db, role_id).await?.is_none() {
487        return Err(Error::NotFound);
488    }
489    permission_role::Entity::delete_many()
490        .filter(permission_role::Column::RoleId.eq(role_id))
491        .exec(db)
492        .await
493        .map_err(db_err)?;
494    for pid in permission_ids {
495        if find_permission(db, *pid).await?.is_none() {
496            return Err(Error::BadRequest(format!("permission {pid} not found")));
497        }
498        permission_role::ActiveModel {
499            role_id: Set(role_id),
500            permission_id: Set(*pid),
501        }
502        .insert(db)
503        .await
504        .map_err(db_err)?;
505    }
506    Ok(())
507}
508
509pub async fn create_permission(db: &DbHandle, name: &str, slug: &str) -> Result<permission::Model> {
510    let slug = slug.trim().to_lowercase();
511    if slug.is_empty() || name.trim().is_empty() {
512        return Err(Error::BadRequest("name and slug required".into()));
513    }
514    permission::ActiveModel {
515        id: Default::default(),
516        name: Set(name.trim().into()),
517        slug: Set(slug),
518    }
519    .insert(db)
520    .await
521    .map_err(db_err)
522}
523
524pub async fn update_permission(
525    db: &DbHandle,
526    id: i64,
527    name: Option<&str>,
528    slug: Option<&str>,
529) -> Result<permission::Model> {
530    let Some(p) = find_permission(db, id).await? else {
531        return Err(Error::NotFound);
532    };
533    let mut am: permission::ActiveModel = p.into();
534    if let Some(n) = name {
535        am.name = Set(n.trim().into());
536    }
537    if let Some(s) = slug {
538        am.slug = Set(s.trim().to_lowercase());
539    }
540    am.update(db).await.map_err(db_err)
541}
542
543pub async fn delete_permission(db: &DbHandle, id: i64) -> Result<()> {
544    if find_permission(db, id).await?.is_none() {
545        return Err(Error::NotFound);
546    }
547    permission_role::Entity::delete_many()
548        .filter(permission_role::Column::PermissionId.eq(id))
549        .exec(db)
550        .await
551        .map_err(db_err)?;
552    permission::Entity::delete_by_id(id)
553        .exec(db)
554        .await
555        .map_err(db_err)?;
556    Ok(())
557}
558
559pub async fn set_user_roles(db: &DbHandle, user_id: i64, role_ids: &[i64]) -> Result<()> {
560    if find_user_by_id(db, user_id).await?.is_none() {
561        return Err(Error::NotFound);
562    }
563    role_user::Entity::delete_many()
564        .filter(role_user::Column::UserId.eq(user_id))
565        .exec(db)
566        .await
567        .map_err(db_err)?;
568    for rid in role_ids {
569        if find_role(db, *rid).await?.is_none() {
570            return Err(Error::BadRequest(format!("role {rid} not found")));
571        }
572        role_user::ActiveModel {
573            user_id: Set(user_id),
574            role_id: Set(*rid),
575        }
576        .insert(db)
577        .await
578        .map_err(db_err)?;
579    }
580    Ok(())
581}
582
583pub async fn revoke_role(db: &DbHandle, user_id: i64, role_id: i64) -> Result<()> {
584    role_user::Entity::delete_many()
585        .filter(role_user::Column::UserId.eq(user_id))
586        .filter(role_user::Column::RoleId.eq(role_id))
587        .exec(db)
588        .await
589        .map_err(db_err)?;
590    Ok(())
591}