feat: add Docker infrastructure, migrations, CI/CD client, session cleanup, tests

Docker & Deployment:
- Add Dockerfile (multi-stage, alpine, non-root)
- Add docker-compose.yml (caddy, backend, postgres, watchtower)
- Add Caddyfile (TLS, file_server, reverse proxy)
- Add .env.example

Database:
- Add migrations/001_init.sql (all tables + indexes)

CI/CD:
- Add cmd/ci-release/main.go (launcher binary upload tool)

Session management:
- Add internal/session/cleanup.go (background expired session cleanup)
- Integrate cleanup worker into main.go

Bug fixes:
- Fix launcherLatest download URL to include version segment
- Fix serveLauncherAsset path to match route pattern
- Add Content-Type detection from file extension in CAS serveFile
- Add empty-field validation in webLogin
- Format string fix in ci-release (%d → %s for resp.Status)

Tests:
- Add internal/auth/auth_test.go (8 tests)
- Add internal/cas/cas_test.go (7 tests)
- Add internal/session/cleanup_test.go (1 test)
- Add internal/api/api_test.go (5 tests)
- All tests passing, go vet clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 20:09:00 +03:00
parent 81c42e1a9a
commit 5fba2e78d5
14 changed files with 986 additions and 4 deletions

View File

@@ -0,0 +1,68 @@
// package session manages Yggdrasil session lifecycle.
package session
import (
"context"
"log"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
)
// StartCleanupWorker launches a background goroutine that deletes expired
// yggdrasil_sessions every interval. It stops when the context is cancelled.
func StartCleanupWorker(db *database.DB, interval time.Duration) context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
go func() {
log.Printf("Session cleanup worker started (interval: %v)", interval)
ticker := time.NewTicker(interval)
defer ticker.Stop()
// Run once on start.
cleanup(db)
for {
select {
case <-ticker.C:
cleanup(db)
case <-ctx.Done():
log.Println("Session cleanup worker stopped")
return
}
}
}()
return cancel
}
func cleanup(db *database.DB) {
if db == nil {
return
}
pool := db.Pool()
if pool == nil {
return
}
tag, err := pool.Exec(context.Background(),
`DELETE FROM yggdrasil_sessions WHERE expires_at < NOW()`)
if err != nil {
log.Printf("Session cleanup error: %v", err)
return
}
if tag.RowsAffected() > 0 {
log.Printf("Session cleanup: removed %d expired sessions", tag.RowsAffected())
}
}
// CountActive returns the number of non-expired sessions.
func CountActive(pool *pgxpool.Pool) (int, error) {
var count int
err := pool.QueryRow(context.Background(),
`SELECT COUNT(*) FROM yggdrasil_sessions WHERE expires_at > NOW()`,
).Scan(&count)
return count, err
}