Changes
7 changed files (+178/-54)
-
-
@@ -19,3 +19,10 @@ CREATE TABLE users_snapshots (-- Protobuf wire format payload BLOB ); CREATE TABLE initial_admin_creation_password_snapshots ( -- Which event is this snapshot taken at? event_seq INTEGER PRIMARY KEY ON CONFLICT ROLLBACK, -- Protobuf wire format payload BLOB );
-
-
-
@@ -10,33 +10,95 @@package initial_admin_creation_password import ( "context" "database/sql" "fmt" "google.golang.org/protobuf/proto" "pocka.jp/x/event_sourcing_user_management_poc/events" "pocka.jp/x/event_sourcing_user_management_poc/gen/event" "pocka.jp/x/event_sourcing_user_management_poc/gen/model" "pocka.jp/x/event_sourcing_user_management_poc/gen/projection" ) type initialAdminCreationPassword struct { Hash []byte Salt []byte func GetProjection(db *sql.DB) (*projection.InitialAdminCreationPassword, int, error) { ctx := context.Background() var p projection.InitialAdminCreationPassword tx, err := db.BeginTx(ctx, nil) if err != nil { return nil, 0, fmt.Errorf("Failed to begin transaction for InitialAdminCreationPasswordProjection: %s", err) } defer tx.Rollback() var eventSeq int var payload []byte err = tx.QueryRow("SELECT event_seq, payload FROM initial_admin_creation_password_snapshots ORDER BY event_seq DESC LIMIT 1").Scan(&eventSeq, &payload) if err == sql.ErrNoRows { p = projection.InitialAdminCreationPassword{} eventSeq = -1 } else if err != nil { return nil, 0, fmt.Errorf("Failed to get latest snapshot: %s", err) } else { if err := proto.Unmarshal(payload, &p); err != nil { return nil, 0, fmt.Errorf("Failed to decode latest snapshot: %s", err) } } stmt, err := tx.Prepare("SELECT seq, event_name, payload FROM user_events WHERE seq > ? ORDER BY seq ASC") if err != nil { return nil, 0, fmt.Errorf("Failed to prepare event fetching query: %s", err) } maxSeq := -1 rows, err := stmt.Query(eventSeq) for rows.Next() { ev, seq, err := events.ScanEvent(rows) if err != nil { return nil, 0, err } maxSeq = max(maxSeq, seq) apply(ev, &p) } return &p, maxSeq, nil } func GetFromUserEvents(events []proto.Message) *initialAdminCreationPassword { var password *initialAdminCreationPassword for _, e := range events { switch v := e.(type) { case *event.InitialAdminCreationPasswordCreated: password = &initialAdminCreationPassword{ Hash: v.PasswordHash, Salt: v.Salt, } case *event.RoleAssigned: if *v.Role == model.Role_ROLE_ADMIN { password = nil } func apply(e proto.Message, p *projection.InitialAdminCreationPassword) { switch v := e.(type) { case *event.InitialAdminCreationPasswordCreated: p.PasswordHash = v.PasswordHash p.Salt = v.Salt case *event.RoleAssigned: if *v.Role == model.Role_ROLE_ADMIN { p.PasswordHash = nil p.Salt = nil } } } func SaveSnapshot(db *sql.DB) error { p, seq, err := GetProjection(db) if err != nil { return err } stmt, err := db.Prepare("INSERT OR ABORT INTO initial_admin_creation_password_snapshots (event_seq, payload) VALUES (?, ?)") if err != nil { return err } payload, err := proto.Marshal(p) if err != nil { return err } _, err = stmt.Exec(seq, payload) return password return err }
-
-
-
@@ -17,31 +17,42 @@ import ("pocka.jp/x/event_sourcing_user_management_poc/gen/event" "pocka.jp/x/event_sourcing_user_management_poc/gen/model" "pocka.jp/x/event_sourcing_user_management_poc/gen/projection" ) func build(events []proto.Message) *projection.InitialAdminCreationPassword { var p projection.InitialAdminCreationPassword for _, e := range events { apply(e, &p) } return &p } func TestReturnsNonNil(t *testing.T) { password := GetFromUserEvents([]proto.Message{ p := build([]proto.Message{ &event.InitialAdminCreationPasswordCreated{ PasswordHash: []byte{0, 1, 2}, Salt: []byte{3, 4, 5}, }, }) if password == nil { if p.PasswordHash == nil { t.Error("Expected found password, got nil") } if !bytes.Equal([]byte{0, 1, 2}, password.Hash) { t.Errorf("Hash does not match to [0,1,2]: %v", password.Hash) if !bytes.Equal([]byte{0, 1, 2}, p.PasswordHash) { t.Errorf("Hash does not match to [0,1,2]: %v", p.PasswordHash) } if !bytes.Equal([]byte{3, 4, 5}, password.Salt) { t.Errorf("Salt does not match to [3,4,5]: %v", password.Salt) if !bytes.Equal([]byte{3, 4, 5}, p.Salt) { t.Errorf("Salt does not match to [3,4,5]: %v", p.Salt) } } func TestAdminCreationExpiresOne(t *testing.T) { password := GetFromUserEvents([]proto.Message{ p := build([]proto.Message{ &event.InitialAdminCreationPasswordCreated{ PasswordHash: []byte{0, 1, 2}, Salt: []byte{3, 4, 5},
-
@@ -52,13 +63,13 @@ func TestAdminCreationExpiresOne(t *testing.T) {}, }) if password != nil { t.Errorf("Expected nil, got %v", password) if p.PasswordHash != nil { t.Errorf("Expected nil, got %v", p) } } func TestNonAdminCreationShouldNotExpiresOne(t *testing.T) { password := GetFromUserEvents([]proto.Message{ p := build([]proto.Message{ &event.InitialAdminCreationPasswordCreated{ PasswordHash: []byte{0, 1, 2}, Salt: []byte{3, 4, 5},
-
@@ -69,7 +80,7 @@ func TestNonAdminCreationShouldNotExpiresOne(t *testing.T) {}, }) if password == nil { if p.PasswordHash == nil { t.Errorf("Expected non-nil, got nil") } }
-
-
-
@@ -0,0 +1,19 @@// Copyright 2025 Shota FUJI // // This source code is licensed under Zero-Clause BSD License. // You can find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt // You may also obtain a copy of the Zero-Clause BSD License at // <https://opensource.org/license/0bsd> // // SPDX-License-Identifier: 0BSD edition = "2023"; package projection; option go_package = "pocka.jp/x/event_sourcing_user_management_poc/gen/projection"; message InitialAdminCreationPassword { bytes password_hash = 1; bytes salt = 2; }
-
-
-
@@ -53,15 +53,16 @@ func Handler(db *sql.DB, logger *log.Logger) (http.Handler, error) {} mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { events, err := events.List(db) initialAdminPass, _, err := initial_admin_creation_password.GetProjection(db) if err != nil { logger.Error(err) http.Error(w, "Server error: event loading failure", http.StatusInternalServerError) logger.Errorf("Error loading initial admin creation password: %s", err) w.Header().Add("Content-Type", "text/html;charset=utf-8") w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, loginHTML) return } initialAdminPass := initial_admin_creation_password.GetFromUserEvents(events) if initialAdminPass != nil { if initialAdminPass.PasswordHash != nil { fmt.Fprint(w, initialAdminCreationHtml) return }
-
@@ -103,15 +104,16 @@ func Handler(db *sql.DB, logger *log.Logger) (http.Handler, error) {return } evs, err := events.List(db) initialAdminPass, _, err := initial_admin_creation_password.GetProjection(db) if err != nil { logger.Error(err) http.Error(w, "Server error: event loading failure", http.StatusInternalServerError) logger.Errorf("Error loading initial admin creation password: %s", err) w.Header().Add("Content-Type", "text/html;charset=utf-8") w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, loginHTML) return } initialAdminPass := initial_admin_creation_password.GetFromUserEvents(evs) if initialAdminPass == nil { if initialAdminPass.PasswordHash == nil { logger.Debug("Found no active initial admin creation password at POST /initial-admin, redirecting") http.Redirect(w, r, "/", http.StatusSeeOther) return
-
@@ -131,7 +133,7 @@ func Handler(db *sql.DB, logger *log.Logger) (http.Handler, error) {} initPwHash := auth.HashPassword(initPassword, initialAdminPass.Salt) if !bytes.Equal(initialAdminPass.Hash, initPwHash) { if !bytes.Equal(initialAdminPass.PasswordHash, initPwHash) { w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, initialAdminCreationHtml) return
-
@@ -164,14 +166,21 @@ func Handler(db *sql.DB, logger *log.Logger) (http.Handler, error) {} go func() { logger.Debug("Creating snapshot (trigger=initial admin creation)") logger.Debug("Creating initial admin creation password snapshot (trigger=initial admin creation)") err := users.SaveSnapshot(db) if err != nil { logger.Errorf("Failed to update users snapshot: %s", err) if err := initial_admin_creation_password.SaveSnapshot(db); err != nil { logger.Warnf("Failed to update initial admin creation password snapshot: %s", err) } else { logger.Debug("Created initial admin creation password snapshot (trigger=initial admin creation)") } logger.Debug("Created snapshot (trigger=initial admin creation)") logger.Debug("Creating snapshot (trigger=initial admin creation)") if err := users.SaveSnapshot(db); err != nil { logger.Warnf("Failed to create user snapshot: %s", err) } else { logger.Debug("Created snapshot (trigger=initial admin creation)") } }() // This project is PoC for event sourcing. UI and security is completely out-of-scope.
-
-
-
@@ -21,6 +21,7 @@ import ("pocka.jp/x/event_sourcing_user_management_poc/events" "pocka.jp/x/event_sourcing_user_management_poc/gen/event" "pocka.jp/x/event_sourcing_user_management_poc/gen/model" "pocka.jp/x/event_sourcing_user_management_poc/projections/initial_admin_creation_password" "pocka.jp/x/event_sourcing_user_management_poc/projections/users" )
-
@@ -52,14 +53,21 @@ func CreateAlice(db *sql.DB, logger *log.Logger) (string, error) {} go func() { logger.Debug("Creating initial admin creation password snapshot (trigger=create alice)") if err := initial_admin_creation_password.SaveSnapshot(db); err != nil { logger.Warnf("Failed to update initial admin creation password snapshot: %s", err) } else { logger.Debug("Created initial admin creation password snapshot (trigger=create alice)") } logger.Debug("Creating snapshot (trigger=create alice)") err := users.SaveSnapshot(db) if err != nil { if err := users.SaveSnapshot(db); err != nil { logger.Warnf("Failed to create user snapshot: %s", err) } else { logger.Debug("Created snapshot (trigger=create alice)") } logger.Debug("Created snapshot (trigger=create alice)") }() return id, nil
-
-
-
@@ -21,6 +21,7 @@ import ("pocka.jp/x/event_sourcing_user_management_poc/events" "pocka.jp/x/event_sourcing_user_management_poc/gen/event" "pocka.jp/x/event_sourcing_user_management_poc/gen/model" "pocka.jp/x/event_sourcing_user_management_poc/projections/initial_admin_creation_password" "pocka.jp/x/event_sourcing_user_management_poc/projections/users" )
-
@@ -52,14 +53,21 @@ func CreateBob(db *sql.DB, logger *log.Logger) (string, error) {} go func() { logger.Debug("Creating snapshot (trigger=create bob)") logger.Debug("Creating initial admin creation password snapshot (trigger=create alice)") err := users.SaveSnapshot(db) if err != nil { logger.Warnf("Failed to create user snapshot: %s", err) if err := initial_admin_creation_password.SaveSnapshot(db); err != nil { logger.Warnf("Failed to update initial admin creation password snapshot: %s", err) } else { logger.Debug("Created initial admin creation password snapshot (trigger=create alice)") } logger.Debug("Created snapshot (trigger=create bob)") logger.Debug("Creating snapshot (trigger=create alice)") if err := users.SaveSnapshot(db); err != nil { logger.Warnf("Failed to create user snapshot: %s", err) } else { logger.Debug("Created snapshot (trigger=create alice)") } }() return id, nil
-