/* * Amsterdam Web Communities System * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ // The database package contains database management and storage logic. package database import ( "context" _ "embed" "fmt" "time" "git.erbosoft.com/amy/amsterdam/config" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) // AuditRefRecord stores the reference data for an audit message. type AuditRefRecord struct { Code int `yaml:"code"` Text string `yaml:"text"` } // AuditReference stores the audit reference data. type AuditReference struct { Ref []AuditRefRecord `yaml:"auditReference"` table map[int]*AuditRefRecord } //go:embed auditref.yaml var initAuditData []byte // auditref is the master audit data reference. var auditRef AuditReference // init loads the audit data. func init() { if err := yaml.Unmarshal(initAuditData, &auditRef); err != nil { panic(err) // can't happen } auditRef.table = make(map[int]*AuditRefRecord) for i := range auditRef.Ref { auditRef.table[auditRef.Ref[i].Code] = &(auditRef.Ref[i]) } } // AmAuditText gets the text of an audit from its code. func AmAuditText(code int) string { rec, ok := auditRef.table[code] if ok { return rec.Text } return fmt.Sprintf("[audit code:%d]", code) } // AuditRecord holds an audit record instance. type AuditRecord struct { Record int64 `db:"record"` // audit record ID OnDate time.Time `db:"on_date"` // timestamp of event Event int32 `db:"event"` // ID of the event Uid int32 `db:"uid"` // user that performed the event CommId int32 `db:"commid"` // community associated with the event IP *string `db:"ip"` // IP address associated with the event Data1 *string `db:"data1"` // first data parameter Data2 *string `db:"data2"` // second data parameter Data3 *string `db:"data3"` // third data parameter Data4 *string `db:"data4"` // fourth data parameter } // These are the audit record types. N.B.: Keep these synchronized with the definitions in database/auditref.yaml // at all times! const ( AuditPublishToFrontPage = 1 AuditStartup = 2 AuditShutdown = 3 AuditLoginOK = 101 AuditLoginFail = 102 AuditAccountCreated = 103 AuditVerifyEmailOK = 104 AuditVerifyEmailFail = 105 AuditSetUserContactInfo = 106 AuditResendEmailConfirm = 107 AuditChangePassword = 108 AuditAdminSetUserContactInfo = 109 AuditAdminChangeUserPassword = 110 AuditAdminChangeUserAccount = 111 AuditAdminSetAccountSecurity = 112 AuditAdminLockUnlockAccount = 113 AuditAdminSetUserName = 114 AuditCommunityCreate = 201 AuditCommunitySetMembership = 202 AuditCommunityContactInfo = 203 AuditCommunityFeatureSet = 204 AuditCommunityName = 205 AuditCommunityAlias = 206 AuditCommunityCategory = 207 AuditCommunityHideInfo = 208 AuditCommunityMembersOnly = 209 AuditCommunityJoinKey = 210 AuditCommunitySecurity = 211 AuditCommunityDelete = 212 AuditConferenceCreate = 301 AuditConferenceSecurity = 302 AuditConferenceName = 303 AuditConferenceAlias = 304 AuditConferenceMembership = 305 AuditConferenceCreateTopic = 306 AuditConferenceDeleteTopic = 307 AuditConferenceFreezeTopic = 308 AuditConferenceArchiveTopic = 309 AuditConferencePostMessage = 310 AuditConferenceHideMessage = 311 AuditConferenceScribbleMessage = 312 AuditConferenceNukeMessage = 313 AuditConferenceUploadAttachment = 314 AuditConferenceDelete = 315 AuditConferenceMoveMessage = 316 AuditConferenceStickyTopic = 317 AuditConferencePruneAttachment = 318 ) // auditWriteQueue is a channel to store audit records in the background. var auditWriteQueue chan *AuditRecord // auditWriter is the routine that stores audit records in trhe background. func auditWriter(workChan chan *AuditRecord, doneChan chan bool) { for ar := range workChan { err := ar.Store(context.Background()) if err != nil { log.Errorf("dropped audit record (%+v) on the floor: %v", *ar, err) } } doneChan <- true } // setupAuditWriter sets up the background audit writer. func setupAuditWriter() func() { auditWriteQueue = make(chan *AuditRecord, config.GlobalConfig.Tuning.Queues.AuditWrites) doneChan := make(chan bool) go auditWriter(auditWriteQueue, doneChan) return func() { close(auditWriteQueue) <-doneChan } } // Store stores the audit record in the database. func (ar *AuditRecord) Store(ctx context.Context) error { if ar.Record > 0 { return fmt.Errorf("audit record %d already stored", ar.Record) } moment := time.Now().UTC() rs, err := amdb.ExecContext(ctx, `INSERT INTO audit (on_date, event, uid, commid, ip, data1, data2, data3, data4) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`, moment, ar.Event, ar.Uid, ar.CommId, ar.IP, ar.Data1, ar.Data2, ar.Data3, ar.Data4) if err != nil { return err } ar.Record, _ = rs.LastInsertId() ar.OnDate = moment return nil } /* AmNewAudit creates a new audit record. * Parameters: * rectype - Audit record type. * uid - User ID of the user. * ip - User's IP address. * data - Argument data values for the audit record. * Returns: * The audit record pointer. */ func AmNewAudit(rectype, uid int32, ip string, data ...string) *AuditRecord { rc := &AuditRecord{Event: rectype, Uid: uid, CommId: 0} if len(ip) > 0 { rc.IP = &ip } if data != nil { l := len(data) if l > 0 { rc.Data1 = &(data[0]) } if l > 1 { rc.Data2 = &(data[1]) } if l > 2 { rc.Data3 = &(data[2]) } if l > 3 { rc.Data4 = &(data[3]) } } return rc } /* AmNewCommAudit creates a new audit record tied to a community. * Parameters: * rectype - Audit record type. * uid - User ID of the user. * commid - Community ID of the community. * ip - User's IP address. * data - Argument data values for the audit record. * Returns: * The audit record pointer. */ func AmNewCommAudit(rectype, uid, commid int32, ip string, data ...string) *AuditRecord { rc := &AuditRecord{Event: rectype, Uid: uid, CommId: commid} if len(ip) > 0 { rc.IP = &ip } if data != nil { l := len(data) if l > 0 { rc.Data1 = &(data[0]) } if l > 1 { rc.Data2 = &(data[1]) } if l > 2 { rc.Data3 = &(data[2]) } if l > 3 { rc.Data4 = &(data[3]) } } return rc } // AmStoreAudit stores the audit record in the background. func AmStoreAudit(rec *AuditRecord) { if rec != nil { auditWriteQueue <- rec } } // AmListAuditRecords lists a section of the audit records. func AmListAuditRecords(ctx context.Context, comm *Community, offset, max int) ([]AuditRecord, int, error) { var err error var count int if comm != nil { err = amdb.GetContext(ctx, &count, "SELECT COUNT(*) FROM audit WHERE commid = ?", comm.Id) } else { err = amdb.GetContext(ctx, &count, "SELECT COUNT(*) FROM audit") } if err != nil { return nil, -1, err } var rc []AuditRecord if comm != nil { if offset > 0 { err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit WHERE commid = ? ORDER BY on_date DESC LIMIT ? OFFSET ?", comm.Id, max, offset) } else { err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit WHERE commid = ? ORDER BY on_date DESC LIMIT ?", comm.Id, max) } } else { if offset > 0 { err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit ORDER BY on_date DESC LIMIT ? OFFSET ?", max, offset) } else { err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit ORDER BY on_date DESC LIMIT ?", max) } } if err != nil { return nil, count, err } return rc, count, nil }