feat(PROJ-18): E-Mail Integritätsprüfung (SHA-256 Verifikation)
- Storage: VerifyIntegrity, GetAllIDs, GetVerifyStatus + DB-Spalten
- main: Hintergrund-Worker alle 5 Minuten (beim Start sofort: 40/40 OK)
- API: verify_ok + verified_at in GET /api/mails/{id} Antwort
- Frontend: Grüner Haken / graues X / rotes X in Mail-Ansicht
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,9 @@ func main() {
|
||||
// Backfill in background: migrate existing files into DB metadata + re-index
|
||||
go runBackfill(context.Background(), mailStore, idx, worker, logger)
|
||||
|
||||
// Background integrity verification — runs every 5 minutes
|
||||
go runIntegrityCheck(context.Background(), mailStore, logger)
|
||||
|
||||
// Start HTTP API
|
||||
go func() {
|
||||
logger.Info("starting API server", "addr", bind)
|
||||
@@ -276,6 +279,47 @@ func runBackfill(ctx context.Context, store *storage.Store, idx index.Indexer, w
|
||||
logger.Info("backfill: complete", "total", count, "submitted_for_index", needIndex, "errors", errCount)
|
||||
}
|
||||
|
||||
// runIntegrityCheck verifies all stored emails every 5 minutes by re-computing
|
||||
// their SHA-256 and comparing it to the stored file ID.
|
||||
func runIntegrityCheck(ctx context.Context, store *storage.Store, logger *slog.Logger) {
|
||||
// run once at startup, then every 5 minutes
|
||||
doVerify := func() {
|
||||
ids, err := store.GetAllIDs(ctx)
|
||||
if err != nil {
|
||||
logger.Error("integrity check: get IDs failed", "err", err)
|
||||
return
|
||||
}
|
||||
ok := 0
|
||||
fail := 0
|
||||
for _, id := range ids {
|
||||
verified, err := store.VerifyIntegrity(ctx, id)
|
||||
if err != nil {
|
||||
fail++
|
||||
continue
|
||||
}
|
||||
if verified {
|
||||
ok++
|
||||
} else {
|
||||
fail++
|
||||
logger.Warn("integrity check: FAILED", "id", id)
|
||||
}
|
||||
}
|
||||
logger.Info("integrity check: complete", "ok", ok, "failed", fail, "total", len(ids))
|
||||
}
|
||||
|
||||
doVerify()
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
doVerify()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seedDefaultUsers creates default admin and auditor accounts if no users exist yet.
|
||||
func seedDefaultUsers(users *userstore.Store, logger *slog.Logger) error {
|
||||
all, err := users.List("")
|
||||
|
||||
Reference in New Issue
Block a user