diff --git a/backend/go.work.sum b/backend/go.work.sum
index b8c077f..407074b 100644
--- a/backend/go.work.sum
+++ b/backend/go.work.sum
@@ -1,17 +1,59 @@
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
+github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
+github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/backend/shared/domain/errors/codes.go b/backend/shared/domain/errors/codes.go
new file mode 100644
index 0000000..7e8ee05
--- /dev/null
+++ b/backend/shared/domain/errors/codes.go
@@ -0,0 +1,49 @@
+// Package errors provides domain-specific error types for the KnowFoolery application.
+package errors
+
+// ErrorCode represents a unique error code for domain errors.
+type ErrorCode string
+
+// Error codes for the KnowFoolery application.
+const (
+ // General errors
+ CodeNotFound ErrorCode = "NOT_FOUND"
+ CodeInvalidInput ErrorCode = "INVALID_INPUT"
+ CodeUnauthorized ErrorCode = "UNAUTHORIZED"
+ CodeForbidden ErrorCode = "FORBIDDEN"
+ CodeConflict ErrorCode = "CONFLICT"
+ CodeInternal ErrorCode = "INTERNAL"
+
+ // Game session errors
+ CodeSessionExpired ErrorCode = "SESSION_EXPIRED"
+ CodeGameInProgress ErrorCode = "GAME_IN_PROGRESS"
+ CodeMaxAttemptsReached ErrorCode = "MAX_ATTEMPTS_REACHED"
+ CodeSessionNotActive ErrorCode = "SESSION_NOT_ACTIVE"
+
+ // Question errors
+ CodeQuestionNotFound ErrorCode = "QUESTION_NOT_FOUND"
+ CodeNoQuestionsAvailable ErrorCode = "NO_QUESTIONS_AVAILABLE"
+
+ // User errors
+ CodeUserNotFound ErrorCode = "USER_NOT_FOUND"
+ CodeUserAlreadyExists ErrorCode = "USER_ALREADY_EXISTS"
+ CodeEmailNotVerified ErrorCode = "EMAIL_NOT_VERIFIED"
+
+ // Validation errors
+ CodeValidationFailed ErrorCode = "VALIDATION_FAILED"
+ CodeInvalidPlayerName ErrorCode = "INVALID_PLAYER_NAME"
+ CodeInvalidAnswer ErrorCode = "INVALID_ANSWER"
+
+ // Authentication errors
+ CodeInvalidToken ErrorCode = "INVALID_TOKEN"
+ CodeTokenExpired ErrorCode = "TOKEN_EXPIRED"
+ CodeMFARequired ErrorCode = "MFA_REQUIRED"
+
+ // Rate limiting errors
+ CodeRateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED"
+)
+
+// String returns the string representation of the error code.
+func (c ErrorCode) String() string {
+ return string(c)
+}
diff --git a/backend/shared/domain/errors/errors.go b/backend/shared/domain/errors/errors.go
new file mode 100644
index 0000000..3aef612
--- /dev/null
+++ b/backend/shared/domain/errors/errors.go
@@ -0,0 +1,83 @@
+// Package errors provides domain-specific error types for the KnowFoolery application.
+package errors
+
+import (
+ "errors"
+ "fmt"
+)
+
+// DomainError represents a domain-level error with an error code.
+type DomainError struct {
+ Code ErrorCode
+ Message string
+ Err error
+}
+
+// Error implements the error interface.
+func (e *DomainError) Error() string {
+ if e.Err != nil {
+ return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
+ }
+ return fmt.Sprintf("[%s] %s", e.Code, e.Message)
+}
+
+// Unwrap returns the underlying error.
+func (e *DomainError) Unwrap() error {
+ return e.Err
+}
+
+// Is checks if the target error matches this DomainError's code.
+func (e *DomainError) Is(target error) bool {
+ var domainErr *DomainError
+ if errors.As(target, &domainErr) {
+ return e.Code == domainErr.Code
+ }
+ return false
+}
+
+// New creates a new DomainError with the given code and message.
+func New(code ErrorCode, message string) *DomainError {
+ return &DomainError{
+ Code: code,
+ Message: message,
+ }
+}
+
+// Wrap wraps an existing error with a DomainError.
+func Wrap(code ErrorCode, message string, err error) *DomainError {
+ return &DomainError{
+ Code: code,
+ Message: message,
+ Err: err,
+ }
+}
+
+// Common domain errors
+var (
+ // ErrNotFound indicates a requested resource was not found.
+ ErrNotFound = New(CodeNotFound, "resource not found")
+
+ // ErrInvalidInput indicates invalid input was provided.
+ ErrInvalidInput = New(CodeInvalidInput, "invalid input")
+
+ // ErrUnauthorized indicates the user is not authenticated.
+ ErrUnauthorized = New(CodeUnauthorized, "unauthorized")
+
+ // ErrForbidden indicates the user lacks permission.
+ ErrForbidden = New(CodeForbidden, "forbidden")
+
+ // ErrConflict indicates a resource conflict.
+ ErrConflict = New(CodeConflict, "resource conflict")
+
+ // ErrInternal indicates an internal server error.
+ ErrInternal = New(CodeInternal, "internal error")
+
+ // ErrSessionExpired indicates a game session has expired.
+ ErrSessionExpired = New(CodeSessionExpired, "session expired")
+
+ // ErrGameInProgress indicates a game is already in progress.
+ ErrGameInProgress = New(CodeGameInProgress, "game already in progress")
+
+ // ErrMaxAttemptsReached indicates the maximum attempts have been reached.
+ ErrMaxAttemptsReached = New(CodeMaxAttemptsReached, "maximum attempts reached")
+)
diff --git a/backend/shared/domain/events/contracts.go b/backend/shared/domain/events/contracts.go
new file mode 100644
index 0000000..689ac5e
--- /dev/null
+++ b/backend/shared/domain/events/contracts.go
@@ -0,0 +1,54 @@
+// Package events provides domain event interfaces and types for the KnowFoolery application.
+package events
+
+// EventType represents the type of a domain event.
+type EventType string
+
+// Game session events
+const (
+ // GameSessionStarted is emitted when a new game session starts.
+ GameSessionStarted EventType = "game_session.started"
+ // GameSessionEnded is emitted when a game session ends.
+ GameSessionEnded EventType = "game_session.ended"
+ // GameSessionTimedOut is emitted when a game session times out.
+ GameSessionTimedOut EventType = "game_session.timed_out"
+ // AnswerSubmitted is emitted when a player submits an answer.
+ AnswerSubmitted EventType = "game_session.answer_submitted"
+ // HintRequested is emitted when a player requests a hint.
+ HintRequested EventType = "game_session.hint_requested"
+ // QuestionAnswered is emitted when a question is fully answered (correct or max attempts).
+ QuestionAnswered EventType = "game_session.question_answered"
+)
+
+// User events
+const (
+ // UserRegistered is emitted when a new user registers.
+ UserRegistered EventType = "user.registered"
+ // UserEmailVerified is emitted when a user verifies their email.
+ UserEmailVerified EventType = "user.email_verified"
+ // UserDeleted is emitted when a user account is deleted.
+ UserDeleted EventType = "user.deleted"
+)
+
+// Question events
+const (
+ // QuestionCreated is emitted when a new question is created.
+ QuestionCreated EventType = "question.created"
+ // QuestionUpdated is emitted when a question is updated.
+ QuestionUpdated EventType = "question.updated"
+ // QuestionDeleted is emitted when a question is deleted.
+ QuestionDeleted EventType = "question.deleted"
+)
+
+// Leaderboard events
+const (
+ // ScoreUpdated is emitted when a score is updated on the leaderboard.
+ ScoreUpdated EventType = "leaderboard.score_updated"
+ // LeaderboardRefreshed is emitted when the leaderboard is refreshed.
+ LeaderboardRefreshed EventType = "leaderboard.refreshed"
+)
+
+// String returns the string representation of the event type.
+func (t EventType) String() string {
+ return string(t)
+}
diff --git a/backend/shared/domain/events/event.go b/backend/shared/domain/events/event.go
new file mode 100644
index 0000000..ffd802d
--- /dev/null
+++ b/backend/shared/domain/events/event.go
@@ -0,0 +1,73 @@
+// Package events provides domain event interfaces and types for the KnowFoolery application.
+package events
+
+import (
+ "context"
+ "time"
+)
+
+// Event represents a domain event.
+type Event interface {
+ // EventType returns the type of the event.
+ EventType() EventType
+ // OccurredAt returns when the event occurred.
+ OccurredAt() time.Time
+ // AggregateID returns the ID of the aggregate that produced the event.
+ AggregateID() string
+ // AggregateType returns the type of the aggregate.
+ AggregateType() string
+}
+
+// BaseEvent provides a base implementation of the Event interface.
+type BaseEvent struct {
+ Type EventType `json:"type"`
+ Timestamp time.Time `json:"timestamp"`
+ AggrID string `json:"aggregate_id"`
+ AggrType string `json:"aggregate_type"`
+}
+
+// EventType returns the type of the event.
+func (e *BaseEvent) EventType() EventType {
+ return e.Type
+}
+
+// OccurredAt returns when the event occurred.
+func (e *BaseEvent) OccurredAt() time.Time {
+ return e.Timestamp
+}
+
+// AggregateID returns the ID of the aggregate that produced the event.
+func (e *BaseEvent) AggregateID() string {
+ return e.AggrID
+}
+
+// AggregateType returns the type of the aggregate.
+func (e *BaseEvent) AggregateType() string {
+ return e.AggrType
+}
+
+// NewBaseEvent creates a new BaseEvent with the given parameters.
+func NewBaseEvent(eventType EventType, aggregateID, aggregateType string) BaseEvent {
+ return BaseEvent{
+ Type: eventType,
+ Timestamp: time.Now(),
+ AggrID: aggregateID,
+ AggrType: aggregateType,
+ }
+}
+
+// EventHandler handles domain events.
+type EventHandler interface {
+ // Handle processes the given event.
+ Handle(ctx context.Context, event Event) error
+ // Handles returns the event types this handler can process.
+ Handles() []EventType
+}
+
+// EventBus publishes and subscribes to domain events.
+type EventBus interface {
+ // Publish publishes an event to all subscribers.
+ Publish(ctx context.Context, event Event) error
+ // Subscribe registers a handler for specific event types.
+ Subscribe(handler EventHandler) error
+}
diff --git a/backend/shared/domain/types/enums.go b/backend/shared/domain/types/enums.go
new file mode 100644
index 0000000..28df70e
--- /dev/null
+++ b/backend/shared/domain/types/enums.go
@@ -0,0 +1,79 @@
+// Package types provides common domain types for the KnowFoolery application.
+package types
+
+// SessionStatus represents the status of a game session.
+type SessionStatus string
+
+const (
+ // SessionStatusCreated indicates a session has been created but not started.
+ SessionStatusCreated SessionStatus = "created"
+ // SessionStatusActive indicates a session is currently active.
+ SessionStatusActive SessionStatus = "active"
+ // SessionStatusCompleted indicates a session has been completed normally.
+ SessionStatusCompleted SessionStatus = "completed"
+ // SessionStatusTimedOut indicates a session has timed out.
+ SessionStatusTimedOut SessionStatus = "timed_out"
+ // SessionStatusAbandoned indicates a session has been abandoned.
+ SessionStatusAbandoned SessionStatus = "abandoned"
+)
+
+// String returns the string representation of the session status.
+func (s SessionStatus) String() string {
+ return string(s)
+}
+
+// IsTerminal checks if the session status is terminal (cannot transition).
+func (s SessionStatus) IsTerminal() bool {
+ return s == SessionStatusCompleted || s == SessionStatusTimedOut || s == SessionStatusAbandoned
+}
+
+// Difficulty represents the difficulty level of a question.
+type Difficulty string
+
+const (
+ // DifficultyEasy represents an easy question.
+ DifficultyEasy Difficulty = "easy"
+ // DifficultyMedium represents a medium difficulty question.
+ DifficultyMedium Difficulty = "medium"
+ // DifficultyHard represents a hard question.
+ DifficultyHard Difficulty = "hard"
+)
+
+// String returns the string representation of the difficulty.
+func (d Difficulty) String() string {
+ return string(d)
+}
+
+// UserRole represents a user's role in the system.
+type UserRole string
+
+const (
+ // RolePlayer represents a regular player.
+ RolePlayer UserRole = "player"
+ // RoleAdmin represents an administrator.
+ RoleAdmin UserRole = "admin"
+ // RoleModerator represents a moderator.
+ RoleModerator UserRole = "moderator"
+)
+
+// String returns the string representation of the user role.
+func (r UserRole) String() string {
+ return string(r)
+}
+
+// CompletionType represents how a game session was completed.
+type CompletionType string
+
+const (
+ // CompletionNormal indicates normal completion.
+ CompletionNormal CompletionType = "normal"
+ // CompletionTimeout indicates the session timed out.
+ CompletionTimeout CompletionType = "timeout"
+ // CompletionAbandoned indicates the session was abandoned.
+ CompletionAbandoned CompletionType = "abandoned"
+)
+
+// String returns the string representation of the completion type.
+func (c CompletionType) String() string {
+ return string(c)
+}
diff --git a/backend/shared/domain/types/id.go b/backend/shared/domain/types/id.go
new file mode 100644
index 0000000..1305b02
--- /dev/null
+++ b/backend/shared/domain/types/id.go
@@ -0,0 +1,50 @@
+// Package types provides common domain types for the KnowFoolery application.
+package types
+
+import (
+ "github.com/google/uuid"
+)
+
+// ID represents a unique identifier.
+type ID string
+
+// NewID generates a new unique ID.
+func NewID() ID {
+ return ID(uuid.New().String())
+}
+
+// IDFromString creates an ID from a string.
+func IDFromString(s string) ID {
+ return ID(s)
+}
+
+// String returns the string representation of the ID.
+func (id ID) String() string {
+ return string(id)
+}
+
+// IsEmpty checks if the ID is empty.
+func (id ID) IsEmpty() bool {
+ return id == ""
+}
+
+// IsValid checks if the ID is a valid UUID.
+func (id ID) IsValid() bool {
+ if id.IsEmpty() {
+ return false
+ }
+ _, err := uuid.Parse(string(id))
+ return err == nil
+}
+
+// SessionID represents a game session identifier.
+type SessionID = ID
+
+// UserID represents a user identifier.
+type UserID = ID
+
+// QuestionID represents a question identifier.
+type QuestionID = ID
+
+// ThemeID represents a theme identifier.
+type ThemeID = ID
diff --git a/backend/shared/domain/types/pagination.go b/backend/shared/domain/types/pagination.go
new file mode 100644
index 0000000..7ebcdbf
--- /dev/null
+++ b/backend/shared/domain/types/pagination.go
@@ -0,0 +1,90 @@
+// Package types provides common domain types for the KnowFoolery application.
+package types
+
+const (
+ // DefaultPageSize is the default number of items per page.
+ DefaultPageSize = 20
+ // MaxPageSize is the maximum number of items per page.
+ MaxPageSize = 100
+)
+
+// Pagination represents pagination parameters.
+type Pagination struct {
+ Page int `json:"page"`
+ PageSize int `json:"page_size"`
+}
+
+// NewPagination creates a new Pagination with default values.
+func NewPagination() Pagination {
+ return Pagination{
+ Page: 1,
+ PageSize: DefaultPageSize,
+ }
+}
+
+// Offset returns the offset for database queries.
+func (p Pagination) Offset() int {
+ if p.Page < 1 {
+ p.Page = 1
+ }
+ return (p.Page - 1) * p.Limit()
+}
+
+// Limit returns the limit for database queries.
+func (p Pagination) Limit() int {
+ if p.PageSize < 1 {
+ return DefaultPageSize
+ }
+ if p.PageSize > MaxPageSize {
+ return MaxPageSize
+ }
+ return p.PageSize
+}
+
+// Normalize ensures pagination values are within valid ranges.
+func (p *Pagination) Normalize() {
+ if p.Page < 1 {
+ p.Page = 1
+ }
+ if p.PageSize < 1 {
+ p.PageSize = DefaultPageSize
+ }
+ if p.PageSize > MaxPageSize {
+ p.PageSize = MaxPageSize
+ }
+}
+
+// PaginatedResult represents a paginated result set.
+type PaginatedResult[T any] struct {
+ Items []T `json:"items"`
+ TotalCount int64 `json:"total_count"`
+ Page int `json:"page"`
+ PageSize int `json:"page_size"`
+ TotalPages int `json:"total_pages"`
+}
+
+// NewPaginatedResult creates a new PaginatedResult.
+func NewPaginatedResult[T any](items []T, totalCount int64, pagination Pagination) PaginatedResult[T] {
+ totalPages := int(totalCount) / pagination.Limit()
+ if int(totalCount)%pagination.Limit() > 0 {
+ totalPages++
+ }
+
+ return PaginatedResult[T]{
+ Items: items,
+ TotalCount: totalCount,
+ Page: pagination.Page,
+ PageSize: pagination.Limit(),
+ TotalPages: totalPages,
+ }
+}
+
+// HasNextPage checks if there is a next page.
+func (r PaginatedResult[T]) HasNextPage() bool {
+ return r.Page < r.TotalPages
+}
+
+// HasPreviousPage checks if there is a previous page.
+func (r PaginatedResult[T]) HasPreviousPage() bool {
+ return r.Page > 1
+}
diff --git a/backend/shared/domain/valueobjects/player_name.go b/backend/shared/domain/valueobjects/player_name.go
new file mode 100644
index 0000000..fe24eb2
--- /dev/null
+++ b/backend/shared/domain/valueobjects/player_name.go
@@ -0,0 +1,77 @@
+// Package valueobjects provides domain value objects for the KnowFoolery application.
+package valueobjects
+
+import (
+ "regexp"
+ "strings"
+
+ "knowfoolery/backend/shared/domain/errors"
+)
+
+const (
+ // MinPlayerNameLength is the minimum length of a player name.
+ MinPlayerNameLength = 2
+ // MaxPlayerNameLength is the maximum length of a player name.
+ MaxPlayerNameLength = 50
+)
+
+// playerNamePattern matches alphanumeric characters, spaces, hyphens, underscores, and dots.
+var playerNamePattern = regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`)
+
+// PlayerName represents a validated player name.
+type PlayerName struct {
+ value string
+}
+
+// NewPlayerName creates a new PlayerName with validation.
+func NewPlayerName(name string) (PlayerName, error) {
+ // Trim whitespace
+ name = strings.TrimSpace(name)
+
+ // Validate length
+ if len(name) < MinPlayerNameLength {
+ return PlayerName{}, errors.Wrap(
+ errors.CodeInvalidPlayerName,
+ "player name too short",
+ nil,
+ )
+ }
+
+ if len(name) > MaxPlayerNameLength {
+ return PlayerName{}, errors.Wrap(
+ errors.CodeInvalidPlayerName,
+ "player name too long",
+ nil,
+ )
+ }
+
+ // Validate characters
+ if !playerNamePattern.MatchString(name) {
+ return PlayerName{}, errors.Wrap(
+ errors.CodeInvalidPlayerName,
+ "player name contains invalid characters",
+ nil,
+ )
+ }
+
+ // Normalize multiple spaces to single space
+ spaceRegex := regexp.MustCompile(`\s+`)
+ name = spaceRegex.ReplaceAllString(name, " ")
+
+ return PlayerName{value: name}, nil
+}
+
+// String returns the string representation of the player name.
+func (p PlayerName) String() string {
+ return p.value
+}
+
+// Equals checks if two player names are equal.
+func (p PlayerName) Equals(other PlayerName) bool {
+ return strings.EqualFold(p.value, other.value)
+}
+
+// IsEmpty checks if the player name is empty.
+func (p PlayerName) IsEmpty() bool {
+ return p.value == ""
+}
diff --git a/backend/shared/domain/valueobjects/score.go b/backend/shared/domain/valueobjects/score.go
new file mode 100644
index 0000000..2f62547
--- /dev/null
+++ b/backend/shared/domain/valueobjects/score.go
@@ -0,0 +1,91 @@
+// Package valueobjects provides domain value objects for the KnowFoolery application.
+package valueobjects
+
+// Scoring constants
+const (
+ // MaxScorePerQuestion is the maximum score for a correct answer without hint.
+ MaxScorePerQuestion = 2
+ // ScoreWithHint is the score for a correct answer with hint.
+ ScoreWithHint = 1
+ // ScoreIncorrect is the score for an incorrect answer.
+ ScoreIncorrect = 0
+ // MaxAttempts is the maximum number of attempts per question.
+ MaxAttempts = 3
+)
+
+// Score represents a game score value.
+type Score struct {
+ value int
+}
+
+// NewScore creates a new Score with validation.
+func NewScore(value int) Score {
+ if value < 0 {
+ value = 0
+ }
+ return Score{value: value}
+}
+
+// Zero returns a zero score.
+func Zero() Score {
+ return Score{value: 0}
+}
+
+// Value returns the score value.
+func (s Score) Value() int {
+ return s.value
+}
+
+// Add adds points to the score and returns a new Score.
+func (s Score) Add(points int) Score {
+ newValue := s.value + points
+ if newValue < 0 {
+ newValue = 0
+ }
+ return Score{value: newValue}
+}
+
+// CalculateQuestionScore calculates the score for a question based on correctness and hint usage.
+func CalculateQuestionScore(isCorrect bool, usedHint bool) int {
+ if !isCorrect {
+ return ScoreIncorrect
+ }
+ if usedHint {
+ return ScoreWithHint
+ }
+ return MaxScorePerQuestion
+}
+
+// Attempt represents an attempt at answering a question.
+type Attempt struct {
+ Number int
+ Answer string
+ Correct bool
+ UsedHint bool
+ Score int
+}
+
+// NewAttempt creates a new Attempt.
+func NewAttempt(number int, answer string, correct bool, usedHint bool) Attempt {
+ return Attempt{
+ Number: number,
+ Answer: answer,
+ Correct: correct,
+ UsedHint: usedHint,
+ Score: CalculateQuestionScore(correct, usedHint),
+ }
+}
+
+// CanRetry checks if another attempt is allowed.
+func CanRetry(attemptNumber int) bool {
+ return attemptNumber < MaxAttempts
+}
+
+// RemainingAttempts returns the number of remaining attempts.
+func RemainingAttempts(attemptNumber int) int {
+ remaining := MaxAttempts - attemptNumber
+ if remaining < 0 {
+ return 0
+ }
+ return remaining
+}
diff --git a/backend/shared/go.mod b/backend/shared/go.mod
index e808c74..efcee3c 100644
--- a/backend/shared/go.mod
+++ b/backend/shared/go.mod
@@ -1,3 +1,37 @@
module knowfoolery/backend/shared
go 1.25.5
+
+require (
+ github.com/go-playground/validator/v10 v10.25.0
+ github.com/gofiber/fiber/v3 v3.0.0-beta.3
+ github.com/google/uuid v1.6.0
+ github.com/prometheus/client_golang v1.20.5
+ github.com/rs/zerolog v1.33.0
+)
+
+require (
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.55.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ golang.org/x/crypto v0.32.0 // indirect
+ golang.org/x/net v0.34.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+)
diff --git a/backend/shared/go.sum b/backend/shared/go.sum
new file mode 100644
index 0000000..936cfee
--- /dev/null
+++ b/backend/shared/go.sum
@@ -0,0 +1,77 @@
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
+github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg=
+github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
+github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
+github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+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=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
+github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
+github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/backend/shared/infra/auth/rbac/roles.go b/backend/shared/infra/auth/rbac/roles.go
new file mode 100644
index 0000000..f66afdc
--- /dev/null
+++ b/backend/shared/infra/auth/rbac/roles.go
@@ -0,0 +1,157 @@
+// Package rbac provides role-based access control for the KnowFoolery application.
+package rbac
+
+// Role represents a user role.
+type Role string
+
+// Predefined roles
+const (
+ RolePlayer Role = "player"
+ RoleAdmin Role = "admin"
+ RoleModerator Role = "moderator"
+)
+
+// Permission represents a permission in the system.
+type Permission string
+
+// Game permissions
+const (
+ PermissionPlayGame Permission = "game:play"
+ PermissionViewGame Permission = "game:view"
+)
+
+// Question permissions
+const (
+ PermissionViewQuestion Permission = "question:view"
+ PermissionCreateQuestion Permission = "question:create"
+ PermissionUpdateQuestion Permission = "question:update"
+ PermissionDeleteQuestion Permission = "question:delete"
+)
+
+// User permissions
+const (
+ PermissionViewOwnProfile Permission = "user:view:own"
+ PermissionUpdateOwnProfile Permission = "user:update:own"
+ PermissionDeleteOwnAccount Permission = "user:delete:own"
+ PermissionViewUsers Permission = "user:view:all"
+ PermissionManageUsers Permission = "user:manage"
+)
+
+// Leaderboard permissions
+const (
+ PermissionViewLeaderboard Permission = "leaderboard:view"
+)
+
+// Admin permissions
+const (
+ PermissionViewAuditLog Permission = "audit:view"
+ PermissionViewDashboard Permission = "dashboard:view"
+ PermissionManageSystem Permission = "system:manage"
+)
+
+// rolePermissions maps roles to their permissions.
+var rolePermissions = map[Role][]Permission{
+ RolePlayer: {
+ PermissionPlayGame,
+ PermissionViewGame,
+ PermissionViewQuestion,
+ PermissionViewLeaderboard,
+ PermissionViewOwnProfile,
+ PermissionUpdateOwnProfile,
+ PermissionDeleteOwnAccount,
+ },
+ RoleModerator: {
+ PermissionPlayGame,
+ PermissionViewGame,
+ PermissionViewQuestion,
+ PermissionCreateQuestion,
+ PermissionUpdateQuestion,
+ PermissionViewLeaderboard,
+ PermissionViewOwnProfile,
+ PermissionUpdateOwnProfile,
+ PermissionDeleteOwnAccount,
+ PermissionViewUsers,
+ },
+ RoleAdmin: {
+ PermissionPlayGame,
+ PermissionViewGame,
+ PermissionViewQuestion,
+ PermissionCreateQuestion,
+ PermissionUpdateQuestion,
+ PermissionDeleteQuestion,
+ PermissionViewLeaderboard,
+ PermissionViewOwnProfile,
+ PermissionUpdateOwnProfile,
+ PermissionDeleteOwnAccount,
+ PermissionViewUsers,
+ PermissionManageUsers,
+ PermissionViewAuditLog,
+ PermissionViewDashboard,
+ PermissionManageSystem,
+ },
+}
+
+// HasPermission checks if a role has a specific permission.
+func HasPermission(role Role, permission Permission) bool {
+ permissions, ok := rolePermissions[role]
+ if !ok {
+ return false
+ }
+
+ for _, p := range permissions {
+ if p == permission {
+ return true
+ }
+ }
+ return false
+}
+
+// HasAnyPermission checks if a role has any of the specified permissions.
+func HasAnyPermission(role Role, permissions ...Permission) bool {
+ for _, permission := range permissions {
+ if HasPermission(role, permission) {
+ return true
+ }
+ }
+ return false
+}
+
+// HasAllPermissions checks if a role has all of the specified permissions.
+func HasAllPermissions(role Role, permissions ...Permission) bool {
+ for _, permission := range permissions {
+ if !HasPermission(role, permission) {
+ return false
+ }
+ }
+ return true
+}
+
+// UserHasPermission checks if a user with the given roles has a specific permission.
+func UserHasPermission(roles []string, permission Permission) bool {
+ for _, roleStr := range roles {
+ if HasPermission(Role(roleStr), permission) {
+ return true
+ }
+ }
+ return false
+}
+
+// GetPermissions returns all permissions for a role.
+func GetPermissions(role Role) []Permission {
+ permissions, ok := rolePermissions[role]
+ if !ok {
+ return nil
+ }
+
+ // Return a copy to prevent modification
+ result := make([]Permission, len(permissions))
+ copy(result, permissions)
+ return result
+}
+
+// IsValidRole checks if a role string is a valid role.
+func IsValidRole(roleStr string) bool {
+ role := Role(roleStr)
+ _, ok := rolePermissions[role]
+ return ok
+}
diff --git a/backend/shared/infra/auth/zitadel/client.go b/backend/shared/infra/auth/zitadel/client.go
new file mode 100644
index 0000000..6cedeb3
--- /dev/null
+++ b/backend/shared/infra/auth/zitadel/client.go
@@ -0,0 +1,143 @@
+// Package zitadel provides Zitadel authentication client for the KnowFoolery application.
+package zitadel
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "sync"
+ "time"
+)
+
+// Config holds the configuration for the Zitadel client.
+type Config struct {
+ BaseURL string
+ ProjectID string
+ AdminToken string
+ Timeout time.Duration
+}
+
+// DefaultConfig returns a default configuration.
+func DefaultConfig() Config {
+ return Config{
+ Timeout: 10 * time.Second,
+ }
+}
+
+// Client provides access to Zitadel authentication services.
+type Client struct {
+ config Config
+ httpClient *http.Client
+ jwksCache *JWKSCache
+}
+
+// JWKSCache caches the JSON Web Key Set for token validation.
+type JWKSCache struct {
+ mu sync.RWMutex
+ keys map[string]interface{}
+ expiry time.Time
+ duration time.Duration
+}
+
+// NewJWKSCache creates a new JWKS cache.
+func NewJWKSCache(cacheDuration time.Duration) *JWKSCache {
+ return &JWKSCache{
+ keys: make(map[string]interface{}),
+ duration: cacheDuration,
+ }
+}
+
+// NewClient creates a new Zitadel client.
+func NewClient(config Config) *Client {
+ return &Client{
+ config: config,
+ httpClient: &http.Client{
+ Timeout: config.Timeout,
+ },
+ jwksCache: NewJWKSCache(5 * time.Minute),
+ }
+}
+
+// AuthClaims represents the claims extracted from a validated JWT.
+type AuthClaims struct {
+ Subject string `json:"sub"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ Roles []string `json:"urn:zitadel:iam:org:project:roles"`
+ Audience []string `json:"aud"`
+ Issuer string `json:"iss"`
+ IssuedAt int64 `json:"iat"`
+ ExpiresAt int64 `json:"exp"`
+ MFAVerified bool `json:"amr"`
+}
+
+// TokenResponse represents a token response from Zitadel.
+type TokenResponse struct {
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int `json:"expires_in"`
+}
+
+// UserInfo represents user information from Zitadel.
+type UserInfo struct {
+ ID string `json:"sub"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ Verified bool `json:"email_verified"`
+ Roles []string `json:"roles"`
+}
+
+// ValidateToken validates a JWT token and returns the claims.
+// This is a placeholder implementation that should be replaced with actual JWT validation.
+func (c *Client) ValidateToken(ctx context.Context, token string) (*AuthClaims, error) {
+ // TODO: Implement actual JWT validation with JWKS
+ // This is a placeholder that should:
+ // 1. Fetch JWKS from Zitadel
+ // 2. Parse and validate the JWT
+ // 3. Verify signature, expiration, issuer, audience
+ // 4. Return the claims
+ return nil, fmt.Errorf("not implemented: token validation requires JWKS integration")
+}
+
+// RefreshToken refreshes an access token using a refresh token.
+func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) {
+ // TODO: Implement token refresh
+ return nil, fmt.Errorf("not implemented: token refresh")
+}
+
+// GetUserInfo retrieves user information using an access token.
+func (c *Client) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error) {
+ url := fmt.Sprintf("%s/oidc/v1/userinfo", c.config.BaseURL)
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get user info: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to get user info: status %d", resp.StatusCode)
+ }
+
+ var userInfo UserInfo
+ if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
+ return nil, fmt.Errorf("failed to decode user info: %w", err)
+ }
+
+ return &userInfo, nil
+}
+
+// RevokeToken revokes a token.
+func (c *Client) RevokeToken(ctx context.Context, token string) error {
+ // TODO: Implement token revocation
+ return fmt.Errorf("not implemented: token revocation")
+}
diff --git a/backend/shared/infra/auth/zitadel/middleware.go b/backend/shared/infra/auth/zitadel/middleware.go
new file mode 100644
index 0000000..12ad40e
--- /dev/null
+++ b/backend/shared/infra/auth/zitadel/middleware.go
@@ -0,0 +1,164 @@
+// Package zitadel provides Zitadel authentication client for the KnowFoolery application.
+package zitadel
+
+import (
+ "context"
+ "strings"
+
+ "github.com/gofiber/fiber/v3"
+)
+
+// ContextKey represents a context key for authentication values.
+type ContextKey string
+
+const (
+ // ContextKeyUserID is the context key for the user ID.
+ ContextKeyUserID ContextKey = "user_id"
+ // ContextKeyUserEmail is the context key for the user email.
+ ContextKeyUserEmail ContextKey = "user_email"
+ // ContextKeyUserName is the context key for the user name.
+ ContextKeyUserName ContextKey = "user_name"
+ // ContextKeyUserRoles is the context key for the user roles.
+ ContextKeyUserRoles ContextKey = "user_roles"
+ // ContextKeyMFAVerified is the context key for MFA verification status.
+ ContextKeyMFAVerified ContextKey = "mfa_verified"
+)
+
+// JWTMiddlewareConfig holds configuration for the JWT middleware.
+type JWTMiddlewareConfig struct {
+ Client *Client
+ Issuer string
+ Audience string
+ RequiredClaims []string
+ AdminEndpoints []string
+ SkipPaths []string
+}
+
+// JWTMiddleware creates a Fiber middleware for JWT validation.
+func JWTMiddleware(config JWTMiddlewareConfig) fiber.Handler {
+ return func(c fiber.Ctx) error {
+ // Check if path should be skipped
+ path := c.Path()
+ for _, skipPath := range config.SkipPaths {
+ if strings.HasPrefix(path, skipPath) {
+ return c.Next()
+ }
+ }
+
+ // Extract token from Authorization header
+ authHeader := c.Get("Authorization")
+ if authHeader == "" {
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
+ "error": true,
+ "message": "Authorization header required",
+ })
+ }
+
+ tokenString := strings.TrimPrefix(authHeader, "Bearer ")
+ if tokenString == authHeader {
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
+ "error": true,
+ "message": "Invalid authorization header format",
+ })
+ }
+
+ // Validate token
+ ctx := c.Context()
+ claims, err := config.Client.ValidateToken(ctx, tokenString)
+ if err != nil {
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
+ "error": true,
+ "message": "Invalid token",
+ "details": err.Error(),
+ })
+ }
+
+ // Set user context
+ c.Locals(string(ContextKeyUserID), claims.Subject)
+ c.Locals(string(ContextKeyUserEmail), claims.Email)
+ c.Locals(string(ContextKeyUserName), claims.Name)
+ c.Locals(string(ContextKeyUserRoles), claims.Roles)
+ c.Locals(string(ContextKeyMFAVerified), claims.MFAVerified)
+
+ // Check admin access for admin endpoints
+ if isAdminEndpoint(path, config.AdminEndpoints) {
+ if !hasAdminRole(claims.Roles) {
+ return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
+ "error": true,
+ "message": "Admin access required",
+ })
+ }
+
+ if !claims.MFAVerified {
+ return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
+ "error": true,
+ "message": "MFA verification required for admin access",
+ })
+ }
+ }
+
+ return c.Next()
+ }
+}
+
+// isAdminEndpoint checks if the given path is an admin endpoint.
+func isAdminEndpoint(path string, adminEndpoints []string) bool {
+ for _, adminPath := range adminEndpoints {
+ if strings.HasPrefix(path, adminPath) {
+ return true
+ }
+ }
+ return false
+}
+
+// hasAdminRole checks if the user has the admin role.
+func hasAdminRole(roles []string) bool {
+ for _, role := range roles {
+ if role == "admin" {
+ return true
+ }
+ }
+ return false
+}
+
+// GetUserID extracts the user ID from the Fiber context.
+func GetUserID(c fiber.Ctx) string {
+ if userID := c.Locals(string(ContextKeyUserID)); userID != nil {
+ return userID.(string)
+ }
+ return ""
+}
+
+// GetUserEmail extracts the user email from the Fiber context.
+func GetUserEmail(c fiber.Ctx) string {
+ if email := c.Locals(string(ContextKeyUserEmail)); email != nil {
+ return email.(string)
+ }
+ return ""
+}
+
+// GetUserRoles extracts the user roles from the Fiber context.
+func GetUserRoles(c fiber.Ctx) []string {
+ if roles := c.Locals(string(ContextKeyUserRoles)); roles != nil {
+ return roles.([]string)
+ }
+ return nil
+}
+
+// IsMFAVerified checks if MFA has been verified for the current user.
+func IsMFAVerified(c fiber.Ctx) bool {
+ if verified := c.Locals(string(ContextKeyMFAVerified)); verified != nil {
+ return verified.(bool)
+ }
+ return false
+}
+
+// GetUserFromContext retrieves user information from a standard context.
+func GetUserFromContext(ctx context.Context) (userID, email, name string, roles []string, ok bool) {
+ userID, _ = ctx.Value(ContextKeyUserID).(string)
+ email, _ = ctx.Value(ContextKeyUserEmail).(string)
+ name, _ = ctx.Value(ContextKeyUserName).(string)
+ roles, _ = ctx.Value(ContextKeyUserRoles).([]string)
+ ok = userID != ""
+ return
+}
diff --git a/backend/shared/infra/database/postgres/client.go b/backend/shared/infra/database/postgres/client.go
new file mode 100644
index 0000000..d779aed
--- /dev/null
+++ b/backend/shared/infra/database/postgres/client.go
@@ -0,0 +1,98 @@
+// Package postgres provides PostgreSQL database client for the KnowFoolery application.
+package postgres
+
+import (
+ "context"
+ "fmt"
+ "time"
+)
+
+// Config holds the configuration for the PostgreSQL client.
+type Config struct {
+ Host string
+ Port int
+ User string
+ Password string
+ Database string
+ SSLMode string
+ MaxOpenConns int
+ MaxIdleConns int
+ ConnMaxLifetime time.Duration
+ ConnMaxIdleTime time.Duration
+}
+
+// DefaultConfig returns a default configuration for development.
+func DefaultConfig() Config {
+ return Config{
+ Host: "localhost",
+ Port: 5432,
+ User: "postgres",
+ Password: "postgres",
+ Database: "knowfoolery",
+ SSLMode: "disable",
+ MaxOpenConns: 25,
+ MaxIdleConns: 10,
+ ConnMaxLifetime: 5 * time.Minute,
+ ConnMaxIdleTime: 1 * time.Minute,
+ }
+}
+
+// DSN returns the PostgreSQL connection string.
+func (c Config) DSN() string {
+ return fmt.Sprintf(
+ "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
+ c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode,
+ )
+}
+
+// URL returns the PostgreSQL connection URL.
+func (c Config) URL() string {
+ return fmt.Sprintf(
+ "postgresql://%s:%s@%s:%d/%s?sslmode=%s",
+ c.User, c.Password, c.Host, c.Port, c.Database, c.SSLMode,
+ )
+}
+
+// Client wraps database operations.
+// This is a placeholder that should be implemented with actual database client.
+type Client struct {
+ config Config
+}
+
+// NewClient creates a new PostgreSQL client.
+// Note: Actual implementation should use Ent client from each service.
+func NewClient(config Config) (*Client, error) {
+ // This is a placeholder. The actual Ent client should be created
+ // in each service's infrastructure layer.
+ return &Client{
+ config: config,
+ }, nil
+}
+
+// Close closes the database connection.
+func (c *Client) Close() error {
+ // Placeholder for closing database connection
+ return nil
+}
+
+// Ping checks if the database connection is alive.
+func (c *Client) Ping(ctx context.Context) error {
+ // Placeholder for ping implementation
+ return nil
+}
+
+// HealthCheck performs a health check on the database.
+func (c *Client) HealthCheck(ctx context.Context) error {
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ return c.Ping(ctx)
+}
+
+// ConfigFromEnv creates a Config from environment variables.
+// This is a placeholder that should be implemented with actual env parsing.
+func ConfigFromEnv() Config {
+ // TODO: Implement environment variable parsing
+ // Should use os.Getenv or a configuration library
+ return DefaultConfig()
+}
diff --git a/backend/shared/infra/database/redis/client.go b/backend/shared/infra/database/redis/client.go
new file mode 100644
index 0000000..c0e3bf0
--- /dev/null
+++ b/backend/shared/infra/database/redis/client.go
@@ -0,0 +1,119 @@
+// Package redis provides Redis client for the KnowFoolery application.
+package redis
+
+import (
+ "context"
+ "fmt"
+ "time"
+)
+
+// Config holds the configuration for the Redis client.
+type Config struct {
+ Host string
+ Port int
+ Password string
+ DB int
+ PoolSize int
+ MinIdleConns int
+ DialTimeout time.Duration
+ ReadTimeout time.Duration
+ WriteTimeout time.Duration
+}
+
+// DefaultConfig returns a default configuration for development.
+func DefaultConfig() Config {
+ return Config{
+ Host: "localhost",
+ Port: 6379,
+ Password: "",
+ DB: 0,
+ PoolSize: 10,
+ MinIdleConns: 5,
+ DialTimeout: 5 * time.Second,
+ ReadTimeout: 3 * time.Second,
+ WriteTimeout: 3 * time.Second,
+ }
+}
+
+// Addr returns the Redis server address.
+func (c Config) Addr() string {
+ return fmt.Sprintf("%s:%d", c.Host, c.Port)
+}
+
+// Client wraps Redis operations.
+// This is a placeholder that should be replaced with an actual Redis client.
+type Client struct {
+ config Config
+}
+
+// NewClient creates a new Redis client.
+// Note: Actual implementation should use go-redis/redis.
+func NewClient(config Config) (*Client, error) {
+ // This is a placeholder. The actual Redis client should be created
+ // using github.com/go-redis/redis/v9
+ return &Client{
+ config: config,
+ }, nil
+}
+
+// Close closes the Redis connection.
+func (c *Client) Close() error {
+ // Placeholder for closing Redis connection
+ return nil
+}
+
+// Ping checks if the Redis connection is alive.
+func (c *Client) Ping(ctx context.Context) error {
+ // Placeholder for ping implementation
+ return nil
+}
+
+// HealthCheck performs a health check on Redis.
+func (c *Client) HealthCheck(ctx context.Context) error {
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ return c.Ping(ctx)
+}
+
+// Set stores a key-value pair with expiration.
+func (c *Client) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
+ // Placeholder for set implementation
+ return fmt.Errorf("not implemented")
+}
+
+// Get retrieves a value by key.
+func (c *Client) Get(ctx context.Context, key string) (string, error) {
+ // Placeholder for get implementation
+ return "", fmt.Errorf("not implemented")
+}
+
+// Delete removes a key.
+func (c *Client) Delete(ctx context.Context, keys ...string) error {
+ // Placeholder for delete implementation
+ return fmt.Errorf("not implemented")
+}
+
+// Exists checks if a key exists.
+func (c *Client) Exists(ctx context.Context, key string) (bool, error) {
+ // Placeholder for exists implementation
+ return false, fmt.Errorf("not implemented")
+}
+
+// Incr increments a counter.
+func (c *Client) Incr(ctx context.Context, key string) (int64, error) {
+ // Placeholder for incr implementation
+ return 0, fmt.Errorf("not implemented")
+}
+
+// Expire sets expiration on a key.
+func (c *Client) Expire(ctx context.Context, key string, expiration time.Duration) error {
+ // Placeholder for expire implementation
+ return fmt.Errorf("not implemented")
+}
+
+// ConfigFromEnv creates a Config from environment variables.
+func ConfigFromEnv() Config {
+ // TODO: Implement environment variable parsing
+ return DefaultConfig()
+}
diff --git a/backend/shared/infra/observability/logging/logger.go b/backend/shared/infra/observability/logging/logger.go
new file mode 100644
index 0000000..9d95748
--- /dev/null
+++ b/backend/shared/infra/observability/logging/logger.go
@@ -0,0 +1,192 @@
+// Package logging provides structured logging for the KnowFoolery application.
+package logging
+
+import (
+ "io"
+ "os"
+ "time"
+
+ "github.com/rs/zerolog"
+)
+
+// Config holds the configuration for the logger.
+type Config struct {
+ Level string
+ Environment string
+ ServiceName string
+ Version string
+ Output io.Writer
+}
+
+// DefaultConfig returns a default configuration.
+func DefaultConfig() Config {
+ return Config{
+ Level: "info",
+ Environment: "development",
+ ServiceName: "knowfoolery",
+ Version: "0.0.0",
+ Output: os.Stdout,
+ }
+}
+
+// Logger wraps zerolog.Logger with application-specific methods.
+type Logger struct {
+ logger zerolog.Logger
+}
+
+// NewLogger creates a new Logger.
+func NewLogger(config Config) *Logger {
+ // Parse log level
+ level, err := zerolog.ParseLevel(config.Level)
+ if err != nil {
+ level = zerolog.InfoLevel
+ }
+
+ // Set global level
+ zerolog.SetGlobalLevel(level)
+ zerolog.TimeFieldFormat = time.RFC3339Nano
+
+ var logger zerolog.Logger
+ output := config.Output
+ if output == nil {
+ output = os.Stdout
+ }
+
+ if config.Environment == "development" {
+ // Human-readable console output for development
+ logger = zerolog.New(zerolog.ConsoleWriter{
+ Out: output,
+ TimeFormat: "15:04:05",
+ }).With().Timestamp().Logger()
+ } else {
+ // JSON output for production
+ logger = zerolog.New(output).With().Timestamp().Logger()
+ }
+
+ // Add service metadata
+ logger = logger.With().
+ Str("service", config.ServiceName).
+ Str("version", config.Version).
+ Str("environment", config.Environment).
+ Logger()
+
+ return &Logger{logger: logger}
+}
+
+// Debug logs a debug message.
+func (l *Logger) Debug(msg string) {
+ l.logger.Debug().Msg(msg)
+}
+
+// Info logs an info message.
+func (l *Logger) Info(msg string) {
+ l.logger.Info().Msg(msg)
+}
+
+// Warn logs a warning message.
+func (l *Logger) Warn(msg string) {
+ l.logger.Warn().Msg(msg)
+}
+
+// Error logs an error message.
+func (l *Logger) Error(msg string) {
+ l.logger.Error().Msg(msg)
+}
+
+// Fatal logs a fatal message and exits.
+func (l *Logger) Fatal(msg string) {
+ l.logger.Fatal().Msg(msg)
+}
+
+// WithError adds an error to the log entry.
+func (l *Logger) WithError(err error) *Logger {
+ return &Logger{
+ logger: l.logger.With().Err(err).Logger(),
+ }
+}
+
+// WithField adds a field to the log entry.
+func (l *Logger) WithField(key string, value interface{}) *Logger {
+ return &Logger{
+ logger: l.logger.With().Interface(key, value).Logger(),
+ }
+}
+
+// WithFields adds multiple fields to the log entry.
+func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
+ ctx := l.logger.With()
+ for k, v := range fields {
+ ctx = ctx.Interface(k, v)
+ }
+ return &Logger{
+ logger: ctx.Logger(),
+ }
+}
+
+// GameEvent logs a game-related event.
+func (l *Logger) GameEvent(event string, gameSessionID, userID string, properties map[string]interface{}) {
+ l.logger.Info().
+ Str("event_type", "game").
+ Str("event", event).
+ Str("game_session_id", gameSessionID).
+ Str("user_id", userID).
+ Fields(properties).
+ Msg("Game event occurred")
+}
+
+// APIRequest logs an API request.
+func (l *Logger) APIRequest(method, path string, statusCode int, duration time.Duration, userID string) {
+ l.logger.Info().
+ Str("event_type", "api_request").
+ Str("method", method).
+ Str("path", path).
+ Int("status_code", statusCode).
+ Dur("duration_ms", duration).
+ Str("user_id", userID).
+ Msg("API request processed")
+}
+
+// DatabaseOperation logs a database operation.
+func (l *Logger) DatabaseOperation(operation, table string, duration time.Duration, rowsAffected int64) {
+ l.logger.Debug().
+ Str("event_type", "database").
+ Str("operation", operation).
+ Str("table", table).
+ Dur("duration_ms", duration).
+ Int64("rows_affected", rowsAffected).
+ Msg("Database operation completed")
+}
+
+// AuthenticationEvent logs an authentication event.
+func (l *Logger) AuthenticationEvent(event, userID, userType string, success bool, details map[string]string) {
+ logEntry := l.logger.Info()
+ if !success {
+ logEntry = l.logger.Warn()
+ }
+
+ logEntry.
+ Str("event_type", "authentication").
+ Str("event", event).
+ Str("user_id", userID).
+ Str("user_type", userType).
+ Bool("success", success).
+ Fields(details).
+ Msg("Authentication event")
+}
+
+// SecurityEvent logs a security event.
+func (l *Logger) SecurityEvent(event, userID, ipAddress string, severity string, details map[string]interface{}) {
+ l.logger.Warn().
+ Str("event_type", "security").
+ Str("event", event).
+ Str("user_id", userID).
+ Str("ip_address", ipAddress).
+ Str("severity", severity).
+ Fields(details).
+ Msg("Security event detected")
+}
+
+// Zerolog returns the underlying zerolog.Logger for advanced usage.
+func (l *Logger) Zerolog() zerolog.Logger {
+ return l.logger
+}
diff --git a/backend/shared/infra/observability/metrics/prometheus.go b/backend/shared/infra/observability/metrics/prometheus.go
new file mode 100644
index 0000000..cbb0b4b
--- /dev/null
+++ b/backend/shared/infra/observability/metrics/prometheus.go
@@ -0,0 +1,202 @@
+// Package metrics provides Prometheus metrics for the KnowFoolery application.
+package metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+)
+
+// Config holds the configuration for metrics.
+type Config struct {
+ ServiceName string
+ Enabled bool
+}
+
+// DefaultConfig returns a default configuration.
+func DefaultConfig() Config {
+ return Config{
+ ServiceName: "knowfoolery",
+ Enabled: true,
+ }
+}
+
+// Metrics holds all Prometheus metrics for the application.
+type Metrics struct {
+ config Config
+
+ // HTTP metrics
+ HTTPRequestsTotal *prometheus.CounterVec
+ HTTPRequestDuration *prometheus.HistogramVec
+
+ // Database metrics
+ DBConnectionsActive *prometheus.GaugeVec
+ DBQueryDuration *prometheus.HistogramVec
+ DBErrors *prometheus.CounterVec
+
+ // Cache metrics
+ CacheOperations *prometheus.CounterVec
+ CacheKeyCount *prometheus.GaugeVec
+
+ // Authentication metrics
+ AuthAttempts *prometheus.CounterVec
+ TokenOperations *prometheus.CounterVec
+
+ // Game metrics
+ GamesStarted *prometheus.CounterVec
+ GamesCompleted *prometheus.CounterVec
+ SessionDuration *prometheus.HistogramVec
+ QuestionsAsked *prometheus.CounterVec
+ AnswersSubmitted *prometheus.CounterVec
+ HintsRequested *prometheus.CounterVec
+ ScoreDistribution *prometheus.HistogramVec
+}
+
+// NewMetrics creates a new Metrics instance with all metrics registered.
+func NewMetrics(config Config) *Metrics {
+ m := &Metrics{config: config}
+
+ // HTTP metrics
+ m.HTTPRequestsTotal = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "http_requests_total",
+ Help: "Total number of HTTP requests",
+ },
+ []string{"method", "endpoint", "status_code", "service"},
+ )
+
+ m.HTTPRequestDuration = promauto.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "http_request_duration_seconds",
+ Help: "HTTP request duration",
+ Buckets: prometheus.DefBuckets,
+ },
+ []string{"method", "endpoint", "service"},
+ )
+
+ // Database metrics
+ m.DBConnectionsActive = promauto.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Name: "db_connections_active",
+ Help: "Number of active database connections",
+ },
+ []string{"database", "service"},
+ )
+
+ m.DBQueryDuration = promauto.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "db_query_duration_seconds",
+ Help: "Database query duration",
+ Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0},
+ },
+ []string{"query_type", "table", "service"},
+ )
+
+ m.DBErrors = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "db_errors_total",
+ Help: "Total number of database errors",
+ },
+ []string{"error_type", "service"},
+ )
+
+ // Cache metrics
+ m.CacheOperations = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "cache_operations_total",
+ Help: "Total number of cache operations",
+ },
+ []string{"operation", "result", "service"},
+ )
+
+ m.CacheKeyCount = promauto.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Name: "cache_keys_total",
+ Help: "Number of keys in cache",
+ },
+ []string{"cache_type", "service"},
+ )
+
+ // Authentication metrics
+ m.AuthAttempts = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "authentication_attempts_total",
+ Help: "Total authentication attempts",
+ },
+ []string{"method", "result", "user_type"},
+ )
+
+ m.TokenOperations = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "token_operations_total",
+ Help: "JWT token operations",
+ },
+ []string{"operation", "result"},
+ )
+
+ // Game metrics
+ m.GamesStarted = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "games_started_total",
+ Help: "Total number of games started",
+ },
+ []string{"player_type", "platform"},
+ )
+
+ m.GamesCompleted = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "games_completed_total",
+ Help: "Total number of games completed",
+ },
+ []string{"completion_type", "platform"},
+ )
+
+ m.SessionDuration = promauto.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "game_session_duration_seconds",
+ Help: "Duration of game sessions",
+ Buckets: []float64{60, 300, 600, 900, 1200, 1500, 1800},
+ },
+ []string{"completion_type"},
+ )
+
+ m.QuestionsAsked = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "questions_asked_total",
+ Help: "Total number of questions asked",
+ },
+ []string{"theme", "difficulty"},
+ )
+
+ m.AnswersSubmitted = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "answers_submitted_total",
+ Help: "Total number of answers submitted",
+ },
+ []string{"theme", "is_correct", "attempt_number", "used_hint"},
+ )
+
+ m.HintsRequested = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "hints_requested_total",
+ Help: "Total number of hints requested",
+ },
+ []string{"theme", "question_difficulty"},
+ )
+
+ m.ScoreDistribution = promauto.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "game_scores",
+ Help: "Distribution of game scores",
+ Buckets: []float64{0, 5, 10, 15, 20, 25, 30, 40, 50, 60, 80, 100},
+ },
+ []string{"session_duration_bucket"},
+ )
+
+ return m
+}
+
+// ConfigFromEnv creates a Config from environment variables.
+func ConfigFromEnv() Config {
+ // TODO: Implement environment variable parsing
+ return DefaultConfig()
+}
diff --git a/backend/shared/infra/observability/tracing/tracer.go b/backend/shared/infra/observability/tracing/tracer.go
new file mode 100644
index 0000000..655351c
--- /dev/null
+++ b/backend/shared/infra/observability/tracing/tracer.go
@@ -0,0 +1,123 @@
+// Package tracing provides distributed tracing for the KnowFoolery application.
+package tracing
+
+import (
+ "context"
+ "fmt"
+)
+
+// Config holds the configuration for the tracer.
+type Config struct {
+ ServiceName string
+ ServiceVersion string
+ Environment string
+ JaegerEndpoint string
+ SampleRate float64
+ Enabled bool
+}
+
+// DefaultConfig returns a default configuration.
+func DefaultConfig() Config {
+ return Config{
+ ServiceName: "knowfoolery",
+ ServiceVersion: "0.0.0",
+ Environment: "development",
+ JaegerEndpoint: "http://localhost:14268/api/traces",
+ SampleRate: 1.0, // Sample all traces in development
+ Enabled: false,
+ }
+}
+
+// Tracer provides distributed tracing functionality.
+// This is a placeholder that should be implemented with OpenTelemetry.
+type Tracer struct {
+ config Config
+}
+
+// NewTracer creates a new Tracer.
+// Note: Actual implementation should use OpenTelemetry SDK.
+func NewTracer(config Config) (*Tracer, error) {
+ if !config.Enabled {
+ return &Tracer{config: config}, nil
+ }
+
+ // TODO: Initialize OpenTelemetry tracer provider
+ // Example implementation:
+ // - Create Jaeger exporter
+ // - Create tracer provider with batching
+ // - Set global tracer provider
+ // - Configure resource with service info
+
+ return &Tracer{config: config}, nil
+}
+
+// Shutdown gracefully shuts down the tracer.
+func (t *Tracer) Shutdown(ctx context.Context) error {
+ if !t.config.Enabled {
+ return nil
+ }
+
+ // TODO: Shutdown tracer provider
+ return nil
+}
+
+// StartSpan starts a new span.
+// This is a placeholder that should be implemented with OpenTelemetry.
+func (t *Tracer) StartSpan(ctx context.Context, name string) (context.Context, Span) {
+ // TODO: Start actual OpenTelemetry span
+ return ctx, &noopSpan{}
+}
+
+// Span represents a tracing span.
+type Span interface {
+ // End ends the span.
+ End()
+ // SetAttribute sets an attribute on the span.
+ SetAttribute(key string, value interface{})
+ // RecordError records an error on the span.
+ RecordError(err error)
+}
+
+// noopSpan is a no-op implementation of Span.
+type noopSpan struct{}
+
+func (s *noopSpan) End() {}
+func (s *noopSpan) SetAttribute(key string, value interface{}) {}
+func (s *noopSpan) RecordError(err error) {}
+
+// TraceServiceOperation traces a service operation.
+func TraceServiceOperation(ctx context.Context, tracer *Tracer, serviceName, operation string, fn func(context.Context) error) error {
+ ctx, span := tracer.StartSpan(ctx, fmt.Sprintf("%s.%s", serviceName, operation))
+ defer span.End()
+
+ err := fn(ctx)
+ if err != nil {
+ span.RecordError(err)
+ span.SetAttribute("error", true)
+ }
+
+ return err
+}
+
+// TraceDatabaseOperation traces a database operation.
+func TraceDatabaseOperation(ctx context.Context, tracer *Tracer, operation, table string, fn func(context.Context) error) error {
+ ctx, span := tracer.StartSpan(ctx, fmt.Sprintf("db.%s.%s", operation, table))
+ defer span.End()
+
+ span.SetAttribute("db.operation", operation)
+ span.SetAttribute("db.table", table)
+ span.SetAttribute("db.system", "postgresql")
+
+ err := fn(ctx)
+ if err != nil {
+ span.RecordError(err)
+ }
+
+ return err
+}
+
+// ConfigFromEnv creates a Config from environment variables.
+func ConfigFromEnv() Config {
+ // TODO: Implement environment variable parsing
+ return DefaultConfig()
+}
diff --git a/backend/shared/infra/security/sanitize.go b/backend/shared/infra/security/sanitize.go
new file mode 100644
index 0000000..79ef297
--- /dev/null
+++ b/backend/shared/infra/security/sanitize.go
@@ -0,0 +1,172 @@
+// Package security provides security utilities for the KnowFoolery application.
+package security
+
+import (
+ "html"
+ "regexp"
+ "strings"
+ "unicode"
+)
+
+// SanitizeOptions configures sanitization behavior.
+type SanitizeOptions struct {
+ TrimWhitespace bool
+ RemoveMultipleSpaces bool
+ HTMLEscape bool
+ MaxLength int
+ AllowedPattern *regexp.Regexp
+}
+
+// DefaultSanitizeOptions returns default sanitization options.
+func DefaultSanitizeOptions() SanitizeOptions {
+ return SanitizeOptions{
+ TrimWhitespace: true,
+ RemoveMultipleSpaces: true,
+ HTMLEscape: true,
+ MaxLength: 0, // No limit
+ }
+}
+
+// Sanitize sanitizes a string according to the given options.
+func Sanitize(input string, opts SanitizeOptions) string {
+ result := input
+
+ // Trim whitespace
+ if opts.TrimWhitespace {
+ result = strings.TrimSpace(result)
+ }
+
+ // Remove multiple consecutive spaces
+ if opts.RemoveMultipleSpaces {
+ spaceRegex := regexp.MustCompile(`\s+`)
+ result = spaceRegex.ReplaceAllString(result, " ")
+ }
+
+ // HTML escape
+ if opts.HTMLEscape {
+ result = html.EscapeString(result)
+ }
+
+ // Apply maximum length
+ if opts.MaxLength > 0 && len(result) > opts.MaxLength {
+ result = result[:opts.MaxLength]
+ }
+
+ // Validate against allowed pattern
+ if opts.AllowedPattern != nil && !opts.AllowedPattern.MatchString(result) {
+ return ""
+ }
+
+ return result
+}
+
+// SanitizePlayerName sanitizes a player name.
+func SanitizePlayerName(input string) string {
+ opts := SanitizeOptions{
+ TrimWhitespace: true,
+ RemoveMultipleSpaces: true,
+ HTMLEscape: true,
+ MaxLength: 50,
+ AllowedPattern: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`),
+ }
+ return Sanitize(input, opts)
+}
+
+// SanitizeAnswer sanitizes an answer submission.
+func SanitizeAnswer(input string) string {
+ opts := SanitizeOptions{
+ TrimWhitespace: true,
+ RemoveMultipleSpaces: true,
+ HTMLEscape: true,
+ MaxLength: 500,
+ }
+
+ result := Sanitize(input, opts)
+
+ // Normalize to lowercase for comparison
+ result = strings.ToLower(result)
+
+ return result
+}
+
+// SanitizeQuestionText sanitizes question text (admin input).
+func SanitizeQuestionText(input string) string {
+ opts := SanitizeOptions{
+ TrimWhitespace: true,
+ RemoveMultipleSpaces: true,
+ HTMLEscape: true,
+ MaxLength: 1000,
+ }
+
+ result := Sanitize(input, opts)
+
+ // Remove potential script content
+ scriptRegex := regexp.MustCompile(`(?i)`)
+ result = scriptRegex.ReplaceAllString(result, "")
+
+ return result
+}
+
+// SanitizeTheme sanitizes a theme name.
+func SanitizeTheme(input string) string {
+ opts := SanitizeOptions{
+ TrimWhitespace: true,
+ RemoveMultipleSpaces: true,
+ HTMLEscape: true,
+ MaxLength: 100,
+ AllowedPattern: regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`),
+ }
+
+ result := Sanitize(input, opts)
+
+ // Title case
+ words := strings.Fields(result)
+ for i, word := range words {
+ if len(word) > 0 {
+ runes := []rune(word)
+ runes[0] = unicode.ToUpper(runes[0])
+ for j := 1; j < len(runes); j++ {
+ runes[j] = unicode.ToLower(runes[j])
+ }
+ words[i] = string(runes)
+ }
+ }
+
+ return strings.Join(words, " ")
+}
+
+// RemoveHTMLTags removes all HTML tags from a string.
+func RemoveHTMLTags(input string) string {
+ tagRegex := regexp.MustCompile(`<[^>]*>`)
+ return tagRegex.ReplaceAllString(input, "")
+}
+
+// ContainsDangerousPatterns checks if input contains potentially dangerous patterns.
+func ContainsDangerousPatterns(input string) bool {
+ dangerousPatterns := []string{
+ "javascript:",
+ "data:",
+ "vbscript:",
+ " 50 {
+ return false
+ }
+
+ // Character validation
+ for _, r := range str {
+ if !unicode.IsLetter(r) && !unicode.IsNumber(r) && !unicode.IsSpace(r) && r != '-' && r != '_' && r != '.' {
+ return false
+ }
+ }
+
+ return true
+}
+
+// Common validation tags for reuse
+const (
+ TagRequired = "required"
+ TagPlayerName = "required,player_name"
+ TagEmail = "required,email"
+ TagUUID = "required,uuid"
+ TagOptionalUUID = "omitempty,uuid"
+)