354 lines
11 KiB
Go
354 lines
11 KiB
Go
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 *Access) error {
|
|
if a.CreatedAt == 0 {
|
|
a.CreatedAt = time.Now().Unix()
|
|
}
|
|
return AccessWrite(a)
|
|
}
|
|
|
|
// AccessDelete removes an access record
|
|
func AccessDelete(granteeID, dossierID string) error {
|
|
return AccessRemove(granteeID, dossierID)
|
|
}
|
|
|
|
// AccessModify updates an access record
|
|
func AccessModify(a *Access) error {
|
|
// Lookup access_id if not provided
|
|
if a.AccessID == "" {
|
|
existing, err := AccessGet(a.GranteeID, a.DossierID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.AccessID = existing.AccessID
|
|
}
|
|
return AccessWrite(a)
|
|
}
|
|
|
|
// AccessListByAccessor lists all dossiers a user can access
|
|
func AccessListByAccessor(granteeID string) ([]*Access, error) {
|
|
return AccessList(&AccessFilter{AccessorID: granteeID})
|
|
}
|
|
|
|
// AccessListByTarget lists all users who can access a dossier
|
|
func AccessListByTarget(dossierID string) ([]*Access, error) {
|
|
return AccessList(&AccessFilter{TargetID: dossierID})
|
|
}
|
|
|
|
// AccessUpdateTimestamp updates the accessed_at timestamp
|
|
func AccessUpdateTimestamp(granteeID, dossierID string) error {
|
|
access, err := AccessGet(granteeID, dossierID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Note: Access struct doesn't have AccessedAt field anymore
|
|
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. Requires delete permission.
|
|
func EntryDeleteTree(ctx *AccessContext, dossierID, entryID string) error {
|
|
if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 0, 'd'); err != nil {
|
|
return err
|
|
}
|
|
// Delete children first
|
|
var children []*Entry
|
|
if err := dbQuery("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 := dbDelete("entries", "entry_id", c.EntryID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return dbDelete("entries", "entry_id", entryID)
|
|
}
|
|
|
|
// EntryModify updates an entry (internal operation)
|
|
func EntryModify(e *Entry) error {
|
|
return EntryWrite(nil, e) // nil ctx = internal operation
|
|
}
|
|
|
|
// EntryQueryOld finds entries by dossier and optional category/type (DEPRECATED - use EntryQuery with RBAC)
|
|
// Use category=-1 to skip category filter, typ="" to skip type filter
|
|
func EntryQueryOld(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, dbQuery(q, args, &result)
|
|
}
|
|
|
|
// EntryQueryByDate retrieves entries within a timestamp range
|
|
func EntryQueryByDate(dossierID string, from, to int64) ([]*Entry, error) {
|
|
var result []*Entry
|
|
return result, dbQuery("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, dbQuery("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, dbQuery("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, dbQuery("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 := dbQuery("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, dbQuery("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 := dbQuery("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 := dbQuery("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. Requires delete permission.
|
|
func EntryDeleteByCategory(ctx *AccessContext, dossierID string, category int) error {
|
|
if err := checkAccess(accessorIDFromContext(ctx), dossierID, "", category, 'd'); err != nil {
|
|
return err
|
|
}
|
|
// Query all entries with this category, then delete each
|
|
var entries []*Entry
|
|
if err := dbQuery("SELECT entry_id FROM entries WHERE dossier_id = ? AND category = ?",
|
|
[]any{dossierID, category}, &entries); err != nil {
|
|
return err
|
|
}
|
|
for _, e := range entries {
|
|
if err := dbDelete("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
|
|
}
|
|
|
|
// EntryAddBatchValues inserts multiple entries from a value slice (internal operation)
|
|
func EntryAddBatchValues(entries []Entry) error {
|
|
return dbSave("entries", entries)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|