feat: add is_admin to User, count_users, ProviderConfigRepository trait, admin migration
This commit is contained in:
@@ -22,6 +22,7 @@ pub struct User {
|
||||
pub subject: String,
|
||||
pub email: Email,
|
||||
pub password_hash: Option<String>,
|
||||
pub is_admin: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -32,6 +33,7 @@ impl User {
|
||||
subject: subject.into(),
|
||||
email,
|
||||
password_hash: None,
|
||||
is_admin: false,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
@@ -41,6 +43,7 @@ impl User {
|
||||
subject: impl Into<String>,
|
||||
email: Email,
|
||||
password_hash: Option<String>,
|
||||
is_admin: bool,
|
||||
created_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -48,6 +51,7 @@ impl User {
|
||||
subject: subject.into(),
|
||||
email,
|
||||
password_hash,
|
||||
is_admin,
|
||||
created_at,
|
||||
}
|
||||
}
|
||||
@@ -58,6 +62,7 @@ impl User {
|
||||
subject: format!("local|{}", Uuid::new_v4()),
|
||||
email,
|
||||
password_hash: Some(password_hash.into()),
|
||||
is_admin: false,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,24 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
/// Delete a user by their ID
|
||||
async fn delete(&self, id: Uuid) -> DomainResult<()>;
|
||||
|
||||
/// Count total number of users (used for first-user admin promotion)
|
||||
async fn count_users(&self) -> DomainResult<u64>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderConfigRow {
|
||||
pub provider_type: String,
|
||||
pub config_json: String,
|
||||
pub enabled: bool,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ProviderConfigRepository: Send + Sync {
|
||||
async fn get_all(&self) -> DomainResult<Vec<ProviderConfigRow>>;
|
||||
async fn upsert(&self, row: &ProviderConfigRow) -> DomainResult<()>;
|
||||
async fn delete(&self, provider_type: &str) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Repository port for `Channel` persistence.
|
||||
|
||||
@@ -31,7 +31,10 @@ impl UserService {
|
||||
}
|
||||
|
||||
let email = Email::try_from(email)?;
|
||||
let user = User::new(subject, email);
|
||||
let mut user = User::new(subject, email);
|
||||
if self.user_repository.count_users().await? == 0 {
|
||||
user.is_admin = true;
|
||||
}
|
||||
self.user_repository.save(&user).await?;
|
||||
Ok(user)
|
||||
}
|
||||
@@ -53,7 +56,10 @@ impl UserService {
|
||||
password_hash: &str,
|
||||
) -> DomainResult<User> {
|
||||
let email = Email::try_from(email)?;
|
||||
let user = User::new_local(email, password_hash);
|
||||
let mut user = User::new_local(email, password_hash);
|
||||
if self.user_repository.count_users().await? == 0 {
|
||||
user.is_admin = true;
|
||||
}
|
||||
self.user_repository.save(&user).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(super) struct UserRow {
|
||||
pub subject: String,
|
||||
pub email: String,
|
||||
pub password_hash: Option<String>,
|
||||
pub is_admin: i64,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ impl TryFrom<UserRow> for User {
|
||||
row.subject,
|
||||
email,
|
||||
row.password_hash,
|
||||
row.is_admin != 0,
|
||||
created_at,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
async fn find_by_id(&self, id: Uuid) -> DomainResult<Option<User>> {
|
||||
let id_str = id.to_string();
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, subject, email, password_hash, created_at FROM users WHERE id = $1",
|
||||
"SELECT id, subject, email, password_hash, is_admin, created_at FROM users WHERE id = $1",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -34,7 +34,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
|
||||
async fn find_by_subject(&self, subject: &str) -> DomainResult<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, subject, email, password_hash, created_at FROM users WHERE subject = $1",
|
||||
"SELECT id, subject, email, password_hash, is_admin, created_at FROM users WHERE subject = $1",
|
||||
)
|
||||
.bind(subject)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -46,7 +46,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
|
||||
async fn find_by_email(&self, email: &str) -> DomainResult<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, subject, email, password_hash, created_at FROM users WHERE email = $1",
|
||||
"SELECT id, subject, email, password_hash, is_admin, created_at FROM users WHERE email = $1",
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -62,18 +62,20 @@ impl UserRepository for PostgresUserRepository {
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users (id, subject, email, password_hash, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
INSERT INTO users (id, subject, email, password_hash, is_admin, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
subject = excluded.subject,
|
||||
email = excluded.email,
|
||||
password_hash = excluded.password_hash
|
||||
password_hash = excluded.password_hash,
|
||||
is_admin = excluded.is_admin
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&user.subject)
|
||||
.bind(user.email.as_ref())
|
||||
.bind(&user.password_hash)
|
||||
.bind(user.is_admin)
|
||||
.bind(&created_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -99,4 +101,12 @@ impl UserRepository for PostgresUserRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn count_users(&self) -> DomainResult<u64> {
|
||||
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
|
||||
Ok(count as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
async fn find_by_id(&self, id: Uuid) -> DomainResult<Option<User>> {
|
||||
let id_str = id.to_string();
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, subject, email, password_hash, created_at FROM users WHERE id = ?",
|
||||
"SELECT id, subject, email, password_hash, is_admin, created_at FROM users WHERE id = ?",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -34,7 +34,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
|
||||
async fn find_by_subject(&self, subject: &str) -> DomainResult<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, subject, email, password_hash, created_at FROM users WHERE subject = ?",
|
||||
"SELECT id, subject, email, password_hash, is_admin, created_at FROM users WHERE subject = ?",
|
||||
)
|
||||
.bind(subject)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -46,7 +46,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
|
||||
async fn find_by_email(&self, email: &str) -> DomainResult<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, subject, email, password_hash, created_at FROM users WHERE email = ?",
|
||||
"SELECT id, subject, email, password_hash, is_admin, created_at FROM users WHERE email = ?",
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -62,18 +62,20 @@ impl UserRepository for SqliteUserRepository {
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users (id, subject, email, password_hash, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO users (id, subject, email, password_hash, is_admin, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
subject = excluded.subject,
|
||||
email = excluded.email,
|
||||
password_hash = excluded.password_hash
|
||||
password_hash = excluded.password_hash,
|
||||
is_admin = excluded.is_admin
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&user.subject)
|
||||
.bind(user.email.as_ref())
|
||||
.bind(&user.password_hash)
|
||||
.bind(user.is_admin as i64)
|
||||
.bind(&created_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -100,6 +102,14 @@ impl UserRepository for SqliteUserRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn count_users(&self) -> DomainResult<u64> {
|
||||
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
|
||||
Ok(count as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user