1use std::collections::HashMap;
19
20use chrono::{DateTime, SecondsFormat, Utc};
21use laterite_core::query::{
22 bind_values, bind_values_as, build, insert_returning_id, on_conflict_ignore,
23};
24use laterite_core::{AnyRowExt, Db};
25use sea_query::{Expr, Order, Query};
26use sqlx::any::AnyRow;
27use sqlx::Row;
28
29use crate::error::AuthError;
30use crate::models::{AccessEvent, BackendUser, BackendUserSummary};
31use crate::schema::{
32 BackendAccessLog, BackendAuditLog, BackendRememberTokens, BackendRoles, BackendSessions,
33 BackendUserPreferences, BackendUserRoles, BackendUsers,
34};
35
36fn now_ts() -> String {
37 Utc::now().to_rfc3339_opts(SecondsFormat::Micros, true)
38}
39
40fn ts(dt: DateTime<Utc>) -> String {
41 dt.to_rfc3339_opts(SecondsFormat::Micros, true)
42}
43
44fn parse_ts(s: &str) -> Result<DateTime<Utc>, AuthError> {
45 DateTime::parse_from_rfc3339(s)
46 .map(|d| d.with_timezone(&Utc))
47 .map_err(|e| AuthError::Data(format!("timestamp `{s}`: {e}")))
48}
49
50fn normalize_key(s: &str) -> String {
57 s.trim().to_lowercase()
58}
59
60fn user_from_row(row: &AnyRow) -> Result<BackendUser, AuthError> {
61 Ok(BackendUser {
62 id: row.try_get::<i64, _>("id")?,
63 username: row.get_text("username")?,
64 email: row.get_text("email")?,
65 first_name: row.get_text("first_name")?,
66 last_name: row.get_text_opt("last_name")?,
67 password_hash: row.get_text("password_hash")?,
68 is_superuser: row.get_bool("is_superuser")?,
69 is_active: row.get_bool("is_active")?,
70 timezone: row.get_text_opt("timezone")?,
71 locale: row.get_text_opt("locale")?,
72 created_at: parse_ts(&row.get_text("created_at")?)?,
73 updated_at: parse_ts(&row.get_text("updated_at")?)?,
74 })
75}
76
77fn summary_from_row(row: &AnyRow) -> Result<BackendUserSummary, AuthError> {
78 Ok(BackendUserSummary {
79 id: row.try_get::<i64, _>("id")?,
80 username: row.get_text("username")?,
81 email: row.get_text("email")?,
82 first_name: row.get_text("first_name")?,
83 last_name: row.get_text_opt("last_name")?,
84 is_superuser: row.get_bool("is_superuser")?,
85 is_active: row.get_bool("is_active")?,
86 created_at: parse_ts(&row.get_text("created_at")?)?,
87 })
88}
89
90const USER_COLS: [BackendUsers; 12] = [
91 BackendUsers::Id,
92 BackendUsers::Username,
93 BackendUsers::Email,
94 BackendUsers::FirstName,
95 BackendUsers::LastName,
96 BackendUsers::PasswordHash,
97 BackendUsers::IsSuperuser,
98 BackendUsers::IsActive,
99 BackendUsers::Timezone,
100 BackendUsers::Locale,
101 BackendUsers::CreatedAt,
102 BackendUsers::UpdatedAt,
103];
104
105pub async fn find_user_by_username(
107 db: &Db,
108 username: &str,
109) -> Result<Option<BackendUser>, AuthError> {
110 let (sql, values) = build(
111 db.backend,
112 Query::select()
113 .columns(USER_COLS)
114 .from(BackendUsers::Table)
115 .and_where(Expr::col(BackendUsers::Username).eq(normalize_key(username)))
116 .to_owned(),
117 );
118 let row = bind_values(sqlx::query(&sql), values)
119 .fetch_optional(&db.pool)
120 .await?;
121 row.map(|r| user_from_row(&r)).transpose()
122}
123
124pub async fn find_user_by_id(db: &Db, id: i64) -> Result<Option<BackendUser>, AuthError> {
130 let (sql, values) = build(
131 db.backend,
132 Query::select()
133 .columns(USER_COLS)
134 .from(BackendUsers::Table)
135 .and_where(Expr::col(BackendUsers::Id).eq(id))
136 .to_owned(),
137 );
138 let row = bind_values(sqlx::query(&sql), values)
139 .fetch_optional(&db.pool)
140 .await?;
141 row.map(|r| user_from_row(&r)).transpose()
142}
143
144pub async fn find_active_user_by_id(db: &Db, id: i64) -> Result<Option<BackendUser>, AuthError> {
145 let (sql, values) = build(
146 db.backend,
147 Query::select()
148 .columns(USER_COLS)
149 .from(BackendUsers::Table)
150 .and_where(Expr::col(BackendUsers::Id).eq(id))
151 .and_where(Expr::col(BackendUsers::IsActive).eq(true))
152 .to_owned(),
153 );
154 let row = bind_values(sqlx::query(&sql), values)
155 .fetch_optional(&db.pool)
156 .await?;
157 row.map(|r| user_from_row(&r)).transpose()
158}
159
160pub async fn load_role_permissions(db: &Db, user_id: i64) -> Result<Vec<Vec<String>>, AuthError> {
163 let (sql, values) = build(
164 db.backend,
165 Query::select()
166 .column((BackendRoles::Table, BackendRoles::Permissions))
167 .from(BackendUserRoles::Table)
168 .inner_join(
169 BackendRoles::Table,
170 Expr::col((BackendRoles::Table, BackendRoles::Id))
171 .equals((BackendUserRoles::Table, BackendUserRoles::BackendRoleId)),
172 )
173 .and_where(
174 Expr::col((BackendUserRoles::Table, BackendUserRoles::BackendUserId)).eq(user_id),
175 )
176 .to_owned(),
177 );
178 let rows = bind_values(sqlx::query(&sql), values)
179 .fetch_all(&db.pool)
180 .await?;
181 let mut out = Vec::with_capacity(rows.len());
182 for row in rows {
183 let json = row.get_text("permissions")?;
184 let perms: Vec<String> = serde_json::from_str(&json)
185 .map_err(|e| AuthError::Data(format!("role permissions: {e}")))?;
186 out.push(perms);
187 }
188 Ok(out)
189}
190
191pub async fn load_user_permission_overrides(
194 db: &Db,
195 user_id: i64,
196) -> Result<HashMap<String, i64>, AuthError> {
197 let (sql, values) = build(
198 db.backend,
199 Query::select()
200 .column(BackendUsers::Permissions)
201 .from(BackendUsers::Table)
202 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
203 .to_owned(),
204 );
205 let row = bind_values(sqlx::query(&sql), values)
206 .fetch_optional(&db.pool)
207 .await?;
208 let overrides = match row {
209 Some(r) => {
210 let json = r.get_text("permissions")?;
211 serde_json::from_str(&json).unwrap_or_default()
212 }
213 None => HashMap::new(),
214 };
215 Ok(overrides)
216}
217
218pub async fn set_user_permissions(
220 db: &Db,
221 user_id: i64,
222 overrides: &HashMap<String, i64>,
223) -> Result<(), AuthError> {
224 let json = serde_json::to_string(overrides).unwrap_or_else(|_| "{}".to_string());
225 let (sql, values) = build(
226 db.backend,
227 Query::update()
228 .table(BackendUsers::Table)
229 .value(BackendUsers::Permissions, json)
230 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
231 .to_owned(),
232 );
233 bind_values(sqlx::query(&sql), values)
234 .execute(&db.pool)
235 .await?;
236 Ok(())
237}
238
239pub(crate) async fn insert_session(
240 db: &Db,
241 token_hash: &str,
242 user_id: i64,
243 expires_at: DateTime<Utc>,
244 ip_address: Option<&str>,
245 user_agent: Option<&str>,
246) -> Result<(), AuthError> {
247 let now = now_ts();
248 let (sql, values) = build(
249 db.backend,
250 Query::insert()
251 .into_table(BackendSessions::Table)
252 .columns([
253 BackendSessions::TokenHash,
254 BackendSessions::BackendUserId,
255 BackendSessions::CreatedAt,
256 BackendSessions::LastSeenAt,
257 BackendSessions::ExpiresAt,
258 BackendSessions::IpAddress,
259 BackendSessions::UserAgent,
260 ])
261 .values_panic([
262 token_hash.into(),
263 user_id.into(),
264 now.clone().into(),
265 now.into(),
266 ts(expires_at).into(),
267 ip_address.into(),
268 user_agent.into(),
269 ])
270 .to_owned(),
271 );
272 bind_values(sqlx::query(&sql), values)
273 .execute(&db.pool)
274 .await?;
275 Ok(())
276}
277
278pub struct ValidSession {
280 pub user_id: i64,
281 pub data: Option<String>,
282 pub created_at: DateTime<Utc>,
284 pub last_seen_at: DateTime<Utc>,
286}
287
288pub(crate) async fn find_valid_session(
292 db: &Db,
293 token_hash: &str,
294 now: DateTime<Utc>,
295) -> Result<Option<ValidSession>, AuthError> {
296 let (sql, values) = build(
297 db.backend,
298 Query::select()
299 .columns([
300 BackendSessions::BackendUserId,
301 BackendSessions::Data,
302 BackendSessions::CreatedAt,
303 BackendSessions::LastSeenAt,
304 ])
305 .from(BackendSessions::Table)
306 .and_where(Expr::col(BackendSessions::TokenHash).eq(token_hash))
307 .and_where(Expr::col(BackendSessions::ExpiresAt).gt(ts(now)))
308 .and_where(Expr::col(BackendSessions::RevokedReason).is_null())
309 .to_owned(),
310 );
311 let row = bind_values(sqlx::query(&sql), values)
312 .fetch_optional(&db.pool)
313 .await?;
314 match row {
315 Some(r) => Ok(Some(ValidSession {
316 user_id: r.try_get::<i64, _>("backend_user_id")?,
317 data: r.get_text_opt("data")?,
318 created_at: parse_ts(&r.get_text("created_at")?)?,
319 last_seen_at: parse_ts(&r.get_text("last_seen_at")?)?,
320 })),
321 None => Ok(None),
322 }
323}
324
325pub(crate) async fn set_session_data(
328 db: &Db,
329 token_hash: &str,
330 data: &str,
331) -> Result<(), AuthError> {
332 let (sql, values) = build(
333 db.backend,
334 Query::update()
335 .table(BackendSessions::Table)
336 .value(BackendSessions::Data, data)
337 .and_where(Expr::col(BackendSessions::TokenHash).eq(token_hash))
338 .to_owned(),
339 );
340 bind_values(sqlx::query(&sql), values)
341 .execute(&db.pool)
342 .await?;
343 Ok(())
344}
345
346pub(crate) async fn renew_session(
350 db: &Db,
351 token_hash: &str,
352 now: DateTime<Utc>,
353 expires_at: DateTime<Utc>,
354) -> Result<(), AuthError> {
355 let (sql, values) = build(
356 db.backend,
357 Query::update()
358 .table(BackendSessions::Table)
359 .value(BackendSessions::LastSeenAt, ts(now))
360 .value(BackendSessions::ExpiresAt, ts(expires_at))
361 .and_where(Expr::col(BackendSessions::TokenHash).eq(token_hash))
362 .to_owned(),
363 );
364 bind_values(sqlx::query(&sql), values)
365 .execute(&db.pool)
366 .await?;
367 Ok(())
368}
369
370pub(crate) struct RememberToken {
374 pub user_id: i64,
375 pub verifier_hash: String,
376}
377
378pub(crate) async fn insert_remember_token(
379 db: &Db,
380 selector: &str,
381 verifier_hash: &str,
382 user_id: i64,
383 expires_at: DateTime<Utc>,
384) -> Result<(), AuthError> {
385 let (sql, values) = build(
386 db.backend,
387 Query::insert()
388 .into_table(BackendRememberTokens::Table)
389 .columns([
390 BackendRememberTokens::Selector,
391 BackendRememberTokens::VerifierHash,
392 BackendRememberTokens::BackendUserId,
393 BackendRememberTokens::CreatedAt,
394 BackendRememberTokens::ExpiresAt,
395 ])
396 .values_panic([
397 selector.into(),
398 verifier_hash.into(),
399 user_id.into(),
400 now_ts().into(),
401 ts(expires_at).into(),
402 ])
403 .to_owned(),
404 );
405 bind_values(sqlx::query(&sql), values)
406 .execute(&db.pool)
407 .await?;
408 Ok(())
409}
410
411pub(crate) async fn find_remember_token(
412 db: &Db,
413 selector: &str,
414 now: DateTime<Utc>,
415) -> Result<Option<RememberToken>, AuthError> {
416 let (sql, values) = build(
417 db.backend,
418 Query::select()
419 .columns([
420 BackendRememberTokens::BackendUserId,
421 BackendRememberTokens::VerifierHash,
422 ])
423 .from(BackendRememberTokens::Table)
424 .and_where(Expr::col(BackendRememberTokens::Selector).eq(selector))
425 .and_where(Expr::col(BackendRememberTokens::ExpiresAt).gt(ts(now)))
426 .and_where(Expr::col(BackendRememberTokens::RevokedReason).is_null())
427 .to_owned(),
428 );
429 let row = bind_values(sqlx::query(&sql), values)
430 .fetch_optional(&db.pool)
431 .await?;
432 match row {
433 Some(r) => Ok(Some(RememberToken {
434 user_id: r.try_get::<i64, _>("backend_user_id")?,
435 verifier_hash: r.get_text("verifier_hash")?,
436 })),
437 None => Ok(None),
438 }
439}
440
441pub(crate) async fn rotate_remember_token(
445 db: &Db,
446 selector: &str,
447 verifier_hash: &str,
448 expires_at: DateTime<Utc>,
449) -> Result<(), AuthError> {
450 let (sql, values) = build(
451 db.backend,
452 Query::update()
453 .table(BackendRememberTokens::Table)
454 .value(BackendRememberTokens::VerifierHash, verifier_hash)
455 .value(BackendRememberTokens::ExpiresAt, ts(expires_at))
456 .and_where(Expr::col(BackendRememberTokens::Selector).eq(selector))
457 .to_owned(),
458 );
459 bind_values(sqlx::query(&sql), values)
460 .execute(&db.pool)
461 .await?;
462 Ok(())
463}
464
465pub(crate) async fn delete_remember_token(db: &Db, selector: &str) -> Result<(), AuthError> {
466 let (sql, values) = build(
467 db.backend,
468 Query::delete()
469 .from_table(BackendRememberTokens::Table)
470 .and_where(Expr::col(BackendRememberTokens::Selector).eq(selector))
471 .to_owned(),
472 );
473 bind_values(sqlx::query(&sql), values)
474 .execute(&db.pool)
475 .await?;
476 Ok(())
477}
478
479pub(crate) async fn revoke_user_remember_tokens(
481 db: &Db,
482 user_id: i64,
483 reason: RevokeReason,
484) -> Result<(), AuthError> {
485 let (sql, values) = revoke_user_remember_tokens_query(db.backend, user_id, reason);
486 bind_values(sqlx::query(&sql), values)
487 .execute(&db.pool)
488 .await?;
489 Ok(())
490}
491
492fn revoke_user_remember_tokens_query(
493 backend: laterite_core::DbBackend,
494 user_id: i64,
495 reason: RevokeReason,
496) -> (String, sea_query::Values) {
497 build(
498 backend,
499 Query::update()
500 .table(BackendRememberTokens::Table)
501 .value(BackendRememberTokens::RevokedReason, reason.as_str())
502 .and_where(Expr::col(BackendRememberTokens::BackendUserId).eq(user_id))
503 .and_where(Expr::col(BackendRememberTokens::RevokedReason).is_null())
504 .to_owned(),
505 )
506}
507
508pub(crate) async fn revoked_remember_token(
510 db: &Db,
511 selector: &str,
512) -> Result<Option<(String, RevokeReason)>, AuthError> {
513 let (sql, values) = build(
514 db.backend,
515 Query::select()
516 .columns([
517 BackendRememberTokens::VerifierHash,
518 BackendRememberTokens::RevokedReason,
519 ])
520 .from(BackendRememberTokens::Table)
521 .and_where(Expr::col(BackendRememberTokens::Selector).eq(selector))
522 .and_where(Expr::col(BackendRememberTokens::RevokedReason).is_not_null())
523 .to_owned(),
524 );
525 let Some(row) = bind_values(sqlx::query(&sql), values)
526 .fetch_optional(&db.pool)
527 .await?
528 else {
529 return Ok(None);
530 };
531 let reason = row
532 .get_text_opt("revoked_reason")?
533 .as_deref()
534 .and_then(RevokeReason::parse);
535 Ok(reason.map(|r| (row.get_text("verifier_hash").unwrap_or_default(), r)))
536}
537
538pub(crate) async fn delete_user_remember_tokens(db: &Db, user_id: i64) -> Result<(), AuthError> {
542 let (sql, values) = build(
543 db.backend,
544 Query::delete()
545 .from_table(BackendRememberTokens::Table)
546 .and_where(Expr::col(BackendRememberTokens::BackendUserId).eq(user_id))
547 .to_owned(),
548 );
549 bind_values(sqlx::query(&sql), values)
550 .execute(&db.pool)
551 .await?;
552 Ok(())
553}
554
555pub(crate) struct SessionSummary {
557 pub token_hash: String,
558 pub created_at: DateTime<Utc>,
559 pub last_seen_at: DateTime<Utc>,
560 pub expires_at: DateTime<Utc>,
561 pub ip_address: Option<String>,
562 pub user_agent: Option<String>,
563}
564
565pub(crate) async fn list_user_sessions(
566 db: &Db,
567 user_id: i64,
568 now: DateTime<Utc>,
569) -> Result<Vec<SessionSummary>, AuthError> {
570 let (sql, values) = build(
571 db.backend,
572 Query::select()
573 .columns([
574 BackendSessions::TokenHash,
575 BackendSessions::CreatedAt,
576 BackendSessions::LastSeenAt,
577 BackendSessions::ExpiresAt,
578 BackendSessions::IpAddress,
579 BackendSessions::UserAgent,
580 ])
581 .from(BackendSessions::Table)
582 .and_where(Expr::col(BackendSessions::BackendUserId).eq(user_id))
583 .and_where(Expr::col(BackendSessions::ExpiresAt).gt(ts(now)))
584 .and_where(Expr::col(BackendSessions::RevokedReason).is_null())
585 .order_by(BackendSessions::LastSeenAt, Order::Desc)
586 .to_owned(),
587 );
588 let rows = bind_values(sqlx::query(&sql), values)
589 .fetch_all(&db.pool)
590 .await?;
591 rows.iter()
592 .map(|r| {
593 Ok(SessionSummary {
594 token_hash: r.get_text("token_hash")?,
595 created_at: parse_ts(&r.get_text("created_at")?)?,
596 last_seen_at: parse_ts(&r.get_text("last_seen_at")?)?,
597 expires_at: parse_ts(&r.get_text("expires_at")?)?,
598 ip_address: r.get_text_opt("ip_address")?,
599 user_agent: r.get_text_opt("user_agent")?,
600 })
601 })
602 .collect()
603}
604
605pub(crate) async fn revoke_user_session(
609 db: &Db,
610 user_id: i64,
611 token_hash: &str,
612 reason: RevokeReason,
613) -> Result<u64, AuthError> {
614 let (sql, values) = build(
615 db.backend,
616 Query::update()
617 .table(BackendSessions::Table)
618 .value(BackendSessions::RevokedReason, reason.as_str())
619 .and_where(Expr::col(BackendSessions::BackendUserId).eq(user_id))
620 .and_where(Expr::col(BackendSessions::TokenHash).eq(token_hash))
621 .and_where(Expr::col(BackendSessions::RevokedReason).is_null())
622 .to_owned(),
623 );
624 let done = bind_values(sqlx::query(&sql), values)
625 .execute(&db.pool)
626 .await?;
627 Ok(done.rows_affected())
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
634#[non_exhaustive]
635pub enum RevokeReason {
636 PasswordChanged,
638 Deactivated,
640 SignedOutElsewhere,
642}
643
644impl RevokeReason {
645 pub fn as_str(self) -> &'static str {
646 match self {
647 RevokeReason::PasswordChanged => "password_changed",
648 RevokeReason::Deactivated => "deactivated",
649 RevokeReason::SignedOutElsewhere => "signed_out_elsewhere",
650 }
651 }
652
653 pub fn parse(code: &str) -> Option<Self> {
654 match code {
655 "password_changed" => Some(RevokeReason::PasswordChanged),
656 "deactivated" => Some(RevokeReason::Deactivated),
657 "signed_out_elsewhere" => Some(RevokeReason::SignedOutElsewhere),
658 _ => None,
659 }
660 }
661}
662
663#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665#[non_exhaustive]
666pub enum SessionEnd {
667 Expired,
669 Revoked(RevokeReason),
671}
672
673impl SessionEnd {
674 pub fn code(self) -> &'static str {
676 match self {
677 SessionEnd::Expired => "expired",
678 SessionEnd::Revoked(reason) => reason.as_str(),
679 }
680 }
681}
682
683pub(crate) async fn revoke_user_sessions(
688 db: &Db,
689 user_id: i64,
690 keep: Option<&str>,
691 reason: RevokeReason,
692) -> Result<u64, AuthError> {
693 let (sql, values) = revoke_user_sessions_query(db.backend, user_id, keep, reason);
694 let done = bind_values(sqlx::query(&sql), values)
695 .execute(&db.pool)
696 .await?;
697 Ok(done.rows_affected())
698}
699
700fn revoke_user_sessions_query(
701 backend: laterite_core::DbBackend,
702 user_id: i64,
703 keep: Option<&str>,
704 reason: RevokeReason,
705) -> (String, sea_query::Values) {
706 let mut update = Query::update();
707 update
708 .table(BackendSessions::Table)
709 .value(BackendSessions::RevokedReason, reason.as_str())
710 .and_where(Expr::col(BackendSessions::BackendUserId).eq(user_id))
711 .and_where(Expr::col(BackendSessions::RevokedReason).is_null());
712 if let Some(keep) = keep {
713 update.and_where(Expr::col(BackendSessions::TokenHash).ne(keep));
714 }
715 build(backend, update.to_owned())
716}
717
718pub(crate) async fn session_end(
720 db: &Db,
721 token_hash: &str,
722 now: DateTime<Utc>,
723) -> Result<Option<SessionEnd>, AuthError> {
724 let (sql, values) = build(
725 db.backend,
726 Query::select()
727 .columns([BackendSessions::RevokedReason, BackendSessions::ExpiresAt])
728 .from(BackendSessions::Table)
729 .and_where(Expr::col(BackendSessions::TokenHash).eq(token_hash))
730 .to_owned(),
731 );
732 let Some(row) = bind_values(sqlx::query(&sql), values)
733 .fetch_optional(&db.pool)
734 .await?
735 else {
736 return Ok(None);
737 };
738 if let Some(reason) = row
739 .get_text_opt("revoked_reason")?
740 .as_deref()
741 .and_then(RevokeReason::parse)
742 {
743 return Ok(Some(SessionEnd::Revoked(reason)));
744 }
745 let expires = parse_ts(&row.get_text("expires_at")?)?;
746 Ok((expires <= now).then_some(SessionEnd::Expired))
747}
748
749pub(crate) async fn change_password(
755 db: &Db,
756 user_id: i64,
757 password_hash: &str,
758 keep: Option<&str>,
759 actor_user_id: Option<i64>,
760 actor_username: &str,
761) -> Result<u64, AuthError> {
762 let mut tx = db.pool.begin().await?;
763 let (sql, values) = build(
764 db.backend,
765 Query::select()
766 .column(BackendUsers::Username)
767 .from(BackendUsers::Table)
768 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
769 .to_owned(),
770 );
771 let Some(username) = bind_values(sqlx::query(&sql), values)
772 .fetch_optional(&mut *tx)
773 .await?
774 .map(|row| row.get_text("username"))
775 .transpose()?
776 else {
777 return Ok(0);
778 };
779 let (sql, values) = build(
780 db.backend,
781 Query::update()
782 .table(BackendUsers::Table)
783 .value(BackendUsers::PasswordHash, password_hash)
784 .value(BackendUsers::PasswordChangedAt, now_ts())
785 .value(BackendUsers::MustChangePassword, false)
786 .value(BackendUsers::UpdatedAt, now_ts())
787 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
788 .to_owned(),
789 );
790 let done = bind_values(sqlx::query(&sql), values)
791 .execute(&mut *tx)
792 .await?
793 .rows_affected();
794 if done == 0 {
795 return Ok(0);
796 }
797 let (sql, values) =
798 revoke_user_sessions_query(db.backend, user_id, keep, RevokeReason::PasswordChanged);
799 bind_values(sqlx::query(&sql), values)
800 .execute(&mut *tx)
801 .await?;
802 let (sql, values) =
803 revoke_user_remember_tokens_query(db.backend, user_id, RevokeReason::PasswordChanged);
804 bind_values(sqlx::query(&sql), values)
805 .execute(&mut *tx)
806 .await?;
807 let target = user_id.to_string();
808 let (sql, values) = insert_audit_log_query(
809 db.backend,
810 actor_user_id,
811 actor_username,
812 "backend.user.password_change",
813 Some("backend_user"),
814 Some(&target),
815 Some(&username),
816 None,
817 );
818 bind_values(sqlx::query(&sql), values)
819 .execute(&mut *tx)
820 .await?;
821 tx.commit().await?;
822 Ok(done)
823}
824
825pub(crate) async fn delete_expired_sessions(db: &Db, now: DateTime<Utc>) -> Result<u64, AuthError> {
828 let (sql, values) = build(
829 db.backend,
830 Query::delete()
831 .from_table(BackendSessions::Table)
832 .and_where(Expr::col(BackendSessions::ExpiresAt).lte(ts(now)))
833 .to_owned(),
834 );
835 let done = bind_values(sqlx::query(&sql), values)
836 .execute(&db.pool)
837 .await?;
838 Ok(done.rows_affected())
839}
840
841pub(crate) async fn delete_expired_remember_tokens(
843 db: &Db,
844 now: DateTime<Utc>,
845) -> Result<u64, AuthError> {
846 let (sql, values) = build(
847 db.backend,
848 Query::delete()
849 .from_table(BackendRememberTokens::Table)
850 .and_where(Expr::col(BackendRememberTokens::ExpiresAt).lte(ts(now)))
851 .to_owned(),
852 );
853 let done = bind_values(sqlx::query(&sql), values)
854 .execute(&db.pool)
855 .await?;
856 Ok(done.rows_affected())
857}
858
859pub(crate) async fn must_change_password(db: &Db, user_id: i64) -> Result<bool, AuthError> {
861 let (sql, values) = build(
862 db.backend,
863 Query::select()
864 .column(BackendUsers::MustChangePassword)
865 .from(BackendUsers::Table)
866 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
867 .to_owned(),
868 );
869 let row = bind_values(sqlx::query(&sql), values)
870 .fetch_optional(&db.pool)
871 .await?;
872 match row {
873 Some(row) => Ok(row.get_bool("must_change_password")?),
874 None => Ok(false),
875 }
876}
877
878pub(crate) async fn set_must_change_password(
880 db: &Db,
881 user_id: i64,
882 must_change: bool,
883) -> Result<u64, AuthError> {
884 let (sql, values) = build(
885 db.backend,
886 Query::update()
887 .table(BackendUsers::Table)
888 .value(BackendUsers::MustChangePassword, must_change)
889 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
890 .to_owned(),
891 );
892 let done = bind_values(sqlx::query(&sql), values)
893 .execute(&db.pool)
894 .await?;
895 Ok(done.rows_affected())
896}
897
898pub(crate) async fn password_changed_at(
901 db: &Db,
902 user_id: i64,
903) -> Result<Option<DateTime<Utc>>, AuthError> {
904 let (sql, values) = build(
905 db.backend,
906 Query::select()
907 .column(BackendUsers::PasswordChangedAt)
908 .from(BackendUsers::Table)
909 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
910 .to_owned(),
911 );
912 let row = bind_values(sqlx::query(&sql), values)
913 .fetch_optional(&db.pool)
914 .await?;
915 match row {
916 Some(row) => row
917 .get_text_opt("password_changed_at")?
918 .map(|s| parse_ts(&s))
919 .transpose(),
920 None => Ok(None),
921 }
922}
923
924pub(crate) async fn delete_session(db: &Db, token_hash: &str) -> Result<(), AuthError> {
925 let (sql, values) = build(
926 db.backend,
927 Query::delete()
928 .from_table(BackendSessions::Table)
929 .and_where(Expr::col(BackendSessions::TokenHash).eq(token_hash))
930 .to_owned(),
931 );
932 bind_values(sqlx::query(&sql), values)
933 .execute(&db.pool)
934 .await?;
935 Ok(())
936}
937
938pub async fn insert_access_log(
939 db: &Db,
940 user_id: Option<i64>,
941 username_attempted: &str,
942 event: AccessEvent,
943 ip_address: Option<&str>,
944 user_agent: Option<&str>,
945) -> Result<(), AuthError> {
946 let (sql, values) = build(
947 db.backend,
948 Query::insert()
949 .into_table(BackendAccessLog::Table)
950 .columns([
951 BackendAccessLog::BackendUserId,
952 BackendAccessLog::UsernameAttempted,
953 BackendAccessLog::Event,
954 BackendAccessLog::IpAddress,
955 BackendAccessLog::UserAgent,
956 BackendAccessLog::CreatedAt,
957 ])
958 .values_panic([
959 user_id.into(),
960 normalize_key(username_attempted).into(),
961 event.as_str().into(),
962 ip_address.map(str::to_string).into(),
963 user_agent.map(str::to_string).into(),
964 now_ts().into(),
965 ])
966 .to_owned(),
967 );
968 bind_values(sqlx::query(&sql), values)
969 .execute(&db.pool)
970 .await?;
971 Ok(())
972}
973
974#[derive(Debug, Clone)]
976#[non_exhaustive]
977pub struct AccessRecord {
978 pub event: String,
980 pub username_attempted: String,
981 pub ip_address: Option<String>,
982 pub user_agent: Option<String>,
983 pub created_at: DateTime<Utc>,
984}
985
986pub async fn recent_access_log(
988 db: &Db,
989 user_id: i64,
990 limit: u64,
991) -> Result<Vec<AccessRecord>, AuthError> {
992 let (sql, values) = build(
993 db.backend,
994 Query::select()
995 .columns([
996 BackendAccessLog::Event,
997 BackendAccessLog::UsernameAttempted,
998 BackendAccessLog::IpAddress,
999 BackendAccessLog::UserAgent,
1000 BackendAccessLog::CreatedAt,
1001 ])
1002 .from(BackendAccessLog::Table)
1003 .and_where(Expr::col(BackendAccessLog::BackendUserId).eq(user_id))
1004 .order_by(BackendAccessLog::Id, Order::Desc)
1005 .limit(limit)
1006 .to_owned(),
1007 );
1008 let rows = bind_values(sqlx::query(&sql), values)
1009 .fetch_all(&db.pool)
1010 .await?;
1011 rows.iter()
1012 .map(|row| {
1013 Ok(AccessRecord {
1014 event: row.get_text("event")?,
1015 username_attempted: row.get_text("username_attempted")?,
1016 ip_address: row.get_text_opt("ip_address")?,
1017 user_agent: row.get_text_opt("user_agent")?,
1018 created_at: parse_ts(&row.get_text("created_at")?)?,
1019 })
1020 })
1021 .collect()
1022}
1023
1024#[derive(Debug, Clone)]
1026#[non_exhaustive]
1027pub struct AuditRecord {
1028 pub actor_user_id: Option<i64>,
1029 pub actor_username: String,
1030 pub action: String,
1031 pub target_type: Option<String>,
1032 pub target_id: Option<String>,
1033 pub target_label: Option<String>,
1035 pub detail: Option<String>,
1036 pub created_at: DateTime<Utc>,
1037}
1038
1039#[allow(clippy::too_many_arguments)]
1041pub async fn insert_audit_log(
1042 db: &Db,
1043 actor_user_id: Option<i64>,
1044 actor_username: &str,
1045 action: &str,
1046 target_type: Option<&str>,
1047 target_id: Option<&str>,
1048 target_label: Option<&str>,
1049 detail: Option<&str>,
1050) -> Result<(), AuthError> {
1051 let (sql, values) = insert_audit_log_query(
1052 db.backend,
1053 actor_user_id,
1054 actor_username,
1055 action,
1056 target_type,
1057 target_id,
1058 target_label,
1059 detail,
1060 );
1061 bind_values(sqlx::query(&sql), values)
1062 .execute(&db.pool)
1063 .await?;
1064 Ok(())
1065}
1066
1067#[allow(clippy::too_many_arguments)]
1068fn insert_audit_log_query(
1069 backend: laterite_core::DbBackend,
1070 actor_user_id: Option<i64>,
1071 actor_username: &str,
1072 action: &str,
1073 target_type: Option<&str>,
1074 target_id: Option<&str>,
1075 target_label: Option<&str>,
1076 detail: Option<&str>,
1077) -> (String, sea_query::Values) {
1078 build(
1079 backend,
1080 Query::insert()
1081 .into_table(BackendAuditLog::Table)
1082 .columns([
1083 BackendAuditLog::ActorUserId,
1084 BackendAuditLog::ActorUsername,
1085 BackendAuditLog::Action,
1086 BackendAuditLog::TargetType,
1087 BackendAuditLog::TargetId,
1088 BackendAuditLog::TargetLabel,
1089 BackendAuditLog::Detail,
1090 BackendAuditLog::CreatedAt,
1091 ])
1092 .values_panic([
1093 actor_user_id.into(),
1094 actor_username.to_string().into(),
1095 action.to_string().into(),
1096 target_type.map(str::to_string).into(),
1097 target_id.map(str::to_string).into(),
1098 target_label.map(str::to_string).into(),
1099 detail.map(str::to_string).into(),
1100 now_ts().into(),
1101 ])
1102 .to_owned(),
1103 )
1104}
1105
1106pub async fn recent_audit(db: &Db, limit: u64) -> Result<Vec<AuditRecord>, AuthError> {
1109 let (sql, values) = build(
1110 db.backend,
1111 Query::select()
1112 .columns([
1113 BackendAuditLog::ActorUserId,
1114 BackendAuditLog::ActorUsername,
1115 BackendAuditLog::Action,
1116 BackendAuditLog::TargetType,
1117 BackendAuditLog::TargetId,
1118 BackendAuditLog::TargetLabel,
1119 BackendAuditLog::Detail,
1120 BackendAuditLog::CreatedAt,
1121 ])
1122 .from(BackendAuditLog::Table)
1123 .order_by(BackendAuditLog::Id, Order::Desc)
1124 .limit(limit)
1125 .to_owned(),
1126 );
1127 let rows = bind_values(sqlx::query(&sql), values)
1128 .fetch_all(&db.pool)
1129 .await?;
1130 rows.iter().map(audit_from_row).collect()
1131}
1132
1133fn audit_from_row(row: &AnyRow) -> Result<AuditRecord, AuthError> {
1134 Ok(AuditRecord {
1135 actor_user_id: row.get_int_opt("actor_user_id")?,
1136 actor_username: row.get_text("actor_username")?,
1137 action: row.get_text("action")?,
1138 target_type: row.get_text_opt("target_type")?,
1139 target_id: row.get_text_opt("target_id")?,
1140 target_label: row.get_text_opt("target_label")?,
1141 detail: row.get_text_opt("detail")?,
1142 created_at: parse_ts(&row.get_text("created_at")?)?,
1143 })
1144}
1145
1146pub async fn count_recent_failures(
1148 db: &Db,
1149 username: &str,
1150 since: DateTime<Utc>,
1151) -> Result<i64, AuthError> {
1152 let (sql, values) = build(
1153 db.backend,
1154 Query::select()
1155 .expr(Expr::col(BackendAccessLog::Id).count())
1156 .from(BackendAccessLog::Table)
1157 .and_where(Expr::col(BackendAccessLog::UsernameAttempted).eq(normalize_key(username)))
1158 .and_where(Expr::col(BackendAccessLog::Event).eq(AccessEvent::LoginFailure.as_str()))
1159 .and_where(Expr::col(BackendAccessLog::CreatedAt).gte(ts(since)))
1160 .to_owned(),
1161 );
1162 let count: i64 = bind_values_as(sqlx::query_as::<_, (i64,)>(&sql), values)
1163 .fetch_one(&db.pool)
1164 .await?
1165 .0;
1166 Ok(count)
1167}
1168
1169pub async fn count_recent_failures_from(
1172 db: &Db,
1173 username: &str,
1174 address: Option<&str>,
1175 since: DateTime<Utc>,
1176) -> Result<i64, AuthError> {
1177 let (sql, values) = build(
1178 db.backend,
1179 Query::select()
1180 .expr(Expr::col(BackendAccessLog::Id).count())
1181 .from(BackendAccessLog::Table)
1182 .and_where(Expr::col(BackendAccessLog::UsernameAttempted).eq(normalize_key(username)))
1183 .and_where(Expr::col(BackendAccessLog::Event).eq(AccessEvent::LoginFailure.as_str()))
1184 .and_where(Expr::col(BackendAccessLog::CreatedAt).gte(ts(since)))
1185 .and_where(address_is(address))
1186 .to_owned(),
1187 );
1188 let count: i64 = bind_values_as(sqlx::query_as::<_, (i64,)>(&sql), values)
1189 .fetch_one(&db.pool)
1190 .await?
1191 .0;
1192 Ok(count)
1193}
1194
1195pub async fn count_recent_failures_by_address(
1197 db: &Db,
1198 address: Option<&str>,
1199 since: DateTime<Utc>,
1200) -> Result<i64, AuthError> {
1201 let (sql, values) = build(
1202 db.backend,
1203 Query::select()
1204 .expr(Expr::col(BackendAccessLog::Id).count())
1205 .from(BackendAccessLog::Table)
1206 .and_where(Expr::col(BackendAccessLog::Event).eq(AccessEvent::LoginFailure.as_str()))
1207 .and_where(Expr::col(BackendAccessLog::CreatedAt).gte(ts(since)))
1208 .and_where(address_is(address))
1209 .to_owned(),
1210 );
1211 let count: i64 = bind_values_as(sqlx::query_as::<_, (i64,)>(&sql), values)
1212 .fetch_one(&db.pool)
1213 .await?
1214 .0;
1215 Ok(count)
1216}
1217
1218pub async fn any_address_locked(
1221 db: &Db,
1222 username: &str,
1223 since: DateTime<Utc>,
1224 max: i64,
1225) -> Result<bool, AuthError> {
1226 let (sql, values) = build(
1227 db.backend,
1228 Query::select()
1229 .column(BackendAccessLog::IpAddress)
1230 .from(BackendAccessLog::Table)
1231 .and_where(Expr::col(BackendAccessLog::UsernameAttempted).eq(normalize_key(username)))
1232 .and_where(Expr::col(BackendAccessLog::Event).eq(AccessEvent::LoginFailure.as_str()))
1233 .and_where(Expr::col(BackendAccessLog::CreatedAt).gte(ts(since)))
1234 .group_by_col(BackendAccessLog::IpAddress)
1235 .and_having(Expr::expr(Expr::col(BackendAccessLog::Id).count()).gte(max))
1236 .limit(1)
1237 .to_owned(),
1238 );
1239 let row = bind_values(sqlx::query(&sql), values)
1240 .fetch_optional(&db.pool)
1241 .await?;
1242 Ok(row.is_some())
1243}
1244
1245fn address_is(address: Option<&str>) -> sea_query::SimpleExpr {
1247 match address {
1248 Some(ip) => Expr::col(BackendAccessLog::IpAddress).eq(ip),
1249 None => Expr::col(BackendAccessLog::IpAddress).is_null(),
1250 }
1251}
1252
1253#[allow(clippy::too_many_arguments)]
1256pub async fn create_user(
1257 db: &Db,
1258 username: &str,
1259 email: &str,
1260 first_name: &str,
1261 last_name: Option<&str>,
1262 password_hash: &str,
1263 is_superuser: bool,
1264) -> Result<i64, AuthError> {
1265 let now = now_ts();
1266 let stmt = Query::insert()
1267 .into_table(BackendUsers::Table)
1268 .columns([
1269 BackendUsers::Username,
1270 BackendUsers::Email,
1271 BackendUsers::FirstName,
1272 BackendUsers::LastName,
1273 BackendUsers::PasswordHash,
1274 BackendUsers::IsSuperuser,
1275 BackendUsers::PasswordChangedAt,
1276 BackendUsers::CreatedAt,
1277 BackendUsers::UpdatedAt,
1278 ])
1279 .values_panic([
1280 normalize_key(username).into(),
1281 normalize_key(email).into(),
1282 first_name.into(),
1283 last_name.map(str::to_string).into(),
1284 password_hash.into(),
1285 is_superuser.into(),
1286 now.clone().into(),
1287 now.clone().into(),
1288 now.into(),
1289 ])
1290 .to_owned();
1291 Ok(insert_returning_id(db, stmt, BackendUsers::Id).await?)
1292}
1293
1294pub async fn any_user_exists(db: &Db) -> Result<bool, AuthError> {
1296 let (sql, values) = build(
1297 db.backend,
1298 Query::select()
1299 .expr(Expr::col(BackendUsers::Id).count())
1300 .from(BackendUsers::Table)
1301 .to_owned(),
1302 );
1303 let count: i64 = bind_values_as(sqlx::query_as::<_, (i64,)>(&sql), values)
1304 .fetch_one(&db.pool)
1305 .await?
1306 .0;
1307 Ok(count > 0)
1308}
1309
1310pub async fn set_user_timezone(
1312 db: &Db,
1313 user_id: i64,
1314 timezone: Option<&str>,
1315) -> Result<(), AuthError> {
1316 let (sql, values) = build(
1317 db.backend,
1318 Query::update()
1319 .table(BackendUsers::Table)
1320 .value(BackendUsers::Timezone, timezone.map(str::to_string))
1321 .value(BackendUsers::UpdatedAt, now_ts())
1322 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
1323 .to_owned(),
1324 );
1325 bind_values(sqlx::query(&sql), values)
1326 .execute(&db.pool)
1327 .await?;
1328 Ok(())
1329}
1330
1331pub async fn set_user_locale(db: &Db, user_id: i64, locale: Option<&str>) -> Result<(), AuthError> {
1333 let (sql, values) = build(
1334 db.backend,
1335 Query::update()
1336 .table(BackendUsers::Table)
1337 .value(BackendUsers::Locale, locale.map(str::to_string))
1338 .value(BackendUsers::UpdatedAt, now_ts())
1339 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
1340 .to_owned(),
1341 );
1342 bind_values(sqlx::query(&sql), values)
1343 .execute(&db.pool)
1344 .await?;
1345 Ok(())
1346}
1347
1348pub async fn create_role(
1349 db: &Db,
1350 code: &str,
1351 name: &str,
1352 permissions: &[String],
1353) -> Result<i64, AuthError> {
1354 let perms = serde_json::to_string(permissions).unwrap_or_else(|_| "[]".to_string());
1355 let stmt = Query::insert()
1356 .into_table(BackendRoles::Table)
1357 .columns([
1358 BackendRoles::Code,
1359 BackendRoles::Name,
1360 BackendRoles::Permissions,
1361 BackendRoles::CreatedAt,
1362 ])
1363 .values_panic([code.into(), name.into(), perms.into(), now_ts().into()])
1364 .to_owned();
1365 Ok(insert_returning_id(db, stmt, BackendRoles::Id).await?)
1366}
1367
1368pub struct SystemRole<'a> {
1371 pub code: &'a str,
1372 pub name: &'a str,
1373 pub description: &'a str,
1374 pub permissions: Vec<String>,
1375}
1376
1377pub async fn sync_system_roles(db: &Db, roles: &[SystemRole<'_>]) -> Result<(), AuthError> {
1384 for role in roles {
1385 let perms = serde_json::to_string(&role.permissions).unwrap_or_else(|_| "[]".to_string());
1386 let (sql, values) = build(
1390 db.backend,
1391 Query::insert()
1392 .into_table(BackendRoles::Table)
1393 .columns([
1394 BackendRoles::Code,
1395 BackendRoles::Name,
1396 BackendRoles::Description,
1397 BackendRoles::Permissions,
1398 BackendRoles::IsSystem,
1399 BackendRoles::CreatedAt,
1400 ])
1401 .values_panic([
1402 role.code.into(),
1403 role.name.into(),
1404 role.description.into(),
1405 perms.clone().into(),
1406 true.into(),
1407 now_ts().into(),
1408 ])
1409 .on_conflict(on_conflict_ignore([BackendRoles::Code]))
1410 .to_owned(),
1411 );
1412 bind_values(sqlx::query(&sql), values)
1413 .execute(&db.pool)
1414 .await?;
1415 let (sql, values) = build(
1416 db.backend,
1417 Query::update()
1418 .table(BackendRoles::Table)
1419 .values([
1420 (BackendRoles::Name, role.name.into()),
1421 (BackendRoles::Description, role.description.into()),
1422 (BackendRoles::Permissions, perms.into()),
1423 (BackendRoles::IsSystem, true.into()),
1424 ])
1425 .and_where(Expr::col(BackendRoles::Code).eq(role.code))
1426 .to_owned(),
1427 );
1428 bind_values(sqlx::query(&sql), values)
1429 .execute(&db.pool)
1430 .await?;
1431 }
1432 Ok(())
1433}
1434
1435pub async fn role_id_by_code(db: &Db, code: &str) -> Result<Option<i64>, AuthError> {
1437 let (sql, values) = build(
1438 db.backend,
1439 Query::select()
1440 .column(BackendRoles::Id)
1441 .from(BackendRoles::Table)
1442 .and_where(Expr::col(BackendRoles::Code).eq(code))
1443 .to_owned(),
1444 );
1445 let row = bind_values(sqlx::query(&sql), values)
1446 .fetch_optional(&db.pool)
1447 .await?;
1448 Ok(match row {
1449 Some(row) => Some(row.try_get::<i64, _>("id")?),
1450 None => None,
1451 })
1452}
1453
1454pub async fn role_permissions(db: &Db, id: i64) -> Result<Vec<String>, AuthError> {
1456 let (sql, values) = build(
1457 db.backend,
1458 Query::select()
1459 .column(BackendRoles::Permissions)
1460 .from(BackendRoles::Table)
1461 .and_where(Expr::col(BackendRoles::Id).eq(id))
1462 .to_owned(),
1463 );
1464 let row = bind_values(sqlx::query(&sql), values)
1465 .fetch_optional(&db.pool)
1466 .await?;
1467 Ok(match row {
1468 Some(row) => serde_json::from_str(&row.get_text("permissions")?).unwrap_or_default(),
1469 None => Vec::new(),
1470 })
1471}
1472
1473pub async fn role_is_system(db: &Db, id: i64) -> Result<bool, AuthError> {
1475 let (sql, values) = is_system_query(db.backend, id);
1476 let row = bind_values(sqlx::query(&sql), values)
1477 .fetch_optional(&db.pool)
1478 .await?;
1479 read_is_system(row)
1480}
1481
1482pub async fn role_is_system_on(
1485 conn: &mut sqlx::AnyConnection,
1486 backend: laterite_core::DbBackend,
1487 id: i64,
1488) -> Result<bool, AuthError> {
1489 let (sql, values) = is_system_query(backend, id);
1490 let row = bind_values(sqlx::query(&sql), values)
1491 .fetch_optional(&mut *conn)
1492 .await?;
1493 read_is_system(row)
1494}
1495
1496fn is_system_query(backend: laterite_core::DbBackend, id: i64) -> (String, sea_query::Values) {
1497 build(
1498 backend,
1499 Query::select()
1500 .column(BackendRoles::IsSystem)
1501 .from(BackendRoles::Table)
1502 .and_where(Expr::col(BackendRoles::Id).eq(id))
1503 .to_owned(),
1504 )
1505}
1506
1507fn read_is_system(row: Option<AnyRow>) -> Result<bool, AuthError> {
1509 Ok(match row {
1510 Some(row) => row.get_bool("is_system")?,
1511 None => false,
1512 })
1513}
1514
1515pub async fn rename_permissions(db: &Db, renames: &[(String, String)]) -> Result<usize, AuthError> {
1525 if renames.is_empty() {
1526 return Ok(0);
1527 }
1528 let mut changed = 0;
1529
1530 let (sql, values) = build(
1532 db.backend,
1533 Query::select()
1534 .columns([BackendRoles::Id, BackendRoles::Permissions])
1535 .from(BackendRoles::Table)
1536 .to_owned(),
1537 );
1538 for row in bind_values(sqlx::query(&sql), values)
1539 .fetch_all(&db.pool)
1540 .await?
1541 {
1542 let id = row.try_get::<i64, _>("id")?;
1543 let held: Vec<String> =
1544 serde_json::from_str(&row.get_text("permissions")?).unwrap_or_default();
1545 let mut next: Vec<String> = held
1546 .iter()
1547 .map(|code| apply_rename(code, renames))
1548 .collect();
1549 next.dedup_by(|a, b| a == b);
1551 let mut seen = std::collections::HashSet::new();
1552 next.retain(|code| seen.insert(code.clone()));
1553 if next != held {
1554 let json = serde_json::to_string(&next).unwrap_or_else(|_| "[]".to_string());
1555 let (sql, values) = build(
1556 db.backend,
1557 Query::update()
1558 .table(BackendRoles::Table)
1559 .value(BackendRoles::Permissions, json)
1560 .and_where(Expr::col(BackendRoles::Id).eq(id))
1561 .to_owned(),
1562 );
1563 bind_values(sqlx::query(&sql), values)
1564 .execute(&db.pool)
1565 .await?;
1566 changed += 1;
1567 }
1568 }
1569
1570 let (sql, values) = build(
1572 db.backend,
1573 Query::select()
1574 .columns([BackendUsers::Id, BackendUsers::Permissions])
1575 .from(BackendUsers::Table)
1576 .to_owned(),
1577 );
1578 for row in bind_values(sqlx::query(&sql), values)
1579 .fetch_all(&db.pool)
1580 .await?
1581 {
1582 let id = row.try_get::<i64, _>("id")?;
1583 let held: HashMap<String, i64> =
1584 serde_json::from_str(&row.get_text("permissions")?).unwrap_or_default();
1585 let next: HashMap<String, i64> = held
1586 .iter()
1587 .map(|(code, state)| (apply_rename(code, renames), *state))
1588 .collect();
1589 if next != held {
1590 let json = serde_json::to_string(&next).unwrap_or_else(|_| "{}".to_string());
1591 let (sql, values) = build(
1592 db.backend,
1593 Query::update()
1594 .table(BackendUsers::Table)
1595 .value(BackendUsers::Permissions, json)
1596 .and_where(Expr::col(BackendUsers::Id).eq(id))
1597 .to_owned(),
1598 );
1599 bind_values(sqlx::query(&sql), values)
1600 .execute(&db.pool)
1601 .await?;
1602 changed += 1;
1603 }
1604 }
1605 Ok(changed)
1606}
1607
1608fn apply_rename(code: &str, renames: &[(String, String)]) -> String {
1610 renames
1611 .iter()
1612 .find(|(old, _)| old == code)
1613 .map(|(_, new)| new.clone())
1614 .unwrap_or_else(|| code.to_string())
1615}
1616
1617pub struct RoleSummary {
1619 pub id: i64,
1620 pub code: String,
1621 pub name: String,
1622 pub is_system: bool,
1623 pub permissions: Vec<String>,
1626}
1627
1628pub async fn list_roles(db: &Db) -> Result<Vec<RoleSummary>, AuthError> {
1630 let (sql, values) = build(
1631 db.backend,
1632 Query::select()
1633 .columns([
1634 BackendRoles::Id,
1635 BackendRoles::Code,
1636 BackendRoles::Name,
1637 BackendRoles::IsSystem,
1638 BackendRoles::Permissions,
1639 ])
1640 .from(BackendRoles::Table)
1641 .order_by(BackendRoles::Name, Order::Asc)
1642 .to_owned(),
1643 );
1644 let rows = bind_values(sqlx::query(&sql), values)
1645 .fetch_all(&db.pool)
1646 .await?;
1647 rows.into_iter()
1648 .map(|row| {
1649 Ok(RoleSummary {
1650 id: row.try_get::<i64, _>("id")?,
1651 code: row.get_text("code")?,
1652 name: row.get_text("name")?,
1653 is_system: row.get_bool("is_system")?,
1654 permissions: serde_json::from_str(&row.get_text("permissions")?)
1655 .unwrap_or_default(),
1656 })
1657 })
1658 .collect()
1659}
1660
1661pub async fn user_role_ids(db: &Db, user_id: i64) -> Result<Vec<i64>, AuthError> {
1663 let (sql, values) = build(
1664 db.backend,
1665 Query::select()
1666 .column(BackendUserRoles::BackendRoleId)
1667 .from(BackendUserRoles::Table)
1668 .and_where(Expr::col(BackendUserRoles::BackendUserId).eq(user_id))
1669 .to_owned(),
1670 );
1671 let rows = bind_values(sqlx::query(&sql), values)
1672 .fetch_all(&db.pool)
1673 .await?;
1674 rows.into_iter()
1675 .map(|row| Ok(row.try_get::<i64, _>("backend_role_id")?))
1676 .collect()
1677}
1678
1679pub async fn set_user_roles(db: &Db, user_id: i64, role_ids: &[i64]) -> Result<(), AuthError> {
1683 let mut tx = db.pool.begin().await?;
1684 let (sql, values) = build(
1685 db.backend,
1686 Query::delete()
1687 .from_table(BackendUserRoles::Table)
1688 .and_where(Expr::col(BackendUserRoles::BackendUserId).eq(user_id))
1689 .to_owned(),
1690 );
1691 bind_values(sqlx::query(&sql), values)
1692 .execute(&mut *tx)
1693 .await?;
1694 for role_id in role_ids {
1695 let (sql, values) = build(
1696 db.backend,
1697 Query::insert()
1698 .into_table(BackendUserRoles::Table)
1699 .columns([
1700 BackendUserRoles::BackendUserId,
1701 BackendUserRoles::BackendRoleId,
1702 ])
1703 .values_panic([user_id.into(), (*role_id).into()])
1704 .on_conflict(on_conflict_ignore([
1705 BackendUserRoles::BackendUserId,
1706 BackendUserRoles::BackendRoleId,
1707 ]))
1708 .to_owned(),
1709 );
1710 bind_values(sqlx::query(&sql), values)
1711 .execute(&mut *tx)
1712 .await?;
1713 }
1714 tx.commit().await?;
1715 Ok(())
1716}
1717
1718pub async fn assign_role(db: &Db, user_id: i64, role_id: i64) -> Result<(), AuthError> {
1719 let (sql, values) = build(
1720 db.backend,
1721 Query::insert()
1722 .into_table(BackendUserRoles::Table)
1723 .columns([
1724 BackendUserRoles::BackendUserId,
1725 BackendUserRoles::BackendRoleId,
1726 ])
1727 .values_panic([user_id.into(), role_id.into()])
1728 .on_conflict(on_conflict_ignore([
1729 BackendUserRoles::BackendUserId,
1730 BackendUserRoles::BackendRoleId,
1731 ]))
1732 .to_owned(),
1733 );
1734 bind_values(sqlx::query(&sql), values)
1735 .execute(&db.pool)
1736 .await?;
1737 Ok(())
1738}
1739
1740pub(crate) async fn set_user_active(db: &Db, user_id: i64, active: bool) -> Result<u64, AuthError> {
1747 let mut tx = db.pool.begin().await?;
1748 let (sql, values) = build(
1749 db.backend,
1750 Query::update()
1751 .table(BackendUsers::Table)
1752 .value(BackendUsers::IsActive, active)
1753 .and_where(Expr::col(BackendUsers::Id).eq(user_id))
1754 .to_owned(),
1755 );
1756 let done = bind_values(sqlx::query(&sql), values)
1757 .execute(&mut *tx)
1758 .await?
1759 .rows_affected();
1760 if done > 0 && !active {
1761 let (sql, values) =
1762 revoke_user_sessions_query(db.backend, user_id, None, RevokeReason::Deactivated);
1763 bind_values(sqlx::query(&sql), values)
1764 .execute(&mut *tx)
1765 .await?;
1766 let (sql, values) =
1767 revoke_user_remember_tokens_query(db.backend, user_id, RevokeReason::Deactivated);
1768 bind_values(sqlx::query(&sql), values)
1769 .execute(&mut *tx)
1770 .await?;
1771 }
1772 tx.commit().await?;
1773 Ok(done)
1774}
1775
1776pub(crate) async fn other_active_superusers(db: &Db, excluding: i64) -> Result<i64, AuthError> {
1781 let (sql, values) = build(
1782 db.backend,
1783 Query::select()
1784 .expr(Expr::col(BackendUsers::Id).count())
1785 .from(BackendUsers::Table)
1786 .and_where(Expr::col(BackendUsers::IsSuperuser).eq(true))
1787 .and_where(Expr::col(BackendUsers::IsActive).eq(true))
1788 .and_where(Expr::col(BackendUsers::Id).ne(excluding))
1789 .to_owned(),
1790 );
1791 let row = bind_values(sqlx::query(&sql), values)
1792 .fetch_one(&db.pool)
1793 .await?;
1794 Ok(row.try_get::<i64, _>(0).unwrap_or(0))
1795}
1796
1797pub async fn list_backend_users(db: &Db) -> Result<Vec<BackendUserSummary>, AuthError> {
1799 let (sql, values) = build(
1800 db.backend,
1801 Query::select()
1802 .columns([
1803 BackendUsers::Id,
1804 BackendUsers::Username,
1805 BackendUsers::Email,
1806 BackendUsers::FirstName,
1807 BackendUsers::LastName,
1808 BackendUsers::IsSuperuser,
1809 BackendUsers::IsActive,
1810 BackendUsers::CreatedAt,
1811 ])
1812 .from(BackendUsers::Table)
1813 .order_by(BackendUsers::CreatedAt, Order::Asc)
1814 .to_owned(),
1815 );
1816 let rows = bind_values(sqlx::query(&sql), values)
1817 .fetch_all(&db.pool)
1818 .await?;
1819 rows.iter().map(summary_from_row).collect()
1820}
1821
1822pub async fn update_password_by_username(
1824 db: &Db,
1825 username: &str,
1826 password_hash: &str,
1827) -> Result<u64, AuthError> {
1828 let (sql, values) = build(
1829 db.backend,
1830 Query::update()
1831 .table(BackendUsers::Table)
1832 .value(BackendUsers::PasswordHash, password_hash)
1833 .value(BackendUsers::PasswordChangedAt, now_ts())
1834 .value(BackendUsers::UpdatedAt, now_ts())
1835 .and_where(Expr::col(BackendUsers::Username).eq(normalize_key(username)))
1836 .to_owned(),
1837 );
1838 let result = bind_values(sqlx::query(&sql), values)
1839 .execute(&db.pool)
1840 .await?;
1841 Ok(result.rows_affected())
1842}
1843
1844pub async fn clear_failed_attempts(db: &Db, username: &str) -> Result<u64, AuthError> {
1846 let (sql, values) = build(
1847 db.backend,
1848 Query::delete()
1849 .from_table(BackendAccessLog::Table)
1850 .and_where(Expr::col(BackendAccessLog::UsernameAttempted).eq(normalize_key(username)))
1851 .and_where(Expr::col(BackendAccessLog::Event).eq(AccessEvent::LoginFailure.as_str()))
1852 .to_owned(),
1853 );
1854 let result = bind_values(sqlx::query(&sql), values)
1855 .execute(&db.pool)
1856 .await?;
1857 Ok(result.rows_affected())
1858}
1859
1860pub async fn user_preference(
1862 db: &Db,
1863 user_id: i64,
1864 key: &str,
1865) -> Result<Option<String>, AuthError> {
1866 let (sql, values) = build(
1867 db.backend,
1868 Query::select()
1869 .column(BackendUserPreferences::Value)
1870 .from(BackendUserPreferences::Table)
1871 .and_where(Expr::col(BackendUserPreferences::UserId).eq(user_id))
1872 .and_where(Expr::col(BackendUserPreferences::PreferenceKey).eq(key))
1873 .to_owned(),
1874 );
1875 let row = bind_values(sqlx::query(&sql), values)
1876 .fetch_optional(&db.pool)
1877 .await?;
1878 Ok(row.and_then(|r| r.get_text_opt("value").ok().flatten()))
1879}
1880
1881pub async fn set_user_preference(
1887 db: &Db,
1888 user_id: i64,
1889 key: &str,
1890 value: &str,
1891) -> Result<(), AuthError> {
1892 let mut tx = db.pool.begin().await?;
1893 let (sql, values) = build(
1894 db.backend,
1895 Query::delete()
1896 .from_table(BackendUserPreferences::Table)
1897 .and_where(Expr::col(BackendUserPreferences::UserId).eq(user_id))
1898 .and_where(Expr::col(BackendUserPreferences::PreferenceKey).eq(key))
1899 .to_owned(),
1900 );
1901 bind_values(sqlx::query(&sql), values)
1902 .execute(&mut *tx)
1903 .await?;
1904 let (sql, values) = build(
1905 db.backend,
1906 Query::insert()
1907 .into_table(BackendUserPreferences::Table)
1908 .columns([
1909 BackendUserPreferences::UserId,
1910 BackendUserPreferences::PreferenceKey,
1911 BackendUserPreferences::Value,
1912 ])
1913 .values_panic([
1914 user_id.into(),
1915 key.to_string().into(),
1916 value.to_string().into(),
1917 ])
1918 .to_owned(),
1919 );
1920 bind_values(sqlx::query(&sql), values)
1921 .execute(&mut *tx)
1922 .await?;
1923 tx.commit().await?;
1924 Ok(())
1925}
1926
1927pub async fn clear_user_preference(db: &Db, user_id: i64, key: &str) -> Result<(), AuthError> {
1929 let (sql, values) = build(
1930 db.backend,
1931 Query::delete()
1932 .from_table(BackendUserPreferences::Table)
1933 .and_where(Expr::col(BackendUserPreferences::UserId).eq(user_id))
1934 .and_where(Expr::col(BackendUserPreferences::PreferenceKey).eq(key))
1935 .to_owned(),
1936 );
1937 bind_values(sqlx::query(&sql), values)
1938 .execute(&db.pool)
1939 .await?;
1940 Ok(())
1941}
1942
1943#[cfg(test)]
1944mod tests {
1945 use super::*;
1946 use laterite_core::testing::connect_test;
1947
1948 async fn operator(db: &Db) -> i64 {
1949 let svc = crate::AuthService::new(db.clone(), crate::AuthConfig::default());
1950 svc.create_superuser(crate::NewOperator {
1951 username: "root",
1952 email: "root@acme.test",
1953 first_name: "Root",
1954 last_name: None,
1955 password: "rootpw12345",
1956 timezone: None,
1957 })
1958 .await
1959 .unwrap()
1960 }
1961
1962 #[tokio::test]
1963 async fn a_preference_round_trips_and_replaces() {
1964 let (db, _guard) = connect_test(&[crate::migrations()]).await;
1965 let id = operator(&db).await;
1966
1967 assert_eq!(
1969 user_preference(&db, id, "list.columns./roles")
1970 .await
1971 .unwrap(),
1972 None
1973 );
1974
1975 set_user_preference(&db, id, "list.columns./roles", "code,name")
1976 .await
1977 .unwrap();
1978 assert_eq!(
1979 user_preference(&db, id, "list.columns./roles")
1980 .await
1981 .unwrap(),
1982 Some("code,name".to_string())
1983 );
1984
1985 set_user_preference(&db, id, "list.columns./roles", "code")
1988 .await
1989 .unwrap();
1990 assert_eq!(
1991 user_preference(&db, id, "list.columns./roles")
1992 .await
1993 .unwrap(),
1994 Some("code".to_string())
1995 );
1996
1997 clear_user_preference(&db, id, "list.columns./roles")
1998 .await
1999 .unwrap();
2000 assert_eq!(
2001 user_preference(&db, id, "list.columns./roles")
2002 .await
2003 .unwrap(),
2004 None
2005 );
2006 }
2007
2008 #[tokio::test]
2010 async fn preferences_are_scoped() {
2011 let (db, _guard) = connect_test(&[crate::migrations()]).await;
2012 let root = operator(&db).await;
2013 let hash = crate::password::hash_password("otherpw12345").unwrap();
2014 let other = create_user(&db, "other", "other@acme.test", "Other", None, &hash, false)
2015 .await
2016 .unwrap();
2017
2018 set_user_preference(&db, root, "list.columns./roles", "code")
2019 .await
2020 .unwrap();
2021 assert_eq!(
2022 user_preference(&db, other, "list.columns./roles")
2023 .await
2024 .unwrap(),
2025 None
2026 );
2027 assert_eq!(
2028 user_preference(&db, root, "list.columns./users")
2029 .await
2030 .unwrap(),
2031 None
2032 );
2033 }
2034}