491 lines
13 KiB
Rust
491 lines
13 KiB
Rust
use super::*;
|
|
use time::OffsetDateTime;
|
|
|
|
impl PostgresRegistry {
|
|
pub async fn upsert_bootstrap_user(
|
|
&self,
|
|
email: &str,
|
|
display_name: &str,
|
|
password_hash: &str,
|
|
) -> Result<UserId, RegistryError> {
|
|
let user_id = format!("user_{}", uuid::Uuid::now_v7().simple());
|
|
let row = sqlx::query!(
|
|
"insert into users (
|
|
id,
|
|
email,
|
|
display_name,
|
|
password_hash,
|
|
status,
|
|
created_at
|
|
) values (
|
|
$1, $2, $3, $4, 'active', now()
|
|
)
|
|
on conflict (email) do update
|
|
set display_name = excluded.display_name,
|
|
password_hash = excluded.password_hash,
|
|
status = 'active'
|
|
returning id",
|
|
user_id,
|
|
email,
|
|
display_name,
|
|
password_hash,
|
|
)
|
|
.fetch_one(&self.pool)
|
|
.await?;
|
|
|
|
Ok(UserId::new(row.id))
|
|
}
|
|
|
|
pub async fn ensure_membership(
|
|
&self,
|
|
workspace_id: &WorkspaceId,
|
|
user_id: &UserId,
|
|
role: MembershipRole,
|
|
) -> Result<(), RegistryError> {
|
|
sqlx::query(
|
|
"insert into memberships (
|
|
workspace_id,
|
|
user_id,
|
|
role,
|
|
created_at
|
|
) values (
|
|
$1, $2, $3, now()
|
|
)
|
|
on conflict (workspace_id, user_id) do update
|
|
set role = excluded.role",
|
|
)
|
|
.bind(workspace_id.as_str())
|
|
.bind(user_id.as_str())
|
|
.bind(serialize_enum_text(&role, "role")?)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_auth_user_by_email(
|
|
&self,
|
|
email: &str,
|
|
) -> Result<Option<AuthUserRecord>, RegistryError> {
|
|
let row = sqlx::query!(
|
|
"select
|
|
id,
|
|
email,
|
|
display_name,
|
|
password_hash as \"password_hash!\",
|
|
status,
|
|
created_at as \"created_at!: OffsetDateTime\"
|
|
from users
|
|
where email = $1
|
|
limit 1",
|
|
email,
|
|
)
|
|
.fetch_optional(&self.pool)
|
|
.await?;
|
|
|
|
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!(
|
|
"select
|
|
id,
|
|
email,
|
|
display_name,
|
|
password_hash as \"password_hash!\",
|
|
status,
|
|
created_at as \"created_at!: OffsetDateTime\"
|
|
from users
|
|
where id = $1
|
|
limit 1",
|
|
user_id.as_str(),
|
|
)
|
|
.fetch_optional(&self.pool)
|
|
.await?;
|
|
|
|
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(
|
|
&self,
|
|
user_id: &UserId,
|
|
email: &str,
|
|
display_name: &str,
|
|
) -> Result<User, RegistryError> {
|
|
let row = sqlx::query!(
|
|
"update users
|
|
set email = $2,
|
|
display_name = $3
|
|
where id = $1
|
|
returning
|
|
id,
|
|
email,
|
|
display_name,
|
|
status,
|
|
created_at as \"created_at!: OffsetDateTime\"",
|
|
user_id.as_str(),
|
|
email,
|
|
display_name,
|
|
)
|
|
.fetch_one(&self.pool)
|
|
.await
|
|
.map_err(|error| map_user_update_error(error, user_id, email))?;
|
|
|
|
build_user(
|
|
row.id,
|
|
row.email,
|
|
row.display_name,
|
|
row.status,
|
|
row.created_at,
|
|
)
|
|
}
|
|
|
|
pub async fn update_user_password(
|
|
&self,
|
|
user_id: &UserId,
|
|
password_hash: &str,
|
|
) -> Result<(), RegistryError> {
|
|
let result = sqlx::query(
|
|
"update users
|
|
set password_hash = $2
|
|
where id = $1",
|
|
)
|
|
.bind(user_id.as_str())
|
|
.bind(password_hash)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
if result.rows_affected() == 0 {
|
|
return Err(RegistryError::UserNotFound {
|
|
user_id: user_id.as_str().to_owned(),
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn create_user_session(
|
|
&self,
|
|
session_id: &UserSessionId,
|
|
user_id: &UserId,
|
|
current_workspace_id: Option<&WorkspaceId>,
|
|
secret_hash: &str,
|
|
expires_at: &OffsetDateTime,
|
|
) -> Result<(), RegistryError> {
|
|
sqlx::query(
|
|
"insert into user_sessions (
|
|
id,
|
|
user_id,
|
|
current_workspace_id,
|
|
secret_hash,
|
|
status,
|
|
expires_at,
|
|
last_seen_at,
|
|
created_at
|
|
) values (
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
'active',
|
|
$5::timestamptz,
|
|
now(),
|
|
now()
|
|
)",
|
|
)
|
|
.bind(session_id.as_str())
|
|
.bind(user_id.as_str())
|
|
.bind(current_workspace_id.map(|id| id.as_str()))
|
|
.bind(secret_hash)
|
|
.bind(*expires_at)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_user_session(
|
|
&self,
|
|
session_id: &UserSessionId,
|
|
secret_hash: &str,
|
|
) -> Result<Option<SessionRecord>, RegistryError> {
|
|
let row = sqlx::query!(
|
|
"select
|
|
s.id,
|
|
s.user_id,
|
|
s.current_workspace_id,
|
|
u.email,
|
|
u.display_name,
|
|
u.status,
|
|
u.created_at as \"created_at!: OffsetDateTime\"
|
|
from user_sessions s
|
|
join users u on u.id = s.user_id
|
|
where s.id = $1
|
|
and s.secret_hash = $2
|
|
and s.status = 'active'
|
|
and s.expires_at > now()
|
|
limit 1",
|
|
session_id.as_str(),
|
|
secret_hash,
|
|
)
|
|
.fetch_optional(&self.pool)
|
|
.await?;
|
|
|
|
let Some(row) = row else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let user_id = UserId::new(row.user_id);
|
|
let user = User {
|
|
id: user_id.clone(),
|
|
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.current_workspace_id.map(WorkspaceId::new);
|
|
let current_workspace_id = stored_workspace_id
|
|
.filter(|workspace_id| {
|
|
memberships
|
|
.iter()
|
|
.any(|membership| membership.workspace.id == *workspace_id)
|
|
})
|
|
.or_else(|| {
|
|
memberships
|
|
.first()
|
|
.map(|membership| membership.workspace.id.clone())
|
|
});
|
|
|
|
Ok(Some(SessionRecord {
|
|
session_id: UserSessionId::new(row.id),
|
|
user,
|
|
memberships,
|
|
current_workspace_id,
|
|
}))
|
|
}
|
|
|
|
pub async fn touch_user_session(
|
|
&self,
|
|
session_id: &UserSessionId,
|
|
) -> Result<(), RegistryError> {
|
|
sqlx::query(
|
|
"update user_sessions
|
|
set last_seen_at = now()
|
|
where id = $1",
|
|
)
|
|
.bind(session_id.as_str())
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn revoke_user_session(
|
|
&self,
|
|
session_id: &UserSessionId,
|
|
) -> Result<(), RegistryError> {
|
|
sqlx::query(
|
|
"update user_sessions
|
|
set status = 'revoked'
|
|
where id = $1",
|
|
)
|
|
.bind(session_id.as_str())
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn set_user_session_current_workspace(
|
|
&self,
|
|
session_id: &UserSessionId,
|
|
workspace_id: &WorkspaceId,
|
|
) -> Result<(), RegistryError> {
|
|
sqlx::query(
|
|
"update user_sessions
|
|
set current_workspace_id = $2
|
|
where id = $1",
|
|
)
|
|
.bind(session_id.as_str())
|
|
.bind(workspace_id.as_str())
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn user_has_workspace_access(
|
|
&self,
|
|
user_id: &UserId,
|
|
workspace_id: &WorkspaceId,
|
|
) -> Result<bool, RegistryError> {
|
|
let row = sqlx::query!(
|
|
"select exists(
|
|
select 1
|
|
from memberships
|
|
where user_id = $1
|
|
and workspace_id = $2
|
|
) as \"allowed!\"",
|
|
user_id.as_str(),
|
|
workspace_id.as_str(),
|
|
)
|
|
.fetch_one(&self.pool)
|
|
.await?;
|
|
|
|
Ok(row.allowed)
|
|
}
|
|
|
|
pub async fn save_auth_profile(
|
|
&self,
|
|
request: SaveAuthProfileRequest<'_>,
|
|
) -> Result<(), RegistryError> {
|
|
sqlx::query(
|
|
"insert into auth_profiles (
|
|
id,
|
|
workspace_id,
|
|
name,
|
|
kind,
|
|
config_json,
|
|
created_at,
|
|
updated_at
|
|
) values ($1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz)
|
|
on conflict(id) do update set
|
|
workspace_id = excluded.workspace_id,
|
|
name = excluded.name,
|
|
kind = excluded.kind,
|
|
config_json = excluded.config_json,
|
|
updated_at = excluded.updated_at",
|
|
)
|
|
.bind(request.profile.id.as_str())
|
|
.bind(request.workspace_id.as_str())
|
|
.bind(&request.profile.name)
|
|
.bind(serialize_enum_text(&request.profile.kind, "kind")?)
|
|
.bind(Json(serialize_json_value(&request.profile.config)?))
|
|
.bind(request.profile.created_at)
|
|
.bind(request.profile.updated_at)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_auth_profile(
|
|
&self,
|
|
workspace_id: &WorkspaceId,
|
|
auth_profile_id: &crank_core::AuthProfileId,
|
|
) -> Result<Option<AuthProfile>, RegistryError> {
|
|
let row = sqlx::query!(
|
|
"select
|
|
id,
|
|
workspace_id,
|
|
name,
|
|
kind,
|
|
config_json,
|
|
created_at as \"created_at!: OffsetDateTime\",
|
|
updated_at as \"updated_at!: OffsetDateTime\"
|
|
from auth_profiles
|
|
where workspace_id = $1 and id = $2",
|
|
workspace_id.as_str(),
|
|
auth_profile_id.as_str(),
|
|
)
|
|
.fetch_optional(&self.pool)
|
|
.await?;
|
|
|
|
row.map(|row| {
|
|
build_auth_profile(
|
|
row.id,
|
|
row.workspace_id,
|
|
row.name,
|
|
row.kind,
|
|
row.config_json,
|
|
row.created_at,
|
|
row.updated_at,
|
|
)
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
pub async fn list_auth_profiles(
|
|
&self,
|
|
workspace_id: &WorkspaceId,
|
|
) -> Result<Vec<AuthProfile>, RegistryError> {
|
|
let rows = sqlx::query!(
|
|
"select
|
|
id,
|
|
workspace_id,
|
|
name,
|
|
kind,
|
|
config_json,
|
|
created_at as \"created_at!: OffsetDateTime\",
|
|
updated_at as \"updated_at!: OffsetDateTime\"
|
|
from auth_profiles
|
|
where workspace_id = $1
|
|
order by name asc",
|
|
workspace_id.as_str(),
|
|
)
|
|
.fetch_all(&self.pool)
|
|
.await?;
|
|
|
|
rows.into_iter()
|
|
.map(|row| {
|
|
build_auth_profile(
|
|
row.id,
|
|
row.workspace_id,
|
|
row.name,
|
|
row.kind,
|
|
row.config_json,
|
|
row.created_at,
|
|
row.updated_at,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub async fn list_auth_profiles_referencing_secret(
|
|
&self,
|
|
workspace_id: &WorkspaceId,
|
|
secret_id: &SecretId,
|
|
) -> Result<Vec<AuthProfile>, RegistryError> {
|
|
let profiles = self.list_auth_profiles(workspace_id).await?;
|
|
|
|
Ok(profiles
|
|
.into_iter()
|
|
.filter(|profile| {
|
|
profile
|
|
.config
|
|
.secret_ids()
|
|
.into_iter()
|
|
.any(|candidate| candidate == secret_id)
|
|
})
|
|
.collect())
|
|
}
|
|
}
|