forked from baron/baron-sso
논리 검사 계속. 스케폴딩 일부 진행
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
)
|
||||
|
||||
type AuditLogger struct {
|
||||
conn driver.Conn
|
||||
}
|
||||
|
||||
func NewAuditLogger(host string, port int, user, password, db string) (*AuditLogger, error) {
|
||||
conn, err := clickhouse.Open(&clickhouse.Options{
|
||||
Addr: []string{fmt.Sprintf("%s:%d", host, port)},
|
||||
Auth: clickhouse.Auth{
|
||||
Database: db,
|
||||
Username: user,
|
||||
Password: password,
|
||||
},
|
||||
Debug: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open clickhouse connection: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.Ping(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping clickhouse: %w", err)
|
||||
}
|
||||
|
||||
return &AuditLogger{conn: conn}, nil
|
||||
}
|
||||
|
||||
func (l *AuditLogger) CreateSchema(ctx context.Context) error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
timestamp DateTime DEFAULT now(),
|
||||
user_id String,
|
||||
event_type String,
|
||||
status String,
|
||||
ip_address String,
|
||||
user_agent String,
|
||||
details String
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY timestamp
|
||||
`
|
||||
return l.conn.Exec(ctx, query)
|
||||
}
|
||||
23
backend/internal/domain/models.go
Normal file
23
backend/internal/domain/models.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditLog represents a single audit event
|
||||
type AuditLog struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
UserID string `json:"user_id"`
|
||||
EventType string `json:"event_type"` // e.g., "login_success", "login_failed", "otp_sent"
|
||||
Status string `json:"status"` // e.g., "success", "failure"
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
Details string `json:"details,omitempty"` // JSON string or simple text
|
||||
}
|
||||
|
||||
// AuditRepository defines interface for storing logs
|
||||
type AuditRepository interface {
|
||||
Create(log *AuditLog) error
|
||||
// FindAll(filter Filter) ([]*AuditLog, error) // Future scope
|
||||
}
|
||||
48
backend/internal/handler/audit_handler.go
Normal file
48
backend/internal/handler/audit_handler.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AuditHandler struct {
|
||||
repo domain.AuditRepository
|
||||
}
|
||||
|
||||
func NewAuditHandler(repo domain.AuditRepository) *AuditHandler {
|
||||
return &AuditHandler{repo: repo}
|
||||
}
|
||||
|
||||
// CreateLog handles POST /api/v1/audit
|
||||
func (h *AuditHandler) CreateLog(c *fiber.Ctx) error {
|
||||
var req domain.AuditLog
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Cannot parse JSON",
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-fill metadata if missing
|
||||
if req.IPAddress == "" {
|
||||
req.IPAddress = c.IP()
|
||||
}
|
||||
if req.UserAgent == "" {
|
||||
req.UserAgent = c.Get("User-Agent")
|
||||
}
|
||||
if req.Timestamp.IsZero() {
|
||||
req.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
if err := h.repo.Create(&req); err != nil {
|
||||
// Log internal error but don't expose details
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Failed to save audit log",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"message": "Audit log saved",
|
||||
})
|
||||
}
|
||||
81
backend/internal/repository/clickhouse_repo.go
Normal file
81
backend/internal/repository/clickhouse_repo.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"baron-sso-backend/internal/domain"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
)
|
||||
|
||||
type ClickHouseRepository struct {
|
||||
conn driver.Conn
|
||||
}
|
||||
|
||||
func NewClickHouseRepository(host string, port int, user, password, db string) (*ClickHouseRepository, error) {
|
||||
conn, err := clickhouse.Open(&clickhouse.Options{
|
||||
Addr: []string{fmt.Sprintf("%s:%d", host, port)},
|
||||
Auth: clickhouse.Auth{
|
||||
Database: db,
|
||||
Username: user,
|
||||
Password: password,
|
||||
},
|
||||
Debug: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open clickhouse connection: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.Ping(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping clickhouse: %w", err)
|
||||
}
|
||||
|
||||
// Ensure Table Exists
|
||||
// Note: In production, use migrations.
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
timestamp DateTime DEFAULT now(),
|
||||
user_id String,
|
||||
event_type String,
|
||||
status String,
|
||||
ip_address String,
|
||||
user_agent String,
|
||||
device_id String,
|
||||
details String
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY timestamp
|
||||
`
|
||||
if err := conn.Exec(context.Background(), query); err != nil {
|
||||
return nil, fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
|
||||
return &ClickHouseRepository{conn: conn}, nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseRepository) Create(log *domain.AuditLog) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if log.Timestamp.IsZero() {
|
||||
log.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO audit_logs (timestamp, user_id, event_type, status, ip_address, user_agent, device_id, details)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
return r.conn.Exec(ctx, query,
|
||||
log.Timestamp,
|
||||
log.UserID,
|
||||
log.EventType,
|
||||
log.Status,
|
||||
log.IPAddress,
|
||||
log.UserAgent,
|
||||
log.DeviceID,
|
||||
log.Details,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user