From 4143ceaf25dca03e3367437ca404d108505b92c9 Mon Sep 17 00:00:00 2001 From: oabrivard Date: Sat, 16 Aug 2025 17:20:08 +0200 Subject: [PATCH] Initial project documentation and architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Complete project requirements and game mechanics specification - Comprehensive technical architecture with microservices design - Security-first approach with authentication and anti-cheating measures - Cross-platform UI strategy using Gluestack UI framework - Development guidelines with Fiber framework and Ent ORM - Observability strategy with Prometheus, Grafana, and Jaeger - Self-hosted Zitadel authentication with repository pattern - Implementation plan with 10 phases covering all aspects πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 360 +++++++ claude-plan.md | 157 +++ docs/development-guidelines.md | 1398 +++++++++++++++++++++++++++ docs/game-mechanics.md | 476 +++++++++ docs/gluestack-ui-strategy.md | 1392 ++++++++++++++++++++++++++ docs/observability-strategy.md | 1487 ++++++++++++++++++++++++++++ docs/security-requirements.md | 1661 ++++++++++++++++++++++++++++++++ docs/technical-architecture.md | 837 ++++++++++++++++ docs/zitadel-integration.md | 882 +++++++++++++++++ requirements.md | 248 +++++ 10 files changed, 8898 insertions(+) create mode 100644 CLAUDE.md create mode 100644 claude-plan.md create mode 100644 docs/development-guidelines.md create mode 100644 docs/game-mechanics.md create mode 100644 docs/gluestack-ui-strategy.md create mode 100644 docs/observability-strategy.md create mode 100644 docs/security-requirements.md create mode 100644 docs/technical-architecture.md create mode 100644 docs/zitadel-integration.md create mode 100644 requirements.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d0f5e4d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,360 @@ +# Know Foolery - Claude Code Project Guide + +## Project Overview + +Know Foolery is a cross-platform quiz game with a Go microservices backend and React/React Native frontend using Gluestack UI. This document provides guidance for future Claude Code sessions. + +## Project Structure + +``` +knowfoolery/ +β”œβ”€β”€ docs/ # Project documentation +β”‚ β”œβ”€β”€ architecture.md # System architecture +β”‚ β”œβ”€β”€ api-specs.md # API specifications +β”‚ └── deployment.md # Deployment guides +β”œβ”€β”€ backend/ # Go microservices +β”‚ β”œβ”€β”€ services/ # Individual microservices +β”‚ β”‚ β”œβ”€β”€ game-service/ # Game logic and session management +β”‚ β”‚ β”œβ”€β”€ question-service/ # Question CRUD operations +β”‚ β”‚ β”œβ”€β”€ user-service/ # User management +β”‚ β”‚ β”œβ”€β”€ leaderboard-service/ # Score and ranking logic +β”‚ β”‚ β”œβ”€β”€ session-service/ # Session and timer management +β”‚ β”‚ β”œβ”€β”€ admin-service/ # Admin panel backend +β”‚ β”‚ └── gateway-service/ # API gateway +β”‚ β”œβ”€β”€ shared/ # Shared Go packages +β”‚ β”‚ β”œβ”€β”€ auth/ # JWT middleware and Zitadel integration +β”‚ β”‚ β”œβ”€β”€ database/ # Ent database client +β”‚ β”‚ β”œβ”€β”€ observability/ # Metrics and tracing +β”‚ β”‚ └── security/ # Security utilities +β”‚ └── scripts/ # Build and deployment scripts +β”œβ”€β”€ frontend/ # Cross-platform frontend +β”‚ β”œβ”€β”€ packages/ # Shared packages +β”‚ β”‚ β”œβ”€β”€ ui-components/ # Gluestack UI components +β”‚ β”‚ β”œβ”€β”€ shared-logic/ # Business logic +β”‚ β”‚ └── shared-types/ # TypeScript types +β”‚ β”œβ”€β”€ web/ # React web application +β”‚ β”œβ”€β”€ mobile/ # React Native mobile app +β”‚ └── desktop/ # Electron desktop app (future) +β”œβ”€β”€ infrastructure/ # Infrastructure as code +β”‚ β”œβ”€β”€ docker/ # Docker configurations +β”‚ β”œβ”€β”€ kubernetes/ # K8s manifests +β”‚ └── monitoring/ # Observability configs +β”œβ”€β”€ tests/ # Testing utilities +β”‚ β”œβ”€β”€ e2e/ # End-to-end tests +β”‚ β”œβ”€β”€ load/ # Performance tests +β”‚ └── security/ # Security tests +β”œβ”€β”€ requirements.md # Project requirements +└── README.md # Setup and development guide +``` + +## Technology Stack + +### Backend (Go) +- **Framework**: Fiber for HTTP routing +- **Database ORM**: Ent with PostgreSQL +- **Authentication**: Zitadel OAuth 2.0/OIDC +- **Observability**: Prometheus metrics, OpenTelemetry tracing +- **Communication**: gRPC for inter-service communication +- **Testing**: Go testing + testcontainers +- **Security**: Comprehensive input validation, JWT middleware + +### Frontend (TypeScript) +- **Web**: React 18 with TypeScript +- **Mobile**: React Native with TypeScript +- **UI Framework**: Gluestack UI with NativeWind/Tailwind CSS +- **State Management**: React Context + useReducer +- **Testing**: Jest + React Testing Library + Playwright + +### Database & Infrastructure +- **Primary Database**: PostgreSQL with Ent ORM +- **Cache**: Redis for sessions and performance +- **Authentication**: Self-hosted Zitadel +- **Deployment**: Docker + Kubernetes +- **Monitoring**: Prometheus + Grafana + Jaeger + +## Key Architectural Decisions + +### 1. Database Strategy +- **Development**: SQLite for fast local development +- **Testing**: PostgreSQL with testcontainers +- **Production**: PostgreSQL with high availability +- **ORM**: Ent (Facebook) for type-safe database operations +- **No Repository Pattern**: Direct Ent usage except for external APIs + +### 2. Authentication Strategy +- **Local Development**: Mock JWT service +- **Production**: Self-hosted Zitadel +- **Repository Pattern**: Only for external integrations (Zitadel) +- **Security**: MFA required for admin accounts + +### 3. Cross-Platform UI Strategy +- **Design System**: Gluestack UI for consistent components +- **Styling**: NativeWind/Tailwind CSS for utility-first styling +- **Code Sharing**: Same components work on web and mobile +- **Performance**: Native performance on mobile, optimized for web + +## Development Guidelines + +### Code Organization +```go +// Backend service structure +services/ +β”œβ”€β”€ game-service/ +β”‚ β”œβ”€β”€ cmd/main.go # Service entry point +β”‚ β”œβ”€β”€ internal/ +β”‚ β”‚ β”œβ”€β”€ handlers/ # HTTP handlers +β”‚ β”‚ β”œβ”€β”€ services/ # Business logic +β”‚ β”‚ └── models/ # Ent models +β”‚ β”œβ”€β”€ api/ # API definitions +β”‚ └── tests/ # Service tests +``` + +```typescript +// Frontend component structure +packages/ui-components/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ GameCard/ +β”‚ β”‚ β”œβ”€β”€ GameCard.tsx # Main component +β”‚ β”‚ β”œβ”€β”€ GameCard.test.tsx # Tests +β”‚ β”‚ └── index.ts # Exports +β”‚ └── index.ts # Package exports +``` + +### Naming Conventions +- **Go**: PascalCase for exported, camelCase for unexported +- **TypeScript**: PascalCase for components, camelCase for functions +- **Files**: kebab-case for directories, PascalCase for components +- **Database**: snake_case for tables and columns + +### Testing Standards +- **Unit Tests**: Required for all business logic +- **Integration Tests**: Required for database operations +- **E2E Tests**: Required for critical user journeys +- **Security Tests**: Required for all input validation +- **Performance Tests**: Required for high-load scenarios + +## Security Requirements + +### Critical Security Measures (Implemented Early) +1. **Input Validation**: All user inputs sanitized and validated +2. **SQL Injection Prevention**: Ent parameterized queries only +3. **XSS Protection**: HTML escaping and CSP headers +4. **Authentication**: JWT validation with secure middleware +5. **Rate Limiting**: Per-user and global rate limits +6. **HTTPS**: TLS encryption for all communications +7. **CORS**: Strict cross-origin policies + +### Game Integrity +- **Server-Side Validation**: All game logic on backend +- **Score Verification**: Server calculates all scores +- **Session Security**: Tamper-proof session management +- **Anti-Cheating**: Multiple validation layers + +## Common Commands + +### Backend Development +```bash +# Start all services locally +docker-compose up -d + +# Run specific service +cd backend/services/game-service +go run cmd/main.go + +# Run tests with coverage +go test -v -cover ./... + +# Generate Ent code +go generate ./... +``` + +### Frontend Development +```bash +# Install dependencies +npm install + +# Start web development +cd frontend/web +npm run dev + +# Start mobile development +cd frontend/mobile +npm run ios # or npm run android + +# Run tests +npm test + +# Build for production +npm run build +``` + +### Database Operations +```bash +# Run migrations +go run backend/scripts/migrate.go + +# Reset database (development only) +go run backend/scripts/reset-db.go + +# Seed test data +go run backend/scripts/seed.go +``` + +## Environment Configuration + +### Development (.env.dev) +```bash +# Database +DATABASE_URL=sqlite://./dev.db +REDIS_URL=redis://localhost:6379 + +# Authentication +JWT_SECRET=dev-secret-key +ZITADEL_URL=http://localhost:8080 + +# Observability +PROMETHEUS_PORT=9090 +JAEGER_ENDPOINT=http://localhost:14268 +``` + +### Production (.env.prod) +```bash +# Database +DATABASE_URL=postgres://user:pass@db:5432/knowfoolery +REDIS_URL=redis://redis:6379 + +# Authentication +JWT_SECRET=${JWT_SECRET} +ZITADEL_URL=${ZITADEL_URL} + +# Observability +PROMETHEUS_PORT=9090 +JAEGER_ENDPOINT=${JAEGER_ENDPOINT} +``` + +## Development Workflow + +### 1. Feature Development +1. Create feature branch from main +2. Implement backend service changes +3. Add comprehensive tests +4. Implement frontend components +5. Test cross-platform compatibility +6. Update documentation +7. Create pull request + +### 2. Testing Checklist +- [ ] Unit tests pass +- [ ] Integration tests pass +- [ ] Security tests pass +- [ ] E2E tests pass +- [ ] Cross-platform UI tests pass +- [ ] Performance tests pass + +### 3. Deployment Process +1. Merge to main branch +2. Automated CI/CD pipeline +3. Deploy to staging environment +4. Run smoke tests +5. Deploy to production +6. Monitor metrics and alerts + +## Troubleshooting + +### Common Issues + +#### Database Connection Errors +```bash +# Check database status +docker-compose ps + +# Reset database connection +docker-compose restart postgres + +# Check logs +docker-compose logs postgres +``` + +#### Authentication Issues +```bash +# Verify JWT token +curl -H "Authorization: Bearer $TOKEN" localhost:8080/api/health + +# Check Zitadel status +curl localhost:8080/.well-known/openid_configuration +``` + +#### Frontend Build Issues +```bash +# Clear node modules +rm -rf node_modules package-lock.json +npm install + +# Clear build cache +npm run clean +npm run build +``` + +## Performance Optimization + +### Backend Optimization +- Use Ent query optimization +- Implement Redis caching +- Profile with pprof +- Monitor with Prometheus + +### Frontend Optimization +- Bundle size analysis +- React Native performance profiling +- Gluestack UI optimization +- Image and asset optimization + +## Security Checklist + +### Before Each Release +- [ ] Security dependencies updated +- [ ] Vulnerability scan passed +- [ ] Penetration testing completed +- [ ] Security headers configured +- [ ] Input validation tested +- [ ] Authentication flows verified +- [ ] Rate limiting tested +- [ ] Audit logs reviewed + +## Monitoring and Alerts + +### Key Metrics to Watch +- **Response Times**: API endpoint performance +- **Error Rates**: Application stability +- **Authentication**: Success/failure rates +- **Game Metrics**: Session duration, completion rates +- **Security Events**: Failed logins, rate limit hits + +### Alert Conditions +- API response time > 500ms +- Error rate > 1% +- Authentication failure rate > 5% +- Database connection issues +- Security events detected + +## Future Development Notes + +### Mobile App Considerations +- Platform-specific optimizations needed +- App store submission requirements +- Push notification integration +- Offline capability planning + +### Desktop App Considerations +- Electron performance optimization +- Native OS integration +- Auto-update mechanism +- Platform-specific packaging + +### Scalability Planning +- Horizontal scaling strategies +- Database sharding considerations +- CDN integration for global reach +- Microservice boundaries refinement + +This guide should be updated as the project evolves and new decisions are made. \ No newline at end of file diff --git a/claude-plan.md b/claude-plan.md new file mode 100644 index 0000000..487fe43 --- /dev/null +++ b/claude-plan.md @@ -0,0 +1,157 @@ +# Know Foolery - Claude Implementation Plan + +## Project Overview + +**Know Foolery** is a comprehensive quiz game application based on the French game "DΓ©connaissance". The project implements a cross-platform solution with web, mobile (iOS/Android), and desktop applications, featuring secure authentication, microservices architecture, and comprehensive observability. + +## Technology Stack + +- **Backend**: Go with Fiber framework, microservices architecture +- **Database**: PostgreSQL (production), SQLite (development) +- **ORM**: Ent for type-safe database operations +- **Cache**: Redis for session management and caching +- **Authentication**: Self-hosted Zitadel (OAuth 2.0/OIDC) +- **Frontend**: React (web), React Native (mobile), Wails (desktop) +- **UI Framework**: Gluestack UI for cross-platform consistency +- **Styling**: NativeWind/Tailwind CSS +- **Monitoring**: Prometheus, Grafana, Jaeger +- **Containerization**: Docker with Docker Compose + +## Implementation Phases + +### Phase 0: Documentation & Planning βœ… COMPLETED +- [x] Create requirements.md with complete application requirements +- [x] Create CLAUDE.md project guidance file +- [x] Document game mechanics and scoring system +- [x] Document technical requirements and architecture decisions +- [x] Create architecture documentation with Zitadel integration +- [x] Document development guidelines and coding standards +- [x] Document observability strategy +- [x] Document security requirements +- [x] Document cross-platform UI strategy with Gluestack UI + +**Completion Date**: Phase 0 completed successfully with comprehensive documentation covering all aspects of the project. + +### Phase 1: Project Setup & Core Security Infrastructure +- [ ] Initialize Go project structure with microservices architecture +- [ ] Set up Ent schema definitions for core entities (User, Quiz, Question, etc.) +- [ ] Create ZitadelRepository interface for external auth integration +- [ ] Set up development environment with Docker Compose +- [ ] Initialize Gluestack UI setup with theme configuration +- [ ] Implement core security middleware and input validation +- [ ] Set up Prometheus metrics collection foundation +- [ ] Create basic CI/CD pipeline structure + +### Phase 2: Authentication & User Management +- [ ] Implement Zitadel integration with repository pattern +- [ ] Create JWT validation middleware with circuit breaker +- [ ] Build user registration and profile management +- [ ] Implement role-based access control (player/admin) +- [ ] Add MFA requirements for admin access +- [ ] Create secure token storage for each platform +- [ ] Implement comprehensive authentication testing + +### Phase 3: Core Game Engine +- [ ] Implement quiz engine with theme and difficulty support +- [ ] Create question management system with validation +- [ ] Build scoring system with anti-cheating measures +- [ ] Implement session management (30-minute time limits) +- [ ] Add hint system with progressive revelation +- [ ] Create leaderboard functionality +- [ ] Implement comprehensive game testing + +### Phase 4: Cross-Platform Frontend +- [ ] Build React web application with Gluestack UI +- [ ] Create React Native mobile applications +- [ ] Develop Wails desktop application +- [ ] Implement shared authentication flows +- [ ] Create responsive game interface components +- [ ] Add platform-specific optimizations +- [ ] Implement comprehensive frontend testing + +### Phase 5: Admin Panel & Content Management +- [ ] Build secure admin dashboard +- [ ] Create question management interface +- [ ] Implement user administration tools +- [ ] Add analytics and reporting features +- [ ] Create content import/export functionality +- [ ] Implement admin-specific security measures +- [ ] Add comprehensive admin testing + +### Phase 6: Performance & Optimization +- [ ] Implement Redis caching strategy +- [ ] Add database query optimization +- [ ] Create CDN integration for assets +- [ ] Implement API rate limiting +- [ ] Add frontend performance optimization +- [ ] Create load testing and benchmarks + +### Phase 7: Monitoring & Observability +- [ ] Complete Prometheus metrics implementation +- [ ] Set up Grafana dashboards +- [ ] Implement distributed tracing with Jaeger +- [ ] Add comprehensive logging strategy +- [ ] Create alerting and notification system +- [ ] Implement health checks and monitoring + +### Phase 8: Security Hardening +- [ ] Conduct comprehensive security audit +- [ ] Implement advanced anti-cheating measures +- [ ] Add security monitoring and incident response +- [ ] Create backup and disaster recovery procedures +- [ ] Implement data encryption at rest +- [ ] Add comprehensive security testing + +### Phase 9: Production Deployment +- [ ] Set up production infrastructure +- [ ] Configure CI/CD for production deployment +- [ ] Implement monitoring and alerting in production +- [ ] Create deployment documentation +- [ ] Conduct production readiness review +- [ ] Implement production security measures + +### Phase 10: Launch & Post-Launch +- [ ] Conduct final testing and quality assurance +- [ ] Deploy to production environment +- [ ] Monitor launch metrics and performance +- [ ] Address any post-launch issues +- [ ] Gather user feedback and analytics +- [ ] Plan future feature development + +## Key Architectural Decisions + +1. **Repository Pattern**: Used only for external API integrations (Zitadel), direct Ent client usage for database operations +2. **Security First**: Critical vulnerabilities addressed in early phases (1-3), enhancements in Phase 8 +3. **Cross-Platform UI**: Gluestack UI for consistent design across web, mobile, and desktop +4. **Microservices**: Go with Fiber framework for high-performance, maintainable services +5. **Self-Hosted Auth**: Zitadel for complete control over authentication and user data +6. **Comprehensive Testing**: Unit and integration testing after each implementation phase + +## Current Status + +**Phase 0 Complete**: All documentation has been created and is ready for implementation. The project has comprehensive documentation covering: + +- Complete requirements and specifications +- Technical architecture with microservices design +- Security requirements and implementation strategy +- Cross-platform UI strategy with Gluestack UI +- Development guidelines and coding standards +- Observability and monitoring strategy +- Game mechanics and anti-cheating measures +- Zitadel authentication integration + +**Next Steps**: Ready to begin Phase 1 (Project Setup & Core Security Infrastructure) upon approval to proceed with implementation. + +## Files Created + +1. `/requirements.md` - Complete project requirements and specifications +2. `/CLAUDE.md` - Project guidance for future Claude Code sessions +3. `/docs/game-mechanics.md` - Game flow, scoring, and anti-cheating measures +4. `/docs/technical-architecture.md` - System architecture and service design +5. `/docs/zitadel-integration.md` - Authentication architecture and implementation +6. `/docs/development-guidelines.md` - Coding standards and patterns +7. `/docs/observability-strategy.md` - Monitoring and metrics strategy +8. `/docs/security-requirements.md` - Security strategy and requirements +9. `/docs/gluestack-ui-strategy.md` - Cross-platform UI implementation strategy + +All documentation includes comprehensive code examples, implementation details, and best practices for building a secure, scalable quiz game application. \ No newline at end of file diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md new file mode 100644 index 0000000..04759ff --- /dev/null +++ b/docs/development-guidelines.md @@ -0,0 +1,1398 @@ +# Know Foolery - Development Guidelines & Coding Standards + +## Overview + +This document establishes coding standards, development practices, and guidelines for the Know Foolery project to ensure consistency, maintainability, and quality across the codebase. + +## Code Organization + +### Project Structure Standards + +``` +knowfoolery/ +β”œβ”€β”€ backend/ +β”‚ β”œβ”€β”€ services/ # Microservices +β”‚ β”‚ β”œβ”€β”€ {service-name}/ +β”‚ β”‚ β”‚ β”œβ”€β”€ cmd/ +β”‚ β”‚ β”‚ β”‚ └── main.go # Service entry point +β”‚ β”‚ β”‚ β”œβ”€β”€ internal/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ handlers/ # Fiber HTTP handlers +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ services/ # Business logic +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ middleware/ # Service-specific middleware +β”‚ β”‚ β”‚ β”‚ └── models/ # Ent models and schemas +β”‚ β”‚ β”‚ β”œβ”€β”€ api/ # API definitions (OpenAPI/gRPC) +β”‚ β”‚ β”‚ β”œβ”€β”€ config/ # Configuration management +β”‚ β”‚ β”‚ β”œβ”€β”€ tests/ # Service-specific tests +β”‚ β”‚ β”‚ β”œβ”€β”€ Dockerfile # Container definition +β”‚ β”‚ β”‚ └── go.mod # Go module +β”‚ β”‚ └── shared/ # Shared packages +β”‚ β”‚ β”œβ”€β”€ auth/ # JWT middleware & Zitadel integration +β”‚ β”‚ β”œβ”€β”€ database/ # Ent database client +β”‚ β”‚ β”œβ”€β”€ observability/ # Metrics and tracing +β”‚ β”‚ β”œβ”€β”€ security/ # Security utilities +β”‚ β”‚ └── utils/ # Common utilities +β”œβ”€β”€ frontend/ +β”‚ β”œβ”€β”€ packages/ +β”‚ β”‚ β”œβ”€β”€ ui-components/ # Shared Gluestack UI components +β”‚ β”‚ β”œβ”€β”€ shared-logic/ # Business logic +β”‚ β”‚ β”œβ”€β”€ shared-types/ # TypeScript types +β”‚ β”‚ └── shared-utils/ # Utility functions +β”‚ β”œβ”€β”€ apps/ +β”‚ β”‚ β”œβ”€β”€ web/ # React web app +β”‚ β”‚ β”œβ”€β”€ mobile/ # React Native app +β”‚ β”‚ └── desktop/ # Wails desktop app +└── infrastructure/ # DevOps and deployment +``` + +### Naming Conventions + +#### Go Code +```go +// Files: snake_case +user_service.go +jwt_middleware.go +database_client.go + +// Packages: lowercase, single word when possible +package auth +package database +package observability + +// Exported types: PascalCase +type GameService struct {} +type AuthClaims struct {} +type DatabaseConfig struct {} + +// Unexported types: camelCase +type userRepository struct {} +type tokenValidator struct {} + +// Functions: PascalCase (exported), camelCase (unexported) +func NewGameService() *GameService {} +func (s *GameService) StartGame() error {} +func validateToken() error {} + +// Constants: PascalCase or SCREAMING_SNAKE_CASE for groups +const DefaultTimeout = 30 * time.Second +const ( + StatusActive = "active" + StatusInactive = "inactive" +) + +// Interfaces: noun or adjective + "er" suffix +type TokenValidator interface {} +type UserRepository interface {} +type ScoreCalculator interface {} +``` + +#### TypeScript Code +```typescript +// Files: kebab-case +game-card.tsx +user-service.ts +auth-context.tsx + +// Components: PascalCase +export const GameCard: React.FC = () => {} +export const LeaderboardList: React.FC = () => {} + +// Hooks: camelCase with "use" prefix +export const useGameSession = () => {} +export const useAuthentication = () => {} + +// Functions: camelCase +export const calculateScore = () => {} +export const validateAnswer = () => {} + +// Types/Interfaces: PascalCase +interface GameSessionProps {} +type AuthenticationState = 'loading' | 'authenticated' | 'unauthenticated' + +// Constants: SCREAMING_SNAKE_CASE +export const MAX_ATTEMPTS = 3 +export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes +``` + +## Go Development Standards + +### Fiber Framework Patterns + +#### Service Structure +```go +// Service initialization with Fiber +package main + +import ( + "log" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/cors" + "github.com/gofiber/fiber/v3/middleware/logger" + "github.com/gofiber/fiber/v3/middleware/recover" +) + +func main() { + // Create Fiber app with configuration + app := fiber.New(fiber.Config{ + AppName: "Know Foolery Game Service", + ServerHeader: "Fiber", + ErrorHandler: customErrorHandler, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + IdleTimeout: time.Second * 120, + }) + + // Global middleware + app.Use(logger.New()) + app.Use(recover.New()) + app.Use(cors.New(cors.Config{ + AllowOrigins: "http://localhost:3000, https://app.knowfoolery.com", + AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + AllowHeaders: "Origin, Content-Type, Authorization", + })) + + // Health check endpoint + app.Get("/health", healthCheck) + + // API routes with versioning + api := app.Group("/api/v1") + setupGameRoutes(api) + + // Start server + log.Fatal(app.Listen(":8080")) +} + +func customErrorHandler(c *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + message := "Internal Server Error" + + if e, ok := err.(*fiber.Error); ok { + code = e.Code + message = e.Message + } + + return c.Status(code).JSON(fiber.Map{ + "error": true, + "message": message, + "code": code, + }) +} +``` + +#### Handler Patterns +```go +// Handler structure and patterns +package handlers + +import ( + "github.com/gofiber/fiber/v3" + "knowfoolery/internal/services" +) + +type GameHandler struct { + gameService *services.GameService + validator *validator.Validate +} + +func NewGameHandler(gameService *services.GameService) *GameHandler { + return &GameHandler{ + gameService: gameService, + validator: validator.New(), + } +} + +// HTTP handler with proper error handling +func (h *GameHandler) StartGame(c *fiber.Ctx) error { + var req StartGameRequest + + // Parse and validate request + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": true, + "message": "Invalid request body", + }) + } + + if err := h.validator.Struct(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": true, + "message": "Validation failed", + "details": err.Error(), + }) + } + + // Extract user context from JWT middleware + userID := c.Locals("user_id").(string) + userRole := c.Locals("user_role").(string) + + // Business logic + gameSession, err := h.gameService.StartGame(c.Context(), userID, req.PlayerName) + if err != nil { + return handleServiceError(c, err) + } + + return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + "success": true, + "data": gameSession, + }) +} + +// Centralized error handling +func handleServiceError(c *fiber.Ctx, err error) error { + switch { + case errors.Is(err, services.ErrInvalidInput): + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": true, + "message": "Invalid input", + "details": err.Error(), + }) + case errors.Is(err, services.ErrNotFound): + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "error": true, + "message": "Resource not found", + }) + case errors.Is(err, services.ErrUnauthorized): + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": true, + "message": "Unauthorized access", + }) + default: + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "message": "Internal server error", + }) + } +} + +// Request/Response structures with validation +type StartGameRequest struct { + PlayerName string `json:"player_name" validate:"required,min=2,max=50,alphanum_space"` +} + +type StartGameResponse struct { + SessionID string `json:"session_id"` + PlayerName string `json:"player_name"` + StartTime time.Time `json:"start_time"` + TimeRemaining int `json:"time_remaining_seconds"` +} +``` + +#### Middleware Patterns +```go +// Custom middleware for authentication +package middleware + +import ( + "github.com/gofiber/fiber/v3" + "knowfoolery/shared/auth" +) + +func JWTMiddleware(zitadelRepo auth.ZitadelRepository) fiber.Handler { + return func(c *fiber.Ctx) error { + // 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", + }) + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + if token == authHeader { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": true, + "message": "Invalid authorization header format", + }) + } + + // Validate token + claims, err := zitadelRepo.ValidateToken(c.Context(), token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": true, + "message": "Invalid token", + }) + } + + // Set user context + c.Locals("user_id", claims.Subject) + c.Locals("user_email", claims.Email) + c.Locals("user_name", claims.Name) + c.Locals("user_roles", claims.Roles) + + return c.Next() + } +} + +// Rate limiting middleware +func RateLimitMiddleware(redisClient *redis.Client) fiber.Handler { + return func(c *fiber.Ctx) error { + userID := c.Locals("user_id") + clientIP := c.IP() + + // Per-user rate limiting + if userID != nil { + userKey := fmt.Sprintf("rate_limit:user:%s", userID) + if !allowRequest(redisClient, userKey, 60, time.Minute) { + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "error": true, + "message": "Rate limit exceeded for user", + }) + } + } + + // Per-IP rate limiting + ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP) + if !allowRequest(redisClient, ipKey, 100, time.Minute) { + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "error": true, + "message": "Rate limit exceeded for IP", + }) + } + + return c.Next() + } +} +``` + +### Database Patterns with Ent + +#### Schema Definition Standards +```go +// Ent schema with proper field validation and indexes +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/schema/edge" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "time" +) + +type Question struct { + ent.Schema +} + +func (Question) Fields() []ent.Field { + return []ent.Field{ + field.String("theme"). + NotEmpty(). + MaxLen(100). + Comment("Question category/theme"), + field.Text("text"). + NotEmpty(). + MaxLen(1000). + Comment("The actual question text"), + field.String("answer"). + NotEmpty(). + MaxLen(500). + Comment("Correct answer"), + field.Text("hint"). + Optional(). + MaxLen(500). + Comment("Optional hint text"), + field.Enum("difficulty"). + Values("easy", "medium", "hard"). + Default("medium"). + Comment("Question difficulty level"), + field.Bool("is_active"). + Default(true). + Comment("Whether question is active"), + field.Time("created_at"). + Default(time.Now). + Immutable(). + Comment("Creation timestamp"), + field.Time("updated_at"). + Default(time.Now). + UpdateDefault(time.Now). + Comment("Last update timestamp"), + } +} + +func (Question) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("theme", "is_active"), + index.Fields("difficulty", "is_active"), + index.Fields("created_at"), + } +} + +func (Question) Edges() []ent.Edge { + return []ent.Edge{ + edge.To("attempts", QuestionAttempt.Type), + } +} +``` + +#### Service Layer Patterns +```go +// Service layer with proper error handling +package services + +import ( + "context" + "errors" + "knowfoolery/internal/models" + "knowfoolery/ent" +) + +var ( + ErrInvalidInput = errors.New("invalid input") + ErrNotFound = errors.New("resource not found") + ErrUnauthorized = errors.New("unauthorized access") + ErrGameInProgress = errors.New("game already in progress") + ErrSessionExpired = errors.New("session expired") +) + +type GameService struct { + client *ent.Client + questionSvc QuestionService + sessionSvc SessionService + validator *validator.Validate +} + +func NewGameService( + client *ent.Client, + questionSvc QuestionService, + sessionSvc SessionService, +) *GameService { + return &GameService{ + client: client, + questionSvc: questionSvc, + sessionSvc: sessionSvc, + validator: validator.New(), + } +} + +func (s *GameService) StartGame(ctx context.Context, userID, playerName string) (*models.GameSession, error) { + // Validate input + if err := s.validatePlayerName(playerName); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) + } + + // Check for existing active session + activeSession, err := s.client.GameSession. + Query(). + Where(gamesession.And( + gamesession.PlayerNameEQ(playerName), + gamesession.StatusEQ(gamesession.StatusActive), + )). + Only(ctx) + + if err != nil && !ent.IsNotFound(err) { + return nil, fmt.Errorf("failed to check existing session: %w", err) + } + + if activeSession != nil { + return nil, ErrGameInProgress + } + + // Start database transaction + tx, err := s.client.Tx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to start transaction: %w", err) + } + defer tx.Rollback() + + // Create new game session + session, err := tx.GameSession. + Create(). + SetPlayerName(playerName). + SetTotalScore(0). + SetQuestionsAsked(0). + SetQuestionsCorrect(0). + SetHintsUsed(0). + SetStartTime(time.Now()). + SetStatus(gamesession.StatusActive). + Save(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create session: %w", err) + } + + // Get first question + question, err := s.questionSvc.GetRandomQuestion(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get question: %w", err) + } + + // Update session with first question + session, err = session.Update(). + SetCurrentQuestionID(question.ID). + SetCurrentAttempts(0). + Save(ctx) + if err != nil { + return nil, fmt.Errorf("failed to update session: %w", err) + } + + // Commit transaction + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return &models.GameSession{ + ID: session.ID, + PlayerName: session.PlayerName, + TotalScore: session.TotalScore, + StartTime: session.StartTime, + Status: string(session.Status), + CurrentQuestion: &models.Question{ + ID: question.ID, + Theme: question.Theme, + Text: question.Text, + Hint: question.Hint, + }, + TimeRemaining: s.calculateTimeRemaining(session.StartTime), + }, nil +} + +func (s *GameService) validatePlayerName(name string) error { + type PlayerNameValidation struct { + Name string `validate:"required,min=2,max=50,alphanum_space"` + } + + return s.validator.Struct(&PlayerNameValidation{Name: name}) +} + +func (s *GameService) calculateTimeRemaining(startTime time.Time) int { + const sessionDuration = 30 * time.Minute + elapsed := time.Since(startTime) + remaining := sessionDuration - elapsed + + if remaining < 0 { + return 0 + } + + return int(remaining.Seconds()) +} +``` + +## TypeScript Development Standards + +### React Component Patterns + +#### Component Structure +```typescript +// Component with proper TypeScript types and patterns +import React, { useState, useEffect, useCallback } from 'react' +import { + Card, + VStack, + HStack, + Text, + Input, + Button, + Badge, + Progress +} from '@gluestack-ui/themed' +import { useGameSession } from '../hooks/useGameSession' +import { useTimer } from '../hooks/useTimer' + +// Props interface with proper documentation +export interface GameCardProps { + /** The question to display */ + question: string + /** Theme/category of the question */ + theme: string + /** Time remaining in seconds */ + timeRemaining: number + /** Number of attempts left */ + attemptsLeft: number + /** Current player score */ + currentScore: number + /** Loading state */ + isLoading?: boolean + /** Callback when answer is submitted */ + onSubmitAnswer: (answer: string) => Promise + /** Callback when hint is requested */ + onRequestHint: () => Promise + /** Callback when time expires */ + onTimeExpire?: () => void +} + +/** + * GameCard component displays a quiz question with input and controls + * + * @param props - GameCard props + * @returns React component + */ +export const GameCard: React.FC = ({ + question, + theme, + timeRemaining, + attemptsLeft, + currentScore, + isLoading = false, + onSubmitAnswer, + onRequestHint, + onTimeExpire +}) => { + // Local state with proper typing + const [answer, setAnswer] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + // Custom hooks + const { formatTime } = useTimer() + + // Memoized handlers + const handleSubmit = useCallback(async () => { + if (!answer.trim() || isSubmitting) return + + setIsSubmitting(true) + try { + await onSubmitAnswer(answer.trim()) + setAnswer('') // Clear input on successful submission + } catch (error) { + console.error('Failed to submit answer:', error) + // Error handling would be managed by parent component + } finally { + setIsSubmitting(false) + } + }, [answer, isSubmitting, onSubmitAnswer]) + + const handleHintRequest = useCallback(async () => { + if (isLoading) return + + try { + await onRequestHint() + } catch (error) { + console.error('Failed to request hint:', error) + } + }, [isLoading, onRequestHint]) + + // Effects + useEffect(() => { + if (timeRemaining === 0 && onTimeExpire) { + onTimeExpire() + } + }, [timeRemaining, onTimeExpire]) + + // Computed values + const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100 + const isTimeWarning = timeRemaining <= 300 // 5 minutes + const isTimeCritical = timeRemaining <= 60 // 1 minute + + return ( + + + {/* Header with theme and timer */} + + + + {theme} + + + + + ⏱️ {formatTime(timeRemaining)} + + + + + {/* Progress indicator */} + + + + + {/* Question */} + + {question} + + + {/* Answer input */} + + + + + {/* Game stats */} + + + Attempts left: {attemptsLeft}/3 + + + Score: {currentScore} points + + + + {/* Action buttons */} + + + + + + + ) +} + +// Default export for dynamic imports +export default GameCard +``` + +#### Custom Hooks Pattern +```typescript +// Custom hook with proper TypeScript types +import { useState, useEffect, useCallback, useRef } from 'react' +import { GameSessionService } from '../services/game-session-service' + +interface UseGameSessionOptions { + playerName: string + onSessionEnd?: (finalScore: number) => void + onError?: (error: Error) => void +} + +interface GameSessionState { + sessionId: string | null + currentQuestion: Question | null + score: number + attemptsLeft: number + timeRemaining: number + isLoading: boolean + error: string | null +} + +interface GameSessionActions { + startGame: () => Promise + submitAnswer: (answer: string) => Promise + requestHint: () => Promise + endGame: () => Promise +} + +export const useGameSession = ( + options: UseGameSessionOptions +): [GameSessionState, GameSessionActions] => { + const [state, setState] = useState({ + sessionId: null, + currentQuestion: null, + score: 0, + attemptsLeft: 3, + timeRemaining: 30 * 60, // 30 minutes + isLoading: false, + error: null, + }) + + const timerRef = useRef(null) + const gameService = useRef(new GameSessionService()) + + // Timer effect + useEffect(() => { + if (state.sessionId && state.timeRemaining > 0) { + timerRef.current = setInterval(() => { + setState(prev => { + const newTimeRemaining = prev.timeRemaining - 1 + if (newTimeRemaining <= 0 && options.onSessionEnd) { + options.onSessionEnd(prev.score) + } + return { ...prev, timeRemaining: Math.max(0, newTimeRemaining) } + }) + }, 1000) + } + + return () => { + if (timerRef.current) { + clearInterval(timerRef.current) + } + } + }, [state.sessionId, state.timeRemaining, options.onSessionEnd]) + + // Actions + const startGame = useCallback(async () => { + setState(prev => ({ ...prev, isLoading: true, error: null })) + + try { + const session = await gameService.current.startGame(options.playerName) + setState(prev => ({ + ...prev, + sessionId: session.sessionId, + currentQuestion: session.firstQuestion, + score: 0, + attemptsLeft: 3, + timeRemaining: session.timeRemaining, + isLoading: false, + })) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to start game' + setState(prev => ({ ...prev, error: errorMessage, isLoading: false })) + options.onError?.(error instanceof Error ? error : new Error(errorMessage)) + } + }, [options.playerName, options.onError]) + + const submitAnswer = useCallback(async (answer: string): Promise => { + if (!state.sessionId || !state.currentQuestion) { + throw new Error('No active game session') + } + + setState(prev => ({ ...prev, isLoading: true })) + + try { + const result = await gameService.current.submitAnswer( + state.sessionId, + state.currentQuestion.id, + answer + ) + + setState(prev => ({ + ...prev, + score: result.newScore, + attemptsLeft: result.attemptsLeft, + currentQuestion: result.nextQuestion, + isLoading: false, + })) + + return result.isCorrect + } catch (error) { + setState(prev => ({ ...prev, isLoading: false })) + throw error + } + }, [state.sessionId, state.currentQuestion]) + + const requestHint = useCallback(async (): Promise => { + if (!state.sessionId || !state.currentQuestion) { + throw new Error('No active game session') + } + + const hint = await gameService.current.requestHint( + state.sessionId, + state.currentQuestion.id + ) + + return hint + }, [state.sessionId, state.currentQuestion]) + + const endGame = useCallback(async () => { + if (!state.sessionId) return + + await gameService.current.endGame(state.sessionId) + + setState({ + sessionId: null, + currentQuestion: null, + score: 0, + attemptsLeft: 3, + timeRemaining: 30 * 60, + isLoading: false, + error: null, + }) + + if (timerRef.current) { + clearInterval(timerRef.current) + } + }, [state.sessionId]) + + return [ + state, + { + startGame, + submitAnswer, + requestHint, + endGame, + }, + ] +} +``` + +## Testing Standards + +### Go Testing Patterns +```go +// Comprehensive testing with testify and testcontainers +package services_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go/modules/postgres" + + "knowfoolery/internal/services" + "knowfoolery/ent/enttest" +) + +type GameServiceTestSuite struct { + suite.Suite + ctx context.Context + gameService *services.GameService + client *ent.Client +} + +func (suite *GameServiceTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Start PostgreSQL container for integration tests + postgresContainer, err := postgres.RunContainer(suite.ctx, + testcontainers.WithImage("postgres:15-alpine"), + postgres.WithDatabase("testdb"), + postgres.WithUsername("testuser"), + postgres.WithPassword("testpass"), + ) + require.NoError(suite.T(), err) + + connStr, err := postgresContainer.ConnectionString(suite.ctx) + require.NoError(suite.T(), err) + + // Create Ent client with test database + suite.client = enttest.Open(suite.T(), "postgres", connStr) + + // Initialize service + suite.gameService = services.NewGameService( + suite.client, + services.NewQuestionService(suite.client), + services.NewSessionService(suite.client), + ) +} + +func (suite *GameServiceTestSuite) TearDownSuite() { + suite.client.Close() +} + +func (suite *GameServiceTestSuite) SetupTest() { + // Clean database before each test + suite.client.GameSession.Delete().ExecX(suite.ctx) + suite.client.Question.Delete().ExecX(suite.ctx) + + // Seed test data + suite.seedTestQuestions() +} + +func (suite *GameServiceTestSuite) TestStartGame_Success() { + // Arrange + playerName := "TestPlayer" + + // Act + session, err := suite.gameService.StartGame(suite.ctx, "user123", playerName) + + // Assert + require.NoError(suite.T(), err) + assert.NotEmpty(suite.T(), session.ID) + assert.Equal(suite.T(), playerName, session.PlayerName) + assert.Equal(suite.T(), 0, session.TotalScore) + assert.NotNil(suite.T(), session.CurrentQuestion) + assert.True(suite.T(), session.TimeRemaining > 0) +} + +func (suite *GameServiceTestSuite) TestStartGame_InvalidPlayerName() { + // Test cases for validation + testCases := []struct { + name string + playerName string + expectedErr string + }{ + {"empty name", "", "required"}, + {"too short", "a", "min"}, + {"too long", strings.Repeat("a", 51), "max"}, + {"invalid chars", "player@123", "alphanum_space"}, + } + + for _, tc := range testCases { + suite.T().Run(tc.name, func(t *testing.T) { + _, err := suite.gameService.StartGame(suite.ctx, "user123", tc.playerName) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + }) + } +} + +func (suite *GameServiceTestSuite) TestStartGame_ExistingActiveSession() { + // Arrange + playerName := "TestPlayer" + + // Start first game + _, err := suite.gameService.StartGame(suite.ctx, "user123", playerName) + require.NoError(suite.T(), err) + + // Act - try to start second game + _, err = suite.gameService.StartGame(suite.ctx, "user123", playerName) + + // Assert + require.Error(suite.T(), err) + assert.Equal(suite.T(), services.ErrGameInProgress, err) +} + +func (suite *GameServiceTestSuite) seedTestQuestions() { + questions := []struct { + theme string + text string + answer string + hint string + }{ + {"Geography", "What is the capital of France?", "Paris", "City of Light"}, + {"History", "Who painted the Mona Lisa?", "Leonardo da Vinci", "Renaissance artist"}, + {"Science", "What is the largest planet?", "Jupiter", "Gas giant"}, + } + + for _, q := range questions { + suite.client.Question.Create(). + SetTheme(q.theme). + SetText(q.text). + SetAnswer(q.answer). + SetHint(q.hint). + SetIsActive(true). + ExecX(suite.ctx) + } +} + +func TestGameServiceSuite(t *testing.T) { + suite.Run(t, new(GameServiceTestSuite)) +} + +// Benchmark tests +func BenchmarkGameService_StartGame(b *testing.B) { + // Setup + ctx := context.Background() + client := enttest.Open(b, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + gameService := services.NewGameService(client, nil, nil) + + // Benchmark + b.ResetTimer() + for i := 0; i < b.N; i++ { + playerName := fmt.Sprintf("Player%d", i) + _, _ = gameService.StartGame(ctx, fmt.Sprintf("user%d", i), playerName) + } +} +``` + +### TypeScript Testing Patterns +```typescript +// React component testing with Jest and Testing Library +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { GluestackUIProvider } from '@gluestack-ui/themed' +import { GameCard, GameCardProps } from '../GameCard' + +// Test wrapper for Gluestack UI +const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +) + +// Mock props factory +const createMockProps = (overrides: Partial = {}): GameCardProps => ({ + question: "What is the capital of France?", + theme: "Geography", + timeRemaining: 1800, // 30 minutes + attemptsLeft: 3, + currentScore: 0, + onSubmitAnswer: jest.fn(), + onRequestHint: jest.fn(), + ...overrides, +}) + +describe('GameCard', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Rendering', () => { + it('should render question and theme correctly', () => { + const props = createMockProps() + + render(, { wrapper: TestWrapper }) + + expect(screen.getByTestId('question-text')).toHaveTextContent(props.question) + expect(screen.getByTestId('theme-badge')).toHaveTextContent(props.theme) + }) + + it('should display timer in correct format', () => { + const props = createMockProps({ timeRemaining: 125 }) // 2:05 + + render(, { wrapper: TestWrapper }) + + expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05') + }) + + it('should show attempts and score correctly', () => { + const props = createMockProps({ attemptsLeft: 2, currentScore: 5 }) + + render(, { wrapper: TestWrapper }) + + expect(screen.getByTestId('attempts-counter')).toHaveTextContent('Attempts left: 2/3') + expect(screen.getByTestId('score-display')).toHaveTextContent('Score: 5 points') + }) + }) + + describe('User Interactions', () => { + it('should call onSubmitAnswer when submit button is clicked', async () => { + const props = createMockProps() + const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined) + props.onSubmitAnswer = mockOnSubmitAnswer + + render(, { wrapper: TestWrapper }) + + // Type an answer + const input = screen.getByTestId('answer-input') + fireEvent.changeText(input, 'Paris') + + // Click submit + const submitButton = screen.getByTestId('submit-button') + fireEvent.press(submitButton) + + await waitFor(() => { + expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris') + }) + }) + + it('should not submit empty answer', () => { + const props = createMockProps() + + render(, { wrapper: TestWrapper }) + + const submitButton = screen.getByTestId('submit-button') + expect(submitButton).toBeDisabled() + }) + + it('should call onRequestHint when hint button is clicked', async () => { + const props = createMockProps() + const mockOnRequestHint = jest.fn().mockResolvedValue('City of Light') + props.onRequestHint = mockOnRequestHint + + render(, { wrapper: TestWrapper }) + + const hintButton = screen.getByTestId('hint-button') + fireEvent.press(hintButton) + + await waitFor(() => { + expect(mockOnRequestHint).toHaveBeenCalled() + }) + }) + }) + + describe('Loading States', () => { + it('should disable inputs when loading', () => { + const props = createMockProps({ isLoading: true }) + + render(, { wrapper: TestWrapper }) + + expect(screen.getByTestId('answer-input')).toBeDisabled() + expect(screen.getByTestId('submit-button')).toBeDisabled() + expect(screen.getByTestId('hint-button')).toBeDisabled() + }) + }) + + describe('Timer States', () => { + it('should show warning color when time is low', () => { + const props = createMockProps({ timeRemaining: 300 }) // 5 minutes + + render(, { wrapper: TestWrapper }) + + const timer = screen.getByTestId('timer-display') + expect(timer).toHaveStyle({ color: expect.stringContaining('warning') }) + }) + + it('should show critical color when time is very low', () => { + const props = createMockProps({ timeRemaining: 30 }) // 30 seconds + + render(, { wrapper: TestWrapper }) + + const timer = screen.getByTestId('timer-display') + expect(timer).toHaveStyle({ color: expect.stringContaining('error') }) + }) + }) +}) + +// Hook testing +import { renderHook, act } from '@testing-library/react' +import { useGameSession } from '../hooks/useGameSession' + +// Mock the service +jest.mock('../services/game-session-service') + +describe('useGameSession', () => { + const mockGameService = { + startGame: jest.fn(), + submitAnswer: jest.fn(), + requestHint: jest.fn(), + endGame: jest.fn(), + } + + beforeEach(() => { + jest.clearAllMocks() + // Set up service mock + ;(GameSessionService as jest.Mock).mockImplementation(() => mockGameService) + }) + + it('should start game successfully', async () => { + mockGameService.startGame.mockResolvedValue({ + sessionId: 'session123', + firstQuestion: { id: 'q1', text: 'Test question' }, + timeRemaining: 1800, + }) + + const { result } = renderHook(() => + useGameSession({ playerName: 'TestPlayer' }) + ) + + await act(async () => { + await result.current[1].startGame() + }) + + expect(result.current[0].sessionId).toBe('session123') + expect(result.current[0].currentQuestion).toBeDefined() + }) + + it('should handle errors during game start', async () => { + const mockError = new Error('Failed to start game') + mockGameService.startGame.mockRejectedValue(mockError) + + const onError = jest.fn() + const { result } = renderHook(() => + useGameSession({ playerName: 'TestPlayer', onError }) + ) + + await act(async () => { + await result.current[1].startGame() + }) + + expect(result.current[0].error).toBe('Failed to start game') + expect(onError).toHaveBeenCalledWith(mockError) + }) +}) +``` + +## Code Quality Standards + +### Linting and Formatting +```yaml +# .golangci.yml +run: + timeout: 5m + tests: true + +linters: + enable: + - gofmt + - goimports + - govet + - errcheck + - staticcheck + - unused + - gosimple + - structcheck + - varcheck + - ineffassign + - deadcode + - typecheck + - gosec + - misspell + - lll + - unconvert + - dupl + - goconst + - gocyclo + +linters-settings: + lll: + line-length: 120 + gocyclo: + min-complexity: 15 + dupl: + threshold: 150 + +issues: + exclude-rules: + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec +``` + +```json +// .eslintrc.json +{ + "extends": [ + "@react-native", + "@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "react-hooks"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "error", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/prefer-const": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "prefer-const": "error", + "no-var": "error", + "object-shorthand": "error", + "prefer-template": "error" + } +} +``` + +### Git Workflow +```bash +# Branch naming conventions +feature/add-game-session-management +bugfix/fix-timer-display-issue +hotfix/security-vulnerability-patch +refactor/improve-database-queries + +# Commit message format +type(scope): description + +# Types: feat, fix, docs, style, refactor, test, chore +# Examples: +feat(game): add session timeout handling +fix(auth): resolve JWT token validation issue +docs(api): update authentication endpoint documentation +test(services): add comprehensive game service tests +``` + +This comprehensive development guidelines document ensures consistent, maintainable, and high-quality code across the Know Foolery project. \ No newline at end of file diff --git a/docs/game-mechanics.md b/docs/game-mechanics.md new file mode 100644 index 0000000..0371d59 --- /dev/null +++ b/docs/game-mechanics.md @@ -0,0 +1,476 @@ +# Know Foolery - Game Mechanics Documentation + +## Overview + +Know Foolery is a quiz game where players answer questions to earn points within a time-limited session. The game emphasizes strategic thinking through its hint system and attempt limitations. + +## Core Game Flow + +### 1. Game Session Initialization +``` +Player Input: Name β†’ Session Creation β†’ Question Selection β†’ Game Start +``` + +**Process:** +1. Player enters their name (required) +2. System creates new game session +3. System randomly selects first question +4. 30-minute timer starts +5. Game begins with question display + +**Validation:** +- Player name: 2-50 characters, alphanumeric + spaces +- Session uniqueness: One active session per player +- Timer initialization: Server-side timestamp + +### 2. Question Presentation + +**Question Structure:** +```typescript +interface Question { + id: string + theme: string // e.g., "Geography", "History", "Science" + text: string // The actual question + answer: string // Correct answer (case-insensitive) + hint: string // Optional hint text + difficulty?: number // Future enhancement +} +``` + +**Display Format:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Theme: Geography ⏱️ 28:45 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ What is the capital of France? β”‚ +β”‚ β”‚ +β”‚ [Your answer here... ] πŸ’‘ β”‚ +β”‚ β”‚ +β”‚ Attempts remaining: 3/3 β”‚ +β”‚ Current score: 4 points β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 3. Answer Submission Process + +**Player Actions:** +1. **Direct Answer**: Submit answer without hint +2. **Request Hint**: View hint before answering +3. **Skip Question**: Move to next question (future enhancement) + +**Answer Validation:** +```go +// Server-side validation logic +func ValidateAnswer(userAnswer, correctAnswer string) bool { + // Normalize both answers + user := strings.ToLower(strings.TrimSpace(userAnswer)) + correct := strings.ToLower(strings.TrimSpace(correctAnswer)) + + // Exact match + if user == correct { + return true + } + + // Fuzzy matching (future enhancement) + similarity := calculateSimilarity(user, correct) + return similarity > 0.85 +} +``` + +## Scoring System + +### Point Values +- **Correct without hint**: 2 points +- **Correct with hint**: 1 point +- **Incorrect answer**: 0 points +- **Session timeout**: 0 points for remaining questions + +### Scoring Logic +```go +type AttemptResult struct { + IsCorrect bool + UsedHint bool + AttemptNum int // 1, 2, or 3 +} + +func CalculateScore(result AttemptResult) int { + if !result.IsCorrect { + return 0 + } + + if result.UsedHint { + return 1 // Reduced score for using hint + } + + return 2 // Full score for direct correct answer +} +``` + +### Score Tracking +```go +type GameSession struct { + PlayerName string + TotalScore int + QuestionsAsked int + QuestionsCorrect int + HintsUsed int + StartTime time.Time + EndTime *time.Time + IsActive bool +} +``` + +## Attempt System + +### Attempt Rules +- **Maximum Attempts**: 3 per question +- **Attempt Tracking**: Server-side validation +- **Attempt Consequences**: + - Attempt 1-3: Normal scoring if correct + - After 3 failed attempts: Move to next question automatically + +### Attempt Flow +```go +type QuestionAttempt struct { + SessionID string + QuestionID string + AttemptNum int // 1, 2, or 3 + Answer string + IsCorrect bool + UsedHint bool + Timestamp time.Time +} + +func ProcessAttempt(sessionID, questionID, answer string) AttemptResult { + // 1. Validate session is active + // 2. Check attempts remaining + // 3. Validate answer + // 4. Update score if correct + // 5. Move to next question if needed +} +``` + +## Hint System + +### Hint Mechanics +- **Availability**: One hint per question +- **Timing**: Can be requested before any attempt +- **Impact**: Reduces maximum score from 2 to 1 points +- **Content**: Provides helpful clue without giving away answer + +### Hint Implementation +```typescript +interface HintRequest { + sessionId: string + questionId: string +} + +interface HintResponse { + hint: string + scoreImpact: string // "Using this hint will reduce your score to 1 point" + attemptsRemaining: number +} +``` + +### Hint Examples +```javascript +const hintExamples = { + question: "What is the capital of France?", + hint: "This city is famous for the Eiffel Tower and is located on the Seine River.", + + question: "Who painted the Mona Lisa?", + hint: "This Italian Renaissance artist was also an inventor and scientist.", + + question: "What is the largest planet in our solar system?", + hint: "This gas giant is named after the king of the Roman gods." +} +``` + +## Time Management + +### Session Timer +- **Duration**: 30 minutes maximum per session +- **Display**: Real-time countdown timer +- **Warnings**: Visual alerts at 5 minutes and 1 minute remaining +- **Timeout**: Automatic session termination when time expires + +### Timer Implementation +```go +type SessionTimer struct { + SessionID string + StartTime time.Time + Duration time.Duration // 30 minutes + WarningsSent map[int]bool // Track warnings sent +} + +func (t *SessionTimer) TimeRemaining() time.Duration { + elapsed := time.Since(t.StartTime) + remaining := t.Duration - elapsed + if remaining < 0 { + return 0 + } + return remaining +} + +func (t *SessionTimer) IsExpired() bool { + return t.TimeRemaining() == 0 +} +``` + +### Timer Events +```typescript +interface TimerEvent { + type: 'warning' | 'timeout' + timeRemaining: number // seconds + message: string +} + +// Timer warnings +const timerWarnings = [ + { at: 300, message: "5 minutes remaining!" }, // 5 minutes + { at: 60, message: "1 minute remaining!" }, // 1 minute + { at: 10, message: "10 seconds left!" } // 10 seconds +] +``` + +## Question Selection + +### Random Selection Algorithm +```go +func SelectRandomQuestion(sessionID string, excludeAnswered bool) (*Question, error) { + var questions []Question + query := db.Question.Query() + + if excludeAnswered { + // Exclude questions already answered in this session + answeredIDs := getAnsweredQuestions(sessionID) + query = query.Where(question.IDNotIn(answeredIDs...)) + } + + questions, err := query.All(ctx) + if err != nil { + return nil, err + } + + if len(questions) == 0 { + return nil, errors.New("no more questions available") + } + + // Random selection + randomIndex := rand.Intn(len(questions)) + return &questions[randomIndex], nil +} +``` + +### Question Difficulty (Future Enhancement) +```go +type DifficultyLevel int + +const ( + Easy DifficultyLevel = iota + 1 + Medium + Hard +) + +// Adaptive difficulty based on player performance +func SelectQuestionByDifficulty(sessionID string, targetDifficulty DifficultyLevel) (*Question, error) { + // Implementation for adaptive difficulty + // Consider player's success rate, current score, etc. +} +``` + +## Leaderboard System + +### Leaderboard Calculation +```go +type LeaderboardEntry struct { + PlayerName string + Score int + QuestionsAnswered int + SuccessRate float64 // Percentage of correct answers + SessionDuration time.Duration + CompletedAt time.Time +} + +func CalculateLeaderboard() ([]LeaderboardEntry, error) { + sessions := getCompletedSessions() + + entries := make([]LeaderboardEntry, 0, len(sessions)) + for _, session := range sessions { + entry := LeaderboardEntry{ + PlayerName: session.PlayerName, + Score: session.TotalScore, + QuestionsAnswered: session.QuestionsAsked, + SuccessRate: float64(session.QuestionsCorrect) / float64(session.QuestionsAsked) * 100, + SessionDuration: session.EndTime.Sub(session.StartTime), + CompletedAt: *session.EndTime, + } + entries = append(entries, entry) + } + + // Sort by score (descending), then by completion time (ascending) + sort.Slice(entries, func(i, j int) bool { + if entries[i].Score == entries[j].Score { + return entries[i].CompletedAt.Before(entries[j].CompletedAt) + } + return entries[i].Score > entries[j].Score + }) + + // Return top 10 + if len(entries) > 10 { + entries = entries[:10] + } + + return entries, nil +} +``` + +### Leaderboard Display +``` +πŸ† LEADERBOARD + +Rank | Player | Score | Questions | Success Rate | Duration +-----|-----------|-------|-----------|--------------|---------- + 1 | Alice | 24 | 14 | 86% | 28m + 2 | Bob | 22 | 13 | 85% | 25m + 3 | Charlie | 20 | 12 | 83% | 30m + 4 | Diana | 18 | 11 | 82% | 22m + 5 | Eve | 16 | 10 | 80% | 27m +``` + +## Game State Management + +### Session States +```go +type SessionState int + +const ( + SessionCreated SessionState = iota + SessionActive + SessionPaused // Future enhancement + SessionCompleted + SessionTimedOut + SessionAbandoned +) + +type GameSession struct { + ID string + PlayerName string + State SessionState + CurrentQuestion *Question + Score int + // ... other fields +} +``` + +### State Transitions +``` +Created β†’ Active β†’ {Completed, TimedOut, Abandoned} + ↓ + Paused β†’ Active (future enhancement) +``` + +## Anti-Cheating Measures + +### Server-Side Validation +```go +type SecurityCheck struct { + SessionID string + QuestionID string + ExpectedAnswer string + SubmittedAnswer string + TimestampSubmit time.Time + TimestampShow time.Time + IsValid bool + Reason string +} + +func ValidateSubmission(check SecurityCheck) bool { + // 1. Check minimum time between question show and answer submit + minTime := 2 * time.Second + if check.TimestampSubmit.Sub(check.TimestampShow) < minTime { + check.Reason = "Answer submitted too quickly" + return false + } + + // 2. Validate session state + session := getSession(check.SessionID) + if session.State != SessionActive { + check.Reason = "Invalid session state" + return false + } + + // 3. Check if question belongs to session + if session.CurrentQuestion.ID != check.QuestionID { + check.Reason = "Question mismatch" + return false + } + + return true +} +``` + +### Client-Side Integrity +```typescript +// Prevent common cheating attempts +class GameIntegrity { + private questionStartTime: number = 0 + + onQuestionDisplayed() { + this.questionStartTime = Date.now() + + // Disable browser dev tools (basic deterrent) + this.disableDevTools() + + // Prevent copy-paste in answer input + this.disableCopyPaste() + } + + onAnswerSubmit(answer: string) { + const timeTaken = Date.now() - this.questionStartTime + + // Include timing in submission for server validation + return { + answer, + timeTaken, + timestamp: Date.now() + } + } + + private disableDevTools() { + // Basic dev tools detection (not foolproof) + const devtools = { + open: false, + orientation: null + } + + const threshold = 160 + setInterval(() => { + if (window.outerHeight - window.innerHeight > threshold || + window.outerWidth - window.innerWidth > threshold) { + console.warn('Developer tools detected') + // Log security event + } + }, 500) + } +} +``` + +## Future Enhancements + +### Planned Features +1. **Question Categories**: Filter questions by subject +2. **Difficulty Progression**: Adaptive difficulty based on performance +3. **Multiplayer Mode**: Real-time competition between players +4. **Daily Challenges**: Special themed question sets +5. **Achievement System**: Badges and milestones +6. **Question Contributions**: Player-submitted questions + +### Scoring Enhancements +1. **Time Bonus**: Extra points for quick correct answers +2. **Streak Bonus**: Consecutive correct answers bonus +3. **Difficulty Multiplier**: Higher points for harder questions +4. **Perfect Game Bonus**: Bonus for 100% correct rate + +This game mechanics documentation ensures consistent implementation across all platforms and provides clear guidelines for future enhancements. \ No newline at end of file diff --git a/docs/gluestack-ui-strategy.md b/docs/gluestack-ui-strategy.md new file mode 100644 index 0000000..ec2d093 --- /dev/null +++ b/docs/gluestack-ui-strategy.md @@ -0,0 +1,1392 @@ +# Know Foolery - Cross-Platform UI Strategy with Gluestack UI + +## Overview + +Gluestack UI provides a comprehensive solution for building consistent, performant user interfaces across web (React) and mobile (React Native) platforms. This document outlines the strategy for implementing Know Foolery's cross-platform UI using Gluestack UI with NativeWind/Tailwind CSS. + +## Gluestack UI Architecture + +### Cross-Platform Component Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Shared UI Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Design β”‚ β”‚ Component β”‚ β”‚ Shared β”‚ β”‚ Theme β”‚ β”‚ +β”‚ β”‚ Tokens β”‚ β”‚ Library β”‚ β”‚ Logic β”‚ β”‚ System β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + Platform Adaptation + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Platform Renderers β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Web DOM β”‚ β”‚ iOS β”‚ β”‚ Android β”‚ β”‚ Desktop β”‚ β”‚ +β”‚ β”‚ (React) β”‚ β”‚ (RN iOS) β”‚ β”‚ (RN Android)β”‚ β”‚ (Wails) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Project Structure for Cross-Platform UI + +### Recommended Package Organization + +``` +frontend/ +β”œβ”€β”€ packages/ +β”‚ β”œβ”€β”€ ui-components/ # Shared Gluestack UI components +β”‚ β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ GameCard/ +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ GameCard.tsx +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ GameCard.stories.tsx +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ GameCard.test.tsx +β”‚ β”‚ β”‚ β”‚ β”‚ └── index.ts +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Leaderboard/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Timer/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ScoreDisplay/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AdminPanel/ +β”‚ β”‚ β”‚ β”‚ └── index.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ theme/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ tokens.ts # Design tokens +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ colors.ts +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ typography.ts +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ spacing.ts +β”‚ β”‚ β”‚ β”‚ └── index.ts +β”‚ β”‚ β”‚ └── index.ts +β”‚ β”‚ β”œβ”€β”€ package.json +β”‚ β”‚ └── gluestack-ui.config.ts +β”‚ β”œβ”€β”€ shared-logic/ # Business logic +β”‚ β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”‚ β”œβ”€β”€ hooks/ +β”‚ β”‚ β”‚ β”œβ”€β”€ services/ +β”‚ β”‚ β”‚ β”œβ”€β”€ utils/ +β”‚ β”‚ β”‚ └── types/ +β”‚ β”‚ └── package.json +β”‚ └── shared-types/ # TypeScript types +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ api.ts +β”‚ β”‚ β”œβ”€β”€ game.ts +β”‚ β”‚ └── index.ts +β”‚ └── package.json +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ web/ # React web app +β”‚ β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”‚ β”œβ”€β”€ pages/ +β”‚ β”‚ β”‚ β”œβ”€β”€ components/ # Web-specific components +β”‚ β”‚ β”‚ └── main.tsx +β”‚ β”‚ β”œβ”€β”€ vite.config.ts +β”‚ β”‚ └── package.json +β”‚ β”œβ”€β”€ mobile/ # React Native app +β”‚ β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”‚ β”œβ”€β”€ screens/ +β”‚ β”‚ β”‚ β”œβ”€β”€ components/ # Mobile-specific components +β”‚ β”‚ β”‚ └── App.tsx +β”‚ β”‚ β”œβ”€β”€ metro.config.js +β”‚ β”‚ └── package.json +β”‚ └── desktop/ # Wails desktop app +β”‚ β”œβ”€β”€ frontend/ +β”‚ β”œβ”€β”€ app/ +β”‚ └── wails.json +└── storybook/ # Component documentation + β”œβ”€β”€ stories/ + └── .storybook/ +``` + +## Theme System Implementation + +### Design Tokens with Quiz Game Branding + +```typescript +// packages/ui-components/src/theme/tokens.ts +export const designTokens = { + colors: { + // Brand colors for Know Foolery + brand: { + 50: '#eff6ff', // Very light blue + 100: '#dbeafe', // Light blue + 200: '#bfdbfe', // Lighter blue + 300: '#93c5fd', // Light blue + 400: '#60a5fa', // Medium blue + 500: '#3b82f6', // Primary blue (main brand) + 600: '#2563eb', // Darker blue + 700: '#1d4ed8', // Dark blue + 800: '#1e40af', // Very dark blue + 900: '#1e3a8a', // Darkest blue + }, + + // Semantic colors for game states + success: { + 50: '#f0fdf4', // Correct answer background + 500: '#22c55e', // Correct answer green + 600: '#16a34a', // Darker correct green + }, + + error: { + 50: '#fef2f2', // Wrong answer background + 500: '#ef4444', // Wrong answer red + 600: '#dc2626', // Darker wrong red + }, + + warning: { + 50: '#fffbeb', // Hint background + 500: '#f59e0b', // Hint orange/yellow + 600: '#d97706', // Darker hint color + }, + + // Neutral colors + gray: { + 50: '#f9fafb', + 100: '#f3f4f6', + 200: '#e5e7eb', + 300: '#d1d5db', + 400: '#9ca3af', + 500: '#6b7280', + 600: '#4b5563', + 700: '#374151', + 800: '#1f2937', + 900: '#111827', + }, + }, + + spacing: { + 0: '0px', + 1: '4px', + 2: '8px', + 3: '12px', + 4: '16px', + 5: '20px', + 6: '24px', + 8: '32px', + 10: '40px', + 12: '48px', + 16: '64px', + 20: '80px', + 24: '96px', + }, + + typography: { + fontSizes: { + xs: '12px', + sm: '14px', + md: '16px', + lg: '18px', + xl: '20px', + '2xl': '24px', + '3xl': '30px', + '4xl': '36px', + }, + + fontWeights: { + normal: '400', + medium: '500', + semibold: '600', + bold: '700', + extrabold: '800', + }, + + lineHeights: { + tight: '1.25', + normal: '1.5', + relaxed: '1.75', + }, + + letterSpacings: { + tight: '-0.025em', + normal: '0em', + wide: '0.025em', + }, + }, + + borderRadius: { + none: '0px', + sm: '4px', + md: '8px', + lg: '12px', + xl: '16px', + full: '9999px', + }, + + shadows: { + sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + md: '0 4px 6px -1px rgb(0 0 0 / 0.1)', + lg: '0 10px 15px -3px rgb(0 0 0 / 0.1)', + xl: '0 20px 25px -5px rgb(0 0 0 / 0.1)', + }, +} + +// Custom semantic tokens for game elements +export const gameTokens = { + // Question difficulty indicators + difficulty: { + easy: designTokens.colors.success[500], + medium: designTokens.colors.warning[500], + hard: designTokens.colors.error[500], + }, + + // Timer states + timer: { + normal: designTokens.colors.gray[600], + warning: designTokens.colors.warning[500], // 5 minutes left + critical: designTokens.colors.error[500], // 1 minute left + }, + + // Score indicators + score: { + excellent: designTokens.colors.success[500], // 80%+ correct + good: designTokens.colors.brand[500], // 60-80% correct + average: designTokens.colors.warning[500], // 40-60% correct + poor: designTokens.colors.error[500], // <40% correct + }, + + // Leaderboard positions + leaderboard: { + first: '#ffd700', // Gold + second: '#c0c0c0', // Silver + third: '#cd7f32', // Bronze + other: designTokens.colors.gray[600], + }, +} + +// Export combined theme +export const knowFooleryTheme = { + ...designTokens, + game: gameTokens, +} +``` + +### Gluestack UI Configuration + +```typescript +// packages/ui-components/gluestack-ui.config.ts +import { createConfig } from '@gluestack-ui/themed' +import { knowFooleryTheme } from './src/theme' + +export const config = createConfig({ + aliases: { + bg: 'backgroundColor', + p: 'padding', + m: 'margin', + w: 'width', + h: 'height', + }, + + tokens: { + colors: knowFooleryTheme.colors, + space: knowFooleryTheme.spacing, + fontSizes: knowFooleryTheme.typography.fontSizes, + fontWeights: knowFooleryTheme.typography.fontWeights, + lineHeights: knowFooleryTheme.typography.lineHeights, + letterSpacings: knowFooleryTheme.typography.letterSpacings, + radii: knowFooleryTheme.borderRadius, + shadows: knowFooleryTheme.shadows, + }, + + globalStyle: { + variants: { + hardShadow: { + shadowColor: '$backgroundLight800', + shadowOffset: { + width: 2, + height: 2, + }, + shadowOpacity: 0.6, + shadowRadius: 8, + elevation: 10, + }, + }, + }, + + // Custom component variants + components: { + Button: { + theme: { + variants: { + solid: { + 'bg': '$brand500', + '_text': { + 'color': '$white', + 'fontWeight': '$semibold', + }, + '_hover': { + 'bg': '$brand600', + }, + '_pressed': { + 'bg': '$brand700', + }, + }, + + outline: { + 'borderWidth': 2, + 'borderColor': '$brand500', + '_text': { + 'color': '$brand500', + 'fontWeight': '$semibold', + }, + '_hover': { + 'bg': '$brand50', + }, + }, + + ghost: { + '_text': { + 'color': '$brand500', + }, + '_hover': { + 'bg': '$brand50', + }, + }, + }, + + sizes: { + sm: { + 'px': '$3', + 'py': '$2', + '_text': { + 'fontSize': '$sm', + }, + }, + md: { + 'px': '$4', + 'py': '$3', + '_text': { + 'fontSize': '$md', + }, + }, + lg: { + 'px': '$6', + 'py': '$4', + '_text': { + 'fontSize': '$lg', + }, + }, + }, + }, + }, + + Card: { + theme: { + 'bg': '$white', + 'rounded': '$lg', + 'shadowColor': '$backgroundLight800', + 'shadowOffset': { + width: 0, + height: 2, + }, + 'shadowOpacity': 0.1, + 'shadowRadius': 8, + 'elevation': 3, + + variants: { + elevated: { + 'shadowOpacity': 0.15, + 'elevation': 6, + }, + + outlined: { + 'borderWidth': 1, + 'borderColor': '$gray200', + 'shadowOpacity': 0, + 'elevation': 0, + }, + }, + }, + }, + + Badge: { + theme: { + 'rounded': '$full', + 'px': '$3', + 'py': '$1', + + variants: { + solid: { + 'bg': '$brand500', + '_text': { + 'color': '$white', + 'fontSize': '$sm', + 'fontWeight': '$semibold', + }, + }, + + outline: { + 'borderWidth': 1, + 'borderColor': '$brand500', + '_text': { + 'color': '$brand500', + 'fontSize': '$sm', + 'fontWeight': '$semibold', + }, + }, + + subtle: { + 'bg': '$brand50', + '_text': { + 'color': '$brand700', + 'fontSize': '$sm', + 'fontWeight': '$semibold', + }, + }, + }, + }, + }, + }, +}) + +export type Config = typeof config +``` + +## Core Game Components + +### GameCard Component - The Heart of the Quiz + +```typescript +// packages/ui-components/src/components/GameCard/GameCard.tsx +import React, { useState, useEffect, useCallback } from 'react' +import { + Card, + VStack, + HStack, + Text, + Input, + InputField, + Button, + ButtonText, + Badge, + Progress, + ProgressFilledTrack, + Box, + Pressable, +} from '@gluestack-ui/themed' +import { knowFooleryTheme } from '../../theme' + +export interface GameCardProps { + // Game data + question: string + theme: string + difficulty?: 'easy' | 'medium' | 'hard' + + // Game state + timeRemaining: number + attemptsLeft: number + currentScore: number + hintUsed: boolean + + // Interaction handlers + onSubmitAnswer: (answer: string) => Promise + onRequestHint: () => Promise + onTimeExpire?: () => void + + // UI state + isLoading?: boolean + hint?: string + showHint?: boolean +} + +export const GameCard: React.FC = ({ + question, + theme, + difficulty = 'medium', + timeRemaining, + attemptsLeft, + currentScore, + hintUsed, + onSubmitAnswer, + onRequestHint, + onTimeExpire, + isLoading = false, + hint, + showHint = false, +}) => { + const [answer, setAnswer] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + // Timer formatting + const formatTime = useCallback((seconds: number): string => { + const minutes = Math.floor(seconds / 60) + const remainingSeconds = seconds % 60 + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}` + }, []) + + // Handle answer submission + const handleSubmit = useCallback(async () => { + if (!answer.trim() || isSubmitting || isLoading) return + + setIsSubmitting(true) + try { + await onSubmitAnswer(answer.trim()) + setAnswer('') // Clear input after successful submission + } catch (error) { + // Error handling managed by parent component + console.error('Failed to submit answer:', error) + } finally { + setIsSubmitting(false) + } + }, [answer, isSubmitting, isLoading, onSubmitAnswer]) + + // Handle hint request + const handleHintRequest = useCallback(async () => { + if (isLoading || isSubmitting || hintUsed) return + + try { + await onRequestHint() + } catch (error) { + console.error('Failed to request hint:', error) + } + }, [isLoading, isSubmitting, hintUsed, onRequestHint]) + + // Time expiration effect + useEffect(() => { + if (timeRemaining === 0 && onTimeExpire) { + onTimeExpire() + } + }, [timeRemaining, onTimeExpire]) + + // Computed values + const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100 + const isTimeWarning = timeRemaining <= 300 // 5 minutes + const isTimeCritical = timeRemaining <= 60 // 1 minute + const difficultyColor = knowFooleryTheme.game.difficulty[difficulty] + const timerColor = isTimeCritical + ? knowFooleryTheme.game.timer.critical + : isTimeWarning + ? knowFooleryTheme.game.timer.warning + : knowFooleryTheme.game.timer.normal + + return ( + + + {/* Header with theme, difficulty, and timer */} + + + + + {theme} + + + + {difficulty && ( + + + {difficulty.toUpperCase()} + + + )} + + + + + ⏱️ {formatTime(timeRemaining)} + + + + + {/* Session progress indicator */} + + + + + {/* Question display */} + + + {question} + + + + {/* Hint display */} + {showHint && hint && ( + + + πŸ’‘ + + {hint} + + + + )} + + {/* Answer input */} + + + + + {/* Game statistics */} + + + {attemptsLeft} attempts left + + + Score: {currentScore} points + + + + {/* Action buttons */} + + + + + + + {/* Hint usage warning */} + {!hintUsed && ( + + Using a hint reduces your score to 1 point for this question + + )} + + + ) +} + +export default GameCard +``` + +### Leaderboard Component + +```typescript +// packages/ui-components/src/components/Leaderboard/Leaderboard.tsx +import React from 'react' +import { + Card, + VStack, + HStack, + Text, + Box, + Badge, + Divider, + ScrollView, +} from '@gluestack-ui/themed' +import { knowFooleryTheme } from '../../theme' + +export interface LeaderboardEntry { + position: number + playerName: string + score: number + questionsAnswered: number + successRate: number + completedAt: string + isCurrentUser?: boolean +} + +export interface LeaderboardProps { + entries: LeaderboardEntry[] + currentUserPosition?: number + isLoading?: boolean + title?: string +} + +export const Leaderboard: React.FC = ({ + entries, + currentUserPosition, + isLoading = false, + title = "πŸ† Leaderboard", +}) => { + const getPositionColor = (position: number) => { + switch (position) { + case 1: return knowFooleryTheme.game.leaderboard.first + case 2: return knowFooleryTheme.game.leaderboard.second + case 3: return knowFooleryTheme.game.leaderboard.third + default: return knowFooleryTheme.game.leaderboard.other + } + } + + const getPositionIcon = (position: number) => { + switch (position) { + case 1: return 'πŸ₯‡' + case 2: return 'πŸ₯ˆ' + case 3: return 'πŸ₯‰' + default: return `#${position}` + } + } + + const formatSuccessRate = (rate: number) => `${Math.round(rate)}%` + + if (isLoading) { + return ( + + + + {title} + + + Loading leaderboard... + + + + ) + } + + return ( + + + {/* Header */} + + + {title} + + {currentUserPosition && ( + + + Your Rank: #{currentUserPosition} + + + )} + + + + + {/* Leaderboard entries */} + + + {entries.map((entry, index) => ( + + ))} + + {entries.length === 0 && ( + + + No scores yet. Be the first to play! + + + )} + + + + + ) +} + +interface LeaderboardRowProps { + entry: LeaderboardEntry + positionColor: string + positionIcon: string +} + +const LeaderboardRow: React.FC = ({ + entry, + positionColor, + positionIcon +}) => { + return ( + + + {/* Position */} + + + {positionIcon} + + + + {/* Player info */} + + + + {entry.playerName} + {entry.isCurrentUser && ( + (You) + )} + + + {entry.score} + + + + + + {entry.questionsAnswered} questions + + + {formatSuccessRate(entry.successRate)} accuracy + + + + + + ) +} + +export default Leaderboard +``` + +### Timer Component + +```typescript +// packages/ui-components/src/components/Timer/Timer.tsx +import React, { useEffect, useState } from 'react' +import { + HStack, + VStack, + Text, + Box, + Progress, + ProgressFilledTrack, +} from '@gluestack-ui/themed' +import { knowFooleryTheme } from '../../theme' + +export interface TimerProps { + timeRemaining: number // in seconds + totalTime?: number // in seconds, defaults to 30 minutes + onTimeExpire?: () => void + onWarning?: (timeLeft: number) => void + showProgress?: boolean + size?: 'sm' | 'md' | 'lg' + variant?: 'compact' | 'detailed' +} + +export const Timer: React.FC = ({ + timeRemaining, + totalTime = 30 * 60, // 30 minutes default + onTimeExpire, + onWarning, + showProgress = true, + size = 'md', + variant = 'detailed', +}) => { + const [hasWarned5Min, setHasWarned5Min] = useState(false) + const [hasWarned1Min, setHasWarned1Min] = useState(false) + + // Format time display + const formatTime = (seconds: number): string => { + const minutes = Math.floor(seconds / 60) + const remainingSeconds = seconds % 60 + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}` + } + + // Calculate progress percentage + const progressValue = ((totalTime - timeRemaining) / totalTime) * 100 + + // Determine timer state and colors + const isTimeCritical = timeRemaining <= 60 // 1 minute + const isTimeWarning = timeRemaining <= 300 // 5 minutes + + const timerColor = isTimeCritical + ? knowFooleryTheme.game.timer.critical + : isTimeWarning + ? knowFooleryTheme.game.timer.warning + : knowFooleryTheme.game.timer.normal + + const progressColor = isTimeCritical + ? '$error500' + : isTimeWarning + ? '$warning500' + : '$brand500' + + // Font sizes based on size prop + const fontSizes = { + sm: { time: '$md', label: '$xs' }, + md: { time: '$lg', label: '$sm' }, + lg: { time: '$2xl', label: '$md' }, + } + + // Handle warnings and expiration + useEffect(() => { + if (timeRemaining === 0 && onTimeExpire) { + onTimeExpire() + } else if (timeRemaining <= 60 && !hasWarned1Min && onWarning) { + setHasWarned1Min(true) + onWarning(timeRemaining) + } else if (timeRemaining <= 300 && !hasWarned5Min && onWarning) { + setHasWarned5Min(true) + onWarning(timeRemaining) + } + }, [timeRemaining, onTimeExpire, onWarning, hasWarned1Min, hasWarned5Min]) + + if (variant === 'compact') { + return ( + + ⏱️ + + {formatTime(timeRemaining)} + + + ) + } + + return ( + + {/* Time display */} + + ⏱️ + + + {formatTime(timeRemaining)} + + + Time Remaining + + + + + {/* Progress bar */} + {showProgress && ( + + + + + {variant === 'detailed' && ( + + + 0:00 + + + {formatTime(totalTime)} + + + )} + + )} + + {/* Warning messages */} + {isTimeCritical && ( + + + ⚠️ Less than 1 minute remaining! + + + )} + + {isTimeWarning && !isTimeCritical && ( + + + ⏰ 5 minutes or less remaining + + + )} + + ) +} + +export default Timer +``` + +## Platform-Specific Adaptations + +### Web-Specific Optimizations + +```typescript +// apps/web/src/components/WebGameCard.tsx +import React from 'react' +import { GameCard, GameCardProps } from '@knowfoolery/ui-components' +import { useHotkeys } from 'react-hotkeys-hook' + +interface WebGameCardProps extends GameCardProps { + enableKeyboardShortcuts?: boolean +} + +export const WebGameCard: React.FC = ({ + enableKeyboardShortcuts = true, + onSubmitAnswer, + onRequestHint, + ...props +}) => { + // Keyboard shortcuts for web + useHotkeys('enter', () => { + const answerInput = document.querySelector('[data-testid="answer-input"] input') as HTMLInputElement + if (answerInput && answerInput.value.trim()) { + onSubmitAnswer(answerInput.value.trim()) + } + }, { enabled: enableKeyboardShortcuts }) + + useHotkeys('ctrl+h, cmd+h', (event) => { + event.preventDefault() + onRequestHint() + }, { enabled: enableKeyboardShortcuts }) + + return ( +
+ + + {enableKeyboardShortcuts && ( +
+ + Press Enter to submit β€’ Ctrl/Cmd + H for hint + +
+ )} +
+ ) +} +``` + +### Mobile-Specific Optimizations + +```typescript +// apps/mobile/src/components/MobileGameCard.tsx +import React from 'react' +import { Vibration, Platform } from 'react-native' +import { GameCard, GameCardProps } from '@knowfoolery/ui-components' + +interface MobileGameCardProps extends GameCardProps { + enableHaptics?: boolean +} + +export const MobileGameCard: React.FC = ({ + enableHaptics = true, + onSubmitAnswer, + onRequestHint, + onTimeExpire, + ...props +}) => { + const handleSubmitWithHaptics = async (answer: string) => { + if (enableHaptics && Platform.OS === 'ios') { + // Use iOS haptics + const { ImpactFeedbackGenerator } = require('expo-haptics') + ImpactFeedbackGenerator.impactAsync(ImpactFeedbackGenerator.ImpactFeedbackStyle.Medium) + } else if (enableHaptics && Platform.OS === 'android') { + // Use Android vibration + Vibration.vibrate(50) + } + + await onSubmitAnswer(answer) + } + + const handleHintWithHaptics = async () => { + if (enableHaptics) { + if (Platform.OS === 'ios') { + const { NotificationFeedbackGenerator } = require('expo-haptics') + NotificationFeedbackGenerator.notificationAsync( + NotificationFeedbackGenerator.NotificationFeedbackType.Warning + ) + } else if (Platform.OS === 'android') { + Vibration.vibrate([0, 100, 50, 100]) + } + } + + await onRequestHint() + } + + const handleTimeExpireWithHaptics = () => { + if (enableHaptics) { + if (Platform.OS === 'ios') { + const { NotificationFeedbackGenerator } = require('expo-haptics') + NotificationFeedbackGenerator.notificationAsync( + NotificationFeedbackGenerator.NotificationFeedbackType.Error + ) + } else if (Platform.OS === 'android') { + Vibration.vibrate([0, 200, 100, 200, 100, 200]) + } + } + + onTimeExpire?.() + } + + return ( + + ) +} +``` + +## Responsive Design Strategy + +### Breakpoint System + +```typescript +// packages/ui-components/src/theme/breakpoints.ts +export const breakpoints = { + sm: 640, // Mobile landscape + md: 768, // Tablet portrait + lg: 1024, // Tablet landscape / Desktop + xl: 1280, // Large desktop + '2xl': 1536, // Extra large desktop +} + +export const useResponsiveValue = (values: { + base: T + sm?: T + md?: T + lg?: T + xl?: T +}) => { + const [screenWidth, setScreenWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : breakpoints.lg + ) + + useEffect(() => { + if (typeof window === 'undefined') return + + const handleResize = () => setScreenWidth(window.innerWidth) + window.addEventListener('resize', handleResize) + + return () => window.removeEventListener('resize', handleResize) + }, []) + + if (screenWidth >= breakpoints.xl && values.xl !== undefined) return values.xl + if (screenWidth >= breakpoints.lg && values.lg !== undefined) return values.lg + if (screenWidth >= breakpoints.md && values.md !== undefined) return values.md + if (screenWidth >= breakpoints.sm && values.sm !== undefined) return values.sm + + return values.base +} + +// Usage example +export const ResponsiveGameLayout: React.FC = ({ children }) => { + const padding = useResponsiveValue({ + base: '$4', + md: '$6', + lg: '$8', + }) + + const columns = useResponsiveValue({ + base: 1, + lg: 2, + }) + + return ( + + + {children} + + + ) +} +``` + +## Testing Strategy for Cross-Platform Components + +### Component Testing + +```typescript +// packages/ui-components/src/components/GameCard/GameCard.test.tsx +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { GluestackUIProvider } from '@gluestack-ui/themed' +import { config } from '../../../gluestack-ui.config' +import { GameCard, GameCardProps } from './GameCard' + +const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + {children} + +) + +const defaultProps: GameCardProps = { + question: "What is the capital of France?", + theme: "Geography", + difficulty: "medium", + timeRemaining: 1800, + attemptsLeft: 3, + currentScore: 0, + hintUsed: false, + onSubmitAnswer: jest.fn(), + onRequestHint: jest.fn(), +} + +describe('GameCard Component', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Cross-Platform Rendering', () => { + it('renders correctly with all required props', () => { + render(, { wrapper: TestWrapper }) + + expect(screen.getByTestId('game-card')).toBeInTheDocument() + expect(screen.getByTestId('question-text')).toHaveTextContent(defaultProps.question) + expect(screen.getByTestId('theme-badge')).toHaveTextContent(defaultProps.theme) + expect(screen.getByTestId('timer-display')).toBeInTheDocument() + }) + + it('handles different screen sizes appropriately', () => { + // Mock different viewport sizes + const viewports = [ + { width: 375, height: 667 }, // Mobile + { width: 768, height: 1024 }, // Tablet + { width: 1440, height: 900 }, // Desktop + ] + + viewports.forEach(viewport => { + // Mock window dimensions + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: viewport.width, + }) + + render(, { wrapper: TestWrapper }) + + const gameCard = screen.getByTestId('game-card') + expect(gameCard).toBeInTheDocument() + + // Component should maintain functionality across all sizes + expect(screen.getByTestId('answer-input')).toBeInTheDocument() + expect(screen.getByTestId('submit-button')).toBeInTheDocument() + }) + }) + }) + + describe('Interactive Functionality', () => { + it('submits answer when submit button is pressed', async () => { + const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined) + + render( + , + { wrapper: TestWrapper } + ) + + const input = screen.getByTestId('answer-input') + const submitButton = screen.getByTestId('submit-button') + + // Enter answer + fireEvent.changeText(input, 'Paris') + + // Submit answer + fireEvent.press(submitButton) + + await waitFor(() => { + expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris') + }) + }) + + it('requests hint when hint button is pressed', async () => { + const mockOnRequestHint = jest.fn().mockResolvedValue(undefined) + + render( + , + { wrapper: TestWrapper } + ) + + const hintButton = screen.getByTestId('hint-button') + fireEvent.press(hintButton) + + await waitFor(() => { + expect(mockOnRequestHint).toHaveBeenCalled() + }) + }) + }) + + describe('Timer and State Management', () => { + it('displays timer with correct formatting', () => { + render(, { wrapper: TestWrapper }) + + expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05') + }) + + it('shows warning colors when time is low', () => { + render(, { wrapper: TestWrapper }) + + const timer = screen.getByTestId('timer-display') + // Check if timer has warning/critical styling applied + expect(timer).toHaveStyle({ color: expect.stringContaining('#ef4444') }) + }) + }) +}) +``` + +This comprehensive Gluestack UI strategy ensures Know Foolery delivers a consistent, performant, and maintainable user experience across all platforms while leveraging the power of a unified component system. \ No newline at end of file diff --git a/docs/observability-strategy.md b/docs/observability-strategy.md new file mode 100644 index 0000000..4c26e91 --- /dev/null +++ b/docs/observability-strategy.md @@ -0,0 +1,1487 @@ +# Know Foolery - Observability Strategy + +## Overview + +Comprehensive observability is essential for maintaining the Know Foolery quiz game's reliability, performance, and user experience. This document outlines the strategy for metrics collection, monitoring, alerting, and distributed tracing across all system components. + +## Observability Architecture + +### Three Pillars of Observability + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Observability Stack β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ METRICS β”‚ β”‚ LOGS β”‚ β”‚ TRACES β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Prometheus β”‚ β”‚ Loki β”‚ β”‚ Jaeger β”‚ β”‚ +β”‚ β”‚ + β”‚ β”‚ + β”‚ β”‚ + β”‚ β”‚ +β”‚ β”‚ Grafana β”‚ β”‚ Grafana β”‚ β”‚ OpenTeleme- β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ try β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Grafana β”‚ β”‚ +β”‚ β”‚ Unified β”‚ β”‚ +β”‚ β”‚ Dashboard β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + Alerts & Notifications + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Alert Management β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Email β”‚ β”‚ Slack β”‚ β”‚ PagerDuty β”‚ β”‚ +β”‚ β”‚ Alerts β”‚ β”‚ Channels β”‚ β”‚ (Critical) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Metrics Strategy + +### Application Metrics Collection + +#### 1. Business Metrics (Game-Specific) +```go +// Business metrics for game insights +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // Game session metrics + gamesStarted = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "games_started_total", + Help: "Total number of games started", + }, + []string{"player_type", "platform"}, + ) + + gamesCompleted = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "games_completed_total", + Help: "Total number of games completed", + }, + []string{"completion_type", "platform"}, // normal, timeout, abandoned + ) + + sessionDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "game_session_duration_seconds", + Help: "Duration of game sessions", + Buckets: []float64{60, 300, 600, 900, 1200, 1500, 1800}, // 1min to 30min + }, + []string{"completion_type"}, + ) + + // Question and answer metrics + questionsAsked = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "questions_asked_total", + Help: "Total number of questions asked", + }, + []string{"theme", "difficulty"}, + ) + + answersSubmitted = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "answers_submitted_total", + Help: "Total number of answers submitted", + }, + []string{"theme", "is_correct", "attempt_number", "used_hint"}, + ) + + hintsRequested = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "hints_requested_total", + Help: "Total number of hints requested", + }, + []string{"theme", "question_difficulty"}, + ) + + // Score distribution + 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"}, + ) + + // Leaderboard metrics + leaderboardUpdates = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "leaderboard_updates_total", + Help: "Total number of leaderboard updates", + }, + ) + + topScoreChanges = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "top_score_changes_total", + Help: "Changes in top 10 scores", + }, + []string{"position"}, // top_1, top_5, top_10 + ) +) + +// Business metrics collection service +type GameMetrics struct { + registry prometheus.Registerer +} + +func NewGameMetrics() *GameMetrics { + return &GameMetrics{ + registry: prometheus.DefaultRegisterer, + } +} + +func (m *GameMetrics) RecordGameStart(playerType, platform string) { + gamesStarted.WithLabelValues(playerType, platform).Inc() +} + +func (m *GameMetrics) RecordGameCompletion(completionType, platform string, duration time.Duration) { + gamesCompleted.WithLabelValues(completionType, platform).Inc() + sessionDuration.WithLabelValues(completionType).Observe(duration.Seconds()) +} + +func (m *GameMetrics) RecordQuestionAsked(theme, difficulty string) { + questionsAsked.WithLabelValues(theme, difficulty).Inc() +} + +func (m *GameMetrics) RecordAnswerSubmitted(theme string, isCorrect bool, attemptNum int, usedHint bool) { + answersSubmitted.WithLabelValues( + theme, + strconv.FormatBool(isCorrect), + strconv.Itoa(attemptNum), + strconv.FormatBool(usedHint), + ).Inc() +} + +func (m *GameMetrics) RecordFinalScore(score int, sessionDuration time.Duration) { + durationBucket := m.getDurationBucket(sessionDuration) + scoreDistribution.WithLabelValues(durationBucket).Observe(float64(score)) +} + +func (m *GameMetrics) getDurationBucket(duration time.Duration) string { + minutes := int(duration.Minutes()) + switch { + case minutes <= 5: + return "0-5min" + case minutes <= 15: + return "5-15min" + case minutes <= 25: + return "15-25min" + default: + return "25-30min" + } +} +``` + +#### 2. Technical Metrics (Infrastructure) +```go +// Technical metrics for system health +var ( + // HTTP metrics + httpRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "endpoint", "status_code", "service"}, + ) + + httpRequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "endpoint", "service"}, + ) + + // Database metrics + dbConnectionsActive = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "db_connections_active", + Help: "Number of active database connections", + }, + []string{"database", "service"}, + ) + + 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"}, + ) + + dbErrors = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "db_errors_total", + Help: "Total number of database errors", + }, + []string{"error_type", "service"}, + ) + + // Cache metrics + cacheOperations = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "cache_operations_total", + Help: "Total number of cache operations", + }, + []string{"operation", "result", "service"}, // get/set/delete, hit/miss/error + ) + + cacheKeyCount = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "cache_keys_total", + Help: "Number of keys in cache", + }, + []string{"cache_type", "service"}, + ) + + // Authentication metrics + authenticationAttempts = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "authentication_attempts_total", + Help: "Total authentication attempts", + }, + []string{"method", "result", "user_type"}, // jwt/oauth, success/failure, player/admin + ) + + tokenOperations = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "token_operations_total", + Help: "JWT token operations", + }, + []string{"operation", "result"}, // validate/refresh, success/failure + ) +) + +// Fiber middleware for HTTP metrics +func PrometheusMiddleware(serviceName string) fiber.Handler { + return func(c *fiber.Ctx) error { + start := time.Now() + + // Process request + err := c.Next() + + // Record metrics + duration := time.Since(start).Seconds() + statusCode := strconv.Itoa(c.Response().StatusCode()) + + httpRequestsTotal.WithLabelValues( + c.Method(), + c.Route().Path, + statusCode, + serviceName, + ).Inc() + + httpRequestDuration.WithLabelValues( + c.Method(), + c.Route().Path, + serviceName, + ).Observe(duration) + + return err + } +} + +// Database metrics middleware for Ent +type MetricsHook struct { + serviceName string +} + +func NewMetricsHook(serviceName string) *MetricsHook { + return &MetricsHook{serviceName: serviceName} +} + +func (h *MetricsHook) Hook() ent.Hook { + return hook.On( + func(next ent.Mutator) ent.Mutator { + return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { + start := time.Now() + + result, err := next.Mutate(ctx, m) + + duration := time.Since(start).Seconds() + queryType := strings.ToLower(m.Op().String()) + table := m.Type() + + dbQueryDuration.WithLabelValues(queryType, table, h.serviceName).Observe(duration) + + if err != nil { + dbErrors.WithLabelValues("query_error", h.serviceName).Inc() + } + + return result, err + }) + }, + ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne|ent.OpDelete|ent.OpDeleteOne, + ) +} +``` + +### Frontend Metrics Collection + +#### Web Application Metrics +```typescript +// Frontend metrics collection +class MetricsCollector { + private endpoint: string + private batchSize: number = 50 + private flushInterval: number = 30000 // 30 seconds + private metrics: MetricEvent[] = [] + + constructor(endpoint: string) { + this.endpoint = endpoint + this.startBatchFlush() + this.setupPerformanceObserver() + } + + // User interaction metrics + trackUserAction(action: string, properties: Record = {}): void { + this.addMetric({ + type: 'user_action', + action, + timestamp: Date.now(), + session_id: this.getSessionId(), + user_agent: navigator.userAgent, + ...properties, + }) + } + + // Game-specific metrics + trackGameEvent(event: GameEvent): void { + this.addMetric({ + type: 'game_event', + event: event.type, + timestamp: Date.now(), + session_id: this.getSessionId(), + game_session_id: event.gameSessionId, + properties: event.properties, + }) + } + + // Performance metrics + trackPerformance(metric: PerformanceMetric): void { + this.addMetric({ + type: 'performance', + metric: metric.name, + value: metric.value, + timestamp: Date.now(), + url: window.location.pathname, + }) + } + + // Error tracking + trackError(error: Error, context: string): void { + this.addMetric({ + type: 'error', + error_message: error.message, + error_stack: error.stack, + context, + timestamp: Date.now(), + url: window.location.pathname, + user_agent: navigator.userAgent, + }) + } + + private setupPerformanceObserver(): void { + // Web Vitals tracking + if ('PerformanceObserver' in window) { + // Largest Contentful Paint + new PerformanceObserver((list) => { + list.getEntries().forEach((entry) => { + this.trackPerformance({ + name: 'largest_contentful_paint', + value: entry.startTime, + }) + }) + }).observe({ entryTypes: ['largest-contentful-paint'] }) + + // First Input Delay + new PerformanceObserver((list) => { + list.getEntries().forEach((entry) => { + this.trackPerformance({ + name: 'first_input_delay', + value: entry.processingStart - entry.startTime, + }) + }) + }).observe({ entryTypes: ['first-input'] }) + + // Cumulative Layout Shift + new PerformanceObserver((list) => { + let cumulativeScore = 0 + list.getEntries().forEach((entry) => { + if (!entry.hadRecentInput) { + cumulativeScore += entry.value + } + }) + this.trackPerformance({ + name: 'cumulative_layout_shift', + value: cumulativeScore, + }) + }).observe({ entryTypes: ['layout-shift'] }) + } + + // API response time tracking + this.interceptFetch() + } + + private interceptFetch(): void { + const originalFetch = window.fetch + window.fetch = async (...args) => { + const start = performance.now() + const url = args[0].toString() + + try { + const response = await originalFetch(...args) + const duration = performance.now() - start + + this.trackPerformance({ + name: 'api_request_duration', + value: duration, + url, + status: response.status, + }) + + return response + } catch (error) { + const duration = performance.now() - start + + this.trackError(error as Error, `API request to ${url}`) + this.trackPerformance({ + name: 'api_request_duration', + value: duration, + url, + status: 0, + }) + + throw error + } + } + } + + private addMetric(metric: MetricEvent): void { + this.metrics.push(metric) + + if (this.metrics.length >= this.batchSize) { + this.flush() + } + } + + private async flush(): Promise { + if (this.metrics.length === 0) return + + const batch = [...this.metrics] + this.metrics = [] + + try { + await fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ metrics: batch }), + }) + } catch (error) { + console.error('Failed to send metrics:', error) + // Re-queue metrics for retry + this.metrics.unshift(...batch) + } + } + + private startBatchFlush(): void { + setInterval(() => { + this.flush() + }, this.flushInterval) + + // Flush on page unload + window.addEventListener('beforeunload', () => { + this.flush() + }) + } + + private getSessionId(): string { + // Implementation depends on session management + return sessionStorage.getItem('session_id') || 'anonymous' + } +} + +// Game-specific metrics tracking +export class GameMetricsTracker { + private collector: MetricsCollector + + constructor(collector: MetricsCollector) { + this.collector = collector + } + + trackGameStart(gameSessionId: string, playerName: string): void { + this.collector.trackGameEvent({ + type: 'game_started', + gameSessionId, + properties: { + player_name: playerName, + platform: this.getPlatform(), + }, + }) + } + + trackQuestionDisplayed(gameSessionId: string, questionId: string, theme: string): void { + this.collector.trackGameEvent({ + type: 'question_displayed', + gameSessionId, + properties: { + question_id: questionId, + theme, + display_time: Date.now(), + }, + }) + } + + trackAnswerSubmitted( + gameSessionId: string, + questionId: string, + isCorrect: boolean, + attemptNumber: number, + timeTaken: number, + usedHint: boolean + ): void { + this.collector.trackGameEvent({ + type: 'answer_submitted', + gameSessionId, + properties: { + question_id: questionId, + is_correct: isCorrect, + attempt_number: attemptNumber, + time_taken_ms: timeTaken, + used_hint: usedHint, + }, + }) + } + + trackHintRequested(gameSessionId: string, questionId: string): void { + this.collector.trackGameEvent({ + type: 'hint_requested', + gameSessionId, + properties: { + question_id: questionId, + request_time: Date.now(), + }, + }) + } + + trackGameCompleted( + gameSessionId: string, + finalScore: number, + questionsAnswered: number, + completionType: 'normal' | 'timeout' | 'abandoned' + ): void { + this.collector.trackGameEvent({ + type: 'game_completed', + gameSessionId, + properties: { + final_score: finalScore, + questions_answered: questionsAnswered, + completion_type: completionType, + platform: this.getPlatform(), + }, + }) + } + + private getPlatform(): string { + // Detect platform + if (/Android/i.test(navigator.userAgent)) return 'android' + if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) return 'ios' + if (window.wails) return 'desktop' // For Wails apps + return 'web' + } +} + +// Usage in React components +export const useGameMetrics = () => { + const collector = useRef(new MetricsCollector('/api/v1/metrics')) + const gameTracker = useRef(new GameMetricsTracker(collector.current)) + + return { + trackGameStart: gameTracker.current.trackGameStart.bind(gameTracker.current), + trackQuestionDisplayed: gameTracker.current.trackQuestionDisplayed.bind(gameTracker.current), + trackAnswerSubmitted: gameTracker.current.trackAnswerSubmitted.bind(gameTracker.current), + trackHintRequested: gameTracker.current.trackHintRequested.bind(gameTracker.current), + trackGameCompleted: gameTracker.current.trackGameCompleted.bind(gameTracker.current), + trackUserAction: collector.current.trackUserAction.bind(collector.current), + trackError: collector.current.trackError.bind(collector.current), + } +} +``` + +## Distributed Tracing + +### OpenTelemetry Integration + +#### Backend Tracing Setup +```go +// OpenTelemetry tracing setup +package observability + +import ( + "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/semconv/v1.12.0/httpconv" + "go.opentelemetry.io/otel/semconv/v1.12.0/netconv" +) + +type TracingConfig struct { + ServiceName string + ServiceVersion string + Environment string + JaegerEndpoint string + SampleRate float64 +} + +func InitTracing(config TracingConfig) (*trace.TracerProvider, error) { + // Create Jaeger exporter + jaegerExporter, err := jaeger.New( + jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(config.JaegerEndpoint)), + ) + if err != nil { + return nil, err + } + + // Create resource with service information + res, err := resource.New( + context.Background(), + resource.WithAttributes( + attribute.String("service.name", config.ServiceName), + attribute.String("service.version", config.ServiceVersion), + attribute.String("environment", config.Environment), + ), + ) + if err != nil { + return nil, err + } + + // Create tracer provider + tp := trace.NewTracerProvider( + trace.WithBatcher(jaegerExporter), + trace.WithResource(res), + trace.WithSampler(trace.TraceIDRatioBased(config.SampleRate)), + ) + + // Set global tracer provider + otel.SetTracerProvider(tp) + + return tp, nil +} + +// Fiber middleware for distributed tracing +func TracingMiddleware(serviceName string) fiber.Handler { + tracer := otel.Tracer(serviceName) + + return func(c *fiber.Ctx) error { + // Start span + ctx, span := tracer.Start(c.Context(), fmt.Sprintf("%s %s", c.Method(), c.Route().Path)) + defer span.End() + + // Set span attributes + span.SetAttributes( + httpconv.HTTPMethodKey.String(c.Method()), + httpconv.HTTPURLKey.String(string(c.Request().URI().FullURI())), + httpconv.HTTPUserAgentKey.String(string(c.Request().Header.UserAgent())), + netconv.NetPeerIPKey.String(c.IP()), + ) + + // Add to context + c.SetUserContext(ctx) + + // Process request + err := c.Next() + + // Set response attributes + span.SetAttributes( + httpconv.HTTPStatusCodeKey.Int(c.Response().StatusCode()), + ) + + if err != nil { + span.RecordError(err) + } + + return err + } +} + +// Service-level tracing helpers +func TraceServiceOperation(ctx context.Context, serviceName, operation string, fn func(context.Context) error) error { + tracer := otel.Tracer(serviceName) + ctx, span := tracer.Start(ctx, operation) + defer span.End() + + err := fn(ctx) + if err != nil { + span.RecordError(err) + span.SetAttributes(attribute.Bool("error", true)) + } + + return err +} + +// Database tracing for Ent +func TraceDatabaseOperation(ctx context.Context, operation, table string, fn func(context.Context) error) error { + tracer := otel.Tracer("database") + ctx, span := tracer.Start(ctx, fmt.Sprintf("db.%s.%s", operation, table)) + defer span.End() + + span.SetAttributes( + attribute.String("db.operation", operation), + attribute.String("db.table", table), + attribute.String("db.system", "postgresql"), + ) + + err := fn(ctx) + if err != nil { + span.RecordError(err) + } + + return err +} +``` + +#### Frontend Tracing Integration +```typescript +// Frontend tracing with OpenTelemetry +import { WebTracerProvider } from '@opentelemetry/sdk-trace-web' +import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web' +import { JaegerExporter } from '@opentelemetry/exporter-jaeger' +import { registerInstrumentations } from '@opentelemetry/instrumentation' + +export class FrontendTracing { + private provider: WebTracerProvider + + constructor(config: TracingConfig) { + this.provider = new WebTracerProvider({ + resource: new Resource({ + 'service.name': config.serviceName, + 'service.version': config.serviceVersion, + }), + }) + + // Configure Jaeger exporter + const jaegerExporter = new JaegerExporter({ + endpoint: config.jaegerEndpoint, + }) + + this.provider.addSpanProcessor( + new BatchSpanProcessor(jaegerExporter) + ) + + // Register provider + this.provider.register() + + // Auto-instrument browser APIs + registerInstrumentations({ + instrumentations: [ + getWebAutoInstrumentations({ + '@opentelemetry/instrumentation-document-load': { + enabled: true, + }, + '@opentelemetry/instrumentation-user-interaction': { + enabled: true, + }, + '@opentelemetry/instrumentation-fetch': { + enabled: true, + propagateTraceHeaderCorsUrls: [ + new RegExp(config.apiBaseUrl), + ], + }, + }), + ], + }) + } + + // Game-specific tracing + traceGameAction(action: string, properties: Record, fn: () => Promise): Promise { + const tracer = trace.getTracer('game-frontend') + + return tracer.startActiveSpan(action, async (span) => { + try { + // Set span attributes + Object.entries(properties).forEach(([key, value]) => { + span.setAttributes({ [key]: value }) + }) + + await fn() + } catch (error) { + span.recordException(error as Error) + span.setStatus({ code: SpanStatusCode.ERROR }) + throw error + } finally { + span.end() + } + }) + } +} + +// React hook for tracing +export const useTracing = () => { + const tracer = trace.getTracer('react-components') + + const traceUserAction = useCallback( + async (action: string, properties: Record, fn: () => Promise) => { + return tracer.startActiveSpan(`user.${action}`, async (span) => { + try { + span.setAttributes(properties) + await fn() + } catch (error) { + span.recordException(error as Error) + throw error + } finally { + span.end() + } + }) + }, + [tracer] + ) + + return { traceUserAction } +} +``` + +## Logging Strategy + +### Structured Logging Implementation + +#### Backend Logging +```go +// Structured logging with zerolog +package logging + +import ( + "os" + "time" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +type Logger struct { + logger zerolog.Logger +} + +type LogConfig struct { + Level string + Environment string + ServiceName string + Version string +} + +func NewLogger(config LogConfig) *Logger { + // Parse log level + level, err := zerolog.ParseLevel(config.Level) + if err != nil { + level = zerolog.InfoLevel + } + + // Configure zerolog + zerolog.SetGlobalLevel(level) + zerolog.TimeFieldFormat = time.RFC3339Nano + + var logger zerolog.Logger + + if config.Environment == "development" { + // Human-readable console output for development + logger = zerolog.New(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: "15:04:05", + }).With().Timestamp().Logger() + } else { + // JSON output for production + logger = zerolog.New(os.Stdout).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} +} + +// Structured logging methods +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") +} + +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") +} + +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") +} + +func (l *Logger) AuthenticationEvent(event, userID, userType string, success bool, details map[string]string) { + level := l.logger.Info() + if !success { + level = l.logger.Warn() + } + + level. + Str("event_type", "authentication"). + Str("event", event). + Str("user_id", userID). + Str("user_type", userType). + Bool("success", success). + Fields(details). + Msg("Authentication 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") +} + +func (l *Logger) Error(err error, context string, fields map[string]interface{}) { + l.logger.Error(). + Err(err). + Str("context", context). + Fields(fields). + Msg("Error occurred") +} + +// Fiber middleware for request logging +func RequestLoggingMiddleware(logger *Logger) fiber.Handler { + return func(c *fiber.Ctx) error { + start := time.Now() + + // Process request + err := c.Next() + + // Log request + duration := time.Since(start) + userID := c.Locals("user_id") + if userID == nil { + userID = "anonymous" + } + + logger.APIRequest( + c.Method(), + c.Route().Path, + c.Response().StatusCode(), + duration, + userID.(string), + ) + + return err + } +} +``` + +#### Frontend Logging +```typescript +// Frontend structured logging +interface LogEntry { + timestamp: string + level: 'debug' | 'info' | 'warn' | 'error' + message: string + context?: string + userId?: string + sessionId?: string + gameSessionId?: string + error?: { + name: string + message: string + stack?: string + } + properties?: Record +} + +export class FrontendLogger { + private buffer: LogEntry[] = [] + private endpoint: string + private maxBufferSize: number = 100 + private flushInterval: number = 30000 + + constructor(endpoint: string) { + this.endpoint = endpoint + this.startPeriodicFlush() + } + + debug(message: string, context?: string, properties?: Record): void { + this.log('debug', message, context, properties) + } + + info(message: string, context?: string, properties?: Record): void { + this.log('info', message, context, properties) + } + + warn(message: string, context?: string, properties?: Record): void { + this.log('warn', message, context, properties) + } + + error(message: string, error?: Error, context?: string, properties?: Record): void { + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level: 'error', + message, + context, + userId: this.getUserId(), + sessionId: this.getSessionId(), + gameSessionId: this.getGameSessionId(), + properties, + } + + if (error) { + entry.error = { + name: error.name, + message: error.message, + stack: error.stack, + } + } + + this.buffer.push(entry) + this.checkFlushConditions() + } + + // Game-specific logging methods + logGameEvent(event: string, gameSessionId: string, properties?: Record): void { + this.info(`Game event: ${event}`, 'game', { + gameSessionId, + ...properties, + }) + } + + logUserAction(action: string, properties?: Record): void { + this.info(`User action: ${action}`, 'user', properties) + } + + logPerformanceMetric(metric: string, value: number, unit: string): void { + this.debug(`Performance metric: ${metric}`, 'performance', { + metric, + value, + unit, + url: window.location.pathname, + }) + } + + private log(level: LogEntry['level'], message: string, context?: string, properties?: Record): void { + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level, + message, + context, + userId: this.getUserId(), + sessionId: this.getSessionId(), + gameSessionId: this.getGameSessionId(), + properties, + } + + this.buffer.push(entry) + this.checkFlushConditions() + } + + private checkFlushConditions(): void { + if (this.buffer.length >= this.maxBufferSize) { + this.flush() + } + } + + private async flush(): Promise { + if (this.buffer.length === 0) return + + const logs = [...this.buffer] + this.buffer = [] + + try { + await fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ logs }), + }) + } catch (error) { + console.error('Failed to send logs:', error) + // Re-queue logs for retry (keep only most recent to avoid memory issues) + this.buffer.unshift(...logs.slice(-50)) + } + } + + private startPeriodicFlush(): void { + setInterval(() => { + this.flush() + }, this.flushInterval) + + // Flush on page unload + window.addEventListener('beforeunload', () => { + // Use sendBeacon for reliable delivery during page unload + if (this.buffer.length > 0) { + navigator.sendBeacon(this.endpoint, JSON.stringify({ logs: this.buffer })) + } + }) + } + + private getUserId(): string | undefined { + // Implementation depends on auth system + return sessionStorage.getItem('user_id') || undefined + } + + private getSessionId(): string | undefined { + return sessionStorage.getItem('session_id') || undefined + } + + private getGameSessionId(): string | undefined { + return sessionStorage.getItem('game_session_id') || undefined + } +} +``` + +## Monitoring Dashboards + +### Grafana Dashboard Configuration + +#### 1. Business Intelligence Dashboard +```json +{ + "dashboard": { + "title": "Know Foolery - Business Intelligence", + "panels": [ + { + "title": "Active Games", + "type": "stat", + "targets": [ + { + "expr": "sum(rate(games_started_total[5m])) * 300", + "legendFormat": "Games per 5min" + } + ] + }, + { + "title": "Game Completion Rate", + "type": "stat", + "targets": [ + { + "expr": "rate(games_completed_total{completion_type=\"normal\"}[1h]) / rate(games_started_total[1h]) * 100", + "legendFormat": "Completion %" + } + ] + }, + { + "title": "Average Session Duration", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.5, rate(game_session_duration_seconds_bucket[5m]))", + "legendFormat": "Median" + }, + { + "expr": "histogram_quantile(0.95, rate(game_session_duration_seconds_bucket[5m]))", + "legendFormat": "95th percentile" + } + ] + }, + { + "title": "Question Accuracy by Theme", + "type": "heatmap", + "targets": [ + { + "expr": "rate(answers_submitted_total{is_correct=\"true\"}[1h]) / rate(answers_submitted_total[1h]) by (theme)", + "legendFormat": "{{theme}}" + } + ] + }, + { + "title": "Hint Usage Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(hints_requested_total[5m]) / rate(questions_asked_total[5m]) * 100", + "legendFormat": "Hint Usage %" + } + ] + }, + { + "title": "Score Distribution", + "type": "histogram", + "targets": [ + { + "expr": "histogram_quantile(0.25, rate(game_scores_bucket[1h]))", + "legendFormat": "25th percentile" + }, + { + "expr": "histogram_quantile(0.5, rate(game_scores_bucket[1h]))", + "legendFormat": "Median" + }, + { + "expr": "histogram_quantile(0.75, rate(game_scores_bucket[1h]))", + "legendFormat": "75th percentile" + } + ] + } + ] + } +} +``` + +#### 2. Technical Performance Dashboard +```json +{ + "dashboard": { + "title": "Know Foolery - Technical Performance", + "panels": [ + { + "title": "API Response Times", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) by (service)", + "legendFormat": "{{service}} - 95th percentile" + } + ] + }, + { + "title": "Error Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(http_requests_total{status_code=~\"5..\"}[5m]) / rate(http_requests_total[5m]) * 100 by (service)", + "legendFormat": "{{service}} - Error %" + } + ] + }, + { + "title": "Database Performance", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(db_query_duration_seconds_bucket[5m])) by (service)", + "legendFormat": "{{service}} - Query Time" + } + ] + }, + { + "title": "Cache Hit Rate", + "type": "stat", + "targets": [ + { + "expr": "rate(cache_operations_total{result=\"hit\"}[5m]) / rate(cache_operations_total{operation=\"get\"}[5m]) * 100", + "legendFormat": "Hit Rate %" + } + ] + }, + { + "title": "Authentication Success Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(authentication_attempts_total{result=\"success\"}[5m]) / rate(authentication_attempts_total[5m]) * 100 by (method)", + "legendFormat": "{{method}} - Success %" + } + ] + } + ] + } +} +``` + +## Alerting Strategy + +### Alert Rules Configuration + +#### Critical Alerts +```yaml +# prometheus-alerts.yml +groups: + - name: know-foolery-critical + rules: + - alert: HighErrorRate + expr: rate(http_requests_total{status_code=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05 + for: 2m + labels: + severity: critical + team: backend + annotations: + summary: "High error rate detected in {{ $labels.service }}" + description: "Error rate is {{ $value | humanizePercentage }} for service {{ $labels.service }}" + + - alert: DatabaseConnectionFailure + expr: db_connections_active == 0 + for: 1m + labels: + severity: critical + team: backend + annotations: + summary: "Database connections dropped to zero" + description: "Service {{ $labels.service }} has no active database connections" + + - alert: AuthenticationSystemDown + expr: up{service="zitadel"} == 0 + for: 1m + labels: + severity: critical + team: security + annotations: + summary: "Authentication system is down" + description: "Zitadel authentication service is unreachable" + + - alert: GameSessionsStuck + expr: increase(games_started_total[5m]) > increase(games_completed_total[5m]) * 2 + for: 5m + labels: + severity: critical + team: backend + annotations: + summary: "Game sessions not completing" + description: "Many games are starting but not completing normally" + + - name: know-foolery-warning + rules: + - alert: HighLatency + expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1.0 + for: 5m + labels: + severity: warning + team: backend + annotations: + summary: "High API latency detected" + description: "95th percentile latency is {{ $value }}s for {{ $labels.service }}" + + - alert: LowGameCompletionRate + expr: rate(games_completed_total{completion_type="normal"}[1h]) / rate(games_started_total[1h]) < 0.7 + for: 10m + labels: + severity: warning + team: product + annotations: + summary: "Low game completion rate" + description: "Only {{ $value | humanizePercentage }} of games are being completed normally" + + - alert: HighHintUsage + expr: rate(hints_requested_total[1h]) / rate(questions_asked_total[1h]) > 0.8 + for: 15m + labels: + severity: warning + team: product + annotations: + summary: "Unusually high hint usage" + description: "{{ $value | humanizePercentage }} of questions are requesting hints" + + - name: know-foolery-security + rules: + - alert: HighAuthenticationFailures + expr: rate(authentication_attempts_total{result="failure"}[5m]) > 10 + for: 2m + labels: + severity: warning + team: security + annotations: + summary: "High authentication failure rate" + description: "{{ $value }} authentication failures per second" + + - alert: SuspiciousUserActivity + expr: rate(answers_submitted_total[1m]) by (user_id) > 5 + for: 1m + labels: + severity: warning + team: security + annotations: + summary: "Suspicious user activity detected" + description: "User {{ $labels.user_id }} is submitting answers at {{ $value }}/second" +``` + +### Alert Routing and Escalation +```yaml +# alertmanager.yml +global: + slack_api_url: 'https://hooks.slack.com/services/...' + +route: + group_by: ['alertname', 'service'] + group_wait: 10s + group_interval: 10s + repeat_interval: 1h + receiver: 'default' + routes: + - match: + severity: critical + receiver: 'critical-alerts' + group_wait: 0s + - match: + team: security + receiver: 'security-team' + - match: + team: product + receiver: 'product-team' + +receivers: +- name: 'default' + slack_configs: + - channel: '#alerts' + title: 'Know Foolery Alert' + text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' + +- name: 'critical-alerts' + slack_configs: + - channel: '#critical-alerts' + title: 'CRITICAL: Know Foolery' + text: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ .Annotations.description }}{{ end }}' + pagerduty_configs: + - service_key: 'your-pagerduty-key' + description: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' + +- name: 'security-team' + slack_configs: + - channel: '#security-alerts' + title: 'Security Alert: Know Foolery' + text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' + +- name: 'product-team' + slack_configs: + - channel: '#product-alerts' + title: 'Product Alert: Know Foolery' + text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' +``` + +This comprehensive observability strategy ensures that Know Foolery has full visibility into its performance, user behavior, and system health, enabling proactive issue resolution and data-driven product improvements. \ No newline at end of file diff --git a/docs/security-requirements.md b/docs/security-requirements.md new file mode 100644 index 0000000..95f9da9 --- /dev/null +++ b/docs/security-requirements.md @@ -0,0 +1,1661 @@ +# Know Foolery - Security Requirements & Implementation + +## Security Overview + +Know Foolery implements a comprehensive security strategy that addresses authentication, authorization, data protection, application security, and compliance requirements. This document outlines security measures implemented across all system components. + +## Security Architecture + +### Defense in Depth Strategy + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Security Layers β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Perimeter Security β”‚ β”‚ +β”‚ β”‚ WAF β€’ DDoS Protection β€’ Rate Limiting β€’ IP Filtering β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Application Security β”‚ β”‚ +β”‚ β”‚ HTTPS β€’ CORS β€’ CSP β€’ Security Headers β€’ Input Validation β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Authentication & Authorization β”‚ β”‚ +β”‚ β”‚ OAuth 2.0 β€’ JWT β€’ MFA β€’ RBAC β€’ Session Management β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Data Security β”‚ β”‚ +β”‚ β”‚ Encryption at Rest β€’ TLS β€’ Field Encryption β€’ Anonymization β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Infrastructure Security β”‚ β”‚ +β”‚ β”‚ Container Security β€’ Network Segmentation β€’ Secrets Management β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Authentication & Authorization + +### OAuth 2.0/OIDC Implementation + +#### Zitadel Authentication Flow +```go +// Secure authentication implementation +package auth + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/argon2" +) + +type SecureAuthService struct { + zitadelRepo ZitadelRepository + keyStore *KeyStore + sessionManager *SessionManager + auditLogger *AuditLogger + rateLimiter *RateLimiter +} + +type AuthClaims struct { + jwt.RegisteredClaims + Email string `json:"email"` + Name string `json:"name"` + Roles []string `json:"urn:zitadel:iam:org:project:roles"` + MFAVerified bool `json:"amr"` + SessionID string `json:"sid"` + DeviceID string `json:"device_id,omitempty"` + IPAddress string `json:"ip_address,omitempty"` +} + +// Token validation with comprehensive security checks +func (s *SecureAuthService) ValidateToken(ctx context.Context, tokenString string, clientIP string) (*AuthClaims, error) { + // Rate limiting check + if !s.rateLimiter.Allow(fmt.Sprintf("token_validation:%s", clientIP)) { + s.auditLogger.LogSecurityEvent("rate_limit_exceeded", "", clientIP, "warning", map[string]interface{}{ + "operation": "token_validation", + }) + return nil, fmt.Errorf("rate limit exceeded") + } + + // Parse and validate JWT structure + token, err := jwt.ParseWithClaims(tokenString, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { + // Verify signing algorithm + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + // Get key ID from header + keyID, ok := token.Header["kid"].(string) + if !ok { + return nil, fmt.Errorf("missing key ID in token header") + } + + // Retrieve public key + return s.keyStore.GetPublicKey(keyID) + }) + + if err != nil { + s.auditLogger.LogSecurityEvent("invalid_token", "", clientIP, "warning", map[string]interface{}{ + "error": err.Error(), + }) + return nil, fmt.Errorf("token validation failed: %w", err) + } + + if !token.Valid { + return nil, fmt.Errorf("invalid token") + } + + claims, ok := token.Claims.(*AuthClaims) + if !ok { + return nil, fmt.Errorf("invalid token claims") + } + + // Additional security validations + if err := s.validateTokenClaims(claims, clientIP); err != nil { + s.auditLogger.LogSecurityEvent("token_validation_failed", claims.Subject, clientIP, "warning", map[string]interface{}{ + "error": err.Error(), + }) + return nil, err + } + + // Check if session is still valid + if !s.sessionManager.IsValidSession(claims.SessionID, claims.Subject) { + s.auditLogger.LogSecurityEvent("invalid_session", claims.Subject, clientIP, "warning", map[string]interface{}{ + "session_id": claims.SessionID, + }) + return nil, fmt.Errorf("session no longer valid") + } + + // Log successful validation + s.auditLogger.LogSecurityEvent("token_validated", claims.Subject, clientIP, "info", map[string]interface{}{ + "session_id": claims.SessionID, + "roles": claims.Roles, + }) + + return claims, nil +} + +func (s *SecureAuthService) validateTokenClaims(claims *AuthClaims, clientIP string) error { + now := time.Now() + + // Check expiration + if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(now) { + return fmt.Errorf("token expired") + } + + // Check not before + if claims.NotBefore != nil && claims.NotBefore.Time.After(now) { + return fmt.Errorf("token not yet valid") + } + + // Check issued at (prevent future tokens) + if claims.IssuedAt != nil && claims.IssuedAt.Time.After(now.Add(5*time.Minute)) { + return fmt.Errorf("token issued in the future") + } + + // Validate audience + expectedAudience := "knowfoolery-quiz-game" + if !contains(claims.Audience, expectedAudience) { + return fmt.Errorf("invalid audience") + } + + // Validate issuer + expectedIssuer := "https://auth.knowfoolery.com" + if claims.Issuer != expectedIssuer { + return fmt.Errorf("invalid issuer") + } + + // IP address validation (if configured) + if claims.IPAddress != "" && claims.IPAddress != clientIP { + return fmt.Errorf("IP address mismatch") + } + + return nil +} + +// Multi-Factor Authentication enforcement +func (s *SecureAuthService) RequireMFA(userID string, roles []string) bool { + // Admin users always require MFA + for _, role := range roles { + if role == "admin" { + return true + } + } + + // Check if user has high-value permissions + return s.hasHighValuePermissions(roles) +} + +func (s *SecureAuthService) hasHighValuePermissions(roles []string) bool { + highValueRoles := []string{"moderator", "question_manager", "user_manager"} + + for _, userRole := range roles { + for _, hvRole := range highValueRoles { + if userRole == hvRole { + return true + } + } + } + + return false +} +``` + +### Session Management +```go +// Secure session management +package session + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "time" + + "github.com/go-redis/redis/v8" +) + +type SessionManager struct { + redis *redis.Client + entropy int + maxAge time.Duration + secureCookie bool +} + +type Session struct { + ID string `json:"id"` + UserID string `json:"user_id"` + CreatedAt time.Time `json:"created_at"` + LastSeen time.Time `json:"last_seen"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + DeviceID string `json:"device_id,omitempty"` + Roles []string `json:"roles"` + MFAVerified bool `json:"mfa_verified"` +} + +func NewSessionManager(redis *redis.Client, secureCookie bool) *SessionManager { + return &SessionManager{ + redis: redis, + entropy: 32, // 256 bits of entropy + maxAge: 24 * time.Hour, + secureCookie: secureCookie, + } +} + +func (sm *SessionManager) CreateSession(ctx context.Context, userID, ipAddress, userAgent string, roles []string, mfaVerified bool) (*Session, error) { + // Generate cryptographically secure session ID + sessionID, err := sm.generateSecureSessionID() + if err != nil { + return nil, fmt.Errorf("failed to generate session ID: %w", err) + } + + session := &Session{ + ID: sessionID, + UserID: userID, + CreatedAt: time.Now(), + LastSeen: time.Now(), + IPAddress: ipAddress, + UserAgent: userAgent, + DeviceID: sm.generateDeviceFingerprint(ipAddress, userAgent), + Roles: roles, + MFAVerified: mfaVerified, + } + + // Store session in Redis with expiration + sessionKey := fmt.Sprintf("session:%s", sessionID) + sessionData, err := json.Marshal(session) + if err != nil { + return nil, fmt.Errorf("failed to marshal session: %w", err) + } + + err = sm.redis.SetEX(ctx, sessionKey, sessionData, sm.maxAge).Err() + if err != nil { + return nil, fmt.Errorf("failed to store session: %w", err) + } + + // Store user session mapping for concurrent session limiting + userSessionKey := fmt.Sprintf("user_sessions:%s", userID) + sm.redis.SAdd(ctx, userSessionKey, sessionID) + sm.redis.Expire(ctx, userSessionKey, sm.maxAge) + + return session, nil +} + +func (sm *SessionManager) GetSession(ctx context.Context, sessionID string) (*Session, error) { + sessionKey := fmt.Sprintf("session:%s", sessionID) + + sessionData, err := sm.redis.Get(ctx, sessionKey).Result() + if err == redis.Nil { + return nil, fmt.Errorf("session not found") + } else if err != nil { + return nil, fmt.Errorf("failed to retrieve session: %w", err) + } + + var session Session + err = json.Unmarshal([]byte(sessionData), &session) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal session: %w", err) + } + + return &session, nil +} + +func (sm *SessionManager) IsValidSession(sessionID, userID string) bool { + ctx := context.Background() + + session, err := sm.GetSession(ctx, sessionID) + if err != nil { + return false + } + + // Check if session belongs to the correct user + if session.UserID != userID { + return false + } + + // Update last seen timestamp + session.LastSeen = time.Now() + sm.updateSession(ctx, session) + + return true +} + +func (sm *SessionManager) InvalidateSession(ctx context.Context, sessionID string) error { + session, err := sm.GetSession(ctx, sessionID) + if err != nil { + return err + } + + // Remove from Redis + sessionKey := fmt.Sprintf("session:%s", sessionID) + sm.redis.Del(ctx, sessionKey) + + // Remove from user sessions set + userSessionKey := fmt.Sprintf("user_sessions:%s", session.UserID) + sm.redis.SRem(ctx, userSessionKey, sessionID) + + return nil +} + +func (sm *SessionManager) InvalidateAllUserSessions(ctx context.Context, userID string) error { + userSessionKey := fmt.Sprintf("user_sessions:%s", userID) + + sessionIDs, err := sm.redis.SMembers(ctx, userSessionKey).Result() + if err != nil { + return err + } + + // Remove all sessions + for _, sessionID := range sessionIDs { + sessionKey := fmt.Sprintf("session:%s", sessionID) + sm.redis.Del(ctx, sessionKey) + } + + // Clear user sessions set + sm.redis.Del(ctx, userSessionKey) + + return nil +} + +func (sm *SessionManager) generateSecureSessionID() (string, error) { + bytes := make([]byte, sm.entropy) + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + + // Hash the random bytes for additional security + hash := sha256.Sum256(bytes) + return hex.EncodeToString(hash[:]), nil +} + +func (sm *SessionManager) generateDeviceFingerprint(ipAddress, userAgent string) string { + data := fmt.Sprintf("%s:%s", ipAddress, userAgent) + hash := sha256.Sum256([]byte(data)) + return hex.EncodeToString(hash[:16]) // 128-bit fingerprint +} + +func (sm *SessionManager) updateSession(ctx context.Context, session *Session) error { + sessionKey := fmt.Sprintf("session:%s", session.ID) + sessionData, err := json.Marshal(session) + if err != nil { + return err + } + + return sm.redis.SetEX(ctx, sessionKey, sessionData, sm.maxAge).Err() +} +``` + +## Input Validation & Sanitization + +### Comprehensive Input Validation +```go +// Input validation and sanitization framework +package validation + +import ( + "fmt" + "html" + "regexp" + "strings" + "unicode" + + "github.com/go-playground/validator/v10" +) + +type InputValidator struct { + validator *validator.Validate + rules map[string]*ValidationRule +} + +type ValidationRule struct { + MaxLength int + MinLength int + Pattern *regexp.Regexp + AllowedChars *regexp.Regexp + Sanitizer func(string) string +} + +func NewInputValidator() *InputValidator { + v := validator.New() + + // Register custom validations + v.RegisterValidation("alphanum_space", validateAlphanumSpace) + v.RegisterValidation("no_html", validateNoHTML) + v.RegisterValidation("safe_text", validateSafeText) + + iv := &InputValidator{ + validator: v, + rules: make(map[string]*ValidationRule), + } + + iv.setupValidationRules() + return iv +} + +func (iv *InputValidator) setupValidationRules() { + // Player name validation + iv.rules["player_name"] = &ValidationRule{ + MaxLength: 50, + MinLength: 2, + AllowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`), + Sanitizer: iv.sanitizePlayerName, + } + + // Answer validation + iv.rules["answer"] = &ValidationRule{ + MaxLength: 500, + MinLength: 1, + AllowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.,'!?()]+$`), + Sanitizer: iv.sanitizeAnswer, + } + + // Question text validation (admin only) + iv.rules["question_text"] = &ValidationRule{ + MaxLength: 1000, + MinLength: 10, + Sanitizer: iv.sanitizeQuestionText, + } + + // Theme validation + iv.rules["theme"] = &ValidationRule{ + MaxLength: 100, + MinLength: 2, + AllowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`), + Sanitizer: iv.sanitizeTheme, + } +} + +// Validate and sanitize input based on field type +func (iv *InputValidator) ValidateAndSanitize(fieldType, input string) (string, error) { + rule, exists := iv.rules[fieldType] + if !exists { + return "", fmt.Errorf("unknown field type: %s", fieldType) + } + + // Basic length validation + if len(input) < rule.MinLength { + return "", fmt.Errorf("input too short: minimum %d characters", rule.MinLength) + } + + if len(input) > rule.MaxLength { + return "", fmt.Errorf("input too long: maximum %d characters", rule.MaxLength) + } + + // Character validation + if rule.AllowedChars != nil && !rule.AllowedChars.MatchString(input) { + return "", fmt.Errorf("input contains invalid characters") + } + + // Sanitize input + sanitized := input + if rule.Sanitizer != nil { + sanitized = rule.Sanitizer(input) + } + + return sanitized, nil +} + +// Sanitization functions +func (iv *InputValidator) sanitizePlayerName(input string) string { + // Remove HTML entities and tags + sanitized := html.EscapeString(input) + + // Trim whitespace + sanitized = strings.TrimSpace(sanitized) + + // Remove multiple consecutive spaces + spaceRegex := regexp.MustCompile(`\s+`) + sanitized = spaceRegex.ReplaceAllString(sanitized, " ") + + return sanitized +} + +func (iv *InputValidator) sanitizeAnswer(input string) string { + // HTML escape + sanitized := html.EscapeString(input) + + // Trim and normalize whitespace + sanitized = strings.TrimSpace(sanitized) + + // Convert to lowercase for comparison + sanitized = strings.ToLower(sanitized) + + // Remove extra punctuation but keep essential ones + punctRegex := regexp.MustCompile(`[^\w\s\-'.]`) + sanitized = punctRegex.ReplaceAllString(sanitized, "") + + return sanitized +} + +func (iv *InputValidator) sanitizeQuestionText(input string) string { + // More permissive sanitization for question text + sanitized := html.EscapeString(input) + sanitized = strings.TrimSpace(sanitized) + + // Remove potential script content + scriptRegex := regexp.MustCompile(`(?i)]*>.*?`) + sanitized = scriptRegex.ReplaceAllString(sanitized, "") + + return sanitized +} + +func (iv *InputValidator) sanitizeTheme(input string) string { + sanitized := html.EscapeString(input) + sanitized = strings.TrimSpace(sanitized) + + // Capitalize first letter of each word + words := strings.Fields(sanitized) + for i, word := range words { + if len(word) > 0 { + words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:]) + } + } + + return strings.Join(words, " ") +} + +// Custom validation functions +func validateAlphanumSpace(fl validator.FieldLevel) bool { + str := fl.Field().String() + for _, r := range str { + if !unicode.IsLetter(r) && !unicode.IsNumber(r) && !unicode.IsSpace(r) && r != '-' && r != '_' && r != '.' { + return false + } + } + return true +} + +func validateNoHTML(fl validator.FieldLevel) bool { + str := fl.Field().String() + return !strings.Contains(str, "<") && !strings.Contains(str, ">") +} + +func validateSafeText(fl validator.FieldLevel) bool { + str := fl.Field().String() + + // Check for potential XSS patterns + dangerousPatterns := []string{ + "javascript:", + "data:", + "vbscript:", + "on\\w+\\s*=", + " int64(limit.Requests) { + return c.Status(429).JSON(fiber.Map{ + "error": true, + "message": "Rate limit exceeded", + "retry_after": int(limit.Window.Seconds()), + }) + } + + // Add rate limit headers + c.Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit.Requests)) + c.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", limit.Requests-int(current))) + c.Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(limit.Window).Unix())) + + return c.Next() + } +} +``` + +## Game Integrity & Anti-Cheating + +### Server-Side Validation +```go +// Game integrity and anti-cheating measures +package integrity + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "math" + "time" +) + +type GameIntegrityService struct { + secretKey []byte + auditLogger *AuditLogger + anomalyDetector *AnomalyDetector +} + +type GameState struct { + SessionID string `json:"session_id"` + PlayerID string `json:"player_id"` + QuestionID string `json:"question_id"` + StartTime time.Time `json:"start_time"` + AttemptsUsed int `json:"attempts_used"` + HintsUsed int `json:"hints_used"` + Score int `json:"score"` + ServerHash string `json:"server_hash"` +} + +type AnswerSubmission struct { + SessionID string `json:"session_id"` + QuestionID string `json:"question_id"` + Answer string `json:"answer"` + AttemptNum int `json:"attempt_num"` + TimeTaken time.Duration `json:"time_taken"` + ClientHash string `json:"client_hash,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +type IntegrityCheck struct { + IsValid bool `json:"is_valid"` + Violations []string `json:"violations"` + RiskScore float64 `json:"risk_score"` + Action string `json:"action"` // allow, warn, block +} + +func NewGameIntegrityService(secretKey []byte, auditLogger *AuditLogger) *GameIntegrityService { + return &GameIntegrityService{ + secretKey: secretKey, + auditLogger: auditLogger, + anomalyDetector: NewAnomalyDetector(), + } +} + +func (gis *GameIntegrityService) ValidateAnswerSubmission(ctx context.Context, submission *AnswerSubmission, gameState *GameState) (*IntegrityCheck, error) { + check := &IntegrityCheck{ + IsValid: true, + Violations: []string{}, + RiskScore: 0.0, + } + + // 1. Validate timing + if err := gis.validateTiming(submission, gameState, check); err != nil { + return check, err + } + + // 2. Validate sequence + if err := gis.validateSequence(submission, gameState, check); err != nil { + return check, err + } + + // 3. Validate game state hash + if err := gis.validateGameStateHash(submission, gameState, check); err != nil { + return check, err + } + + // 4. Check for behavioral anomalies + if err := gis.checkBehavioralAnomalies(ctx, submission, check); err != nil { + return check, err + } + + // 5. Calculate final risk score and action + gis.calculateRiskAndAction(check) + + // 6. Log integrity check results + gis.auditLogger.LogSecurityEvent("game_integrity_check", submission.SessionID, "", "info", map[string]interface{}{ + "session_id": submission.SessionID, + "question_id": submission.QuestionID, + "risk_score": check.RiskScore, + "violations": check.Violations, + "action": check.Action, + }) + + return check, nil +} + +func (gis *GameIntegrityService) validateTiming(submission *AnswerSubmission, gameState *GameState, check *IntegrityCheck) error { + // Minimum time check (prevent instant answers) + minTime := 2 * time.Second + if submission.TimeTaken < minTime { + check.Violations = append(check.Violations, "answer_too_fast") + check.RiskScore += 0.3 + } + + // Maximum time check (session timeout) + maxTime := 30 * time.Minute + totalTime := time.Since(gameState.StartTime) + if totalTime > maxTime { + check.Violations = append(check.Violations, "session_expired") + check.RiskScore += 0.8 + check.IsValid = false + } + + // Check for time manipulation + if submission.Timestamp.Before(gameState.StartTime) { + check.Violations = append(check.Violations, "timestamp_manipulation") + check.RiskScore += 0.5 + } + + return nil +} + +func (gis *GameIntegrityService) validateSequence(submission *AnswerSubmission, gameState *GameState, check *IntegrityCheck) error { + // Validate attempt number + if submission.AttemptNum != gameState.AttemptsUsed+1 { + check.Violations = append(check.Violations, "invalid_attempt_sequence") + check.RiskScore += 0.4 + } + + // Validate attempt limits + if submission.AttemptNum > 3 { + check.Violations = append(check.Violations, "exceeded_attempt_limit") + check.RiskScore += 0.6 + check.IsValid = false + } + + // Validate question sequence + if submission.QuestionID != gameState.QuestionID { + check.Violations = append(check.Violations, "question_mismatch") + check.RiskScore += 0.5 + check.IsValid = false + } + + return nil +} + +func (gis *GameIntegrityService) validateGameStateHash(submission *AnswerSubmission, gameState *GameState, check *IntegrityCheck) error { + // Calculate expected hash + expectedHash := gis.calculateGameStateHash(gameState) + + if gameState.ServerHash != expectedHash { + check.Violations = append(check.Violations, "game_state_tampering") + check.RiskScore += 0.7 + check.IsValid = false + } + + return nil +} + +func (gis *GameIntegrityService) checkBehavioralAnomalies(ctx context.Context, submission *AnswerSubmission, check *IntegrityCheck) error { + // Check answer patterns + anomalies := gis.anomalyDetector.DetectAnomalies(ctx, submission.SessionID, map[string]interface{}{ + "time_taken": submission.TimeTaken.Seconds(), + "answer_length": len(submission.Answer), + "attempt_num": submission.AttemptNum, + }) + + for _, anomaly := range anomalies { + check.Violations = append(check.Violations, fmt.Sprintf("anomaly_%s", anomaly.Type)) + check.RiskScore += anomaly.Severity + } + + return nil +} + +func (gis *GameIntegrityService) calculateRiskAndAction(check *IntegrityCheck) { + // Apply penalties for multiple violations + if len(check.Violations) > 1 { + check.RiskScore += float64(len(check.Violations)) * 0.1 + } + + // Determine action based on risk score + switch { + case check.RiskScore >= 0.8: + check.Action = "block" + check.IsValid = false + case check.RiskScore >= 0.5: + check.Action = "warn" + default: + check.Action = "allow" + } +} + +func (gis *GameIntegrityService) calculateGameStateHash(gameState *GameState) string { + data := fmt.Sprintf("%s:%s:%s:%d:%d:%d:%d", + gameState.SessionID, + gameState.PlayerID, + gameState.QuestionID, + gameState.StartTime.Unix(), + gameState.AttemptsUsed, + gameState.HintsUsed, + gameState.Score, + ) + + h := hmac.New(sha256.New, gis.secretKey) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (gis *GameIntegrityService) UpdateGameState(gameState *GameState) { + gameState.ServerHash = gis.calculateGameStateHash(gameState) +} + +// Anomaly detection for behavioral patterns +type AnomalyDetector struct { + patterns map[string]*PatternTracker +} + +type PatternTracker struct { + Samples []float64 + Mean float64 + StdDev float64 + SampleCount int +} + +type Anomaly struct { + Type string + Severity float64 + Details string +} + +func NewAnomalyDetector() *AnomalyDetector { + return &AnomalyDetector{ + patterns: make(map[string]*PatternTracker), + } +} + +func (ad *AnomalyDetector) DetectAnomalies(ctx context.Context, sessionID string, metrics map[string]interface{}) []Anomaly { + var anomalies []Anomaly + + for metricName, value := range metrics { + if floatValue, ok := value.(float64); ok { + if anomaly := ad.checkMetricAnomaly(sessionID, metricName, floatValue); anomaly != nil { + anomalies = append(anomalies, *anomaly) + } + } + } + + return anomalies +} + +func (ad *AnomalyDetector) checkMetricAnomaly(sessionID, metricName string, value float64) *Anomaly { + key := fmt.Sprintf("%s:%s", sessionID, metricName) + + tracker, exists := ad.patterns[key] + if !exists { + tracker = &PatternTracker{ + Samples: []float64{}, + } + ad.patterns[key] = tracker + } + + // Add sample + tracker.Samples = append(tracker.Samples, value) + tracker.SampleCount++ + + // Need at least 3 samples to detect anomalies + if tracker.SampleCount < 3 { + return nil + } + + // Calculate statistics + ad.updateStatistics(tracker) + + // Check for anomaly (z-score > 2) + if tracker.StdDev > 0 { + zScore := math.Abs(value-tracker.Mean) / tracker.StdDev + if zScore > 2.0 { + severity := math.Min(zScore/5.0, 0.3) // Cap at 0.3 + return &Anomaly{ + Type: metricName, + Severity: severity, + Details: fmt.Sprintf("z-score: %.2f", zScore), + } + } + } + + return nil +} + +func (ad *AnomalyDetector) updateStatistics(tracker *PatternTracker) { + n := float64(len(tracker.Samples)) + + // Calculate mean + sum := 0.0 + for _, sample := range tracker.Samples { + sum += sample + } + tracker.Mean = sum / n + + // Calculate standard deviation + sumSquares := 0.0 + for _, sample := range tracker.Samples { + diff := sample - tracker.Mean + sumSquares += diff * diff + } + tracker.StdDev = math.Sqrt(sumSquares / n) +} +``` + +## Security Monitoring & Incident Response + +### Security Event Monitoring +```go +// Security monitoring and incident response +package security + +import ( + "context" + "encoding/json" + "fmt" + "time" +) + +type SecurityMonitor struct { + alertManager *AlertManager + incidentManager *IncidentManager + auditLogger *AuditLogger + redis *redis.Client +} + +type SecurityEvent struct { + ID string `json:"id"` + Type string `json:"type"` + Severity string `json:"severity"` + UserID string `json:"user_id,omitempty"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent,omitempty"` + Timestamp time.Time `json:"timestamp"` + Details map[string]interface{} `json:"details"` + Context string `json:"context"` +} + +type IncidentResponse struct { + IncidentID string `json:"incident_id"` + Action string `json:"action"` + AutomatedActions []string `json:"automated_actions"` + RequiresManualReview bool `json:"requires_manual_review"` + Timestamp time.Time `json:"timestamp"` +} + +func NewSecurityMonitor(alertManager *AlertManager, incidentManager *IncidentManager, auditLogger *AuditLogger, redis *redis.Client) *SecurityMonitor { + return &SecurityMonitor{ + alertManager: alertManager, + incidentManager: incidentManager, + auditLogger: auditLogger, + redis: redis, + } +} + +func (sm *SecurityMonitor) ProcessSecurityEvent(ctx context.Context, event *SecurityEvent) (*IncidentResponse, error) { + // Enrich event with additional context + if err := sm.enrichSecurityEvent(ctx, event); err != nil { + return nil, fmt.Errorf("failed to enrich security event: %w", err) + } + + // Analyze threat level + threatLevel := sm.analyzeThreatLevel(ctx, event) + + // Create incident response + response := &IncidentResponse{ + IncidentID: generateIncidentID(), + Timestamp: time.Now(), + AutomatedActions: []string{}, + } + + // Execute automated response based on threat level + switch threatLevel { + case "critical": + response.Action = "block_immediately" + response.RequiresManualReview = true + response.AutomatedActions = append(response.AutomatedActions, + "block_user", "invalidate_sessions", "notify_security_team") + sm.executeAutomatedResponse(ctx, event, response) + + case "high": + response.Action = "temporary_restriction" + response.RequiresManualReview = true + response.AutomatedActions = append(response.AutomatedActions, + "rate_limit_user", "require_additional_auth", "alert_security_team") + sm.executeAutomatedResponse(ctx, event, response) + + case "medium": + response.Action = "monitor_closely" + response.RequiresManualReview = false + response.AutomatedActions = append(response.AutomatedActions, + "increase_logging", "flag_for_review") + sm.executeAutomatedResponse(ctx, event, response) + + case "low": + response.Action = "log_and_continue" + response.RequiresManualReview = false + response.AutomatedActions = append(response.AutomatedActions, "log_event") + + default: + response.Action = "no_action" + } + + // Log incident response + sm.auditLogger.LogSecurityEvent("incident_response", event.UserID, event.IPAddress, "info", map[string]interface{}{ + "incident_id": response.IncidentID, + "original_event": event.Type, + "threat_level": threatLevel, + "response_action": response.Action, + "automated_actions": response.AutomatedActions, + }) + + return response, nil +} + +func (sm *SecurityMonitor) enrichSecurityEvent(ctx context.Context, event *SecurityEvent) error { + // Add geolocation data + if geoData, err := sm.getGeolocation(event.IPAddress); err == nil { + event.Details["geolocation"] = geoData + } + + // Add user behavior history + if event.UserID != "" { + if userHistory, err := sm.getUserBehaviorHistory(ctx, event.UserID); err == nil { + event.Details["user_history"] = userHistory + } + } + + // Add IP reputation data + if reputation, err := sm.getIPReputation(event.IPAddress); err == nil { + event.Details["ip_reputation"] = reputation + } + + return nil +} + +func (sm *SecurityMonitor) analyzeThreatLevel(ctx context.Context, event *SecurityEvent) string { + score := 0 + + // Base severity score + switch event.Severity { + case "critical": + score += 40 + case "high": + score += 30 + case "medium": + score += 20 + case "low": + score += 10 + } + + // Event type scoring + threatScores := map[string]int{ + "brute_force_attack": 35, + "sql_injection_attempt": 40, + "xss_attempt": 30, + "session_hijack": 35, + "privilege_escalation": 40, + "data_exfiltration": 45, + "unusual_access_pattern": 20, + "rate_limit_exceeded": 15, + "authentication_failure": 10, + "suspicious_user_agent": 15, + "geo_anomaly": 25, + } + + if typeScore, exists := threatScores[event.Type]; exists { + score += typeScore + } + + // User history factor + if userHistory, exists := event.Details["user_history"].(map[string]interface{}); exists { + if previousIncidents, ok := userHistory["incident_count"].(int); ok { + score += previousIncidents * 5 + } + } + + // IP reputation factor + if reputation, exists := event.Details["ip_reputation"].(map[string]interface{}); exists { + if malicious, ok := reputation["is_malicious"].(bool); ok && malicious { + score += 25 + } + if proxy, ok := reputation["is_proxy"].(bool); ok && proxy { + score += 10 + } + } + + // Geographic anomaly + if geoData, exists := event.Details["geolocation"].(map[string]interface{}); exists { + if country, ok := geoData["country"].(string); ok { + if sm.isHighRiskCountry(country) { + score += 15 + } + } + } + + // Convert score to threat level + switch { + case score >= 70: + return "critical" + case score >= 50: + return "high" + case score >= 30: + return "medium" + case score >= 15: + return "low" + default: + return "minimal" + } +} + +func (sm *SecurityMonitor) executeAutomatedResponse(ctx context.Context, event *SecurityEvent, response *IncidentResponse) error { + for _, action := range response.AutomatedActions { + switch action { + case "block_user": + if event.UserID != "" { + sm.blockUser(ctx, event.UserID, response.IncidentID) + } + + case "invalidate_sessions": + if event.UserID != "" { + sm.invalidateUserSessions(ctx, event.UserID) + } + + case "block_ip": + sm.blockIP(ctx, event.IPAddress, time.Hour*24) // 24-hour block + + case "rate_limit_user": + if event.UserID != "" { + sm.applyRateLimit(ctx, event.UserID, time.Hour) // 1-hour rate limit + } + + case "require_additional_auth": + if event.UserID != "" { + sm.requireAdditionalAuth(ctx, event.UserID) + } + + case "notify_security_team": + sm.notifySecurityTeam(event, response) + + case "alert_security_team": + sm.alertSecurityTeam(event, response) + + case "increase_logging": + sm.increaseLoggingLevel(event.UserID, event.IPAddress) + + case "flag_for_review": + sm.flagForManualReview(event, response) + } + } + + return nil +} + +func (sm *SecurityMonitor) blockUser(ctx context.Context, userID, incidentID string) error { + // Add user to blocked list + key := fmt.Sprintf("blocked_users:%s", userID) + blockData := map[string]interface{}{ + "blocked_at": time.Now().Unix(), + "incident_id": incidentID, + "reason": "automated_security_response", + } + + blockJSON, _ := json.Marshal(blockData) + return sm.redis.Set(ctx, key, blockJSON, 24*time.Hour).Err() +} + +func (sm *SecurityMonitor) blockIP(ctx context.Context, ipAddress string, duration time.Duration) error { + key := fmt.Sprintf("blocked_ips:%s", ipAddress) + blockData := map[string]interface{}{ + "blocked_at": time.Now().Unix(), + "expires_at": time.Now().Add(duration).Unix(), + "reason": "automated_security_response", + } + + blockJSON, _ := json.Marshal(blockData) + return sm.redis.Set(ctx, key, blockJSON, duration).Err() +} + +func (sm *SecurityMonitor) applyRateLimit(ctx context.Context, userID string, duration time.Duration) error { + key := fmt.Sprintf("rate_limit_strict:%s", userID) + limitData := map[string]interface{}{ + "limit": 5, // 5 requests per minute + "window": 60, // 1 minute window + "applied_at": time.Now().Unix(), + } + + limitJSON, _ := json.Marshal(limitData) + return sm.redis.Set(ctx, key, limitJSON, duration).Err() +} + +func (sm *SecurityMonitor) notifySecurityTeam(event *SecurityEvent, response *IncidentResponse) { + notification := map[string]interface{}{ + "type": "security_incident", + "severity": "critical", + "incident_id": response.IncidentID, + "event_type": event.Type, + "user_id": event.UserID, + "ip_address": event.IPAddress, + "timestamp": event.Timestamp, + "details": event.Details, + } + + // Send to security team via multiple channels + sm.alertManager.SendSlackAlert("#security-incidents", notification) + sm.alertManager.SendPagerDutyAlert(notification) + sm.alertManager.SendEmailAlert("security@knowfoolery.com", notification) +} + +// Security health checks +func (sm *SecurityMonitor) PerformSecurityHealthCheck(ctx context.Context) (*SecurityHealthStatus, error) { + status := &SecurityHealthStatus{ + Timestamp: time.Now(), + Checks: make(map[string]CheckResult), + } + + // Check authentication system + status.Checks["auth_system"] = sm.checkAuthSystemHealth(ctx) + + // Check rate limiting + status.Checks["rate_limiting"] = sm.checkRateLimitingHealth(ctx) + + // Check encryption services + status.Checks["encryption"] = sm.checkEncryptionHealth(ctx) + + // Check security monitoring + status.Checks["monitoring"] = sm.checkMonitoringHealth(ctx) + + // Check blocked IPs/users + status.Checks["blocked_entities"] = sm.checkBlockedEntitiesHealth(ctx) + + // Calculate overall health + status.OverallHealth = sm.calculateOverallHealth(status.Checks) + + return status, nil +} + +type SecurityHealthStatus struct { + Timestamp time.Time `json:"timestamp"` + OverallHealth string `json:"overall_health"` + Checks map[string]CheckResult `json:"checks"` +} + +type CheckResult struct { + Status string `json:"status"` // healthy, degraded, unhealthy + Message string `json:"message"` + Details map[string]interface{} `json:"details,omitempty"` +} +``` + +This comprehensive security implementation ensures Know Foolery has robust protection against common threats while maintaining usability and performance. The security measures are layered and include proactive monitoring, automated incident response, and detailed audit trails for compliance and forensic analysis. \ No newline at end of file diff --git a/docs/technical-architecture.md b/docs/technical-architecture.md new file mode 100644 index 0000000..3287a6a --- /dev/null +++ b/docs/technical-architecture.md @@ -0,0 +1,837 @@ +# Know Foolery - Technical Architecture Documentation + +## Architecture Overview + +Know Foolery follows a microservices architecture with clear separation between frontend presentation, backend services, and data persistence layers. The system is designed for cross-platform compatibility, scalability, and maintainability. + +## System Architecture Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Client Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Web App β”‚ β”‚ Mobile iOS β”‚ β”‚ Mobile Androiβ”‚ β”‚ Desktop ctrβ”‚ β”‚ +β”‚ β”‚ (React) β”‚ β”‚(React Native)β”‚ β”‚(React Native)β”‚ β”‚ (Wails) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + HTTPS/WSS + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ API Gateway Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ NGINX + API Gateway Service (Go) β”‚ β”‚ +β”‚ β”‚ β€’ Authentication & Authorization β”‚ β”‚ +β”‚ β”‚ β€’ Rate Limiting & CORS β”‚ β”‚ +β”‚ β”‚ β€’ Request Routing & Load Balancing β”‚ β”‚ +β”‚ β”‚ β€’ Security Headers & Input Validation β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + gRPC/HTTP + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Microservices Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Game β”‚ β”‚ Question β”‚ β”‚ User β”‚ β”‚ Leaderboard β”‚ β”‚ +β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Session β”‚ β”‚ Admin β”‚ β”‚ +β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + Database Connections + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Data & External Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PostgreSQL β”‚ β”‚ Redis β”‚ β”‚ Zitadel β”‚ β”‚ Observabi- β”‚ β”‚ +β”‚ β”‚ Primary β”‚ β”‚ Cache & β”‚ β”‚ OAuth β”‚ β”‚ lity β”‚ β”‚ +β”‚ β”‚ Database β”‚ β”‚ Sessions β”‚ β”‚ Provider β”‚ β”‚ Stack β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Technology Stack + +### Frontend Technologies +```yaml +Web Application: + Framework: React 18.2+ + Language: TypeScript 5.0+ + Build Tool: Vite 4.0+ + UI Library: Gluestack UI + Styling: NativeWind/Tailwind CSS 3.0+ + State Management: React Context + useReducer + Testing: Jest + React Testing Library + Playwright + +Mobile Applications: + Framework: React Native 0.81+ + Language: TypeScript 5.9+ + UI Library: Gluestack UI (Native) + Navigation: React Navigation 6+ + State Management: React Context + useReducer + Testing: Jest + React Native Testing Library + +Desktop Application: + Framework: Wails 2.10+ + Template: wails-vite-react-ts-tailwind-template +``` + +### Backend Technologies +```yaml +Microservices: + Language: Go 1.25+ + HTTP Framework: Fiber 3.0+ + gRPC: Protocol Buffers + gRPC-Go + Database ORM: Ent (Facebook) + Authentication: JWT + Zitadel Integration + Testing: Go testing + testcontainers + +API Gateway: + Reverse Proxy: NGINX + Gateway Service: Go + Fiber + Load Balancing: Round-robin + Health checks + Rate Limiting: Redis-based sliding window +``` + +### Data Technologies +```yaml +Primary Database: + Engine: PostgreSQL 15+ + Connection Pooling: pgxpool + Migrations: Ent migrations + Backup: pg_dump + Object Storage + +Cache Layer: + Engine: Redis 7+ + Use Cases: Sessions, Rate limiting, Cache + Clustering: Redis Cluster (Production) + Persistence: RDB + AOF + +Authentication: + Provider: Zitadel (Self-hosted) + Protocol: OAuth 2.0 + OpenID Connect + Tokens: JWT with RS256 signing + MFA: TOTP + WebAuthn +``` + +### Infrastructure Technologies +```yaml +Containerization: + Runtime: Docker 24+ + Orchestration: Kubernetes 1.28+ + Registry: Private Docker Registry + +Observability: + Metrics: Prometheus + Node Exporter + Visualization: Grafana + Custom Dashboards + Tracing: Jaeger + OpenTelemetry + Logging: Structured logging + Loki + +CI/CD: + Platform: GitHub Actions + Testing: Automated test suites + Security: SAST/DAST scanning + Deployment: GitOps with ArgoCD +``` + +## Microservices Design + +### Service Boundaries + +#### 1. Game Service +```go +// Responsibilities +type GameService struct { + // Core game logic and session management + SessionManager *SessionManager + ScoreCalculator *ScoreCalculator + AttemptTracker *AttemptTracker + TimerManager *TimerManager +} + +// API Endpoints +POST /api/v1/game/start // Start new game session +GET /api/v1/game/session/{id} // Get session details +POST /api/v1/game/answer // Submit answer +POST /api/v1/game/hint // Request hint +POST /api/v1/game/end // End session +GET /api/v1/game/status/{id} // Get session status +``` + +#### 2. Question Service +```go +// Responsibilities +type QuestionService struct { + QuestionRepo *QuestionRepository + ThemeManager *ThemeManager + Randomizer *QuestionRandomizer + Validator *AnswerValidator +} + +// API Endpoints +GET /api/v1/questions/random // Get random question +GET /api/v1/questions/{id} // Get specific question +GET /api/v1/questions/themes // List available themes +POST /api/v1/questions // Create question (admin) +PUT /api/v1/questions/{id} // Update question (admin) +DELETE /api/v1/questions/{id} // Delete question (admin) +``` + +#### 3. User Service +```go +// Responsibilities +type UserService struct { + UserRepo *UserRepository + ProfileManager *ProfileManager + SessionTracker *SessionTracker +} + +// API Endpoints +POST /api/v1/users/register // Register new player +GET /api/v1/users/profile // Get user profile +PUT /api/v1/users/profile // Update profile +GET /api/v1/users/sessions // Get session history +DELETE /api/v1/users/profile // Delete account (GDPR) +``` + +#### 4. Leaderboard Service +```go +// Responsibilities +type LeaderboardService struct { + ScoreAggregator *ScoreAggregator + RankingEngine *RankingEngine + StatsCalculator *StatsCalculator +} + +// API Endpoints +GET /api/v1/leaderboard/top10 // Get top 10 scores +GET /api/v1/leaderboard/stats // Get game statistics +GET /api/v1/leaderboard/player/{id} // Get player ranking +POST /api/v1/leaderboard/update // Update scores (internal) +``` + +#### 5. Session Service +```go +// Responsibilities +type SessionService struct { + SessionStore *SessionStore + TimerManager *TimerManager + StateManager *StateManager +} + +// API Endpoints +POST /api/v1/sessions // Create session +GET /api/v1/sessions/{id} // Get session +PUT /api/v1/sessions/{id} // Update session +DELETE /api/v1/sessions/{id} // End session +GET /api/v1/sessions/{id}/timer // Get timer status +``` + +#### 6. Admin Service +```go +// Responsibilities +type AdminService struct { + AuthManager *AuthManager + QuestionMgmt *QuestionManagement + UserMgmt *UserManagement + AuditLogger *AuditLogger +} + +// API Endpoints +POST /api/v1/admin/auth // Admin authentication +GET /api/v1/admin/dashboard // Dashboard data +GET /api/v1/admin/questions // List all questions +POST /api/v1/admin/questions/bulk // Bulk question import +GET /api/v1/admin/users // User management +GET /api/v1/admin/audit // Audit logs +``` + +### Inter-Service Communication + +#### Service Mesh Architecture +```yaml +Communication Patterns: + Synchronous: gRPC for real-time operations + Asynchronous: Event-driven via message queues (future) + +Service Discovery: + Registry: Kubernetes DNS + Health Checks: HTTP /health endpoints + Load Balancing: Round-robin with health awareness + +Circuit Breaker: + Pattern: Hystrix-style circuit breakers + Fallback: Graceful degradation + Timeout: Context-based timeouts +``` + +#### gRPC Service Definitions +```protobuf +// game_service.proto +service GameService { + rpc StartGame(StartGameRequest) returns (StartGameResponse); + rpc SubmitAnswer(SubmitAnswerRequest) returns (SubmitAnswerResponse); + rpc GetSession(GetSessionRequest) returns (GetSessionResponse); + rpc EndGame(EndGameRequest) returns (EndGameResponse); +} + +// question_service.proto +service QuestionService { + rpc GetRandomQuestion(RandomQuestionRequest) returns (Question); + rpc ValidateAnswer(ValidateAnswerRequest) returns (ValidationResponse); + rpc GetQuestionHint(HintRequest) returns (HintResponse); +} + +// leaderboard_service.proto +service LeaderboardService { + rpc UpdateScore(UpdateScoreRequest) returns (UpdateScoreResponse); + rpc GetTopScores(TopScoresRequest) returns (TopScoresResponse); + rpc GetPlayerRank(PlayerRankRequest) returns (PlayerRankResponse); +} +``` + +## Database Architecture + +### Data Model Design + +#### Core Entities (Ent Schemas) +```go +// Question Entity +type Question struct { + ent.Schema +} + +func (Question) Fields() []ent.Field { + return []ent.Field{ + field.String("theme").NotEmpty(), + field.Text("text").NotEmpty(), + field.String("answer").NotEmpty(), + field.Text("hint").Optional(), + field.Enum("difficulty").Values("easy", "medium", "hard").Default("medium"), + field.Bool("is_active").Default(true), + field.Time("created_at").Default(time.Now), + field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now), + } +} + +func (Question) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("theme"), + index.Fields("difficulty"), + index.Fields("is_active"), + index.Fields("created_at"), + } +} + +// GameSession Entity +type GameSession struct { + ent.Schema +} + +func (GameSession) Fields() []ent.Field { + return []ent.Field{ + field.String("player_name").NotEmpty(), + field.Int("total_score").Default(0), + field.Int("questions_asked").Default(0), + field.Int("questions_correct").Default(0), + field.Int("hints_used").Default(0), + field.Time("start_time").Default(time.Now), + field.Time("end_time").Optional().Nillable(), + field.Enum("status").Values("active", "completed", "timeout", "abandoned").Default("active"), + field.String("current_question_id").Optional(), + field.Int("current_attempts").Default(0), + } +} + +func (GameSession) Edges() []ent.Edge { + return []ent.Edge{ + edge.To("attempts", QuestionAttempt.Type), + edge.To("current_question", Question.Type).Unique().Field("current_question_id"), + } +} + +// QuestionAttempt Entity +type QuestionAttempt struct { + ent.Schema +} + +func (QuestionAttempt) Fields() []ent.Field { + return []ent.Field{ + field.String("session_id"), + field.String("question_id"), + field.Int("attempt_number"), + field.String("submitted_answer"), + field.Bool("is_correct"), + field.Bool("used_hint"), + field.Int("points_awarded"), + field.Time("submitted_at").Default(time.Now), + field.Duration("time_taken"), + } +} + +func (QuestionAttempt) Edges() []ent.Edge { + return []ent.Edge{ + edge.From("session", GameSession.Type).Ref("attempts").Unique().Field("session_id"), + edge.To("question", Question.Type).Unique().Field("question_id"), + } +} +``` + +#### Database Performance Optimization +```sql +-- Indexes for optimal query performance +CREATE INDEX idx_questions_theme_active ON questions(theme, is_active); +CREATE INDEX idx_sessions_status_start_time ON game_sessions(status, start_time); +CREATE INDEX idx_attempts_session_question ON question_attempts(session_id, question_id); +CREATE INDEX idx_leaderboard_score_time ON game_sessions(total_score DESC, end_time ASC) WHERE status = 'completed'; + +-- Partitioning for large datasets (future scaling) +CREATE TABLE game_sessions_y2024m01 PARTITION OF game_sessions + FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); +``` + +### Database Connection Management +```go +// Database configuration +type DatabaseConfig struct { + Primary DatabaseInstance + Replicas []DatabaseInstance + Pool PoolConfig +} + +type DatabaseInstance struct { + Host string + Port int + Database string + Username string + Password string + SSLMode string +} + +type PoolConfig struct { + MaxOpenConns int // 25 + MaxIdleConns int // 5 + ConnMaxLifetime time.Duration // 30 minutes + ConnMaxIdleTime time.Duration // 15 minutes +} + +// Connection pooling with read/write splitting +func NewDatabaseClient(config DatabaseConfig) (*ent.Client, error) { + primaryDSN := formatDSN(config.Primary) + + // Primary database for writes + primaryDB, err := sql.Open("postgres", primaryDSN) + if err != nil { + return nil, err + } + + configurePools(primaryDB, config.Pool) + + // Create Ent client with instrumented driver + drv := entsql.OpenDB("postgres", primaryDB) + return ent.NewClient(ent.Driver(drv)), nil +} +``` + +## Security Architecture + +### Authentication Flow +```mermaid +sequenceDiagram + participant C as Client + participant G as API Gateway + participant Z as Zitadel + participant S as Service + + C->>G: Request with credentials + G->>Z: Validate credentials + Z->>G: Return JWT token + G->>C: Return token + user info + C->>G: API request with JWT + G->>G: Validate JWT signature + G->>S: Forward request with user context + S->>G: Return response + G->>C: Return response +``` + +### Authorization Strategy +```go +// Role-based access control +type Role string + +const ( + RolePlayer Role = "player" + RoleAdmin Role = "admin" +) + +type Permissions struct { + CanPlayGame bool + CanViewScores bool + CanManageQuestions bool + CanViewAuditLogs bool + CanManageUsers bool +} + +var RolePermissions = map[Role]Permissions{ + RolePlayer: { + CanPlayGame: true, + CanViewScores: true, + }, + RoleAdmin: { + CanPlayGame: true, + CanViewScores: true, + CanManageQuestions: true, + CanViewAuditLogs: true, + CanManageUsers: true, + }, +} + +// JWT middleware with role checking +func AuthMiddleware() fiber.Handler { + return func(c *fiber.Ctx) error { + token := extractToken(c.Get("Authorization")) + + claims, err := validateJWT(token) + if err != nil { + return c.Status(401).JSON(fiber.Map{"error": "Invalid token"}) + } + + c.Locals("user_id", claims.Subject) + c.Locals("user_role", claims.Role) + c.Locals("permissions", RolePermissions[Role(claims.Role)]) + + return c.Next() + } +} + +func RequirePermission(permission func(Permissions) bool) fiber.Handler { + return func(c *fiber.Ctx) error { + perms := c.Locals("permissions").(Permissions) + if !permission(perms) { + return c.Status(403).JSON(fiber.Map{"error": "Insufficient permissions"}) + } + return c.Next() + } +} +``` + +### Input Validation & Security +```go +// Comprehensive input validation +type InputValidator struct { + maxAnswerLength int + maxNameLength int + allowedChars *regexp.Regexp +} + +func NewInputValidator() *InputValidator { + return &InputValidator{ + maxAnswerLength: 500, + maxNameLength: 50, + allowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`), + } +} + +func (v *InputValidator) ValidateAnswer(answer string) error { + if len(answer) == 0 { + return errors.New("answer cannot be empty") + } + + if len(answer) > v.maxAnswerLength { + return errors.New("answer too long") + } + + // Sanitize HTML and potential XSS + clean := html.EscapeString(strings.TrimSpace(answer)) + if clean != answer { + return errors.New("invalid characters in answer") + } + + return nil +} + +func (v *InputValidator) ValidatePlayerName(name string) error { + if len(name) < 2 || len(name) > v.maxNameLength { + return errors.New("name must be 2-50 characters") + } + + if !v.allowedChars.MatchString(name) { + return errors.New("name contains invalid characters") + } + + return nil +} + +// Rate limiting implementation +type RateLimiter struct { + redis *redis.Client + limits map[string]RateLimit +} + +type RateLimit struct { + Requests int // Number of requests + Window time.Duration // Time window +} + +func (rl *RateLimiter) Allow(key string, limit RateLimit) bool { + current, err := rl.redis.Incr(key).Result() + if err != nil { + return false // Fail closed + } + + if current == 1 { + rl.redis.Expire(key, limit.Window) + } + + return current <= int64(limit.Requests) +} + +// Usage in middleware +func RateLimitMiddleware() fiber.Handler { + limiter := NewRateLimiter() + + return func(c *fiber.Ctx) error { + userID := c.Locals("user_id").(string) + clientIP := c.IP() + + // Per-user rate limiting + userKey := fmt.Sprintf("rate_limit:user:%s", userID) + if !limiter.Allow(userKey, RateLimit{Requests: 60, Window: time.Minute}) { + return c.Status(429).JSON(fiber.Map{"error": "Rate limit exceeded"}) + } + + // Per-IP rate limiting + ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP) + if !limiter.Allow(ipKey, RateLimit{Requests: 100, Window: time.Minute}) { + return c.Status(429).JSON(fiber.Map{"error": "Rate limit exceeded"}) + } + + return c.Next() + } +} +``` + +## Cross-Platform Frontend Architecture + +### Gluestack UI Integration +```typescript +// Shared component library structure +// packages/ui-components/src/index.ts + +export { GameCard } from './GameCard' +export { Leaderboard } from './Leaderboard' +export { Timer } from './Timer' +export { ScoreDisplay } from './ScoreDisplay' +export { AdminPanel } from './AdminPanel' + +// Component example with Gluestack UI +// packages/ui-components/src/GameCard/GameCard.tsx +import { + Card, + VStack, + HStack, + Text, + Input, + Button, + Badge, + Progress, + Box +} from '@gluestack-ui/themed' + +export interface GameCardProps { + question: string + theme: string + timeRemaining: number + attemptsLeft: number + currentScore: number + onSubmitAnswer: (answer: string) => void + onRequestHint: () => void + isLoading?: boolean +} + +export const GameCard: React.FC = ({ + question, + theme, + timeRemaining, + attemptsLeft, + currentScore, + onSubmitAnswer, + onRequestHint, + isLoading = false +}) => { + const [answer, setAnswer] = useState('') + + const handleSubmit = () => { + if (answer.trim()) { + onSubmitAnswer(answer.trim()) + setAnswer('') + } + } + + return ( + + + {/* Header with theme and timer */} + + + + {theme} + + + + + ⏱️ {Math.floor(timeRemaining / 60)}:{(timeRemaining % 60).toString().padStart(2, '0')} + + + + + {/* Progress indicator */} + + + + + {/* Question */} + + + {question} + + + + {/* Answer input */} + + + + + {/* Game stats */} + + + Attempts left: {attemptsLeft}/3 + + + Score: {currentScore} points + + + + {/* Action buttons */} + + + + + + + ) +} +``` + +### Platform-Specific Adaptations +```typescript +// Platform detection and adaptation +// packages/shared-logic/src/platform.ts + +export interface PlatformCapabilities { + hasTouch: boolean + hasKeyboard: boolean + hasCamera: boolean + canVibrate: boolean + supportsNotifications: boolean + isOfflineCapable: boolean +} + +export const getPlatformCapabilities = (): PlatformCapabilities => { + // Web platform + if (typeof window !== 'undefined') { + return { + hasTouch: 'ontouchstart' in window, + hasKeyboard: true, + hasCamera: !!navigator.mediaDevices?.getUserMedia, + canVibrate: !!navigator.vibrate, + supportsNotifications: 'Notification' in window, + isOfflineCapable: 'serviceWorker' in navigator + } + } + + // React Native platform + if (typeof require !== 'undefined') { + try { + const { Platform } = require('react-native') + return { + hasTouch: true, + hasKeyboard: Platform.OS === 'ios' || Platform.OS === 'android', + hasCamera: true, + canVibrate: true, + supportsNotifications: true, + isOfflineCapable: true + } + } catch { + // Fallback for other environments + } + } + + // Default capabilities + return { + hasTouch: false, + hasKeyboard: true, + hasCamera: false, + canVibrate: false, + supportsNotifications: false, + isOfflineCapable: false + } +} + +// Responsive design utilities +export const useResponsiveValue = (values: { + mobile: T + tablet: T + desktop: T +}) => { + const [screenSize, setScreenSize] = useState<'mobile' | 'tablet' | 'desktop'>('desktop') + + useEffect(() => { + const updateScreenSize = () => { + const width = window.innerWidth + if (width < 768) { + setScreenSize('mobile') + } else if (width < 1024) { + setScreenSize('tablet') + } else { + setScreenSize('desktop') + } + } + + updateScreenSize() + window.addEventListener('resize', updateScreenSize) + return () => window.removeEventListener('resize', updateScreenSize) + }, []) + + return values[screenSize] +} +``` + +This technical architecture provides a solid foundation for building a scalable, secure, and maintainable quiz game platform with cross-platform capabilities. \ No newline at end of file diff --git a/docs/zitadel-integration.md b/docs/zitadel-integration.md new file mode 100644 index 0000000..4f7b136 --- /dev/null +++ b/docs/zitadel-integration.md @@ -0,0 +1,882 @@ +# Know Foolery - Zitadel Integration Architecture + +## Overview + +Zitadel serves as the self-hosted OAuth 2.0/OpenID Connect authentication provider for Know Foolery, providing secure authentication for both players and administrators while maintaining complete control over user data and authentication flows. + +## Architecture Integration + +### Zitadel Deployment Architecture +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Client Applications β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Web App β”‚ β”‚ Mobile iOS β”‚ β”‚Mobile Androidβ”‚ β”‚Desktop Wailsβ”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + OAuth 2.0/OIDC Flows + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ API Gateway β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ JWT Validation Middleware β”‚ β”‚ +β”‚ β”‚ β€’ Token signature verification β”‚ β”‚ +β”‚ β”‚ β€’ Claims validation & role extraction β”‚ β”‚ +β”‚ β”‚ β€’ Token refresh handling β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + Validated Requests + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Microservices Layer β”‚ +β”‚ (Receives user context from JWT claims) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + Zitadel Admin API + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Zitadel Instance β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ OAuth β”‚ β”‚ Admin API β”‚ β”‚ PostgreSQL β”‚ β”‚ OIDC β”‚ β”‚ +β”‚ β”‚ Provider β”‚ β”‚ β”‚ β”‚ Database β”‚ β”‚ Discovery β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Zitadel Configuration + +### Project and Application Setup +```yaml +# Zitadel Project Configuration +project: + name: "Know Foolery Quiz Game" + description: "Cross-platform quiz game authentication" + +applications: + web_app: + name: "Know Foolery Web" + type: "WEB" + auth_method: "PKCE" + redirect_uris: + - "http://localhost:3000/auth/callback" # Development + - "https://app.knowfoolery.com/auth/callback" # Production + post_logout_redirect_uris: + - "http://localhost:3000/" + - "https://app.knowfoolery.com/" + + mobile_app: + name: "Know Foolery Mobile" + type: "NATIVE" + auth_method: "PKCE" + redirect_uris: + - "knowfoolery://auth/callback" + + admin_app: + name: "Know Foolery Admin" + type: "WEB" + auth_method: "PKCE" + redirect_uris: + - "http://localhost:3001/admin/auth/callback" + - "https://admin.knowfoolery.com/auth/callback" + +roles: + - name: "player" + display_name: "Quiz Player" + description: "Can play quiz games and view leaderboards" + - name: "admin" + display_name: "Game Administrator" + description: "Can manage questions, users, and system settings" + +policies: + password_policy: + min_length: 8 + has_uppercase: true + has_lowercase: true + has_number: true + has_symbol: false + + lockout_policy: + max_password_attempts: 5 + show_lockout_failure: true + + mfa_policy: + force_mfa: true # Required for admin role + force_mfa_local_only: false +``` + +### Zitadel Repository Pattern Implementation + +```go +// ZitadelRepository interface for external integration +package auth + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type ZitadelRepository interface { + ValidateToken(ctx context.Context, token string) (*AuthClaims, error) + RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) + GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error) + RevokeToken(ctx context.Context, token string) error + CreateUser(ctx context.Context, user *CreateUserRequest) (*UserResponse, error) + GetUserByID(ctx context.Context, userID string) (*UserInfo, error) + UpdateUserRoles(ctx context.Context, userID string, roles []string) error +} + +type zitadelRepository struct { + client *http.Client + baseURL string + projectID string + adminToken string + publicKeys map[string]*PublicKey + publicKeysExpiry time.Time + circuitBreaker *CircuitBreaker +} + +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"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +type UserInfo struct { + ID string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + Verified bool `json:"email_verified"` + Roles []string +} + +func NewZitadelRepository(config ZitadelConfig) ZitadelRepository { + return &zitadelRepository{ + client: &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + DisableCompression: true, + }, + }, + baseURL: config.BaseURL, + projectID: config.ProjectID, + adminToken: config.AdminToken, + publicKeys: make(map[string]*PublicKey), + circuitBreaker: NewCircuitBreaker(), + } +} + +func (r *zitadelRepository) ValidateToken(ctx context.Context, token string) (*AuthClaims, error) { + // Circuit breaker pattern for resilience + result, err := r.circuitBreaker.Execute(func() (interface{}, error) { + return r.validateTokenInternal(ctx, token) + }) + + if err == ErrCircuitOpen { + // Fallback: validate token locally if Zitadel is unavailable + return r.validateTokenLocally(token) + } + + if err != nil { + return nil, fmt.Errorf("token validation failed: %w", err) + } + + return result.(*AuthClaims), nil +} + +func (r *zitadelRepository) validateTokenInternal(ctx context.Context, token string) (*AuthClaims, error) { + // Parse JWT token + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + // Verify signing algorithm + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + // Get public key from Zitadel JWKS endpoint + keyID := token.Header["kid"].(string) + return r.getPublicKey(ctx, keyID) + }) + + if err != nil { + return nil, fmt.Errorf("failed to parse token: %w", err) + } + + if !parsedToken.Valid { + return nil, fmt.Errorf("invalid token") + } + + // Extract claims + claims, ok := parsedToken.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid token claims") + } + + return r.extractAuthClaims(claims) +} + +func (r *zitadelRepository) getPublicKey(ctx context.Context, keyID string) (*rsa.PublicKey, error) { + // Check cache first + if key, exists := r.publicKeys[keyID]; exists && time.Now().Before(r.publicKeysExpiry) { + return key.RSAPublicKey, nil + } + + // Fetch from Zitadel JWKS endpoint + jwksURL := fmt.Sprintf("%s/.well-known/openid_configuration", r.baseURL) + + req, err := http.NewRequestWithContext(ctx, "GET", jwksURL, nil) + if err != nil { + return nil, err + } + + resp, err := r.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jwks JWKS + if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { + return nil, err + } + + // Update cache + r.updatePublicKeysCache(jwks) + + key, exists := r.publicKeys[keyID] + if !exists { + return nil, fmt.Errorf("key ID %s not found", keyID) + } + + return key.RSAPublicKey, nil +} + +func (r *zitadelRepository) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) { + data := url.Values{ + "grant_type": {"refresh_token"}, + "refresh_token": {refreshToken}, + "client_id": {r.projectID}, + } + + req, err := http.NewRequestWithContext(ctx, "POST", + fmt.Sprintf("%s/oauth/v2/token", r.baseURL), + strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := r.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("token refresh failed with status: %d", resp.StatusCode) + } + + var tokenResp TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return nil, err + } + + return &tokenResp, nil +} + +func (r *zitadelRepository) CreateUser(ctx context.Context, user *CreateUserRequest) (*UserResponse, error) { + reqBody := map[string]interface{}{ + "userName": user.Email, + "profile": map[string]interface{}{ + "firstName": user.FirstName, + "lastName": user.LastName, + "displayName": user.DisplayName, + }, + "email": map[string]interface{}{ + "email": user.Email, + "isEmailVerified": false, + }, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", + fmt.Sprintf("%s/management/v1/users/human", r.baseURL), + bytes.NewBuffer(jsonBody)) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) + req.Header.Set("Content-Type", "application/json") + + resp, err := r.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var userResp UserResponse + if err := json.NewDecoder(resp.Body).Decode(&userResp); err != nil { + return nil, err + } + + return &userResp, nil +} + +// Circuit breaker implementation for resilience +type CircuitBreaker struct { + state CircuitState + failureCount int + threshold int + timeout time.Duration + lastFailTime time.Time + mutex sync.RWMutex +} + +type CircuitState int + +const ( + Closed CircuitState = iota + Open + HalfOpen +) + +var ErrCircuitOpen = errors.New("circuit breaker is open") + +func NewCircuitBreaker() *CircuitBreaker { + return &CircuitBreaker{ + state: Closed, + threshold: 5, + timeout: 30 * time.Second, + } +} + +func (cb *CircuitBreaker) Execute(fn func() (interface{}, error)) (interface{}, error) { + cb.mutex.RLock() + state := cb.state + cb.mutex.RUnlock() + + switch state { + case Open: + if time.Since(cb.lastFailTime) > cb.timeout { + cb.setState(HalfOpen) + return cb.tryExecute(fn) + } + return nil, ErrCircuitOpen + + case HalfOpen: + return cb.tryExecute(fn) + + default: // Closed + return cb.tryExecute(fn) + } +} + +func (cb *CircuitBreaker) tryExecute(fn func() (interface{}, error)) (interface{}, error) { + result, err := fn() + + cb.mutex.Lock() + defer cb.mutex.Unlock() + + if err != nil { + cb.failureCount++ + cb.lastFailTime = time.Now() + + if cb.failureCount >= cb.threshold { + cb.state = Open + } + return nil, err + } + + // Success - reset circuit breaker + cb.failureCount = 0 + cb.state = Closed + return result, nil +} + +func (cb *CircuitBreaker) setState(state CircuitState) { + cb.mutex.Lock() + defer cb.mutex.Unlock() + cb.state = state +} +``` + +## Authentication Flows + +### Player Authentication Flow (Web/Mobile) +```typescript +// Frontend authentication service +export class AuthService { + private zitadelConfig: ZitadelConfig + + constructor(config: ZitadelConfig) { + this.zitadelConfig = config + } + + async loginPlayer(): Promise { + // PKCE flow for security + const codeVerifier = this.generateCodeVerifier() + const codeChallenge = await this.generateCodeChallenge(codeVerifier) + + // Store code verifier securely + await this.secureStorage.store('code_verifier', codeVerifier) + + // Redirect to Zitadel authorization endpoint + const authURL = new URL(`${this.zitadelConfig.baseURL}/oauth/v2/authorize`) + authURL.searchParams.set('client_id', this.zitadelConfig.clientId) + authURL.searchParams.set('response_type', 'code') + authURL.searchParams.set('scope', 'openid profile email urn:zitadel:iam:org:project:roles') + authURL.searchParams.set('redirect_uri', this.zitadelConfig.redirectUri) + authURL.searchParams.set('code_challenge', codeChallenge) + authURL.searchParams.set('code_challenge_method', 'S256') + authURL.searchParams.set('state', this.generateState()) + + // Platform-specific redirect + if (this.isPlatform('web')) { + window.location.href = authURL.toString() + } else if (this.isPlatform('mobile')) { + await this.openAuthSession(authURL.toString()) + } + } + + async handleAuthCallback(code: string, state: string): Promise { + // Verify state parameter + const storedState = await this.secureStorage.get('auth_state') + if (state !== storedState) { + throw new Error('Invalid state parameter') + } + + // Exchange code for tokens + const codeVerifier = await this.secureStorage.get('code_verifier') + + const tokenResponse = await fetch(`${this.zitadelConfig.baseURL}/oauth/v2/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + client_id: this.zitadelConfig.clientId, + code, + redirect_uri: this.zitadelConfig.redirectUri, + code_verifier: codeVerifier, + }), + }) + + if (!tokenResponse.ok) { + throw new Error('Token exchange failed') + } + + const tokens = await tokenResponse.json() + + // Store tokens securely + await this.storeTokens(tokens) + + // Decode and validate JWT claims + const userInfo = await this.getUserInfo(tokens.access_token) + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + userInfo, + } + } + + async refreshAccessToken(): Promise { + const refreshToken = await this.secureStorage.get('refresh_token') + if (!refreshToken) { + throw new Error('No refresh token available') + } + + const response = await fetch(`${this.zitadelConfig.baseURL}/oauth/v2/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: this.zitadelConfig.clientId, + }), + }) + + if (!response.ok) { + // Refresh token expired, need to re-authenticate + await this.logout() + throw new Error('Refresh token expired') + } + + const tokens = await response.json() + await this.storeTokens(tokens) + + return tokens.access_token + } + + private async storeTokens(tokens: TokenResponse): Promise { + // Platform-specific secure storage + if (this.isPlatform('web')) { + // Use secure HttpOnly cookies for web + await this.apiClient.post('/auth/store-tokens', { tokens }) + } else if (this.isPlatform('mobile')) { + // Use Keychain/Keystore for mobile + await this.secureStorage.store('access_token', tokens.access_token) + await this.secureStorage.store('refresh_token', tokens.refresh_token) + } + } +} +``` + +### Admin Authentication Flow with MFA +```typescript +export class AdminAuthService extends AuthService { + async loginAdmin(): Promise { + // Standard OAuth flow first + const authResult = await super.loginPlayer() + + // Check if user has admin role + if (!authResult.userInfo.roles.includes('admin')) { + throw new Error('Insufficient permissions') + } + + // Verify MFA is completed + if (!authResult.userInfo.mfaVerified) { + return this.redirectToMFA() + } + + return authResult + } + + private async redirectToMFA(): Promise { + // Redirect to Zitadel MFA setup/verification + const mfaURL = new URL(`${this.zitadelConfig.baseURL}/ui/login/mfa/init`) + + if (this.isPlatform('web')) { + window.location.href = mfaURL.toString() + } + + // Handle MFA callback + return this.handleMFACallback() + } + + private async handleMFACallback(): Promise { + // After MFA completion, refresh token to get updated claims + const newAccessToken = await this.refreshAccessToken() + const userInfo = await this.getUserInfo(newAccessToken) + + if (!userInfo.mfaVerified) { + throw new Error('MFA verification failed') + } + + return { + accessToken: newAccessToken, + userInfo, + } + } +} +``` + +## Backend JWT Validation + +### JWT Middleware Implementation +```go +// JWT validation middleware for API Gateway +package middleware + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +type JWTMiddleware struct { + zitadelRepo auth.ZitadelRepository + config JWTConfig +} + +type JWTConfig struct { + Issuer string + Audience string + RequiredClaims []string + AdminEndpoints []string + RateLimitByUserID bool +} + +func NewJWTMiddleware(zitadelRepo auth.ZitadelRepository, config JWTConfig) *JWTMiddleware { + return &JWTMiddleware{ + zitadelRepo: zitadelRepo, + config: config, + } +} + +func (m *JWTMiddleware) ValidateToken() fiber.Handler { + return func(c *fiber.Ctx) error { + // Extract token from Authorization header + authHeader := c.Get("Authorization") + if authHeader == "" { + return c.Status(401).JSON(fiber.Map{"error": "Authorization header required"}) + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == authHeader { + return c.Status(401).JSON(fiber.Map{"error": "Invalid authorization header format"}) + } + + // Validate token with Zitadel + ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + defer cancel() + + claims, err := m.zitadelRepo.ValidateToken(ctx, tokenString) + if err != nil { + return c.Status(401).JSON(fiber.Map{"error": "Invalid token", "details": err.Error()}) + } + + // Validate claims + if err := m.validateClaims(claims); err != nil { + return c.Status(401).JSON(fiber.Map{"error": "Invalid token claims", "details": err.Error()}) + } + + // Set user context + c.Locals("user_id", claims.Subject) + c.Locals("user_email", claims.Email) + c.Locals("user_name", claims.Name) + c.Locals("user_roles", claims.Roles) + c.Locals("mfa_verified", claims.MFAVerified) + + // Check admin access for admin endpoints + if m.isAdminEndpoint(c.Path()) { + if !m.hasAdminRole(claims.Roles) { + return c.Status(403).JSON(fiber.Map{"error": "Admin access required"}) + } + + if !claims.MFAVerified { + return c.Status(403).JSON(fiber.Map{"error": "MFA verification required for admin access"}) + } + } + + return c.Next() + } +} + +func (m *JWTMiddleware) validateClaims(claims *auth.AuthClaims) error { + // Validate issuer + if claims.Issuer != m.config.Issuer { + return fmt.Errorf("invalid issuer: expected %s, got %s", m.config.Issuer, claims.Issuer) + } + + // Validate audience + validAudience := false + for _, aud := range claims.Audience { + if aud == m.config.Audience { + validAudience = true + break + } + } + if !validAudience { + return fmt.Errorf("invalid audience") + } + + // Validate expiration + if time.Unix(claims.ExpiresAt, 0).Before(time.Now()) { + return fmt.Errorf("token expired") + } + + // Validate required claims + for _, requiredClaim := range m.config.RequiredClaims { + switch requiredClaim { + case "email": + if claims.Email == "" { + return fmt.Errorf("email claim required") + } + case "name": + if claims.Name == "" { + return fmt.Errorf("name claim required") + } + case "roles": + if len(claims.Roles) == 0 { + return fmt.Errorf("roles claim required") + } + } + } + + return nil +} + +func (m *JWTMiddleware) isAdminEndpoint(path string) bool { + for _, adminPath := range m.config.AdminEndpoints { + if strings.HasPrefix(path, adminPath) { + return true + } + } + return false +} + +func (m *JWTMiddleware) hasAdminRole(roles []string) bool { + for _, role := range roles { + if role == "admin" { + return true + } + } + return false +} + +// Token refresh middleware +func (m *JWTMiddleware) RefreshTokenIfNeeded() fiber.Handler { + return func(c *fiber.Ctx) error { + userID := c.Locals("user_id") + if userID == nil { + return c.Next() + } + + // Check if token is close to expiration (within 5 minutes) + tokenString := strings.TrimPrefix(c.Get("Authorization"), "Bearer ") + + token, err := jwt.Parse(tokenString, nil) + if err != nil { + // Token parsing failed, but that's handled by ValidateToken middleware + return c.Next() + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + if exp, ok := claims["exp"].(float64); ok { + expirationTime := time.Unix(int64(exp), 0) + if time.Until(expirationTime) < 5*time.Minute { + // Signal frontend to refresh token + c.Set("X-Token-Refresh-Required", "true") + } + } + } + + return c.Next() + } +} +``` + +## Monitoring and Metrics + +### Zitadel Integration Metrics +```go +// Metrics for Zitadel integration monitoring +package metrics + +import ( + "context" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + zitadelRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "zitadel_requests_total", + Help: "Total number of requests to Zitadel", + }, + []string{"operation", "status"}, + ) + + zitadelRequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "zitadel_request_duration_seconds", + Help: "Duration of Zitadel requests", + Buckets: prometheus.DefBuckets, + }, + []string{"operation"}, + ) + + authenticationAttempts = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "authentication_attempts_total", + Help: "Total number of authentication attempts", + }, + []string{"method", "result"}, + ) + + circuitBreakerState = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "circuit_breaker_state", + Help: "Circuit breaker state (0=closed, 1=open, 2=half-open)", + }, + []string{"service"}, + ) +) + +// Instrumented Zitadel repository +type InstrumentedZitadelRepository struct { + repo auth.ZitadelRepository +} + +func NewInstrumentedZitadelRepository(repo auth.ZitadelRepository) auth.ZitadelRepository { + return &InstrumentedZitadelRepository{repo: repo} +} + +func (r *InstrumentedZitadelRepository) ValidateToken(ctx context.Context, token string) (*auth.AuthClaims, error) { + start := time.Now() + + claims, err := r.repo.ValidateToken(ctx, token) + + duration := time.Since(start).Seconds() + status := "success" + if err != nil { + status = "error" + } + + zitadelRequestsTotal.WithLabelValues("validate_token", status).Inc() + zitadelRequestDuration.WithLabelValues("validate_token").Observe(duration) + + if err == nil { + authenticationAttempts.WithLabelValues("jwt", "success").Inc() + } else { + authenticationAttempts.WithLabelValues("jwt", "failure").Inc() + } + + return claims, err +} + +func (r *InstrumentedZitadelRepository) RefreshToken(ctx context.Context, refreshToken string) (*auth.TokenResponse, error) { + start := time.Now() + + tokenResp, err := r.repo.RefreshToken(ctx, refreshToken) + + duration := time.Since(start).Seconds() + status := "success" + if err != nil { + status = "error" + } + + zitadelRequestsTotal.WithLabelValues("refresh_token", status).Inc() + zitadelRequestDuration.WithLabelValues("refresh_token").Observe(duration) + + return tokenResp, err +} +``` + +This Zitadel integration architecture provides secure, scalable authentication with proper monitoring and resilience patterns for the Know Foolery quiz game. \ No newline at end of file diff --git a/requirements.md b/requirements.md new file mode 100644 index 0000000..ff1d314 --- /dev/null +++ b/requirements.md @@ -0,0 +1,248 @@ +# Know Foolery - Project Requirements + +## Overview + +Know Foolery is a quiz game inspired by the French game "DΓ©connaissance" (https://escaleajeux.fr/jeu/decon.0.1). It's a web-based quiz platform with plans for cross-platform expansion to mobile and desktop applications. + +## Game Concept + +### Core Gameplay +- **Objective**: Players answer randomly selected questions to score points +- **Question Format**: Each question belongs to a theme, displayed above the question +- **Scoring System**: + - 2 points for correct answer without hint + - 1 point for correct answer with hint + - 0 points for incorrect answer or timeout +- **Attempts**: Players have exactly 3 attempts per question +- **Time Limit**: Each game session is limited to 30 minutes maximum +- **Hints**: Players can request one hint per question, but this reduces their score + +### Player Experience +- **Registration**: Players must enter their name before starting a game +- **Session Management**: Game tracks progress, attempts, and time remaining +- **Leaderboard**: Display top 10 scores accessible to all players +- **Themes**: Questions are categorized by themes (Geography, History, Science, etc.) + +## System Requirements + +### Functional Requirements + +#### Player Features +1. **Game Session Management** + - Start new game with player name registration + - Track current question, attempts remaining, and score + - Display timer with 30-minute session limit + - Automatic session termination on timeout + +2. **Question Interaction** + - Display question with theme category + - Accept text-based answers + - Provide hint system (1 hint per question) + - Validate answers server-side to prevent cheating + +3. **Scoring and Leaderboard** + - Real-time score calculation + - Display top 10 scores with player names + - Anonymous leaderboard (no personal data exposure) + - Score persistence across sessions + +#### Administrative Features +1. **Question Management** + - CRUD operations for questions, themes, hints, and answers + - Secure admin panel with OAuth authentication + - Question categorization by themes + - Bulk import/export capabilities + +2. **User Management** + - Admin role-based access control + - Multi-factor authentication for admin access + - Audit logging for all administrative actions + +### Non-Functional Requirements + +#### Performance +- **Response Time**: API responses under 200ms for 95% of requests +- **Concurrent Users**: Support for 1000+ simultaneous players +- **Database Performance**: Query response times under 100ms +- **Cross-Platform**: Consistent performance across web, mobile, and desktop + +#### Security +- **Authentication**: OAuth 2.0/OIDC with Zitadel +- **Authorization**: Role-based access control (RBAC) +- **Data Protection**: Encryption at rest and in transit +- **Input Validation**: Comprehensive sanitization of all user inputs +- **Game Integrity**: Server-side validation to prevent cheating + +#### Scalability +- **Microservices Architecture**: Independent scaling of components +- **Database**: PostgreSQL with read replicas for high availability +- **Caching**: Redis for session state and frequently accessed data +- **Auto-scaling**: Kubernetes-based horizontal scaling + +#### Reliability +- **Uptime**: 99.9% availability target +- **Backup**: Automated daily backups with point-in-time recovery +- **Monitoring**: Comprehensive observability with alerting +- **Failover**: Automatic failover for critical components + +## Technical Architecture + +### Frontend Architecture +- **Web Application**: React with TypeScript +- **Mobile Applications**: React Native for iOS and Android +- **Desktop Applications**: Wails for macOS, Windows, and Linux +- **UI Framework**: Gluestack UI for consistent cross-platform design +- **Styling**: NativeWind/Tailwind CSS for utility-first styling + +### Backend Architecture +- **Microservices**: Go-based services with clear domain boundaries +- **API Gateway**: Centralized routing, authentication, and rate limiting +- **Database ORM**: Ent for type-safe database operations +- **Communication**: gRPC for inter-service communication +- **External APIs**: Repository pattern for external integrations + +### Microservices Design + +#### Core Services +1. **Game Service**: Session management, scoring logic, attempts tracking +2. **Question Service**: CRUD operations for questions, themes, hints +3. **User Service**: Player registration and profile management +4. **Leaderboard Service**: Score calculations, rankings, statistics +5. **Session Service**: Timer management, session state persistence +6. **Admin Service**: Question management with OAuth protection +7. **Gateway Service**: API routing, authentication, rate limiting + +#### External Services +1. **Zitadel**: Self-hosted OAuth 2.0/OIDC authentication +2. **PostgreSQL**: Primary database for application data +3. **Redis**: Session state and caching layer + +### Database Design + +#### Core Entities +- **Questions**: ID, theme, text, answer, hint, metadata +- **GameSessions**: ID, player_name, score, start_time, end_time, is_active +- **Users**: ID, name, created_at (minimal player data) +- **Leaderboard**: Aggregated scores and rankings +- **AuditLogs**: Admin actions and security events + +#### Data Relationships +- Questions belong to themes +- GameSessions track multiple question attempts +- Leaderboard aggregates from GameSessions +- AuditLogs reference users and actions + +## Security Requirements + +### Authentication & Authorization +- **OAuth 2.0/OIDC**: Industry-standard authentication via Zitadel +- **JWT Tokens**: Secure token-based authentication with short expiration +- **MFA**: Multi-factor authentication required for admin accounts +- **Role-Based Access**: Granular permissions (player, admin roles) + +### Data Security +- **Encryption**: AES-256 encryption for sensitive data at rest +- **TLS**: All communications encrypted in transit +- **Input Validation**: Comprehensive sanitization against XSS/injection +- **SQL Injection**: Prevention through parameterized queries (Ent) + +### Application Security +- **Rate Limiting**: Per-user and global rate limits +- **CORS**: Strict cross-origin resource sharing policies +- **Security Headers**: CSP, HSTS, X-Frame-Options implementation +- **Audit Logging**: Comprehensive logging of security events + +### Game Integrity +- **Server-Side Validation**: All game logic validated on backend +- **Anti-Cheating**: Session state verification and tampering detection +- **Score Validation**: Server-side score calculation and verification + +## Compliance & Privacy + +### Data Protection +- **GDPR Compliance**: Right to be forgotten, data portability +- **Data Minimization**: Collect only necessary player information +- **Anonymization**: Leaderboard data anonymized for privacy +- **Consent Management**: Clear privacy controls and consent + +### Audit & Compliance +- **Audit Trails**: Comprehensive logging of all administrative actions +- **Compliance Reporting**: SOC 2, ISO 27001 compliance capabilities +- **Data Retention**: Defined policies for data lifecycle management + +## Deployment & Infrastructure + +### Development Environment +- **Local Development**: SQLite for rapid iteration +- **Docker Compose**: Containerized development environment +- **Testing**: Comprehensive unit, integration, and E2E testing + +### Production Environment +- **Container Orchestration**: Kubernetes for production deployment +- **Database**: PostgreSQL with high availability configuration +- **Monitoring**: Prometheus, Grafana, Jaeger for observability +- **CI/CD**: GitHub Actions for automated testing and deployment + +### Cross-Platform Deployment +- **Web**: Standard web application deployment +- **Mobile**: iOS App Store and Google Play Store distribution +- **Desktop**: Electron applications for major operating systems + +## Quality Assurance + +### Testing Strategy +- **Unit Testing**: Go testing framework + Jest for frontend +- **Integration Testing**: testcontainers for database testing +- **E2E Testing**: Playwright for complete user journeys +- **Performance Testing**: k6 for load and stress testing +- **Security Testing**: Automated security scanning and penetration testing + +### Code Quality +- **Type Safety**: TypeScript for frontend, Go's type system for backend +- **Code Review**: Mandatory peer review for all changes +- **Static Analysis**: Automated code quality checks +- **Documentation**: Comprehensive API and component documentation + +## Success Metrics + +### User Engagement +- **Daily Active Users**: Track player engagement +- **Session Duration**: Average time spent playing +- **Question Accuracy**: Player success rates by theme +- **Hint Usage**: Frequency and effectiveness of hints + +### Technical Performance +- **Response Times**: API and database performance metrics +- **Error Rates**: Application stability and reliability +- **Uptime**: System availability and reliability +- **Security Events**: Monitoring for security incidents + +### Business Metrics +- **Player Retention**: User return rates and engagement +- **Content Quality**: Question difficulty and engagement +- **Platform Usage**: Cross-platform adoption rates +- **Admin Efficiency**: Content management effectiveness + +## Future Roadmap + +### Phase 1: Core Platform (Weeks 1-6) +- Basic web application with core game mechanics +- Essential microservices and database setup +- Admin panel for question management + +### Phase 2: Enhanced Features (Weeks 7-12) +- Complete security implementation +- Comprehensive observability and monitoring +- Production-ready deployment + +### Phase 3: Mobile Expansion (Weeks 13-18) +- React Native mobile applications +- Cross-platform component optimization +- Mobile app store deployment + +### Phase 4: Advanced Features (Weeks 19-24) +- Desktop applications +- Advanced analytics and reporting +- Enhanced security and compliance features + +This requirements document serves as the foundation for the Know Foolery quiz game development, ensuring all stakeholders understand the scope, technical approach, and quality standards for the project. \ No newline at end of file