UI: password change enforcement + migration recovery
- Web UI admin password reset now enforces admin role server-side (was cookie-auth + CSRF only; any logged-in user could previously reset any account's password) - Added self-service password change UI at GET/PUT /profile: current_password + new_password + confirm_password; server-side equality check; lockout + Argon2id verification; revokes all other sessions on success - password_change_form.html fragment and profile.html page - Nav bar actor name now links to /profile - policy: ActionChangePassword + default rule -7 allowing human accounts to change their own password - openapi.yaml: built-in rules count updated to -7 Migration recovery: - mciasdb schema force --version N: new subcommand to clear dirty migration state without running SQL (break-glass) - schema subcommands bypass auto-migration on open so the tool stays usable when the database is dirty - Migrate(): shim no longer overrides schema_migrations when it already has an entry; duplicate-column error on the latest migration is force-cleaned and treated as success (handles columns added outside the runner) Security: - Admin role is now validated in handleAdminResetPassword before any DB access; non-admin receives 403 - handleSelfChangePassword follows identical lockout + constant-time Argon2id path as the REST self-service handler; current password required to prevent token-theft account takeover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
||||
@@ -8,13 +9,15 @@ import (
|
||||
|
||||
func (t *tool) runSchema(args []string) {
|
||||
if len(args) == 0 {
|
||||
fatalf("schema requires a subcommand: verify, migrate")
|
||||
fatalf("schema requires a subcommand: verify, migrate, force")
|
||||
}
|
||||
switch args[0] {
|
||||
case "verify":
|
||||
t.schemaVerify()
|
||||
case "migrate":
|
||||
t.schemaMigrate()
|
||||
case "force":
|
||||
t.schemaForce(args[1:])
|
||||
default:
|
||||
fatalf("unknown schema subcommand %q", args[0])
|
||||
}
|
||||
@@ -39,6 +42,26 @@ func (t *tool) schemaVerify() {
|
||||
fmt.Println("schema is up-to-date")
|
||||
}
|
||||
|
||||
// schemaForce marks the database as being at a specific migration version
|
||||
// without running any SQL. Use this to clear a dirty migration state after
|
||||
// you have verified that the schema already reflects the target version.
|
||||
//
|
||||
// Example: mciasdb schema force --version 6
|
||||
func (t *tool) schemaForce(args []string) {
|
||||
fs := flag.NewFlagSet("schema force", flag.ExitOnError)
|
||||
version := fs.Int("version", 0, "schema version to force (required)")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
if *version <= 0 {
|
||||
fatalf("--version must be a positive integer")
|
||||
}
|
||||
|
||||
if err := db.ForceSchemaVersion(t.db, *version); err != nil {
|
||||
fatalf("force schema version: %v", err)
|
||||
}
|
||||
fmt.Printf("schema version forced to %d; run 'schema migrate' to apply any remaining migrations\n", *version)
|
||||
}
|
||||
|
||||
// schemaMigrate applies any pending migrations and reports each one.
|
||||
func (t *tool) schemaMigrate() {
|
||||
before, err := db.SchemaVersion(t.db)
|
||||
|
||||
Reference in New Issue
Block a user