package models import ( "context" "database/sql" "fmt" "time" "sneak.berlin/go/upaas/internal/database" ) // AuditAction represents the type of audited user action. type AuditAction string // Audit action constants. const ( AuditActionLogin AuditAction = "login" AuditActionLogout AuditAction = "logout" AuditActionAppCreate AuditAction = "app.create" AuditActionAppUpdate AuditAction = "app.update" AuditActionAppDelete AuditAction = "app.delete" AuditActionAppDeploy AuditAction = "app.deploy" AuditActionAppRollback AuditAction = "app.rollback" AuditActionAppRestart AuditAction = "app.restart" AuditActionAppStop AuditAction = "app.stop" AuditActionAppStart AuditAction = "app.start" AuditActionDeployCancel AuditAction = "deploy.cancel" AuditActionEnvVarSave AuditAction = "env_var.save" AuditActionLabelAdd AuditAction = "label.add" AuditActionLabelEdit AuditAction = "label.edit" AuditActionLabelDelete AuditAction = "label.delete" AuditActionVolumeAdd AuditAction = "volume.add" AuditActionVolumeEdit AuditAction = "volume.edit" AuditActionVolumeDelete AuditAction = "volume.delete" AuditActionPortAdd AuditAction = "port.add" AuditActionPortDelete AuditAction = "port.delete" AuditActionSetup AuditAction = "setup" AuditActionWebhookReceive AuditAction = "webhook.receive" ) // AuditResourceType represents the type of resource being acted on. type AuditResourceType string // Audit resource type constants. const ( AuditResourceApp AuditResourceType = "app" AuditResourceUser AuditResourceType = "user" AuditResourceSession AuditResourceType = "session" AuditResourceEnvVar AuditResourceType = "env_var" AuditResourceLabel AuditResourceType = "label" AuditResourceVolume AuditResourceType = "volume" AuditResourcePort AuditResourceType = "port" AuditResourceDeployment AuditResourceType = "deployment" AuditResourceWebhook AuditResourceType = "webhook" ) // AuditEntry represents a single audit log entry. type AuditEntry struct { db *database.Database ID int64 UserID sql.NullInt64 Username string Action AuditAction ResourceType AuditResourceType ResourceID sql.NullString Detail sql.NullString RemoteIP sql.NullString CreatedAt time.Time } // NewAuditEntry creates a new AuditEntry with a database reference. func NewAuditEntry(db *database.Database) *AuditEntry { return &AuditEntry{db: db} } // Save inserts the audit entry into the database. func (a *AuditEntry) Save(ctx context.Context) error { query := ` INSERT INTO audit_log ( user_id, username, action, resource_type, resource_id, detail, remote_ip ) VALUES (?, ?, ?, ?, ?, ?, ?)` result, err := a.db.Exec(ctx, query, a.UserID, a.Username, a.Action, a.ResourceType, a.ResourceID, a.Detail, a.RemoteIP, ) if err != nil { return fmt.Errorf("inserting audit entry: %w", err) } id, err := result.LastInsertId() if err != nil { return fmt.Errorf("getting audit entry id: %w", err) } a.ID = id return nil } // FindAuditEntries returns recent audit log entries, newest first. func FindAuditEntries( ctx context.Context, db *database.Database, limit int, ) ([]*AuditEntry, error) { query := ` SELECT id, user_id, username, action, resource_type, resource_id, detail, remote_ip, created_at FROM audit_log ORDER BY created_at DESC LIMIT ?` rows, err := db.Query(ctx, query, limit) if err != nil { return nil, fmt.Errorf("querying audit entries: %w", err) } defer func() { _ = rows.Close() }() return scanAuditRows(rows) } // FindAuditEntriesByResource returns audit log entries for a specific resource. func FindAuditEntriesByResource( ctx context.Context, db *database.Database, resourceType AuditResourceType, resourceID string, limit int, ) ([]*AuditEntry, error) { query := ` SELECT id, user_id, username, action, resource_type, resource_id, detail, remote_ip, created_at FROM audit_log WHERE resource_type = ? AND resource_id = ? ORDER BY created_at DESC LIMIT ?` rows, err := db.Query(ctx, query, resourceType, resourceID, limit) if err != nil { return nil, fmt.Errorf("querying audit entries by resource: %w", err) } defer func() { _ = rows.Close() }() return scanAuditRows(rows) } // CountAuditEntries returns the total number of audit log entries. func CountAuditEntries( ctx context.Context, db *database.Database, ) (int, error) { var count int row := db.QueryRow(ctx, "SELECT COUNT(*) FROM audit_log") err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("counting audit entries: %w", err) } return count, nil } func scanAuditRows(rows *sql.Rows) ([]*AuditEntry, error) { var entries []*AuditEntry for rows.Next() { entry := &AuditEntry{} scanErr := rows.Scan( &entry.ID, &entry.UserID, &entry.Username, &entry.Action, &entry.ResourceType, &entry.ResourceID, &entry.Detail, &entry.RemoteIP, &entry.CreatedAt, ) if scanErr != nil { return nil, fmt.Errorf("scanning audit entry: %w", scanErr) } entries = append(entries, entry) } rowsErr := rows.Err() if rowsErr != nil { return nil, fmt.Errorf("iterating audit entries: %w", rowsErr) } return entries, nil }