feat: add Docker first-start admin bootstrap
This commit is contained in:
1 parent
25577b4b83
commit
8e0bcbe580
9 files changed
+152
-6
No files matched your search
+3
-3
@@ -8,10 +8,10 @@ COOKIE_SECURE=false
|
|||||||
SESSION_SECRET=bzod-default-session-secret-change-me-in-production-please-do-it
|
SESSION_SECRET=bzod-default-session-secret-change-me-in-production-please-do-it
|
||||||
|
|
||||||
# Bootstrap Admin Credentials
|
# Bootstrap Admin Credentials
|
||||||
# Default username: admin
|
|
||||||
# Default password: admin
|
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD_SHA256=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
|
# REQUIRED for a fresh deployment.
|
||||||
|
# Use a strong unique password.
|
||||||
|
ADMIN_PASSWORD=
|
||||||
|
|
||||||
# Cron & Cleaner Intervals (in minutes)
|
# Cron & Cleaner Intervals (in minutes)
|
||||||
LINK_CHECK_INTERVAL_MINS=60
|
LINK_CHECK_INTERVAL_MINS=60
|
||||||
|
|||||||
+4
-2
@@ -51,6 +51,7 @@ RUN groupadd --gid 1000 bzod && \
|
|||||||
|
|
||||||
# Application binary
|
# Application binary
|
||||||
COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod
|
COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
# Application-owned immutable assets
|
# Application-owned immutable assets
|
||||||
COPY --from=builder /app/templates /app/templates
|
COPY --from=builder /app/templates /app/templates
|
||||||
@@ -68,7 +69,8 @@ RUN mkdir -p \
|
|||||||
/app/images \
|
/app/images \
|
||||||
/app/templates \
|
/app/templates \
|
||||||
/app/www \
|
/app/www \
|
||||||
/usr/local/bin/bzod
|
/usr/local/bin/bzod \
|
||||||
|
/usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
# Runtime configuration
|
# Runtime configuration
|
||||||
ENV DATA_DIR=/app/data \
|
ENV DATA_DIR=/app/data \
|
||||||
@@ -89,5 +91,5 @@ HEALTHCHECK \
|
|||||||
|
|
||||||
USER bzod
|
USER bzod
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/bzod"]
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
CMD ["serve"]
|
CMD ["serve"]
|
||||||
@@ -14,6 +14,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- COOKIE_SECURE=false
|
- COOKIE_SECURE=false
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
|
- ADMIN_USERNAME=${ADMIN_USERNAME}
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=8654
|
- PORT=8654
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
|
|||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = 'serve' ]; then
|
||||||
|
/usr/local/bin/bzod init-admin
|
||||||
|
exec /usr/local/bin/bzod serve
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /usr/local/bin/bzod "$@"
|
||||||
+15
-1
@@ -45,7 +45,21 @@ cd nx9-url-shortener
|
|||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create Administrator
|
## Automated Administrator Bootstrap (First Start Only)
|
||||||
|
|
||||||
|
For fresh deployments, you can supply administrator credentials via environment variables so the container initializes the admin automatically:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
ADMIN_USERNAME: "admin"
|
||||||
|
ADMIN_PASSWORD: "<your-secure-password>"
|
||||||
|
```
|
||||||
|
|
||||||
|
These credentials are used **only** when no administrator exists. If an administrator is already present, this step is safely skipped and existing accounts are preserved.
|
||||||
|
|
||||||
|
## Manual Administrator Creation
|
||||||
|
|
||||||
|
Alternatively, if you prefer not to use environment variables, you can create the admin manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it bzod bzod create-admin
|
docker exec -it bzod bzod create-admin
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use crate::auth::hash_password;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::db::Db;
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
pub async fn run(
|
||||||
|
data_dir: Option<String>,
|
||||||
|
mut config: Config,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if let Some(d) = data_dir {
|
||||||
|
config.data_dir = PathBuf::from(d);
|
||||||
|
}
|
||||||
|
let db = Db::init(&config)?;
|
||||||
|
let conn = db.users.lock().unwrap();
|
||||||
|
|
||||||
|
let admin_count: i64 = conn.query_row(
|
||||||
|
"SELECT COUNT(*) FROM users WHERE account_type = 'admin';",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if admin_count > 0 {
|
||||||
|
info!("Administrator already exists; initialization skipped.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = match env::var("ADMIN_USERNAME") {
|
||||||
|
Ok(u) if !u.trim().is_empty() => u.trim().to_string(),
|
||||||
|
_ => {
|
||||||
|
let msg = "No administrator exists.\nADMIN_USERNAME and ADMIN_PASSWORD are required for first-time initialization.";
|
||||||
|
error!("{}", msg);
|
||||||
|
return Err(msg.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = match env::var("ADMIN_PASSWORD") {
|
||||||
|
Ok(p) if !p.trim().is_empty() => p.trim().to_string(),
|
||||||
|
_ => {
|
||||||
|
let msg = "No administrator exists.\nADMIN_USERNAME and ADMIN_PASSWORD are required for first-time initialization.";
|
||||||
|
error!("{}", msg);
|
||||||
|
return Err(msg.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hash = hash_password(&password).map_err(|e| e.to_string())?;
|
||||||
|
let u = crate::db::users::create_admin_user(&conn, &username, &hash)?;
|
||||||
|
db.init_user_databases(u.id)?;
|
||||||
|
info!("Administrator initialized successfully.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ pub mod disable_user;
|
|||||||
pub mod doctor;
|
pub mod doctor;
|
||||||
pub mod enable_user;
|
pub mod enable_user;
|
||||||
pub mod expand;
|
pub mod expand;
|
||||||
|
pub mod init_admin;
|
||||||
pub mod list_users;
|
pub mod list_users;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
pub mod repair;
|
pub mod repair;
|
||||||
@@ -86,6 +87,11 @@ pub enum Commands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
data_dir: Option<String>,
|
data_dir: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// Initialize the first administrator for automated/container deployments
|
||||||
|
InitAdmin {
|
||||||
|
#[arg(long)]
|
||||||
|
data_dir: Option<String>,
|
||||||
|
},
|
||||||
/// Run database diagnostics and health checks
|
/// Run database diagnostics and health checks
|
||||||
Doctor {
|
Doctor {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Commands::CreateAdmin { username, data_dir } => {
|
Commands::CreateAdmin { username, data_dir } => {
|
||||||
bzod::cli::create_admin::run(username, data_dir, config).await?;
|
bzod::cli::create_admin::run(username, data_dir, config).await?;
|
||||||
}
|
}
|
||||||
|
Commands::InitAdmin { data_dir } => {
|
||||||
|
bzod::cli::init_admin::run(data_dir, config).await?;
|
||||||
|
}
|
||||||
Commands::Doctor { data_dir } => {
|
Commands::Doctor { data_dir } => {
|
||||||
bzod::cli::doctor::run(data_dir, config).await?;
|
bzod::cli::doctor::run(data_dir, config).await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use bzod::config::Config;
|
||||||
|
use bzod::db::Db;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn create_temp_config(temp_dir: PathBuf) -> Config {
|
||||||
|
let mut config = Config::load();
|
||||||
|
config.data_dir = temp_dir.clone();
|
||||||
|
config.backup_dir = temp_dir.clone();
|
||||||
|
config.base_url = Some("http://bzo.in".to_string());
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_init_admin() {
|
||||||
|
let temp_dir =
|
||||||
|
std::env::temp_dir().join(format!("bzod_test_init_admin_{}", uuid::Uuid::new_v4()));
|
||||||
|
fs::create_dir_all(&temp_dir).unwrap();
|
||||||
|
let config = create_temp_config(temp_dir.clone());
|
||||||
|
|
||||||
|
// Case C/D - missing env vars
|
||||||
|
env::remove_var("ADMIN_USERNAME");
|
||||||
|
env::remove_var("ADMIN_PASSWORD");
|
||||||
|
let res = bzod::cli::init_admin::run(None, config.clone()).await;
|
||||||
|
assert!(res.is_err(), "Should fail without env vars");
|
||||||
|
|
||||||
|
// Case A - No admin + valid ENV
|
||||||
|
env::set_var("ADMIN_USERNAME", "admin");
|
||||||
|
env::set_var("ADMIN_PASSWORD", "securepass");
|
||||||
|
let res = bzod::cli::init_admin::run(None, config.clone()).await;
|
||||||
|
assert!(res.is_ok(), "Should succeed with valid env vars");
|
||||||
|
|
||||||
|
let db = Db::init(&config).unwrap();
|
||||||
|
{
|
||||||
|
let conn = db.users.lock().unwrap();
|
||||||
|
let user = bzod::db::users::get_user_by_username(&conn, "admin")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(user.account_type, "admin");
|
||||||
|
assert!(bzod::auth::verify_password(
|
||||||
|
"securepass",
|
||||||
|
&user.password_hash
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case B/F - Admin already exists
|
||||||
|
env::remove_var("ADMIN_USERNAME");
|
||||||
|
env::remove_var("ADMIN_PASSWORD");
|
||||||
|
let res = bzod::cli::init_admin::run(None, config.clone()).await;
|
||||||
|
assert!(
|
||||||
|
res.is_ok(),
|
||||||
|
"Should skip and succeed if admin exists even without env vars"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user