1use 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#[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 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
368pub 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
386pub 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}