package lib import ( "database/sql" "fmt" "time" ) // ============================================================================ // DOSSIER HELPERS (auth code management) // ============================================================================ // DossierSetAuthCode updates auth code and expiry (internal/auth operation) func DossierSetAuthCode(dossierID string, code int, expiresAt int64) error { d, err := DossierGet(nil, dossierID) // nil ctx = internal operation if err != nil { return err } d.AuthCode = code d.AuthCodeExpiresAt = expiresAt return DossierWrite(nil, d) } // DossierClearAuthCode clears auth code and sets last login (internal/auth operation) func DossierClearAuthCode(dossierID string) error { d, err := DossierGet(nil, dossierID) // nil ctx = internal operation if err != nil { return err } d.AuthCode = 0 d.AuthCodeExpiresAt = 0 d.LastLogin = time.Now().Unix() return DossierWrite(nil, d) } // ============================================================================ // DOSSIER ACCESS // ============================================================================ // AccessAdd inserts a new access record func AccessAdd(a *DossierAccess) error { if a.CreatedAt == 0 { a.CreatedAt = time.Now().Unix() } return AccessWrite(a) } // AccessDelete removes an access record func AccessDelete(accessorID, targetID string) error { return AccessRemove(accessorID, targetID) } // AccessModify updates an access record func AccessModify(a *DossierAccess) error { // Lookup access_id if not provided if a.AccessID == "" { existing, err := AccessGet(a.AccessorDossierID, a.TargetDossierID) if err != nil { return err } a.AccessID = existing.AccessID } return AccessWrite(a) } // AccessListByAccessor lists all dossiers a user can access func AccessListByAccessor(accessorID string) ([]*DossierAccess, error) { return AccessList(&AccessFilter{AccessorID: accessorID}) } // AccessListByTarget lists all users who can access a dossier func AccessListByTarget(targetID string) ([]*DossierAccess, error) { return AccessList(&AccessFilter{TargetID: targetID}) } // AccessUpdateTimestamp updates the accessed_at timestamp func AccessUpdateTimestamp(accessorID, targetID string) error { access, err := AccessGet(accessorID, targetID) if err != nil { return err } access.AccessedAt = time.Now().Unix() return AccessWrite(access) } // ============================================================================ // ENTRY // ============================================================================ // EntryAdd inserts a new entry. Generates EntryID if empty. (internal operation) func EntryAdd(e *Entry) error { return EntryWrite(nil, e) // nil ctx = internal operation } // EntryDelete removes a single entry (internal operation) func EntryDelete(entryID string) error { return EntryRemove(nil, entryID) // nil ctx = internal operation } // EntryDeleteTree removes an entry and all its children func EntryDeleteTree(dossierID, entryID string) error { // Delete children first var children []*Entry if err := Query("SELECT entry_id FROM entries WHERE dossier_id = ? AND parent_id = ?", []any{dossierID, entryID}, &children); err != nil { return err } for _, c := range children { if err := Delete("entries", "entry_id", c.EntryID); err != nil { return err } } return Delete("entries", "entry_id", entryID) } // EntryModify updates an entry (internal operation) func EntryModify(e *Entry) error { return EntryWrite(nil, e) // nil ctx = internal operation } // EntryQuery finds entries by dossier and optional category/type // Use category=-1 to skip category filter, typ="" to skip type filter func EntryQuery(dossierID string, category int, typ string) ([]*Entry, error) { q := "SELECT * FROM entries WHERE dossier_id = ?" args := []any{dossierID} if category >= 0 { q += " AND category = ?" args = append(args, category) } if typ != "" { q += " AND type = ?" args = append(args, CryptoEncrypt(typ)) } q += " ORDER BY timestamp DESC" var result []*Entry return result, Query(q, args, &result) } // EntryQueryByDate retrieves entries within a timestamp range func EntryQueryByDate(dossierID string, from, to int64) ([]*Entry, error) { var result []*Entry return result, Query("SELECT * FROM entries WHERE dossier_id = ? AND timestamp >= ? AND timestamp < ? ORDER BY timestamp DESC", []any{dossierID, from, to}, &result) } // EntryChildren retrieves child entries ordered by ordinal func EntryChildren(dossierID, parentID string) ([]*Entry, error) { var result []*Entry return result, Query("SELECT * FROM entries WHERE dossier_id = ? AND parent_id = ? ORDER BY ordinal", []any{dossierID, parentID}, &result) } // EntryChildrenByCategory retrieves child entries filtered by category, ordered by ordinal func EntryChildrenByCategory(dossierID, parentID string, category int) ([]*Entry, error) { var result []*Entry return result, Query("SELECT * FROM entries WHERE dossier_id = ? AND parent_id = ? AND category = ? ORDER BY ordinal", []any{dossierID, parentID, category}, &result) } // EntryChildrenByType retrieves child entries filtered by type string, ordered by ordinal func EntryChildrenByType(dossierID, parentID string, typ string) ([]*Entry, error) { var result []*Entry return result, Query("SELECT * FROM entries WHERE dossier_id = ? AND parent_id = ? AND type = ? ORDER BY ordinal", []any{dossierID, parentID, CryptoEncrypt(typ)}, &result) } // EntryRootByType finds the root entry (parent_id = 0 or NULL) for a given type func EntryRootByType(dossierID string, typ string) (*Entry, error) { var result []*Entry err := Query("SELECT * FROM entries WHERE dossier_id = ? AND type = ? AND (parent_id IS NULL OR parent_id = '' OR parent_id = '0') LIMIT 1", []any{dossierID, CryptoEncrypt(typ)}, &result) if err != nil { return nil, err } if len(result) == 0 { return nil, fmt.Errorf("not found") } return result[0], nil } // EntryRootsByType finds all root entries (parent_id = '' or NULL) for a given type func EntryRootsByType(dossierID string, typ string) ([]*Entry, error) { var result []*Entry return result, Query("SELECT * FROM entries WHERE dossier_id = ? AND type = ? AND (parent_id IS NULL OR parent_id = '' OR parent_id = '0') ORDER BY timestamp DESC", []any{dossierID, CryptoEncrypt(typ)}, &result) } // EntryRootByCategory finds the root entry (parent_id IS NULL) for a category func EntryRootByCategory(dossierID string, category int) (*Entry, error) { var result []*Entry err := Query("SELECT * FROM entries WHERE dossier_id = ? AND category = ? AND (parent_id IS NULL OR parent_id = '') LIMIT 1", []any{dossierID, category}, &result) if err != nil { return nil, err } if len(result) == 0 { return nil, fmt.Errorf("not found") } return result[0], nil } // EntryTypes returns distinct types for a dossier+category func EntryTypes(dossierID string, category int) ([]string, error) { var entries []*Entry if err := Query("SELECT DISTINCT type FROM entries WHERE dossier_id = ? AND category = ?", []any{dossierID, category}, &entries); err != nil { return nil, err } var types []string for _, e := range entries { if e.Type != "" { types = append(types, e.Type) } } return types, nil } // ============================================================================ // AUDIT // ============================================================================ // AuditAdd inserts an audit entry. Generates AuditID if empty. func AuditAdd(a *AuditEntry) error { if a.Timestamp == 0 { a.Timestamp = time.Now().Unix() } return AuditWrite(a) } // AuditLog is a convenience function for quick audit logging func AuditLog(actor1ID string, action string, targetID string, details string) { AuditAdd(&AuditEntry{ Actor1ID: actor1ID, Action: action, TargetID: targetID, Details: details, }) } // AuditLogFull logs an audit entry with all fields func AuditLogFull(actor1ID, actor2ID, targetID string, action, details string, relationID int) { AuditAdd(&AuditEntry{ Actor1ID: actor1ID, Actor2ID: actor2ID, TargetID: targetID, Action: action, Details: details, RelationID: relationID, }) } // AuditQueryByDossier retrieves audit entries by actor func AuditQueryByActor(actor1ID string, from, to int64) ([]*AuditEntry, error) { return AuditList(&AuditFilter{ActorID: actor1ID, FromDate: from, ToDate: to}) } // AuditQueryByTarget retrieves audit entries by target dossier func AuditQueryByTarget(targetID string, from, to int64) ([]*AuditEntry, error) { return AuditList(&AuditFilter{TargetID: targetID, FromDate: from, ToDate: to}) } // ============================================================================ // HELPERS // ============================================================================ func nilIfZero(v int64) any { if v == 0 { return nil } return v } func nilIfEmpty(s string) any { if s == "" || s == "0000000000000000" { return nil } return s } func nullStr(ns sql.NullString) string { if ns.Valid { return ns.String } return "" } func nullInt(ni sql.NullInt64) int64 { if ni.Valid { return ni.Int64 } return 0 } func nullIntToHex(ni sql.NullInt64) string { if ni.Valid { return fmt.Sprintf("%016x", ni.Int64) } return "" } func boolToInt(b bool) int { if b { return 1 } return 0 } // EntryDeleteByCategory removes all entries with a given category for a dossier func EntryDeleteByCategory(dossierID string, category int) error { // Query all entries with this category, then delete each var entries []*Entry if err := Query("SELECT entry_id FROM entries WHERE dossier_id = ? AND category = ?", []any{dossierID, category}, &entries); err != nil { return err } for _, e := range entries { if err := Delete("entries", "entry_id", e.EntryID); err != nil { return err } } return nil } // OpenReadOnly opens a SQLite database in read-only mode func OpenReadOnly(path string) (*sql.DB, error) { return sql.Open("sqlite3", path+"?mode=ro") } // EntryAddBatch inserts multiple entries (internal operation) func EntryAddBatch(entries []*Entry) error { return EntryWrite(nil, entries...) // nil ctx = internal operation } // DossierSetSessionToken sets the mobile session token (internal/auth operation) func DossierSetSessionToken(dossierID string, token string) error { d, err := DossierGet(nil, dossierID) // nil ctx = internal operation if err != nil { return err } d.SessionToken = token return DossierWrite(nil, d) }