chore: publish clean community baseline
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user