Initial project documentation and architecture
- 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 <noreply@anthropic.com>master
commit
4143ceaf25
@ -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.
|
||||
@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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<GameCardProps> = ({
|
||||
question,
|
||||
theme,
|
||||
timeRemaining,
|
||||
attemptsLeft,
|
||||
currentScore,
|
||||
onSubmitAnswer,
|
||||
onRequestHint,
|
||||
isLoading = false
|
||||
}) => {
|
||||
const [answer, setAnswer] = useState('')
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (answer.trim()) {
|
||||
onSubmitAnswer(answer.trim())
|
||||
setAnswer('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card size="lg" variant="elevated" m="$4">
|
||||
<VStack space="md" p="$4">
|
||||
{/* Header with theme and timer */}
|
||||
<HStack justifyContent="space-between" alignItems="center">
|
||||
<Badge size="sm" variant="solid" action="info">
|
||||
<Text color="$white" fontSize="$sm" fontWeight="$semibold">
|
||||
{theme}
|
||||
</Text>
|
||||
</Badge>
|
||||
<HStack space="sm" alignItems="center">
|
||||
<Text fontSize="$sm" color="$textLight600">
|
||||
⏱️ {Math.floor(timeRemaining / 60)}:{(timeRemaining % 60).toString().padStart(2, '0')}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<Progress value={((30 * 60 - timeRemaining) / (30 * 60)) * 100} size="sm">
|
||||
<ProgressFilledTrack />
|
||||
</Progress>
|
||||
|
||||
{/* Question */}
|
||||
<Box>
|
||||
<Text fontSize="$lg" fontWeight="$semibold" lineHeight="$xl">
|
||||
{question}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Answer input */}
|
||||
<Input size="lg" isDisabled={isLoading}>
|
||||
<InputField
|
||||
placeholder="Enter your answer..."
|
||||
value={answer}
|
||||
onChangeText={setAnswer}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</Input>
|
||||
|
||||
{/* Game stats */}
|
||||
<HStack justifyContent="space-between" alignItems="center">
|
||||
<Text fontSize="$sm" color="$textLight600">
|
||||
Attempts left: {attemptsLeft}/3
|
||||
</Text>
|
||||
<Text fontSize="$sm" color="$textLight600">
|
||||
Score: {currentScore} points
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* Action buttons */}
|
||||
<HStack space="sm">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="solid"
|
||||
action="primary"
|
||||
flex={1}
|
||||
onPress={handleSubmit}
|
||||
isDisabled={!answer.trim() || isLoading}
|
||||
>
|
||||
<ButtonText>Submit Answer</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
action="secondary"
|
||||
onPress={onRequestHint}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<ButtonText>💡 Hint</ButtonText>
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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 = <T>(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.
|
||||
@ -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<AuthResult> {
|
||||
// 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<TokenResult> {
|
||||
// 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<string> {
|
||||
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<void> {
|
||||
// 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<AuthResult> {
|
||||
// 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<AuthResult> {
|
||||
// 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<AuthResult> {
|
||||
// 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.
|
||||
@ -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.
|
||||
Loading…
Reference in New Issue