mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
ae3b792e16
- add util.Go with panic recovery for background goroutines - add util.EscapeLike and explicit ESCAPE clause for SQL LIKE queries - add DummyCheckPassword and subtle.ConstantTimeCompare against timing attacks - enforce session ID rotation upon login/oauth callback to prevent session fixation - add sliding window login failure rate limiting and oauth state rate limiting - fix redis client capture race in pubsub listeners and wait on stop channel - adjust global --primary to oklch(51.1% 0.262 276.966) for WCAG AA contrast - fix semantic heading levels and missing aria-labels across UI components - document security, concurrency, and a11y standards in AGENTS.md
122 lines
3.5 KiB
Go
122 lines
3.5 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package analytics provides ClickHouse data access for analytics tables.
|
|
package analytics
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
|
analyticsmodel "github.com/Rain-kl/Wavelet/internal/model/analytics"
|
|
"github.com/Rain-kl/Wavelet/pkg/util"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// CountAccessLogs returns the number of access logs matching filter.
|
|
func CountAccessLogs(ctx context.Context, filter AccessLogFilter) (uint64, error) {
|
|
ch := db.ChDB(ctx)
|
|
if ch == nil {
|
|
return 0, fmt.Errorf("clickhouse gorm connection is not initialized")
|
|
}
|
|
|
|
var count int64
|
|
query := applyFilter(ch.Model(&analyticsmodel.UserAccessLog{}), filter)
|
|
if err := query.Count(&count).Error; err != nil {
|
|
return 0, fmt.Errorf("count access logs: %w", err)
|
|
}
|
|
return safeUint64Count(count), nil
|
|
}
|
|
|
|
// ListAccessLogs returns paginated access logs and the total match count.
|
|
func ListAccessLogs(ctx context.Context, filter AccessLogFilter, page, pageSize int) ([]analyticsmodel.UserAccessLog, uint64, error) {
|
|
ch := db.ChDB(ctx)
|
|
if ch == nil {
|
|
return nil, 0, fmt.Errorf("clickhouse gorm connection is not initialized")
|
|
}
|
|
|
|
if filter.UserIDs != nil && len(filter.UserIDs) == 0 {
|
|
return []analyticsmodel.UserAccessLog{}, 0, nil
|
|
}
|
|
|
|
var total int64
|
|
baseQuery := applyFilter(ch.Model(&analyticsmodel.UserAccessLog{}), filter)
|
|
if err := baseQuery.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("count access logs: %w", err)
|
|
}
|
|
if total == 0 {
|
|
return []analyticsmodel.UserAccessLog{}, 0, nil
|
|
}
|
|
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
|
|
var logs []analyticsmodel.UserAccessLog
|
|
err := applyFilter(ch.Model(&analyticsmodel.UserAccessLog{}), filter).
|
|
Order("created_at DESC, id DESC").
|
|
Limit(pageSize).
|
|
Offset(offset).
|
|
Find(&logs).Error
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list access logs: %w", err)
|
|
}
|
|
|
|
return logs, safeUint64Count(total), nil
|
|
}
|
|
|
|
// DeleteAllUserAccessLogs hard-deletes all user access logs via TRUNCATE.
|
|
func DeleteAllUserAccessLogs(ctx context.Context) (int64, error) {
|
|
if db.ChConn == nil {
|
|
return 0, fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
if err := db.ChConn.Exec(ctx, "TRUNCATE TABLE "+analyticsmodel.UserAccessLog{}.TableName()); err != nil {
|
|
return 0, fmt.Errorf("truncate user access logs: %w", err)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
// DeleteUserAccessLogsBefore deletes user access logs older than cutoff.
|
|
func DeleteUserAccessLogsBefore(ctx context.Context, cutoff time.Time) (int64, error) {
|
|
if db.ChConn == nil {
|
|
return 0, fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
if err := db.ChConn.Exec(ctx, "ALTER TABLE "+analyticsmodel.UserAccessLog{}.TableName()+" DELETE WHERE created_at < ?", cutoff); err != nil {
|
|
return 0, fmt.Errorf("delete expired user access logs: %w", err)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func safeUint64Count(count int64) uint64 {
|
|
if count < 0 {
|
|
return 0
|
|
}
|
|
return uint64(count)
|
|
}
|
|
|
|
func applyFilter(query *gorm.DB, filter AccessLogFilter) *gorm.DB {
|
|
if filter.UserIDs != nil {
|
|
if len(filter.UserIDs) == 0 {
|
|
return query.Where("1 = 0")
|
|
}
|
|
query = query.Where("user_id IN ?", filter.UserIDs)
|
|
}
|
|
if filter.Path != "" {
|
|
query = query.Where("path LIKE ?", "%"+util.EscapeLike(filter.Path)+"%")
|
|
}
|
|
if filter.StartTime != nil {
|
|
query = query.Where("created_at >= ?", *filter.StartTime)
|
|
}
|
|
if filter.EndTime != nil {
|
|
query = query.Where("created_at <= ?", *filter.EndTime)
|
|
}
|
|
return query
|
|
}
|