273 lines
7.9 KiB
Go
273 lines
7.9 KiB
Go
/*
|
|
* 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
|
|
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
|
|
}
|