security measures

This commit is contained in:
Fran Jurmanović
2025-06-25 22:37:38 +02:00
parent 1ecd558e18
commit 69733e4940
16 changed files with 614 additions and 506 deletions

View File

@@ -2,87 +2,90 @@ package repository
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/cache"
"context"
"time"
"gorm.io/gorm"
)
const (
cacheDuration = 1 * time.Hour
tracksCacheKey = "tracks"
carModelsCacheKey = "carModels"
driverCategoriesCacheKey = "driverCategories"
cupCategoriesCacheKey = "cupCategories"
sessionTypesCacheKey = "sessionTypes"
)
type LookupRepository struct {
db *gorm.DB
db *gorm.DB
cache *cache.InMemoryCache
}
func NewLookupRepository(db *gorm.DB) *LookupRepository {
func NewLookupRepository(db *gorm.DB, cache *cache.InMemoryCache) *LookupRepository {
return &LookupRepository{
db: db,
db: db,
cache: cache,
}
}
// GetTracks
// Gets Tracks rows from Lookup table.
//
// Args:
// context.Context: Application context
// Returns:
// model.LookupModel: Lookup object from database.
func (as LookupRepository) GetTracks(ctx context.Context) *[]model.Track {
db := as.db.WithContext(ctx)
TrackModel := new([]model.Track)
db.Find(&TrackModel)
return TrackModel
func (r *LookupRepository) GetTracks(ctx context.Context) (*[]model.Track, error) {
fetcher := func() (*[]model.Track, error) {
db := r.db.WithContext(ctx)
items := new([]model.Track)
if err := db.Find(items).Error; err != nil {
return nil, err
}
return items, nil
}
return cache.GetOrSet(r.cache, tracksCacheKey, cacheDuration, fetcher)
}
// GetCarModels
// Gets CarModels rows from Lookup table.
//
// Args:
// context.Context: Application context
// Returns:
// model.LookupModel: Lookup object from database.
func (as LookupRepository) GetCarModels(ctx context.Context) *[]model.CarModel {
db := as.db.WithContext(ctx)
CarModelModel := new([]model.CarModel)
db.Find(&CarModelModel)
return CarModelModel
func (r *LookupRepository) GetCarModels(ctx context.Context) (*[]model.CarModel, error) {
fetcher := func() (*[]model.CarModel, error) {
db := r.db.WithContext(ctx)
items := new([]model.CarModel)
if err := db.Find(items).Error; err != nil {
return nil, err
}
return items, nil
}
return cache.GetOrSet(r.cache, carModelsCacheKey, cacheDuration, fetcher)
}
// GetDriverCategories
// Gets DriverCategories rows from Lookup table.
//
// Args:
// context.Context: Application context
// Returns:
// model.LookupModel: Lookup object from database.
func (as LookupRepository) GetDriverCategories(ctx context.Context) *[]model.DriverCategory {
db := as.db.WithContext(ctx)
DriverCategoryModel := new([]model.DriverCategory)
db.Find(&DriverCategoryModel)
return DriverCategoryModel
func (r *LookupRepository) GetDriverCategories(ctx context.Context) (*[]model.DriverCategory, error) {
fetcher := func() (*[]model.DriverCategory, error) {
db := r.db.WithContext(ctx)
items := new([]model.DriverCategory)
if err := db.Find(items).Error; err != nil {
return nil, err
}
return items, nil
}
return cache.GetOrSet(r.cache, driverCategoriesCacheKey, cacheDuration, fetcher)
}
// GetCupCategories
// Gets CupCategories rows from Lookup table.
//
// Args:
// context.Context: Application context
// Returns:
// model.LookupModel: Lookup object from database.
func (as LookupRepository) GetCupCategories(ctx context.Context) *[]model.CupCategory {
db := as.db.WithContext(ctx)
CupCategoryModel := new([]model.CupCategory)
db.Find(&CupCategoryModel)
return CupCategoryModel
func (r *LookupRepository) GetCupCategories(ctx context.Context) (*[]model.CupCategory, error) {
fetcher := func() (*[]model.CupCategory, error) {
db := r.db.WithContext(ctx)
items := new([]model.CupCategory)
if err := db.Find(items).Error; err != nil {
return nil, err
}
return items, nil
}
return cache.GetOrSet(r.cache, cupCategoriesCacheKey, cacheDuration, fetcher)
}
// GetSessionTypes
// Gets SessionTypes rows from Lookup table.
//
// Args:
// context.Context: Application context
// Returns:
// model.LookupModel: Lookup object from database.
func (as LookupRepository) GetSessionTypes(ctx context.Context) *[]model.SessionType {
db := as.db.WithContext(ctx)
SessionTypesModel := new([]model.SessionType)
db.Find(&SessionTypesModel)
return SessionTypesModel
func (r *LookupRepository) GetSessionTypes(ctx context.Context) (*[]model.SessionType, error) {
fetcher := func() (*[]model.SessionType, error) {
db := r.db.WithContext(ctx)
items := new([]model.SessionType)
if err := db.Find(items).Error; err != nil {
return nil, err
}
return items, nil
}
return cache.GetOrSet(r.cache, sessionTypesCacheKey, cacheDuration, fetcher)
}

View File

@@ -13,11 +13,76 @@ type ServerRepository struct {
}
func NewServerRepository(db *gorm.DB) *ServerRepository {
return &ServerRepository{
repo := &ServerRepository{
BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}),
}
// Run migrations
if err := repo.migrateServerTable(); err != nil {
panic(err)
}
return repo
}
// migrateServerTable ensures all required columns exist with proper defaults
func (r *ServerRepository) migrateServerTable() error {
// Create a temporary table with all required columns
if err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS servers_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
ip TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 9600,
path TEXT NOT NULL,
service_name TEXT NOT NULL,
date_created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
from_steam_cmd BOOLEAN NOT NULL DEFAULT 1
)
`).Error; err != nil {
return err
}
// Copy data from old table, setting defaults for new columns
if err := r.db.Exec(`
INSERT INTO servers_new (
id,
name,
ip,
port,
path,
service_name,
date_created,
from_steam_cmd
)
SELECT
id,
COALESCE(name, 'Server ' || id) as name,
COALESCE(ip, '127.0.0.1') as ip,
COALESCE(port, 9600) as port,
path,
COALESCE(service_name, 'ACC-Server-' || id) as service_name,
COALESCE(date_created, CURRENT_TIMESTAMP) as date_created,
COALESCE(from_steam_cmd, 1) as from_steam_cmd
FROM servers
`).Error; err != nil {
// If the old table doesn't exist, this is a fresh install
if err := r.db.Exec(`DROP TABLE IF EXISTS servers_new`).Error; err != nil {
return err
}
return nil
}
// Replace old table with new one
if err := r.db.Exec(`DROP TABLE IF EXISTS servers`).Error; err != nil {
return err
}
if err := r.db.Exec(`ALTER TABLE servers_new RENAME TO servers`).Error; err != nil {
return err
}
return nil
}
// GetFirstByServiceName
// Gets first row from Server table.

View File

@@ -44,3 +44,116 @@ func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID
return lastSession.SessionID, nil
}
// GetSummaryStats calculates peak players, total sessions, and average players.
func (r *StateHistoryRepository) GetSummaryStats(ctx context.Context, filter *model.StateHistoryFilter) (model.StateHistoryStats, error) {
var stats model.StateHistoryStats
query := r.db.WithContext(ctx).Model(&model.StateHistory{}).
Select(`
COALESCE(MAX(player_count), 0) as peak_players,
COUNT(DISTINCT session_id) as total_sessions,
COALESCE(AVG(player_count), 0) as average_players
`).
Where("server_id = ?", filter.ServerID)
if !filter.StartDate.IsZero() && !filter.EndDate.IsZero() {
query = query.Where("date_created BETWEEN ? AND ?", filter.StartDate, filter.EndDate)
}
if err := query.Scan(&stats).Error; err != nil {
return model.StateHistoryStats{}, err
}
return stats, nil
}
// GetTotalPlaytime calculates the total playtime in minutes.
func (r *StateHistoryRepository) GetTotalPlaytime(ctx context.Context, filter *model.StateHistoryFilter) (int, error) {
var totalPlaytime struct {
TotalMinutes float64
}
rawQuery := `
SELECT SUM(duration_minutes) as total_minutes FROM (
SELECT (strftime('%s', MAX(date_created)) - strftime('%s', MIN(date_created))) / 60.0 as duration_minutes
FROM state_histories
WHERE server_id = ? AND date_created BETWEEN ? AND ?
GROUP BY session_id
HAVING COUNT(*) > 1 AND MAX(player_count) > 0
)
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&totalPlaytime).Error
if err != nil {
return 0, err
}
return int(totalPlaytime.TotalMinutes), nil
}
// GetPlayerCountOverTime gets downsampled player count data.
func (r *StateHistoryRepository) GetPlayerCountOverTime(ctx context.Context, filter *model.StateHistoryFilter) ([]model.PlayerCountPoint, error) {
var points []model.PlayerCountPoint
rawQuery := `
SELECT
strftime('%Y-%m-%d %H:00:00', date_created) as timestamp,
AVG(player_count) as count
FROM state_histories
WHERE server_id = ? AND date_created BETWEEN ? AND ?
GROUP BY 1
ORDER BY 1
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&points).Error
return points, err
}
// GetSessionTypes counts sessions by type.
func (r *StateHistoryRepository) GetSessionTypes(ctx context.Context, filter *model.StateHistoryFilter) ([]model.SessionCount, error) {
var sessionTypes []model.SessionCount
rawQuery := `
SELECT session as name, COUNT(*) as count FROM (
SELECT session
FROM state_histories
WHERE server_id = ? AND date_created BETWEEN ? AND ?
GROUP BY session_id
)
GROUP BY session
ORDER BY count DESC
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&sessionTypes).Error
return sessionTypes, err
}
// GetDailyActivity counts sessions per day.
func (r *StateHistoryRepository) GetDailyActivity(ctx context.Context, filter *model.StateHistoryFilter) ([]model.DailyActivity, error) {
var dailyActivity []model.DailyActivity
rawQuery := `
SELECT
DATE(date_created) as date,
COUNT(DISTINCT session_id) as sessions_count
FROM state_histories
WHERE server_id = ? AND date_created BETWEEN ? AND ?
GROUP BY 1
ORDER BY 1
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&dailyActivity).Error
return dailyActivity, err
}
// GetRecentSessions retrieves the 10 most recent sessions.
func (r *StateHistoryRepository) GetRecentSessions(ctx context.Context, filter *model.StateHistoryFilter) ([]model.RecentSession, error) {
var recentSessions []model.RecentSession
rawQuery := `
SELECT
session_id as id,
MIN(date_created) as date,
session as type,
track,
MAX(player_count) as players,
CAST((strftime('%s', MAX(date_created)) - strftime('%s', MIN(date_created))) / 60 AS INTEGER) as duration
FROM state_histories
WHERE server_id = ? AND date_created BETWEEN ? AND ?
GROUP BY session_id
HAVING COUNT(*) > 1 AND MAX(player_count) > 0
ORDER BY date DESC
LIMIT 10
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&recentSessions).Error
return recentSessions, err
}