Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ base64 = "0.22"
reqwest = { workspace = true }
urlencoding = "2.1"
once_cell = "1.20"
# Coffre de secrets (GitHub App P0) : AEAD au repos + primitives pour JWT/HMAC des phases suivantes.
aes-gcm = "0.10"
rand = "0.8"
zeroize = { workspace = true }
jsonwebtoken = "9"
hmac = "0.12"
subtle = "2.6"
png = "0.18"

[target.'cfg(target_os = "linux")'.dependencies]
Expand Down
91 changes: 91 additions & 0 deletions apps/server/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,9 +1017,100 @@ pub async fn migrate(pool: &PgPool) -> Result<(), sqlx::Error> {
// Mémoire durable des agents (P3-01) : table, index, pg_trgm facultatif.
devforge_memory::migrate(pool).await?;

// GitHub App (P0) : schéma + scellement des PAT existants.
migrate_github_app(pool).await?;

Ok(())
}

/// GitHub App (P0-03/P0-04) : tables dédiées, colonnes projet, PAT scellés au repos.
pub async fn migrate_github_app(pool: &PgPool) -> Result<(), sqlx::Error> {
for sql in [
r#"CREATE TABLE IF NOT EXISTS github_app (
id INTEGER PRIMARY KEY CHECK (id = 1),
app_id BIGINT NOT NULL, slug TEXT NOT NULL, client_id TEXT NOT NULL,
client_secret_enc TEXT NOT NULL, webhook_secret_enc TEXT NOT NULL,
private_key_enc TEXT NOT NULL, html_url TEXT NOT NULL,
owner_login TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())"#,
r#"CREATE TABLE IF NOT EXISTS github_installations (
installation_id BIGINT PRIMARY KEY, account_login TEXT NOT NULL,
account_type TEXT NOT NULL, repository_selection TEXT NOT NULL,
permissions JSONB NOT NULL DEFAULT '{}', suspended_at TIMESTAMPTZ,
created_by_user_uuid TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now())"#,
r#"CREATE TABLE IF NOT EXISTS github_installation_repos (
installation_id BIGINT NOT NULL REFERENCES github_installations ON DELETE CASCADE,
repo_id BIGINT NOT NULL, full_name TEXT NOT NULL, private BOOLEAN NOT NULL,
default_branch TEXT, PRIMARY KEY (installation_id, repo_id))"#,
r#"CREATE TABLE IF NOT EXISTS github_webhook_deliveries (
delivery_id TEXT PRIMARY KEY, event TEXT NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(), status TEXT NOT NULL)"#,
r#"CREATE TABLE IF NOT EXISTS github_manifest_states (
state TEXT PRIMARY KEY, user_uuid TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL)"#,
r#"CREATE TABLE IF NOT EXISTS pr_previews (
project_uuid TEXT NOT NULL, pr_number INTEGER NOT NULL, head_sha TEXT NOT NULL,
url TEXT, deployment_uuid TEXT, comment_id BIGINT, check_run_id BIGINT,
status TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (project_uuid, pr_number))"#,
] {
sqlx::query(sql).execute(pool).await?;
}
// Échec toléré : colonne déjà présente.
for (col, def) in [
("github_installation_id", "BIGINT"),
("github_repo_id", "BIGINT"),
("pr_previews_enabled", "BOOLEAN NOT NULL DEFAULT true"),
("legacy_webhook_id", "BIGINT"),
] {
let sql = format!("ALTER TABLE projects ADD COLUMN {col} {def}");
let _ = sqlx::query(&sql).execute(pool).await;
}
seal_plain_github_tokens(pool).await;
Ok(())
}

/// Scelle (`v1:`) les PAT encore en clair. Sans coffre disponible, rien n'est modifié.
async fn seal_plain_github_tokens(pool: &PgPool) {
if crate::secret_box::global().is_err() {
return;
}
let users: Vec<(String, String)> = sqlx::query_as(
"SELECT user_uuid, github_token FROM user_settings WHERE github_token <> '' AND github_token NOT LIKE 'v1:%'",
)
.fetch_all(pool)
.await
.unwrap_or_default();
for (uuid, tok) in users {
let sealed = crate::secret_box::seal_compat(&tok);
let _ = sqlx::query(
"UPDATE user_settings SET github_token = $1 WHERE user_uuid = $2 AND github_token = $3",
)
.bind(&sealed)
.bind(&uuid)
.bind(&tok)
.execute(pool)
.await;
}
let inst: Option<(String,)> = sqlx::query_as(
"SELECT github_token FROM instance_settings WHERE id = 1 AND github_token <> '' AND github_token NOT LIKE 'v1:%'",
)
.fetch_optional(pool)
.await
.ok()
.flatten();
if let Some((tok,)) = inst {
let sealed = crate::secret_box::seal_compat(&tok);
let _ = sqlx::query(
"UPDATE instance_settings SET github_token = $1 WHERE id = 1 AND github_token = $2",
)
.bind(&sealed)
.bind(&tok)
.execute(pool)
.await;
}
}

/// Remove leftover demo fixtures from earlier greenfield seeds.
pub async fn purge_demo_data(pool: &PgPool) -> Result<(), sqlx::Error> {
let demo_uuid = "uji97r70f1jaq9m7l9btm61d";
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/dev_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ pub(crate) async fn run_dev_deploy(
.await
.ok()
.flatten()
.map(|(t,)| t)
.map(|(t,)| crate::secret_box::open_compat(&t))
.filter(|t| !t.trim().is_empty());

// Mêmes variables que la prod (jamais loguées).
Expand Down
Loading
Loading