diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index 0274ac6..1bf97e9 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -29,6 +29,61 @@ jobs: - name: Replace D1_DATABASE_ID_DEV in wrangler.toml run: sed -i "s/\${D1_DATABASE_ID_DEV}/${{ secrets.D1_DATABASE_ID_DEV }}/g" wrangler.toml + - name: Apply D1 database migrations (dev) + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + set -o pipefail # Ensure pipe returns first non-zero exit code + + # Apply migrations with retry loop to handle legacy ensure_schema columns + MAX_RETRIES=10 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + echo "🔄 Attempting to apply migrations (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..." + + if npx wrangler d1 migrations apply vault1 --env dev --remote 2>&1 | tee migration_output.txt; then + echo "✅ All migrations applied successfully" + exit 0 + fi + + # Check if failure is due to duplicate column (from legacy ensure_schema) + if grep -q "duplicate column name" migration_output.txt; then + # Extract the migration name that failed + # Format: "Migration 0001_add_password_salt.sql failed with the following errors:" + FAILED_MIGRATION=$(grep -oP "Migration \K[0-9]+_[a-zA-Z0-9_]+\.sql(?= failed)" migration_output.txt) + if [ -z "$FAILED_MIGRATION" ]; then + echo "❌ Detected duplicate column error, but could not identify the migration filename from output." + cat migration_output.txt + exit 1 + fi + + echo "⚠️ Migration '$FAILED_MIGRATION' failed: column already exists" + echo "📝 Marking migration as applied..." + + # Initialize d1_migrations table and mark this migration as applied + npx wrangler d1 execute vault1 --env dev --remote --command " + CREATE TABLE IF NOT EXISTS d1_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$FAILED_MIGRATION'); + " + + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "🔁 Retrying remaining migrations..." + else + echo "❌ Migration failed with unexpected error:" + cat migration_output.txt + exit 1 + fi + done + + echo "❌ Max retries ($MAX_RETRIES) exceeded" + exit 1 + - uses: cloudflare/wrangler-action@v3 id: cf env: diff --git a/.github/workflows/push-cloudflare.yaml b/.github/workflows/push-cloudflare.yaml index a0778b2..e2dcbeb 100644 --- a/.github/workflows/push-cloudflare.yaml +++ b/.github/workflows/push-cloudflare.yaml @@ -42,6 +42,61 @@ jobs: - name: Replace D1_DATABASE_ID in wrangler.toml run: sed -i "s/\${D1_DATABASE_ID}/${{ secrets.D1_DATABASE_ID }}/g" wrangler.toml + - name: Apply D1 database migrations + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + set -o pipefail # Ensure pipe returns first non-zero exit code + + # Apply migrations with retry loop to handle legacy ensure_schema columns + MAX_RETRIES=10 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + echo "🔄 Attempting to apply migrations (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..." + + if npx wrangler d1 migrations apply vault1 --remote 2>&1 | tee migration_output.txt; then + echo "✅ All migrations applied successfully" + exit 0 + fi + + # Check if failure is due to duplicate column (from legacy ensure_schema) + if grep -q "duplicate column name" migration_output.txt; then + # Extract the migration name that failed + # Format: "Migration 0001_add_password_salt.sql failed with the following errors:" + FAILED_MIGRATION=$(grep -oP "Migration \K[0-9]+_[a-zA-Z0-9_]+\.sql(?= failed)" migration_output.txt) + if [ -z "$FAILED_MIGRATION" ]; then + echo "❌ Detected duplicate column error, but could not identify the migration filename from output." + cat migration_output.txt + exit 1 + fi + + echo "⚠️ Migration '$FAILED_MIGRATION' failed: column already exists" + echo "📝 Marking migration as applied..." + + # Initialize d1_migrations table and mark this migration as applied + npx wrangler d1 execute vault1 --remote --command " + CREATE TABLE IF NOT EXISTS d1_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$FAILED_MIGRATION'); + " + + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "🔁 Retrying remaining migrations..." + else + echo "❌ Migration failed with unexpected error:" + cat migration_output.txt + exit 1 + fi + done + + echo "❌ Max retries ($MAX_RETRIES) exceeded" + exit 1 + - uses: cloudflare/wrangler-action@v3 id: cf env: diff --git a/migrations/0001_add_password_salt.sql b/migrations/0001_add_password_salt.sql new file mode 100644 index 0000000..cb7e256 --- /dev/null +++ b/migrations/0001_add_password_salt.sql @@ -0,0 +1,9 @@ +-- Migration: Add password_salt column to users table +-- This column stores the salt for server-side PBKDF2 hashing +-- NULL for legacy users pending migration +-- +-- Note: This migration is applied via GitHub Actions which handles +-- the "duplicate column" error gracefully for existing databases. + +ALTER TABLE users ADD COLUMN password_salt TEXT; + diff --git a/src/db.rs b/src/db.rs index 7d069c5..7416df1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -5,29 +5,3 @@ use worker::{D1Database, Env}; pub fn get_db(env: &Arc) -> Result { env.d1("vault1").map_err(AppError::Worker) } - -/// Ensures the password_salt column exists in the users table. -/// This provides seamless migration for existing databases. -/// Ignores "duplicate column name" errors if the column already exists. -pub async fn ensure_schema(env: &Env) { - let db = match env.d1("vault1") { - Ok(db) => db, - Err(e) => { - log::error!("Failed to get database: {:?}", e); - return; - } - }; - - // Try to add the column - if let Err(e) = db - .prepare("ALTER TABLE users ADD COLUMN password_salt TEXT") - .run() - .await - { - let err_msg = format!("{:?}", e); - // Ignore "duplicate column name" error (column already exists) - if !err_msg.to_lowercase().contains("duplicate column name") { - log::error!("Failed to ensure schema: {}", err_msg); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index b464b1a..9db7306 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,6 @@ pub async fn main( uri.authority().map(|a| a.as_str()).unwrap_or("localhost") ); - // Ensure database schema is up to date (adds password_salt column if missing) - db::ensure_schema(&env).await; - // Allow all origins for CORS, which is typical for a public API like Bitwarden's. let cors = CorsLayer::new() .allow_methods(Any) diff --git a/wrangler.toml b/wrangler.toml index e7ac6d3..04b37a6 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,6 +24,7 @@ crons = ["0 3 * * *"] binding = "vault1" database_name = "vault1" database_id = "${D1_DATABASE_ID}" +migrations_dir = "migrations" [env.dev] name = "warden-worker-dev" @@ -36,4 +37,5 @@ crons = ["0 3 * * *"] [[env.dev.d1_databases]] binding = "vault1" database_name = "vault1-dev" -database_id = "${D1_DATABASE_ID_DEV}" \ No newline at end of file +database_id = "${D1_DATABASE_ID_DEV}" +migrations_dir = "migrations" \ No newline at end of file