Skip to main content

miryad_core/users/
user.rs

1use chrono::Utc;
2use sea_orm::entity::prelude::*;
3use sea_orm::{ConnectionTrait, Set};
4
5#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
6#[sea_orm(table_name = "miryad_users")]
7pub struct Model {
8    #[sea_orm(primary_key)]
9    pub id: i32,
10    /// Claim `sub` OIDC — lien avec `AuthPrincipal.subject` (feature 2b).
11    #[sea_orm(unique)]
12    pub subject: String,
13    pub email: Option<String>,
14    pub display_name: Option<String>,
15    pub created_at: DateTimeUtc,
16}
17
18#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
19pub enum Relation {}
20
21impl ActiveModelBehavior for ActiveModel {}
22
23pub type User = Entity;
24
25/// Get-or-create par `subject`. Pas de vraie contrainte `ON CONFLICT` portable entre SQLite/
26/// Postgres exploitée ici — en cas de course (deux premiers logins concurrents du même
27/// `subject`), l'`insert` échoue sur la contrainte unique et on retombe sur un `find` pour
28/// récupérer la ligne posée par l'autre requête, plutôt que de propager l'erreur.
29pub async fn resolve_user<C: ConnectionTrait>(
30    db: &C,
31    subject: &str,
32    email: Option<&str>,
33) -> Result<Model, DbErr> {
34    if let Some(existing) = Entity::find().filter(Column::Subject.eq(subject)).one(db).await? {
35        return Ok(existing);
36    }
37
38    let active = ActiveModel {
39        subject: Set(subject.to_string()),
40        email: Set(email.map(str::to_string)),
41        display_name: Set(None),
42        created_at: Set(Utc::now()),
43        ..Default::default()
44    };
45
46    match active.insert(db).await {
47        Ok(model) => Ok(model),
48        Err(_) => Entity::find()
49            .filter(Column::Subject.eq(subject))
50            .one(db)
51            .await?
52            .ok_or_else(|| DbErr::RecordNotFound(format!("user with subject {subject} vanished"))),
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::migration::Migrator;
60    use sea_orm_migration::MigratorTrait;
61
62    async fn test_db() -> DatabaseConnection {
63        let db = sea_orm::Database::connect("sqlite::memory:")
64            .await
65            .expect("in-memory sqlite connects");
66        Migrator::up(&db, None).await.expect("migrations apply cleanly");
67        db
68    }
69
70    #[tokio::test]
71    async fn resolve_user_creates_then_reuses_same_row() {
72        let db = test_db().await;
73        let first = resolve_user(&db, "sub-1", Some("a@example.com"))
74            .await
75            .expect("first resolve succeeds");
76        let second = resolve_user(&db, "sub-1", Some("a@example.com"))
77            .await
78            .expect("second resolve succeeds");
79        assert_eq!(first.id, second.id);
80    }
81}