优化部署流程

This commit is contained in:
ChuNa7a committed 2026-10-17 14:21:52 +08:00
1 parent 16e5db3b26
commit 2ecc72d027
20 files changed
+597 -116

No files matched your search

+3 -43
View File
@@ -120,55 +120,15 @@ pub async fn load(
},
Err(error) => return Err(error.into()),
};
let mut accounts = Accounts {
Ok(Accounts {
credentials,
grants,
sessions: BTreeMap::new(),
attempts: VecDeque::new(),
changes: VecDeque::new(),
};
if accounts.credentials.is_empty() {
let name = std::env::var("ADMIN_USERNAME").unwrap_or_else(|_| "admin".into());
username(&name)?;
let hash =
std::env::var("ADMIN_PASSWORD_HASH").context("首次启动必须设置 ADMIN_PASSWORD_HASH")?;
let parsed = PasswordHash::new(&hash).map_err(|_| anyhow::anyhow!("管理员哈希格式无效"))?;
ensure!(
parsed.algorithm.as_str() == "argon2id",
"管理员哈希必须为 Argon2id"
);
accounts
.credentials
.insert(name.clone(), Credential { hash });
accounts.grants.publishers.insert(
name,
Grant {
enabled: true,
role: Role::Admin,
prefixes: vec![],
ssh_key: None,
},
);
atomic(config, &serde_json::to_vec_pretty(&accounts.grants)?, 0o644).await?;
atomic(
&data.join("users.json"),
&serde_json::to_vec(&accounts.credentials)?,
0o600,
)
.await?;
}
ensure!(
accounts
.grants
.publishers
.iter()
.any(|(name, g)| g.role == Role::Admin
&& g.enabled
&& accounts.credentials.contains_key(name)),
"至少需要一个启用的管理员"
);
Ok(accounts)
})
}
fn rate(accounts: &mut Accounts) -> ApiResult<()> {
let now = Instant::now();
while accounts
+60
View File
@@ -602,6 +602,66 @@ async fn request(
)
}
#[tokio::test]
async fn empty_accounts_start_and_first_user_remains_reader_after_reload() {
let dir = tempfile::tempdir().unwrap();
let app = app(dir.path()).await;
tokio::fs::remove_file(dir.path().join("users.json"))
.await
.unwrap();
tokio::fs::remove_file(&app.ssh_config).await.unwrap();
*app.accounts.lock().await =
accounts::load(dir.path(), &app.ssh_config, dir.path().join("ingress.git"))
.await
.unwrap();
assert!(app.accounts.lock().await.grants.publishers.is_empty());
let (status, _, value) = request(
app.clone(),
"POST",
"/api/auth/register",
json!({"username":"first", "password":"First-user-password-123"}),
None,
None,
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(value["role"], "reader");
*app.accounts.lock().await =
accounts::load(dir.path(), &app.ssh_config, dir.path().join("ingress.git"))
.await
.unwrap();
let (status, headers, value) = request(
app.clone(),
"POST",
"/api/auth/login",
json!({"username":"first", "password":"First-user-password-123"}),
None,
None,
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(app.accounts.lock().await.grants.publishers.len(), 1);
assert_eq!(
app.accounts.lock().await.grants.publishers["first"].role,
Role::Reader
);
let cookie = headers[header::SET_COOKIE]
.to_str()
.unwrap()
.split(';')
.next()
.unwrap();
let (status, _, _) = request(
app,
"GET",
"/api/admin/users",
json!(null),
Some(cookie),
value["csrf"].as_str(),
)
.await;
assert_eq!(status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn registration_login_grants_revocation_and_private_content_boundaries() {
let dir = tempfile::tempdir().unwrap();
let app = app(dir.path()).await;