1
0
forked from baron/baron-sso

논리 검사 계속. 스케폴딩 일부 진행

This commit is contained in:
Lectom C Han
2025-12-23 17:59:37 +09:00
parent 48589dca5d
commit 904b35e6e6
15 changed files with 556 additions and 77 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-alpine
FROM golang:1.25-alpine
WORKDIR /app

View File

@@ -2,15 +2,44 @@ package main
import (
"log"
"os"
"strconv"
"baron-sso-backend/internal/handler"
"baron-sso-backend/internal/repository"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/encryptcookie"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
)
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func main() {
// Initialize Fiber
// 1. Initialize DB Connections
chHost := getEnv("CLICKHOUSE_HOST", "localhost")
chPort, _ := strconv.Atoi(getEnv("CLICKHOUSE_PORT_NATIVE", "9000"))
chUser := getEnv("CLICKHOUSE_USER", "default")
chPass := getEnv("CLICKHOUSE_PASSWORD", "")
chDB := getEnv("CLICKHOUSE_DB", "default")
auditRepo, err := repository.NewClickHouseRepository(chHost, chPort, chUser, chPass, chDB)
if err != nil {
log.Printf("Warning: Failed to connect to ClickHouse: %v. Audit logs will fail.", err)
// Proceeding mostly for Dev purposes, but in Prod should generally fail or fallback.
}
// 2. Initialize Handlers
auditHandler := handler.NewAuditHandler(auditRepo)
// 3. Initialize Fiber
app := fiber.New(fiber.Config{
AppName: "Baron SSO Backend",
})
@@ -18,8 +47,9 @@ func main() {
// Middleware
app.Use(logger.New())
app.Use(recover.New())
app.Use(cors.New()) // Allow Frontend Access
app.Use(encryptcookie.New(encryptcookie.Config{
Key: "secret-key-must-be-32-bytes-long!", // TODO: Externalize to env
Key: getEnv("COOKIE_SECRET", "secret-key-must-be-32-bytes-long!"),
}))
// Routes
@@ -27,13 +57,15 @@ func main() {
return c.SendString("Baron SSO Audit Backend Online")
})
// Health Check
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
})
return c.JSON(fiber.Map{"status": "ok"})
})
// API Group
api := app.Group("/api/v1")
api.Post("/audit", auditHandler.CreateLog)
// Start Server
log.Fatal(app.Listen(":3000"))
port := getEnv("PORT", "3000")
log.Fatal(app.Listen(":" + port))
}

View File

@@ -2,17 +2,19 @@ module baron-sso-backend
go 1.25.4
require (
github.com/ClickHouse/clickhouse-go/v2 v2.42.0
github.com/gofiber/fiber/v2 v2.52.10
)
require (
github.com/ClickHouse/ch-go v0.69.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.42.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/gofiber/fiber/v2 v2.52.10 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect

View File

@@ -7,6 +7,7 @@ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
@@ -19,6 +20,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -26,11 +29,12 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -45,6 +49,7 @@ github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKf
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -54,6 +59,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@@ -64,6 +71,8 @@ github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7Fw
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -115,6 +124,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -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)
}

View 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
}

View 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",
})
}

View 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,
)
}