registry: add sqlx compile-time checks for auth and secrets

This commit is contained in:
a.tolmachev
2026-04-12 12:24:27 +03:00
parent dec311b143
commit 5f187a6817
13 changed files with 749 additions and 87 deletions
+52 -29
View File
@@ -7,7 +7,8 @@ impl PostgresRegistry {
display_name: &str,
password_hash: &str,
) -> Result<UserId, RegistryError> {
let row = sqlx::query(
let user_id = format!("user_{}", uuid::Uuid::now_v7().simple());
let row = sqlx::query!(
"insert into users (
id,
email,
@@ -23,15 +24,15 @@ impl PostgresRegistry {
password_hash = excluded.password_hash,
status = 'active'
returning id",
user_id,
email,
display_name,
password_hash,
)
.bind(format!("user_{}", uuid::Uuid::now_v7().simple()))
.bind(email)
.bind(display_name)
.bind(password_hash)
.fetch_one(&self.pool)
.await?;
Ok(UserId::new(row.try_get::<String, _>("id")?))
Ok(UserId::new(row.id))
}
pub async fn ensure_membership(
@@ -65,46 +66,70 @@ impl PostgresRegistry {
&self,
email: &str,
) -> Result<Option<AuthUserRecord>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
id,
email,
display_name,
password_hash,
password_hash as \"password_hash!\",
status,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
from users
where email = $1
limit 1",
email,
)
.bind(email)
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_auth_user_record).transpose()
row.map(|row| {
Ok(AuthUserRecord {
user: User {
id: UserId::new(row.id),
email: row.email,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
},
password_hash: row.password_hash,
})
})
.transpose()
}
pub async fn get_auth_user_by_id(
&self,
user_id: &UserId,
) -> Result<Option<AuthUserRecord>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
id,
email,
display_name,
password_hash,
password_hash as \"password_hash!\",
status,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
from users
where id = $1
limit 1",
user_id.as_str(),
)
.bind(user_id.as_str())
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_auth_user_record).transpose()
row.map(|row| {
Ok(AuthUserRecord {
user: User {
id: UserId::new(row.id),
email: row.email,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
},
password_hash: row.password_hash,
})
})
.transpose()
}
pub async fn update_user_profile(
@@ -204,7 +229,7 @@ impl PostgresRegistry {
session_id: &UserSessionId,
secret_hash: &str,
) -> Result<Option<SessionRecord>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
s.id,
s.user_id,
@@ -212,7 +237,7 @@ impl PostgresRegistry {
u.email,
u.display_name,
u.status,
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
from user_sessions s
join users u on u.id = s.user_id
where s.id = $1
@@ -220,9 +245,9 @@ impl PostgresRegistry {
and s.status = 'active'
and s.expires_at > now()
limit 1",
session_id.as_str(),
secret_hash,
)
.bind(session_id.as_str())
.bind(secret_hash)
.fetch_optional(&self.pool)
.await?;
@@ -230,18 +255,16 @@ impl PostgresRegistry {
return Ok(None);
};
let user_id = UserId::new(row.try_get::<String, _>("user_id")?);
let user_id = UserId::new(row.user_id);
let user = User {
id: user_id.clone(),
email: row.try_get("email")?,
display_name: row.try_get("display_name")?,
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
created_at: row.try_get("created_at")?,
email: row.email,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
};
let memberships = self.list_workspaces_for_user(&user_id).await?;
let stored_workspace_id = row
.try_get::<Option<String>, _>("current_workspace_id")?
.map(WorkspaceId::new);
let stored_workspace_id = row.current_workspace_id.map(WorkspaceId::new);
let current_workspace_id = stored_workspace_id
.filter(|workspace_id| {
memberships
@@ -255,7 +278,7 @@ impl PostgresRegistry {
});
Ok(Some(SessionRecord {
session_id: UserSessionId::new(row.try_get::<String, _>("id")?),
session_id: UserSessionId::new(row.id),
user,
memberships,
current_workspace_id,
-41
View File
@@ -530,13 +530,6 @@ fn map_membership_record(row: &PgRow) -> Result<MembershipRecord, RegistryError>
})
}
fn map_auth_user_record(row: &PgRow) -> Result<AuthUserRecord, RegistryError> {
Ok(AuthUserRecord {
user: map_user(row)?,
password_hash: row.try_get("password_hash")?,
})
}
fn map_user(row: &PgRow) -> Result<User, RegistryError> {
Ok(User {
id: UserId::new(row.try_get::<String, _>("id")?),
@@ -593,40 +586,6 @@ fn map_platform_api_key_record(row: &PgRow) -> Result<PlatformApiKeyRecord, Regi
})
}
fn map_secret_record(row: &PgRow) -> Result<SecretRecord, RegistryError> {
Ok(SecretRecord {
secret: Secret {
id: SecretId::new(row.try_get::<String, _>("id")?),
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
name: row.try_get("name")?,
kind: deserialize_enum_text(&row.try_get::<String, _>("kind")?, "kind")?,
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
current_version: from_db_version(
row.try_get::<i32, _>("current_version")?,
"current_version",
)?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
last_used_at: row.try_get("last_used_at")?,
},
})
}
fn map_secret_version_record(row: &PgRow) -> Result<SecretVersionRecord, RegistryError> {
Ok(SecretVersionRecord {
secret_version: SecretVersion {
secret_id: SecretId::new(row.try_get::<String, _>("secret_id")?),
version: from_db_version(row.try_get::<i32, _>("version")?, "version")?,
ciphertext: row.try_get("ciphertext")?,
key_version: row.try_get("key_version")?,
created_at: row.try_get("created_at")?,
created_by: row
.try_get::<Option<String>, _>("created_by")?
.map(UserId::new),
},
})
}
fn map_stream_session(row: &PgRow) -> Result<StreamSession, RegistryError> {
Ok(StreamSession {
id: StreamSessionId::new(row.try_get::<String, _>("id")?),
+59 -16
View File
@@ -5,7 +5,7 @@ impl PostgresRegistry {
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<SecretRecord>, RegistryError> {
let rows = sqlx::query(
let rows = sqlx::query!(
"select
id,
workspace_id,
@@ -13,18 +13,34 @@ impl PostgresRegistry {
kind,
status,
current_version,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\",
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
from secrets
where workspace_id = $1
order by name asc",
workspace_id.as_str(),
)
.bind(workspace_id.as_str())
.fetch_all(&self.pool)
.await?;
rows.iter().map(map_secret_record).collect()
rows.into_iter()
.map(|row| {
Ok(SecretRecord {
secret: Secret {
id: SecretId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
name: row.name,
kind: deserialize_enum_text(&row.kind, "kind")?,
status: deserialize_enum_text(&row.status, "status")?,
current_version: from_db_version(row.current_version, "current_version")?,
created_at: row.created_at,
updated_at: row.updated_at,
last_used_at: row.last_used_at,
},
})
})
.collect()
}
pub async fn get_secret(
@@ -32,7 +48,7 @@ impl PostgresRegistry {
workspace_id: &WorkspaceId,
secret_id: &SecretId,
) -> Result<Option<SecretRecord>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
id,
workspace_id,
@@ -40,18 +56,33 @@ impl PostgresRegistry {
kind,
status,
current_version,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\",
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
from secrets
where workspace_id = $1 and id = $2",
workspace_id.as_str(),
secret_id.as_str(),
)
.bind(workspace_id.as_str())
.bind(secret_id.as_str())
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_secret_record).transpose()
row.map(|row| {
Ok(SecretRecord {
secret: Secret {
id: SecretId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
name: row.name,
kind: deserialize_enum_text(&row.kind, "kind")?,
status: deserialize_enum_text(&row.status, "status")?,
current_version: from_db_version(row.current_version, "current_version")?,
created_at: row.created_at,
updated_at: row.updated_at,
last_used_at: row.last_used_at,
},
})
})
.transpose()
}
pub async fn get_current_secret_version(
@@ -59,25 +90,37 @@ impl PostgresRegistry {
workspace_id: &WorkspaceId,
secret_id: &SecretId,
) -> Result<Option<SecretVersionRecord>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
sv.secret_id,
sv.version,
sv.ciphertext,
sv.key_version,
to_char(sv.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(sv.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
sv.created_by
from secrets s
join secret_versions sv
on sv.secret_id = s.id and sv.version = s.current_version
where s.workspace_id = $1 and s.id = $2",
workspace_id.as_str(),
secret_id.as_str(),
)
.bind(workspace_id.as_str())
.bind(secret_id.as_str())
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_secret_version_record).transpose()
row.map(|row| {
Ok(SecretVersionRecord {
secret_version: SecretVersion {
secret_id: SecretId::new(row.secret_id),
version: from_db_version(row.version, "version")?,
ciphertext: row.ciphertext,
key_version: row.key_version,
created_at: row.created_at,
created_by: row.created_by.map(UserId::new),
},
})
})
.transpose()
}
pub async fn create_secret(