├── run.bat ├── dev.bat ├── migrations ├── 00_create_migrations_table_rollback.sql ├── 20240103_add_activity_log_smart_fields_rollback.sql ├── 00_create_migrations_table.sql └── 20240103_add_activity_log_smart_fields.sql ├── dev.sh ├── .gosec.json ├── run_test_secret.sh ├── run_test_secret.bat ├── SECURITY.md ├── docs ├── implementation_phases │ ├── README.md │ ├── Phase_8_Two_Factor_Authentication_Implementation.md │ ├── Phase_9_User_Activity_Logs_Implementation.md │ └── Phase_6_Testing_and_Deployment_Strategy.md ├── ARCHITECTURE.md ├── implementation │ ├── QUICK_FIX.md │ ├── IMPLEMENTATION_SUMMARY.md │ ├── VERSION_BUMP_1.1.0.md │ └── SWAGGER_UPDATE_SUMMARY.md ├── features │ ├── QUICK_SETUP_LOGGING.md │ ├── PROFILE_SYNC_QUICK_REFERENCE.md │ ├── QUICK_REFERENCE_SOCIAL_DATA.md │ ├── SMART_LOGGING_QUICK_REFERENCE.md │ └── SMART_LOGGING_SUMMARY.md ├── guides │ ├── auth-api-validation-endpoint.md │ ├── multi-app-oauth-config.md │ └── NANCY_SETUP.md └── migrations │ ├── MIGRATIONS.md │ └── README_SMART_LOGGING.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── Dockerfile.dev ├── docker-compose.dev.yml ├── pkg ├── models │ ├── schema_migration.go │ ├── activity_log.go │ ├── user.go │ └── social_account.go ├── errors │ └── errors.go ├── dto │ ├── activity_log.go │ └── auth.go └── jwt │ ├── jwt.go │ └── jwt_test.go ├── Dockerfile ├── .air.toml ├── .env.example ├── LICENSE ├── CODE_OF_CONDUCT.md ├── test_specific_secret.go ├── .gitignore ├── internal ├── social │ ├── repository.go │ └── oauth_state.go ├── util │ └── client_info.go ├── middleware │ ├── cors.go │ └── auth.go ├── database │ └── db.go ├── user │ ├── repository.go │ └── service_test.go ├── log │ ├── repository.go │ ├── handler.go │ └── query_service.go └── email │ └── service.go ├── setup-network.sh ├── test_api.sh ├── docker-compose.yml ├── .cursor └── rules │ ├── project-structure.mdc │ ├── development-workflow.mdc │ ├── api-development.mdc │ ├── security-patterns.mdc │ └── code-patterns.mdc ├── go.mod ├── test_logout.sh ├── CONTRIBUTING.md └── cmd └── api └── main.go /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Starting Auth API with Air (Hot Reload)... 3 | air -------------------------------------------------------------------------------- /dev.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Starting Authentication API in Development Mode with Hot Reload... 3 | echo. 4 | echo Services will be available at: 5 | echo - Auth API: http://localhost:8080 6 | echo - Redis Commander: http://localhost:8081 7 | echo - PostgreSQL: localhost:5433 8 | echo. 9 | docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build -------------------------------------------------------------------------------- /migrations/00_create_migrations_table_rollback.sql: -------------------------------------------------------------------------------- 1 | -- Rollback: Drop migration tracking table 2 | -- WARNING: This will remove all migration history! 3 | 4 | BEGIN; 5 | 6 | DROP INDEX IF EXISTS idx_schema_migrations_version; 7 | DROP INDEX IF EXISTS idx_schema_migrations_applied_at; 8 | DROP TABLE IF EXISTS schema_migrations; 9 | 10 | COMMIT; 11 | 12 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting Authentication API in Development Mode with Hot Reload..." 4 | echo "" 5 | echo "Services will be available at:" 6 | echo "- Auth API: http://localhost:8080" 7 | echo "- Redis Commander: http://localhost:8081" 8 | echo "- PostgreSQL: localhost:5433" 9 | echo "" 10 | 11 | docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build -------------------------------------------------------------------------------- /.gosec.json: -------------------------------------------------------------------------------- 1 | { 2 | "severity": "medium", 3 | "confidence": "medium", 4 | "exclude-rules": [], 5 | "include-rules": [], 6 | "exclude-dirs": ["vendor/", "node_modules/"], 7 | "tests": true, 8 | "exclude-generated": true, 9 | "nosec": false, 10 | "nosec-tag": "nosec", 11 | "fmt": "json", 12 | "stdout": true, 13 | "verbose": false, 14 | "output": "", 15 | "log": "" 16 | } 17 | -------------------------------------------------------------------------------- /run_test_secret.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run the test with a TOTP secret 4 | # This demonstrates how to run the test safely without hardcoded credentials 5 | 6 | # Example secret (replace with your actual secret when testing) 7 | export TEST_TOTP_SECRET="RZCH2POUGIOAIDZJ2R2M4E62AIACDYVLF6WLDXG3KHWBCLZQL2ZA====" 8 | 9 | echo "Running TOTP test with environment variable..." 10 | go run test_specific_secret.go 11 | -------------------------------------------------------------------------------- /run_test_secret.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Script to run the test with a TOTP secret 4 | REM This demonstrates how to run the test safely without hardcoded credentials 5 | 6 | REM Example secret (replace with your actual secret when testing) 7 | set TEST_TOTP_SECRET=RZCH2POUGIOAIDZJ2R2M4E62AIACDYVLF6WLDXG3KHWBCLZQL2ZA==== 8 | 9 | echo Running TOTP test with environment variable... 10 | go run test_specific_secret.go -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a security vulnerability, please open an issue or contact the maintainer directly. We will respond as quickly as possible to address the issue. 6 | 7 | - Do not disclose security issues publicly until they have been resolved. 8 | - Provide as much detail as possible to help us resolve the issue quickly. 9 | 10 | Thank you for helping keep this project secure! 11 | -------------------------------------------------------------------------------- /docs/implementation_phases/README.md: -------------------------------------------------------------------------------- 1 | # Development Phases 2 | 3 | This folder contains detailed plans for each phase of the project: 4 | 5 | - Phase 1: Database and Project Setup 6 | - Phase 2: Core Authentication Implementation 7 | - Phase 3: Social Authentication Integration 8 | - Phase 4: Email Verification and Redis Integration 9 | - Phase 5: API Endpoints and Middleware Implementation 10 | - Phase 6: Testing and Deployment Strategy 11 | - Phase 7: Documentation and Deployment 12 | - Phase 8: Two Factor Authentication Implementation 13 | - Phase 9: User Activity Logs Implementation -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | labels: enhancement 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /migrations/20240103_add_activity_log_smart_fields_rollback.sql: -------------------------------------------------------------------------------- 1 | -- Rollback Migration: Remove smart logging fields from activity_logs table 2 | -- Date: 2024-01-03 3 | -- Description: Rolls back the addition of severity, expires_at, and is_anomaly fields 4 | 5 | -- Drop constraint 6 | ALTER TABLE activity_logs DROP CONSTRAINT IF EXISTS chk_activity_logs_severity; 7 | 8 | -- Drop indexes 9 | DROP INDEX IF EXISTS idx_activity_logs_expires; 10 | DROP INDEX IF EXISTS idx_activity_logs_cleanup; 11 | DROP INDEX IF EXISTS idx_activity_logs_user_timestamp; 12 | 13 | -- Drop columns 14 | ALTER TABLE activity_logs 15 | DROP COLUMN IF EXISTS severity, 16 | DROP COLUMN IF EXISTS expires_at, 17 | DROP COLUMN IF EXISTS is_anomaly; 18 | 19 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # Development Dockerfile with Air hot reload 2 | FROM golang:1.23-alpine 3 | 4 | # Install git and Air for hot reload (compatible version) 5 | RUN apk add --no-cache git 6 | RUN go install github.com/cosmtrek/air@v1.49.0 7 | 8 | # Set the current working directory inside the container 9 | WORKDIR /app 10 | 11 | # Set Go toolchain to local to prevent version conflicts 12 | ENV GOTOOLCHAIN=local 13 | ENV GIN_MODE=debug 14 | 15 | # Copy go.mod and go.sum files and download dependencies 16 | COPY go.mod go.sum ./ 17 | RUN go mod download 18 | 19 | # Copy the source code into the container 20 | COPY . . 21 | 22 | # Expose the port the application runs on 23 | EXPOSE 8080 24 | 25 | # Run Air for hot reload 26 | CMD ["air"] -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | # Override auth-api for development with hot reload 5 | auth-api: 6 | build: 7 | context: . 8 | dockerfile: Dockerfile.dev 9 | container_name: auth_api_dev 10 | ports: 11 | - "8080:8080" 12 | volumes: 13 | - .:/app 14 | - /app/tmp # Exclude air tmp directory 15 | - go_modules:/go/pkg/mod # Cache Go modules 16 | working_dir: /app 17 | env_file: 18 | - .env 19 | depends_on: 20 | postgres: 21 | condition: service_healthy 22 | redis: 23 | condition: service_healthy 24 | restart: unless-stopped 25 | networks: 26 | - default 27 | - shared-api-network 28 | 29 | volumes: 30 | go_modules: 31 | 32 | networks: 33 | shared-api-network: 34 | external: true 35 | -------------------------------------------------------------------------------- /pkg/models/schema_migration.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // SchemaMigration tracks which database migrations have been applied 6 | type SchemaMigration struct { 7 | ID uint `gorm:"primaryKey" json:"id"` 8 | Version string `gorm:"uniqueIndex;not null;size:255" json:"version"` // YYYYMMDD_HHMMSS format 9 | Name string `gorm:"not null;size:255" json:"name"` 10 | AppliedAt time.Time `gorm:"index;not null;default:CURRENT_TIMESTAMP" json:"applied_at"` 11 | ExecutionTimeMs int `json:"execution_time_ms"` // How long the migration took 12 | Success bool `gorm:"not null;default:true" json:"success"` 13 | ErrorMessage string `gorm:"type:text" json:"error_message,omitempty"` 14 | Checksum string `gorm:"size:64" json:"checksum,omitempty"` // SHA256 of migration file 15 | } 16 | 17 | // TableName specifies the table name for GORM 18 | func (SchemaMigration) TableName() string { 19 | return "schema_migrations" 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image as a base image 2 | FROM golang:1.23-alpine AS builder 3 | 4 | # Set the current working directory inside the container 5 | WORKDIR /app 6 | 7 | # Set Go toolchain to local to prevent version conflicts 8 | ENV GOTOOLCHAIN=local 9 | 10 | # Copy go.mod and go.sum files and download dependencies 11 | COPY go.mod go.sum ./ 12 | RUN go mod download 13 | 14 | # Copy the source code into the container 15 | COPY . . 16 | 17 | # Build the Go application 18 | RUN go build -o /go-auth-api ./cmd/api 19 | 20 | # Use a minimal image for the final stage 21 | FROM alpine:latest 22 | 23 | # Install ca-certificates for HTTPS connections 24 | RUN apk --no-cache add ca-certificates 25 | 26 | # Set the current working directory inside the container 27 | WORKDIR /root/ 28 | 29 | # Copy the built binary from the builder stage 30 | COPY --from=builder /go-auth-api . 31 | 32 | # Expose the port the application runs on 33 | EXPOSE 8080 34 | 35 | # Run the application 36 | CMD ["./go-auth-api"] -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main.exe" 8 | cmd = "go build -o ./tmp/main.exe ./cmd/api" 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", ".git", "docs"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = true 22 | poll_interval = 1000 23 | rerun = false 24 | rerun_delay = 500 25 | send_interrupt = false 26 | stop_on_root = false 27 | 28 | [color] 29 | app = "" 30 | build = "yellow" 31 | main = "magenta" 32 | runner = "green" 33 | watcher = "cyan" 34 | 35 | [log] 36 | main_only = false 37 | time = false 38 | 39 | [misc] 40 | clean_on_exit = false 41 | 42 | [screen] 43 | clear_on_rebuild = false 44 | keep_scroll = true -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | Thank you for your contribution! 4 | 5 | ## Description 6 | Please include a summary of the change and which issue is fixed. Also include relevant motivation and context. 7 | 8 | Fixes # (issue) 9 | 10 | ## Type of change 11 | - [ ] Bug fix 12 | - [ ] New feature 13 | - [ ] Breaking change 14 | - [ ] Documentation update 15 | - [ ] Other (please describe): 16 | 17 | ## Checklist 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | - [ ] I have added tests that prove my fix is effective or that my feature works 24 | - [ ] New and existing unit tests pass locally with my changes 25 | - [ ] Any dependent changes have been merged and published in downstream modules 26 | 27 | ## Additional context 28 | Add any other context or screenshots about the pull request here. 29 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # .env.example - Environment variable template for auth_api 2 | 3 | DB_HOST=postgres 4 | DB_PORT=5432 5 | DB_USER=postgres 6 | DB_PASSWORD=your_db_password 7 | DB_NAME=auth_db 8 | 9 | JWT_SECRET=your_jwt_secret 10 | ACCESS_TOKEN_EXPIRATION_MINUTES=15 11 | REFRESH_TOKEN_EXPIRATION_HOURS=720 12 | 13 | # Use 'redis:6379' for Docker Compose, 'localhost:6379' for local/manual run 14 | REDIS_ADDR=redis:6379 15 | REDIS_PASSWORD=your_redis_password 16 | REDIS_DB=0 17 | 18 | GOOGLE_CLIENT_ID=your_google_client_id 19 | GOOGLE_CLIENT_SECRET=your_google_client_secret 20 | GOOGLE_REDIRECT_URL=http://localhost:8080/auth/google/callback 21 | 22 | FACEBOOK_CLIENT_ID=your_facebook_client_id 23 | FACEBOOK_CLIENT_SECRET=your_facebook_client_secret 24 | FACEBOOK_REDIRECT_URL=http://localhost:8080/auth/facebook/callback 25 | 26 | GITHUB_CLIENT_ID=your_github_client_id 27 | GITHUB_CLIENT_SECRET=your_github_client_secret 28 | GITHUB_REDIRECT_URL=http://localhost:8080/auth/github/callback 29 | 30 | EMAIL_HOST=smtp.example.com 31 | EMAIL_PORT=587 32 | EMAIL_USERNAME=your_email@example.com 33 | EMAIL_PASSWORD=your_email_password 34 | EMAIL_FROM=your_email@example.com 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to a positive environment: 10 | - Using welcoming and inclusive language 11 | - Being respectful of differing viewpoints 12 | - Gracefully accepting constructive criticism 13 | - Showing empathy towards others 14 | 15 | Examples of unacceptable behavior: 16 | - Trolling, insulting/derogatory comments 17 | - Public or private harassment 18 | - Publishing others' private information 19 | 20 | ## Enforcement 21 | 22 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer. 23 | 24 | --- 25 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/). 26 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "net/http" 4 | 5 | // Error codes 6 | const ( 7 | ErrInternal = iota 8 | ErrUnauthorized 9 | ErrForbidden 10 | ErrNotFound 11 | ErrConflict 12 | ErrBadRequest 13 | ) 14 | 15 | // AppError represents a custom application error 16 | type AppError struct { 17 | Code int `json:"code"` 18 | Message string `json:"message"` 19 | } 20 | 21 | // Error implements the error interface 22 | func (e *AppError) Error() string { 23 | return e.Message 24 | } 25 | 26 | // NewAppError creates a new AppError 27 | func NewAppError(errType int, message string) *AppError { 28 | var httpCode int 29 | switch errType { 30 | case ErrInternal: 31 | httpCode = http.StatusInternalServerError 32 | case ErrUnauthorized: 33 | httpCode = http.StatusUnauthorized 34 | case ErrForbidden: 35 | httpCode = http.StatusForbidden 36 | case ErrNotFound: 37 | httpCode = http.StatusNotFound 38 | case ErrConflict: 39 | httpCode = http.StatusConflict 40 | case ErrBadRequest: 41 | httpCode = http.StatusBadRequest 42 | default: 43 | httpCode = http.StatusInternalServerError 44 | } 45 | 46 | return &AppError{ 47 | Code: httpCode, 48 | Message: message, 49 | } 50 | } -------------------------------------------------------------------------------- /test_specific_secret.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/pquerna/otp/totp" 9 | ) 10 | 11 | func main() { 12 | // Get the secret from environment variable or use a test default 13 | secret := os.Getenv("TEST_TOTP_SECRET") 14 | if secret == "" { 15 | fmt.Println("Error: TEST_TOTP_SECRET environment variable is required") 16 | fmt.Println("Please set it with: export TEST_TOTP_SECRET=your_secret_here") 17 | return 18 | } 19 | 20 | fmt.Printf("Testing with secret: %s\n", secret) 21 | fmt.Printf("Secret length: %d\n", len(secret)) 22 | 23 | // Generate current TOTP code 24 | code, err := totp.GenerateCode(secret, time.Now()) 25 | if err != nil { 26 | fmt.Printf("Error generating code: %v\n", err) 27 | return 28 | } 29 | 30 | fmt.Printf("Current TOTP code for this secret: %s\n", code) 31 | 32 | // Test validation with the code you tried earlier 33 | userCode := "779254" 34 | valid := totp.Validate(userCode, secret) 35 | fmt.Printf("Validation of user code %s: %t\n", userCode, valid) 36 | 37 | // Test validation with current generated code 38 | valid = totp.Validate(code, secret) 39 | fmt.Printf("Validation of current code %s: %t\n", code, valid) 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. API endpoint called: `POST /api/your-endpoint` 13 | 2. Request payload (if any): 14 | ```json 15 | { 16 | "key": "value" 17 | } 18 | ``` 19 | 3. Request headers (if any): 20 | ``` 21 | Authorization: Bearer 22 | ``` 23 | 4. Relevant environment variables or configuration: 24 | ``` 25 | DB_HOST=... 26 | JWT_SECRET=... 27 | ``` 28 | 5. Run the request (e.g., with curl, Postman, or frontend) 29 | 6. See error/response: 30 | ```json 31 | { 32 | "error": "..." 33 | } 34 | ``` 35 | 7. (Optional) Include relevant logs or stack traces: 36 | ``` 37 | [2025-06-21 12:00:00] ERROR ... 38 | ``` 39 | 40 | **Expected behavior** 41 | A clear and concise description of what you expected to happen. 42 | 43 | **Screenshots** 44 | If applicable, add screenshots to help explain your problem. 45 | 46 | **Environment (please complete the following information):** 47 | - OS: [e.g. Windows, Mac, Linux] 48 | - Go version: [e.g. 1.22] 49 | - Docker version: [if applicable] 50 | 51 | **Additional context** 52 | Add any other context about the problem here. 53 | -------------------------------------------------------------------------------- /docs/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | ## High-Level Diagram 4 | 5 | ``` 6 | +-----------+ +-----------+ +-----------+ 7 | | Client |<---> | API |<---> | Database | 8 | | (Frontend)| | (Gin) | | (Postgres)| 9 | +-----------+ +-----------+ +-----------+ 10 | | 11 | v 12 | +-----------+ 13 | | Redis | 14 | +-----------+ 15 | ``` 16 | 17 | - **API**: Go (Gin), handles authentication, authorization, and business logic 18 | - **Database**: PostgreSQL, stores users, tokens, etc. 19 | - **Redis**: Session/token management, caching 20 | - **Email**: SMTP for verification and password reset 21 | - **Social Auth**: OAuth2 with Google, Facebook, GitHub 22 | 23 | ## Key Components 24 | - `internal/auth` — Core authentication logic 25 | - `internal/user` — User management 26 | - `internal/social` — Social login 27 | - `internal/email` — Email verification/reset 28 | - `internal/middleware` — JWT, RBAC, etc. 29 | - `pkg/jwt` — JWT helpers 30 | 31 | ## Flow Example 32 | 1. User registers or logs in 33 | 2. API validates credentials, issues JWT 34 | 3. JWT used for protected endpoints 35 | 4. Redis manages sessions/tokens 36 | 5. Social login handled via OAuth2 callback 37 | 38 | --- 39 | For more details, see the code and comments in each package. 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Environment variables and configuration files 21 | .env 22 | .env.local 23 | .env.development 24 | .env.test 25 | .env.production 26 | 27 | # Logs 28 | *.log 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # IDE and editor files 37 | .vscode/ 38 | .idea/ 39 | *.swp 40 | *.swo 41 | *~ 42 | 43 | # OS generated files 44 | .DS_Store 45 | .DS_Store? 46 | ._* 47 | .Spotlight-V100 48 | .Trashes 49 | ehthumbs.db 50 | Thumbs.db 51 | 52 | # Build outputs 53 | /api 54 | /main 55 | /auth_api 56 | /cmd/api/api 57 | 58 | # Test coverage 59 | coverage.out 60 | coverage.html 61 | 62 | # Air live reload tool (for hot reloading during development) 63 | tmp/ 64 | *.air.toml.bak 65 | .air.toml.bak 66 | 67 | # Build artifacts 68 | bin/ 69 | *.exe 70 | 71 | # Batch files (optional - remove if you want to track run.bat) 72 | # *.bat 73 | 74 | # Delve debugger 75 | __debug_bin 76 | 77 | # Database files (for local development) 78 | *.db 79 | *.sqlite 80 | *.sqlite3 81 | 82 | # Application specific temporary files 83 | uploads/ 84 | storage/ 85 | cache/ -------------------------------------------------------------------------------- /pkg/models/activity_log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // ActivityLog captures essential details about each user action 11 | type ActivityLog struct { 12 | ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` 13 | UserID uuid.UUID `gorm:"index:idx_user_timestamp;index:idx_cleanup" json:"user_id"` // Composite indexes for performance 14 | EventType string `gorm:"index;not null" json:"event_type"` 15 | Timestamp time.Time `gorm:"index:idx_user_timestamp;index:idx_cleanup;not null" json:"timestamp"` 16 | IPAddress string `json:"ip_address"` 17 | UserAgent string `json:"user_agent"` 18 | Details json.RawMessage `gorm:"type:jsonb" json:"details"` // Use json.RawMessage for flexible JSONB 19 | 20 | // New fields for smart logging 21 | Severity string `gorm:"index:idx_cleanup;not null;default:'INFORMATIONAL'" json:"severity"` // CRITICAL, IMPORTANT, INFORMATIONAL 22 | ExpiresAt *time.Time `gorm:"index:idx_expires" json:"expires_at"` // Automatic expiration timestamp for cleanup 23 | IsAnomaly bool `gorm:"default:false" json:"is_anomaly"` // Flag if this was logged due to anomaly detection 24 | } 25 | 26 | // TableName specifies the table name for ActivityLog 27 | func (ActivityLog) TableName() string { 28 | return "activity_logs" 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dto/activity_log.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // ActivityLogResponse represents a single activity log entry in API responses 4 | type ActivityLogResponse struct { 5 | ID string `json:"id"` 6 | UserID string `json:"user_id"` 7 | EventType string `json:"event_type"` 8 | Timestamp string `json:"timestamp"` 9 | IPAddress string `json:"ip_address"` 10 | UserAgent string `json:"user_agent"` 11 | Details interface{} `json:"details" swaggertype:"object"` 12 | } 13 | 14 | // ActivityLogListRequest represents query parameters for listing activity logs 15 | type ActivityLogListRequest struct { 16 | Page int `form:"page" binding:"omitempty,min=1"` 17 | Limit int `form:"limit" binding:"omitempty,min=1,max=100"` 18 | EventType string `form:"event_type" binding:"omitempty"` 19 | StartDate string `form:"start_date" binding:"omitempty"` // Format: 2006-01-02 20 | EndDate string `form:"end_date" binding:"omitempty"` // Format: 2006-01-02 21 | } 22 | 23 | // ActivityLogListResponse represents the paginated response for activity logs 24 | type ActivityLogListResponse struct { 25 | Data []ActivityLogResponse `json:"data"` 26 | Pagination PaginationResponse `json:"pagination"` 27 | } 28 | 29 | // PaginationResponse represents pagination metadata 30 | type PaginationResponse struct { 31 | Page int `json:"page"` 32 | Limit int `json:"limit"` 33 | TotalRecords int64 `json:"total_records"` 34 | TotalPages int `json:"total_pages"` 35 | HasNext bool `json:"has_next"` 36 | HasPrevious bool `json:"has_previous"` 37 | } 38 | -------------------------------------------------------------------------------- /internal/social/repository.go: -------------------------------------------------------------------------------- 1 | package social 2 | 3 | import ( 4 | "github.com/gjovanovicst/auth_api/pkg/models" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type Repository struct { 9 | DB *gorm.DB 10 | } 11 | 12 | func NewRepository(db *gorm.DB) *Repository { 13 | return &Repository{DB: db} 14 | } 15 | 16 | func (r *Repository) CreateSocialAccount(socialAccount *models.SocialAccount) error { 17 | return r.DB.Create(socialAccount).Error 18 | } 19 | 20 | func (r *Repository) GetSocialAccountByProviderAndUserID(provider, providerUserID string) (*models.SocialAccount, error) { 21 | var socialAccount models.SocialAccount 22 | err := r.DB.Where("provider = ? AND provider_user_id = ?", provider, providerUserID).First(&socialAccount).Error 23 | return &socialAccount, err 24 | } 25 | 26 | func (r *Repository) GetSocialAccountsByUserID(userID string) ([]models.SocialAccount, error) { 27 | var socialAccounts []models.SocialAccount 28 | err := r.DB.Where("user_id = ?", userID).Find(&socialAccounts).Error 29 | return socialAccounts, err 30 | } 31 | 32 | func (r *Repository) UpdateSocialAccount(socialAccount *models.SocialAccount) error { 33 | return r.DB.Save(socialAccount).Error 34 | } 35 | 36 | func (r *Repository) UpdateSocialAccountTokens(id string, accessToken, refreshToken string) error { 37 | return r.DB.Model(&models.SocialAccount{}).Where("id = ?", id).Updates(map[string]interface{}{ 38 | "access_token": accessToken, 39 | "refresh_token": refreshToken, 40 | }).Error 41 | } 42 | 43 | func (r *Repository) DeleteSocialAccount(id string) error { 44 | return r.DB.Delete(&models.SocialAccount{}, "id = ?", id).Error 45 | } -------------------------------------------------------------------------------- /internal/util/client_info.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // GetClientIP extracts the real client IP address from the request 11 | func GetClientIP(c *gin.Context) string { 12 | // Check for X-Forwarded-For header (most common) 13 | forwarded := c.GetHeader("X-Forwarded-For") 14 | if forwarded != "" { 15 | // X-Forwarded-For can contain multiple IPs, use the first one 16 | ips := strings.Split(forwarded, ",") 17 | if len(ips) > 0 { 18 | ip := strings.TrimSpace(ips[0]) 19 | if ip != "" && ip != "unknown" { 20 | return ip 21 | } 22 | } 23 | } 24 | 25 | // Check for X-Real-IP header 26 | realIP := c.GetHeader("X-Real-IP") 27 | if realIP != "" && realIP != "unknown" { 28 | return realIP 29 | } 30 | 31 | // Check for CF-Connecting-IP header (Cloudflare) 32 | cfIP := c.GetHeader("CF-Connecting-IP") 33 | if cfIP != "" && cfIP != "unknown" { 34 | return cfIP 35 | } 36 | 37 | // Fallback to RemoteAddr 38 | ip, _, err := net.SplitHostPort(c.Request.RemoteAddr) 39 | if err != nil { 40 | return c.Request.RemoteAddr // Return as is if parsing fails 41 | } 42 | 43 | return ip 44 | } 45 | 46 | // GetUserAgent extracts the User-Agent from the request 47 | func GetUserAgent(c *gin.Context) string { 48 | userAgent := c.GetHeader("User-Agent") 49 | if userAgent == "" { 50 | return "Unknown" 51 | } 52 | return userAgent 53 | } 54 | 55 | // GetClientInfo returns both IP address and User-Agent 56 | func GetClientInfo(c *gin.Context) (string, string) { 57 | return GetClientIP(c), GetUserAgent(c) 58 | } 59 | -------------------------------------------------------------------------------- /setup-network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to manage the shared API network for inter-container communication 4 | 5 | NETWORK_NAME="shared-api-network" 6 | 7 | case "$1" in 8 | "create") 9 | echo "Creating shared network: $NETWORK_NAME" 10 | docker network create $NETWORK_NAME 11 | echo "Network created successfully!" 12 | echo "" 13 | echo "This network allows containers from different projects to communicate." 14 | echo "Make sure other API projects also use this network in their docker-compose files." 15 | ;; 16 | "remove") 17 | echo "Removing shared network: $NETWORK_NAME" 18 | docker network rm $NETWORK_NAME 19 | echo "Network removed successfully!" 20 | ;; 21 | "inspect") 22 | echo "Inspecting shared network: $NETWORK_NAME" 23 | docker network inspect $NETWORK_NAME 24 | ;; 25 | "list") 26 | echo "Connected containers in network: $NETWORK_NAME" 27 | docker network inspect $NETWORK_NAME --format='{{range .Containers}}{{.Name}} ({{.IPv4Address}}){{println}}{{end}}' 28 | ;; 29 | *) 30 | echo "Usage: $0 {create|remove|inspect|list}" 31 | echo "" 32 | echo "Commands:" 33 | echo " create - Create the shared network" 34 | echo " remove - Remove the shared network" 35 | echo " inspect - Show detailed network information" 36 | echo " list - List connected containers and their IPs" 37 | echo "" 38 | echo "Example usage:" 39 | echo " $0 create # Run this once to set up the network" 40 | echo " $0 list # Check which containers are connected" 41 | exit 1 42 | ;; 43 | esac -------------------------------------------------------------------------------- /pkg/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "gorm.io/datatypes" 8 | ) 9 | 10 | // User represents the core user entity in our system 11 | type User struct { 12 | ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` 13 | Email string `gorm:"uniqueIndex;not null" json:"email"` 14 | PasswordHash string `gorm:"" json:"-"` // Stored hashed, not exposed via JSON - not required for social logins 15 | EmailVerified bool `gorm:"default:false" json:"email_verified"` 16 | Name string `gorm:"" json:"name"` // Full name from social login or user input 17 | FirstName string `gorm:"" json:"first_name"` // First name from social login 18 | LastName string `gorm:"" json:"last_name"` // Last name from social login 19 | ProfilePicture string `gorm:"" json:"profile_picture"` // Profile picture URL from social login 20 | Locale string `gorm:"" json:"locale"` // User's locale/language preference 21 | TwoFAEnabled bool `gorm:"default:false" json:"two_fa_enabled"` 22 | TwoFASecret string `gorm:"" json:"-"` // Stored encrypted, not exposed via JSON 23 | TwoFARecoveryCodes datatypes.JSON `gorm:"type:jsonb" json:"-"` // Stored encrypted, not exposed via JSON 24 | CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` 25 | UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` 26 | SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts"` // One-to-many relationship 27 | } 28 | -------------------------------------------------------------------------------- /internal/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-gonic/gin" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // CORSMiddleware creates and configures CORS middleware 12 | func CORSMiddleware() gin.HandlerFunc { 13 | config := cors.Config{ 14 | AllowOrigins: []string{ 15 | "http://localhost:3000", // React dev server 16 | "http://localhost:5173", // Vite dev server 17 | "http://localhost:8080", // API server itself 18 | "https://accounts.google.com", // Google OAuth 19 | "https://www.facebook.com", // Facebook OAuth 20 | "https://github.com", // GitHub OAuth 21 | }, 22 | AllowMethods: []string{ 23 | "GET", 24 | "POST", 25 | "PUT", 26 | "DELETE", 27 | "OPTIONS", 28 | "HEAD", 29 | }, 30 | AllowHeaders: []string{ 31 | "Origin", 32 | "Content-Type", 33 | "Content-Length", 34 | "Accept-Encoding", 35 | "X-CSRF-Token", 36 | "Authorization", 37 | "Accept", 38 | "Cache-Control", 39 | "X-Requested-With", 40 | }, 41 | ExposeHeaders: []string{ 42 | "Content-Length", 43 | "Access-Control-Allow-Origin", 44 | "Access-Control-Allow-Headers", 45 | "Content-Type", 46 | }, 47 | AllowCredentials: true, 48 | MaxAge: 12 * time.Hour, 49 | } 50 | 51 | // In production, restrict origins to specific domains 52 | if viper.GetString("GIN_MODE") == "release" { 53 | // Get frontend URLs from environment 54 | frontendURL := viper.GetString("FRONTEND_URL") 55 | if frontendURL != "" { 56 | config.AllowOrigins = []string{ 57 | frontendURL, 58 | "https://accounts.google.com", 59 | "https://www.facebook.com", 60 | "https://github.com", 61 | } 62 | } 63 | } 64 | 65 | return cors.New(config) 66 | } 67 | -------------------------------------------------------------------------------- /migrations/00_create_migrations_table.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Create schema_migrations tracking table 2 | -- Date: 2024-01-03 3 | -- Description: Creates a table to track which migrations have been applied 4 | 5 | BEGIN; 6 | 7 | -- Create migrations tracking table 8 | CREATE TABLE IF NOT EXISTS schema_migrations ( 9 | id SERIAL PRIMARY KEY, 10 | version VARCHAR(255) NOT NULL UNIQUE, 11 | name VARCHAR(255) NOT NULL, 12 | applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | execution_time_ms INTEGER, 14 | success BOOLEAN NOT NULL DEFAULT true, 15 | error_message TEXT, 16 | checksum VARCHAR(64) 17 | ); 18 | 19 | -- Add indexes for faster lookups 20 | CREATE INDEX IF NOT EXISTS idx_schema_migrations_version ON schema_migrations(version); 21 | CREATE INDEX IF NOT EXISTS idx_schema_migrations_applied_at ON schema_migrations(applied_at); 22 | 23 | -- Add comments 24 | COMMENT ON TABLE schema_migrations IS 'Tracks which database migrations have been applied'; 25 | COMMENT ON COLUMN schema_migrations.version IS 'Migration version (YYYYMMDD_HHMMSS format)'; 26 | COMMENT ON COLUMN schema_migrations.name IS 'Migration name/description'; 27 | COMMENT ON COLUMN schema_migrations.applied_at IS 'When the migration was applied'; 28 | COMMENT ON COLUMN schema_migrations.execution_time_ms IS 'How long the migration took to execute'; 29 | COMMENT ON COLUMN schema_migrations.success IS 'Whether the migration succeeded'; 30 | COMMENT ON COLUMN schema_migrations.error_message IS 'Error message if migration failed'; 31 | COMMENT ON COLUMN schema_migrations.checksum IS 'SHA256 checksum of migration file content'; 32 | 33 | -- Insert initial migration record (this migration itself) 34 | INSERT INTO schema_migrations (version, name, success, execution_time_ms) 35 | VALUES ('00000000_000000', 'create_migrations_table', true, 0) 36 | ON CONFLICT (version) DO NOTHING; 37 | 38 | COMMIT; 39 | 40 | -------------------------------------------------------------------------------- /docs/implementation/QUICK_FIX.md: -------------------------------------------------------------------------------- 1 | # QUICK FIX - Profile Missing Fields 2 | 3 | ## Problem 4 | Profile only shows: `id`, `email`, `email_verified`, `two_fa_enabled`, `created_at`, `updated_at` 5 | 6 | Missing: `name`, `first_name`, `last_name`, `profile_picture`, `locale`, `social_accounts` 7 | 8 | ## Why 9 | **Database columns don't exist yet.** Migration needs to run. 10 | 11 | ## Fix (3 Steps) 12 | 13 | ### 1. Stop & Rebuild 14 | ```bash 15 | # Stop current app (Ctrl+C) 16 | cd /c/work/AI/Cursor/auth_api/v1.0.0 17 | go build -o auth_api.exe cmd/api/main.go 18 | ``` 19 | 20 | ### 2. Start & Watch 21 | ```bash 22 | ./auth_api.exe 23 | 24 | # WAIT FOR THIS MESSAGE: 25 | # "Database migration completed!" 26 | ``` 27 | 28 | ### 3. Re-login 29 | ``` 30 | Visit: http://localhost:8080/auth/google/login 31 | Complete OAuth flow 32 | Get new token 33 | ``` 34 | 35 | ### 4. Check Profile 36 | ```bash 37 | curl -H "Authorization: Bearer YOUR_NEW_TOKEN" \ 38 | http://localhost:8080/profile 39 | ``` 40 | 41 | **Should now show all fields!** ✅ 42 | 43 | --- 44 | 45 | ## Verify Migration Ran 46 | 47 | Check database has new columns: 48 | ```sql 49 | \d users 50 | 51 | -- Look for these columns: 52 | -- name 53 | -- first_name 54 | -- last_name 55 | -- profile_picture 56 | -- locale 57 | ``` 58 | 59 | If columns missing, see [FIX_MISSING_FIELDS.md](FIX_MISSING_FIELDS.md) 60 | 61 | --- 62 | 63 | ## One-Liner Diagnosis 64 | 65 | ```bash 66 | # Check if columns exist 67 | psql -U -d -c "SELECT column_name FROM information_schema.columns WHERE table_name='users' AND column_name IN ('name','first_name','last_name','profile_picture','locale');" 68 | 69 | # Should return 5 rows 70 | # If returns 0 rows → restart application to run migration 71 | ``` 72 | 73 | --- 74 | 75 | **TL;DR:** Restart app → Migration adds columns → Login again → All fields appear ✅ 76 | 77 | -------------------------------------------------------------------------------- /internal/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gjovanovicst/auth_api/pkg/models" 10 | "gorm.io/driver/postgres" 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/logger" 13 | ) 14 | 15 | var DB *gorm.DB 16 | 17 | // ConnectDatabase establishes connection to PostgreSQL database 18 | func ConnectDatabase() { 19 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", 20 | os.Getenv("DB_HOST"), 21 | os.Getenv("DB_USER"), 22 | os.Getenv("DB_PASSWORD"), 23 | os.Getenv("DB_NAME"), 24 | os.Getenv("DB_PORT"), 25 | ) 26 | 27 | newLogger := logger.New( 28 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 29 | logger.Config{ 30 | SlowThreshold: time.Second, // Slow SQL threshold 31 | LogLevel: logger.Info, // Log level 32 | IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger 33 | Colorful: true, // Enable color 34 | }, 35 | ) 36 | 37 | var err error 38 | DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ 39 | Logger: newLogger, 40 | }) 41 | 42 | if err != nil { 43 | log.Fatalf("Failed to connect to database: %v", err) 44 | } 45 | 46 | log.Println("Database connected successfully!") 47 | } 48 | 49 | // MigrateDatabase runs GORM auto-migration for all models 50 | func MigrateDatabase() { 51 | // AutoMigrate will create tables, missing columns, and missing indexes 52 | // It will NOT change existing column types or delete unused columns 53 | err := DB.AutoMigrate( 54 | &models.User{}, 55 | &models.SocialAccount{}, 56 | &models.ActivityLog{}, 57 | &models.SchemaMigration{}, // Migration tracking table 58 | ) 59 | 60 | if err != nil { 61 | log.Fatalf("Failed to migrate database: %v", err) 62 | } 63 | 64 | log.Println("Database migration completed!") 65 | } 66 | -------------------------------------------------------------------------------- /test_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🔍 Testing Authentication API..." 4 | 5 | # Test 1: API is responding 6 | echo "1. Testing API health..." 7 | curl -s http://localhost:8080/register > /dev/null 8 | if [ $? -eq 0 ]; then 9 | echo "✅ API is responding" 10 | else 11 | echo "❌ API is not responding" 12 | exit 1 13 | fi 14 | 15 | # Test 2: Registration 16 | echo "2. Testing user registration..." 17 | REGISTER_RESPONSE=$(curl -s -X POST http://localhost:8080/register \ 18 | -H "Content-Type: application/json" \ 19 | -d '{"email": "test@example.com", "password": "password123"}') 20 | 21 | if [[ $REGISTER_RESPONSE == *"User registered successfully"* ]]; then 22 | echo "✅ Registration working" 23 | else 24 | echo "❌ Registration failed: $REGISTER_RESPONSE" 25 | fi 26 | 27 | # Test 3: Invalid login (email not verified) 28 | echo "3. Testing login with unverified email..." 29 | LOGIN_RESPONSE=$(curl -s -X POST http://localhost:8080/login \ 30 | -H "Content-Type: application/json" \ 31 | -d '{"email": "test@example.com", "password": "password123"}') 32 | 33 | if [[ $LOGIN_RESPONSE == *"Email not verified"* ]]; then 34 | echo "✅ Email verification check working" 35 | else 36 | echo "⚠️ Login response: $LOGIN_RESPONSE" 37 | fi 38 | 39 | # Test 4: Invalid credentials 40 | echo "4. Testing invalid credentials..." 41 | INVALID_LOGIN=$(curl -s -X POST http://localhost:8080/login \ 42 | -H "Content-Type: application/json" \ 43 | -d '{"email": "wrong@email.com", "password": "wrongpass"}') 44 | 45 | if [[ $INVALID_LOGIN == *"Invalid credentials"* ]]; then 46 | echo "✅ Invalid credentials check working" 47 | else 48 | echo "⚠️ Invalid login response: $INVALID_LOGIN" 49 | fi 50 | 51 | # Test 5: Protected route without token 52 | echo "5. Testing protected route without token..." 53 | PROTECTED_RESPONSE=$(curl -s http://localhost:8080/profile) 54 | 55 | if [[ $PROTECTED_RESPONSE == *"Authorization header required"* ]]; then 56 | echo "✅ Protected route security working" 57 | else 58 | echo "⚠️ Protected route response: $PROTECTED_RESPONSE" 59 | fi 60 | 61 | echo "🎉 API testing completed!" -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | # PostgreSQL Database 5 | postgres: 6 | image: postgres:15-alpine 7 | container_name: auth_db 8 | environment: 9 | POSTGRES_DB: auth_db 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: root 12 | ports: 13 | - "5433:5432" 14 | volumes: 15 | - postgres_data:/var/lib/postgresql/data 16 | healthcheck: 17 | test: ["CMD-SHELL", "pg_isready -U postgres"] 18 | interval: 10s 19 | timeout: 5s 20 | retries: 5 21 | networks: 22 | - default 23 | - shared-api-network 24 | 25 | # Redis Cache 26 | redis: 27 | image: redis:7-alpine 28 | container_name: auth_redis 29 | ports: 30 | - "6379:6379" 31 | volumes: 32 | - redis_data:/data 33 | healthcheck: 34 | test: ["CMD", "redis-cli", "ping"] 35 | interval: 10s 36 | timeout: 5s 37 | retries: 5 38 | networks: 39 | - default 40 | - shared-api-network 41 | 42 | # Redis Commander - Web UI for Redis Management 43 | redis-commander: 44 | image: rediscommander/redis-commander:latest 45 | container_name: redis_commander 46 | hostname: redis-commander 47 | ports: 48 | - "8081:8081" 49 | environment: 50 | - REDIS_HOSTS=local:redis:6379 51 | - HTTP_USER=admin 52 | - HTTP_PASSWORD=admin 53 | depends_on: 54 | - redis 55 | restart: unless-stopped 56 | networks: 57 | - default 58 | 59 | # Go Auth API Application 60 | auth-api: 61 | build: 62 | context: . 63 | dockerfile: Dockerfile 64 | container_name: auth_api 65 | ports: 66 | - "8080:8080" 67 | env_file: 68 | - .env 69 | depends_on: 70 | postgres: 71 | condition: service_healthy 72 | redis: 73 | condition: service_healthy 74 | restart: unless-stopped 75 | networks: 76 | - default 77 | - shared-api-network 78 | 79 | volumes: 80 | postgres_data: 81 | redis_data: 82 | 83 | networks: 84 | shared-api-network: 85 | external: true 86 | -------------------------------------------------------------------------------- /pkg/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt/v5" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var jwtSecret []byte 12 | 13 | func init() { 14 | // Initialize viper to read from environment 15 | viper.AutomaticEnv() 16 | jwtSecret = []byte(viper.GetString("JWT_SECRET")) 17 | } 18 | 19 | // Claims struct that will be embedded in JWT 20 | type Claims struct { 21 | UserID string `json:"user_id"` 22 | jwt.RegisteredClaims 23 | } 24 | 25 | // GenerateAccessToken generates a new access token 26 | func GenerateAccessToken(userID string) (string, error) { 27 | expirationTime := time.Now().Add(time.Minute * time.Duration(viper.GetInt("ACCESS_TOKEN_EXPIRATION_MINUTES"))) 28 | claims := &Claims{ 29 | UserID: userID, 30 | RegisteredClaims: jwt.RegisteredClaims{ 31 | ExpiresAt: jwt.NewNumericDate(expirationTime), 32 | IssuedAt: jwt.NewNumericDate(time.Now()), 33 | }, 34 | } 35 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 36 | return token.SignedString(jwtSecret) 37 | } 38 | 39 | // GenerateRefreshToken generates a new refresh token 40 | func GenerateRefreshToken(userID string) (string, error) { 41 | expirationTime := time.Now().Add(time.Hour * time.Duration(viper.GetInt("REFRESH_TOKEN_EXPIRATION_HOURS"))) 42 | claims := &Claims{ 43 | UserID: userID, 44 | RegisteredClaims: jwt.RegisteredClaims{ 45 | ExpiresAt: jwt.NewNumericDate(expirationTime), 46 | IssuedAt: jwt.NewNumericDate(time.Now()), 47 | }, 48 | } 49 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 50 | return token.SignedString(jwtSecret) 51 | } 52 | 53 | // ParseToken parses and validates a JWT token 54 | func ParseToken(tokenString string) (*Claims, error) { 55 | token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 56 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 57 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 58 | } 59 | return jwtSecret, nil 60 | }) 61 | 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if claims, ok := token.Claims.(*Claims); ok && token.Valid { 67 | return claims, nil 68 | } 69 | return nil, fmt.Errorf("invalid token") 70 | } -------------------------------------------------------------------------------- /pkg/models/social_account.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "gorm.io/datatypes" 8 | ) 9 | 10 | // SocialAccount stores information related to a user's social media logins 11 | type SocialAccount struct { 12 | ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` 13 | UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` 14 | Provider string `gorm:"not null;index;uniqueIndex:idx_provider_user_id" json:"provider"` 15 | ProviderUserID string `gorm:"not null;uniqueIndex:idx_provider_user_id" json:"provider_user_id"` // Composite unique index with Provider 16 | Email string `gorm:"" json:"email"` // Email from social provider 17 | Name string `gorm:"" json:"name"` // Name from social provider 18 | FirstName string `gorm:"" json:"first_name"` // First name from social provider 19 | LastName string `gorm:"" json:"last_name"` // Last name from social provider 20 | ProfilePicture string `gorm:"" json:"profile_picture"` // Profile picture URL from social provider 21 | Username string `gorm:"" json:"username"` // Username/login from social provider (e.g., GitHub login) 22 | Locale string `gorm:"" json:"locale"` // Locale from social provider 23 | RawData datatypes.JSON `gorm:"type:jsonb" json:"raw_data"` // Complete raw JSON data from provider 24 | AccessToken string `json:"-"` // Stored encrypted, not exposed via JSON 25 | RefreshToken string `json:"-"` // Stored encrypted, not exposed via JSON 26 | ExpiresAt *time.Time `json:"expires_at"` 27 | CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` 28 | UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` 29 | } 30 | -------------------------------------------------------------------------------- /docs/features/QUICK_SETUP_LOGGING.md: -------------------------------------------------------------------------------- 1 | # Quick Setup: Activity Logging Configuration 2 | 3 | ## Add to Your .env File 4 | 5 | Copy and paste these lines into your `.env` file. All are optional - the system works great with defaults! 6 | 7 | ```bash 8 | # ============================================================================ 9 | # Activity Logging Configuration (Optional) 10 | # ============================================================================ 11 | 12 | # Basic settings (recommended defaults) 13 | LOG_CLEANUP_ENABLED=true 14 | LOG_CLEANUP_INTERVAL=24h 15 | LOG_ANOMALY_DETECTION_ENABLED=true 16 | LOG_RETENTION_CRITICAL=365 17 | LOG_RETENTION_IMPORTANT=180 18 | LOG_RETENTION_INFORMATIONAL=90 19 | 20 | # Advanced settings (uncomment to customize) 21 | # LOG_TOKEN_REFRESH=false 22 | # LOG_PROFILE_ACCESS=false 23 | # LOG_SAMPLE_TOKEN_REFRESH=0.01 24 | # LOG_ANOMALY_NEW_IP=true 25 | # LOG_ANOMALY_NEW_USER_AGENT=true 26 | # LOG_CLEANUP_BATCH_SIZE=1000 27 | ``` 28 | 29 | ## Presets 30 | 31 | ### 🔒 High Security 32 | 33 | ```bash 34 | LOG_TOKEN_REFRESH=true 35 | LOG_PROFILE_ACCESS=true 36 | LOG_SAMPLE_TOKEN_REFRESH=0.1 37 | LOG_SAMPLE_PROFILE_ACCESS=0.1 38 | LOG_RETENTION_CRITICAL=730 39 | LOG_RETENTION_IMPORTANT=365 40 | LOG_RETENTION_INFORMATIONAL=180 41 | ``` 42 | 43 | ### ⚡ High Performance 44 | 45 | ```bash 46 | LOG_DISABLED_EVENTS=TOKEN_REFRESH,PROFILE_ACCESS,EMAIL_VERIFY 47 | LOG_RETENTION_CRITICAL=180 48 | LOG_RETENTION_IMPORTANT=90 49 | LOG_RETENTION_INFORMATIONAL=30 50 | LOG_CLEANUP_INTERVAL=12h 51 | ``` 52 | 53 | ### 🧪 Development 54 | 55 | ```bash 56 | LOG_TOKEN_REFRESH=true 57 | LOG_PROFILE_ACCESS=true 58 | LOG_CLEANUP_ENABLED=false 59 | LOG_RETENTION_CRITICAL=7 60 | ``` 61 | 62 | ### 🔐 GDPR Minimal 63 | 64 | ```bash 65 | LOG_DISABLED_EVENTS=TOKEN_REFRESH,PROFILE_ACCESS 66 | LOG_ANOMALY_DETECTION_ENABLED=false 67 | LOG_RETENTION_CRITICAL=90 68 | LOG_RETENTION_IMPORTANT=60 69 | LOG_RETENTION_INFORMATIONAL=30 70 | ``` 71 | 72 | ## No Configuration Needed! 73 | 74 | The system works perfectly without any environment variables: 75 | - ✅ High-frequency events (TOKEN_REFRESH, PROFILE_ACCESS) disabled 76 | - ✅ Anomaly detection enabled 77 | - ✅ Automatic cleanup runs daily 78 | - ✅ Smart retention policies applied 79 | 80 | ## See Also 81 | 82 | - [Complete Guide](ACTIVITY_LOGGING_GUIDE.md) 83 | - [All Variables](ENV_VARIABLES.md) 84 | - [Quick Reference](SMART_LOGGING_QUICK_REFERENCE.md) 85 | 86 | -------------------------------------------------------------------------------- /.cursor/rules/project-structure.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Project structure 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Project Structure and Architecture 7 | 8 | ## Overview 9 | This is a Go REST API for authentication and authorization with JWT tokens, 2FA, social login, and comprehensive security features. 10 | 11 | ## Key Entry Points 12 | - [cmd/api/main.go](mdc:cmd/api/main.go) - Application entry point with route definitions and dependency injection 13 | - [go.mod](mdc:go.mod) - Go module dependencies 14 | - [Makefile](mdc:Makefile) - Development, testing, and deployment commands 15 | - [.air.toml](mdc:.air.toml) - Hot reload configuration for development 16 | 17 | ## Core Architecture Layers 18 | 19 | ### Repository Layer (`internal/*/repository.go`) 20 | - Data access layer using GORM 21 | - Database operations and queries 22 | - Located in each feature package 23 | 24 | ### Service Layer (`internal/*/service.go`) 25 | - Business logic implementation 26 | - Orchestrates repository calls 27 | - Handles complex business rules 28 | 29 | ### Handler Layer (`internal/*/handler.go`) 30 | - HTTP request/response handling 31 | - Input validation and Swagger documentation 32 | - Route-specific logic 33 | 34 | ## Package Structure 35 | 36 | ### `internal/` - Core Business Logic 37 | - `auth/` - Authentication handlers and logic 38 | - `user/` - User management (registration, profile, etc.) 39 | - `social/` - OAuth2 social login providers 40 | - `twofa/` - Two-Factor Authentication with TOTP 41 | - `log/` - Activity logging system 42 | - `email/` - Email verification and password reset 43 | - `middleware/` - JWT authentication middleware 44 | - `database/` - Database connection and migrations 45 | - `redis/` - Redis session management 46 | - `config/` - Configuration management 47 | - `util/` - Utility functions 48 | 49 | ### `pkg/` - Shared Packages 50 | - `models/` - Database models with GORM tags 51 | - `dto/` - Data Transfer Objects for API requests/responses 52 | - `errors/` - Custom error types 53 | - `jwt/` - JWT token utilities 54 | 55 | ### `docs/` - API Documentation 56 | - Auto-generated Swagger documentation 57 | - [docs/swagger.json](mdc:docs/swagger.json) and [docs/swagger.yaml](mdc:docs/swagger.yaml) 58 | 59 | ## Development Patterns 60 | - Constructor functions for dependency injection (e.g., `NewService()`, `NewHandler()`) 61 | - Interface-based design for testability 62 | - Clean separation between layers 63 | - Comprehensive error handling with custom types 64 | - Activity logging for security auditing 65 | 66 | -------------------------------------------------------------------------------- /docs/guides/auth-api-validation-endpoint.md: -------------------------------------------------------------------------------- 1 | # Optional: Add Dedicated Token Validation Endpoint 2 | 3 | If you want a dedicated endpoint for external services to validate tokens, you could add this to your existing Auth API: 4 | 5 | ## 1. Add Handler Method 6 | 7 | Add this to `internal/user/handler.go`: 8 | 9 | ```go 10 | // ValidateToken godoc 11 | // @Summary Validate JWT Token 12 | // @Description Validates a JWT token and returns basic user info 13 | // @Tags auth 14 | // @Accept json 15 | // @Produce json 16 | // @Security BearerAuth 17 | // @Success 200 {object} map[string]interface{} 18 | // @Failure 401 {object} map[string]string 19 | // @Router /auth/validate [get] 20 | func (h *Handler) ValidateToken(c *gin.Context) { 21 | // Get user ID from context (set by AuthMiddleware) 22 | userID, exists := c.Get("userID") 23 | if !exists { 24 | c.JSON(http.StatusInternalServerError, gin.H{ 25 | "error": "User ID not found in context", 26 | }) 27 | return 28 | } 29 | 30 | // Get user basic info 31 | user, err := h.UserRepo.GetUserByID(userID.(string)) 32 | if err != nil { 33 | c.JSON(http.StatusUnauthorized, gin.H{ 34 | "error": "User not found", 35 | }) 36 | return 37 | } 38 | 39 | c.JSON(http.StatusOK, gin.H{ 40 | "valid": true, 41 | "userID": user.ID, 42 | "email": user.Email, 43 | "name": user.Name, 44 | }) 45 | } 46 | ``` 47 | 48 | ## 2. Add Route 49 | 50 | Add this to `cmd/api/main.go` in the protected routes section: 51 | 52 | ```go 53 | // Protected routes (require JWT authentication) 54 | protected := r.Group("/") 55 | protected.Use(middleware.AuthMiddleware()) 56 | { 57 | protected.GET("/profile", userHandler.GetProfile) 58 | protected.GET("/auth/validate", userHandler.ValidateToken) // Add this line 59 | protected.POST("/logout", userHandler.Logout) 60 | // ... other routes 61 | } 62 | ``` 63 | 64 | ## 3. Usage from Permisio API 65 | 66 | Then your Permisio API would call: 67 | 68 | ```bash 69 | GET /auth/validate 70 | Authorization: Bearer 71 | ``` 72 | 73 | **Response (Success):** 74 | ```json 75 | { 76 | "valid": true, 77 | "userID": "uuid-here", 78 | "email": "user@example.com", 79 | "name": "User Name" 80 | } 81 | ``` 82 | 83 | **Response (Invalid):** 84 | ```json 85 | { 86 | "error": "Invalid or expired token" 87 | } 88 | ``` 89 | 90 | This endpoint would be lighter than `/profile` since it only returns essential validation data. -------------------------------------------------------------------------------- /docs/migrations/MIGRATIONS.md: -------------------------------------------------------------------------------- 1 | # Database Migrations 2 | 3 | This project uses a two-tier migration system for database schema management. 4 | 5 | --- 6 | 7 | ## Quick Start 8 | 9 | ```bash 10 | # Start development (creates all tables automatically) 11 | make docker-dev 12 | 13 | # Apply additional enhancements (optional) 14 | make migrate-up 15 | 16 | # Check database status 17 | make migrate-check 18 | ``` 19 | 20 | --- 21 | 22 | ## Two-Tier System 23 | 24 | ### 1. GORM AutoMigrate (Automatic) 25 | - Runs on every app startup 26 | - Creates tables from Go models 27 | - Adds new columns safely 28 | - **No manual action needed** 29 | 30 | ### 2. SQL Migrations (Manual) 31 | - Complex changes requiring control 32 | - Data transformations 33 | - Performance optimizations 34 | - Run via `make migrate-up` 35 | 36 | --- 37 | 38 | ## Documentation 39 | 40 | ### For Users 41 | - **[User Guide](docs/migrations/USER_GUIDE.md)** - How to run migrations 42 | - **[Upgrade Guide](docs/migrations/UPGRADE_GUIDE.md)** - Version upgrades 43 | - **[Docker Guide](docs/MIGRATIONS_DOCKER.md)** - Docker-specific commands 44 | 45 | ### For Contributors 46 | - **[Developer Guide](migrations/README.md)** - Creating migrations 47 | - **[Migration Template](migrations/TEMPLATE.md)** - Template for new migrations 48 | - **[Strategy Guide](docs/MIGRATION_STRATEGY.md)** - Complete strategy 49 | 50 | ### Reference 51 | - **[Migration Tracking](docs/MIGRATION_TRACKING.md)** - Tracking system 52 | - **[AutoMigrate in Production](docs/AUTOMIGRATE_PRODUCTION.md)** - Production considerations 53 | - **[Quick Reference](docs/MIGRATION_QUICK_REFERENCE.md)** - One-page reference 54 | 55 | --- 56 | 57 | ## Common Commands 58 | 59 | ```bash 60 | # Migration commands 61 | make migrate-status # Show database tables 62 | make migrate-up # Apply migrations 63 | make migrate-down # Rollback migration 64 | make migrate-check # Check schema 65 | make migrate-backup # Create backup 66 | 67 | # Migration tracking 68 | make migrate-init # Initialize tracking (first time) 69 | make migrate-status-tracked # Show tracked migrations 70 | ``` 71 | 72 | --- 73 | 74 | ## Need Help? 75 | 76 | - Read [docs/migrations/USER_GUIDE.md](docs/migrations/USER_GUIDE.md) for detailed instructions 77 | - Check [BREAKING_CHANGES.md](BREAKING_CHANGES.md) before upgrading 78 | - See [docs/MIGRATION_QUICK_REFERENCE.md](docs/MIGRATION_QUICK_REFERENCE.md) for quick reference 79 | 80 | -------------------------------------------------------------------------------- /.cursor/rules/development-workflow.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: development workflow 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Development Workflow and Tools 7 | 8 | ## Development Setup 9 | Reference [Makefile](mdc:Makefile) for all available commands. 10 | 11 | ### Initial Setup 12 | ```bash 13 | make setup # Install dependencies and tools 14 | cp .env.example .env # Configure environment variables 15 | make docker-dev # Start development environment 16 | ``` 17 | 18 | ### Daily Development 19 | ```bash 20 | make dev # Start with hot reload using Air 21 | make swag-init # Regenerate Swagger docs after API changes 22 | make test # Run tests 23 | make security # Run security scans 24 | make fmt # Format code 25 | ``` 26 | 27 | ## Key Development Tools 28 | 29 | ### Hot Reload with Air 30 | - Configuration in [.air.toml](mdc:.air.toml) 31 | - Watches for Go file changes and rebuilds automatically 32 | - Excludes test files and temporary directories 33 | 34 | ### Docker Development 35 | - [Dockerfile.dev](mdc:Dockerfile.dev) - Development container 36 | - [docker-compose.dev.yml](mdc:docker-compose.dev.yml) - Development services 37 | - [Dockerfile](mdc:Dockerfile) - Production container 38 | - [docker-compose.yml](mdc:docker-compose.yml) - Production services 39 | 40 | ### Security Scanning 41 | - Configuration in [.gosec.json](mdc:.gosec.json) 42 | - `make security` runs gosec and nancy vulnerability scans 43 | - Required before committing 44 | 45 | ### API Documentation 46 | - Swagger annotations in handler functions 47 | - Auto-generated docs in `docs/` directory 48 | - Accessible at `/swagger/index.html` during development 49 | 50 | ## Testing Strategy 51 | - Unit tests for service layer functions 52 | - Integration tests for complete flows 53 | - Table-driven tests where appropriate 54 | - Mock external dependencies 55 | - Coverage tracking 56 | 57 | ## Git Workflow 58 | - Feature branches from main 59 | - Descriptive commit messages 60 | - Include tests for new functionality 61 | - Run `make security` before committing 62 | - Update documentation when needed 63 | 64 | ## Environment Variables 65 | - Use [.env](mdc:.env) for local development 66 | - Never commit sensitive data 67 | - Reference environment variables in code via viper 68 | - Default values defined in [cmd/api/main.go](mdc:cmd/api/main.go) 69 | 70 | ## Build and Deployment 71 | - `make build-prod` for production builds 72 | - Multi-stage Docker builds for optimization 73 | - Environment-specific configurations 74 | 75 | -------------------------------------------------------------------------------- /migrations/20240103_add_activity_log_smart_fields.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Add smart logging fields to activity_logs table 2 | -- Date: 2024-01-03 3 | -- Description: Adds severity, expires_at, and is_anomaly fields for professional activity logging 4 | 5 | -- Add new columns 6 | ALTER TABLE activity_logs 7 | ADD COLUMN IF NOT EXISTS severity VARCHAR(20) NOT NULL DEFAULT 'INFORMATIONAL', 8 | ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE, 9 | ADD COLUMN IF NOT EXISTS is_anomaly BOOLEAN NOT NULL DEFAULT false; 10 | 11 | -- Add comments for documentation 12 | COMMENT ON COLUMN activity_logs.severity IS 'Event severity: CRITICAL, IMPORTANT, or INFORMATIONAL'; 13 | COMMENT ON COLUMN activity_logs.expires_at IS 'Automatic expiration timestamp for log cleanup based on retention policies'; 14 | COMMENT ON COLUMN activity_logs.is_anomaly IS 'Flag indicating if this log was created due to anomaly detection'; 15 | 16 | -- Create indexes for efficient cleanup queries 17 | CREATE INDEX IF NOT EXISTS idx_activity_logs_expires ON activity_logs(expires_at) WHERE expires_at IS NOT NULL; 18 | CREATE INDEX IF NOT EXISTS idx_activity_logs_cleanup ON activity_logs(severity, expires_at, timestamp); 19 | 20 | -- Create composite index for user activity pattern queries 21 | CREATE INDEX IF NOT EXISTS idx_activity_logs_user_timestamp ON activity_logs(user_id, timestamp DESC); 22 | 23 | -- Update existing records to set severity based on event type 24 | UPDATE activity_logs SET severity = 'CRITICAL' 25 | WHERE event_type IN ( 26 | 'LOGIN', 'LOGOUT', 'REGISTER', 'PASSWORD_CHANGE', 'PASSWORD_RESET', 27 | 'EMAIL_CHANGE', '2FA_ENABLE', '2FA_DISABLE', 'ACCOUNT_DELETION', 'RECOVERY_CODE_USED' 28 | ); 29 | 30 | UPDATE activity_logs SET severity = 'IMPORTANT' 31 | WHERE event_type IN ( 32 | 'EMAIL_VERIFY', '2FA_LOGIN', 'SOCIAL_LOGIN', 'PROFILE_UPDATE', 'RECOVERY_CODE_GENERATE' 33 | ); 34 | 35 | UPDATE activity_logs SET severity = 'INFORMATIONAL' 36 | WHERE event_type IN ( 37 | 'TOKEN_REFRESH', 'PROFILE_ACCESS' 38 | ); 39 | 40 | -- Set expiration dates for existing records based on severity 41 | -- Critical: 365 days (1 year) 42 | UPDATE activity_logs SET expires_at = timestamp + INTERVAL '365 days' 43 | WHERE severity = 'CRITICAL' AND expires_at IS NULL; 44 | 45 | -- Important: 180 days (6 months) 46 | UPDATE activity_logs SET expires_at = timestamp + INTERVAL '180 days' 47 | WHERE severity = 'IMPORTANT' AND expires_at IS NULL; 48 | 49 | -- Informational: 90 days (3 months) 50 | UPDATE activity_logs SET expires_at = timestamp + INTERVAL '90 days' 51 | WHERE severity = 'INFORMATIONAL' AND expires_at IS NULL; 52 | 53 | -- Add constraint to ensure valid severity values (if not exists) 54 | DO $$ 55 | BEGIN 56 | IF NOT EXISTS ( 57 | SELECT 1 FROM pg_constraint 58 | WHERE conname = 'chk_activity_logs_severity' 59 | ) THEN 60 | ALTER TABLE activity_logs 61 | ADD CONSTRAINT chk_activity_logs_severity 62 | CHECK (severity IN ('CRITICAL', 'IMPORTANT', 'INFORMATIONAL')); 63 | END IF; 64 | END $$; 65 | 66 | -------------------------------------------------------------------------------- /docs/features/PROFILE_SYNC_QUICK_REFERENCE.md: -------------------------------------------------------------------------------- 1 | # Profile Sync - Quick Reference 2 | 3 | ## ✨ What's New? 4 | 5 | **Profile data automatically syncs from social providers on every login!** 6 | 7 | Change your picture on Google/Facebook/GitHub → Log in to app → Picture updates automatically ✅ 8 | 9 | ## 🎯 Your Question 10 | 11 | > "When I change picture or data on social, this should be updated in our database on login. Why doesn't this happen?" 12 | 13 | **Answer:** Now it does! We fixed it. 🎉 14 | 15 | ## 🔄 What Happens Now 16 | 17 | Every social login: 18 | 1. Fetches latest data from provider 19 | 2. Updates `social_accounts` table 20 | 3. Updates `users` table (if changed) 21 | 4. You see fresh data immediately 22 | 23 | ## 🧪 Quick Test 24 | 25 | ```bash 26 | # 1. Change your profile picture on Google 27 | # 2. Rebuild and restart app 28 | go build -o auth_api.exe cmd/api/main.go && ./auth_api.exe 29 | 30 | # 3. Login via social 31 | http://localhost:8080/auth/google/login 32 | 33 | # 4. Check profile - picture should be updated 34 | curl -H "Authorization: Bearer TOKEN" http://localhost:8080/profile 35 | ``` 36 | 37 | ## 📊 What Gets Synced 38 | 39 | | Data | Google | Facebook | GitHub | 40 | |------|--------|----------|--------| 41 | | Profile Picture | ✅ | ✅ | ✅ | 42 | | Name | ✅ | ✅ | ✅ | 43 | | First/Last Name | ✅ | ✅ | - | 44 | | Email | ✅ | ✅ | ✅ | 45 | | Locale | ✅ | ✅ | - | 46 | | Username | - | - | ✅ | 47 | | Raw Data | ✅ | ✅ | ✅ | 48 | 49 | ## 🛠️ Files Changed 50 | 51 | - `internal/social/repository.go` - Added `UpdateSocialAccount()` 52 | - `internal/social/service.go` - Added sync logic to all 3 providers 53 | 54 | ## ✅ Testing Checklist 55 | 56 | - [ ] Restart application after code changes 57 | - [ ] Change profile picture on social platform 58 | - [ ] Login via social provider 59 | - [ ] Check `/profile` endpoint shows new picture 60 | - [ ] Verify `updated_at` timestamp in database is recent 61 | 62 | ## 📝 SQL Verification 63 | 64 | ```sql 65 | -- Check if your data is syncing 66 | SELECT 67 | u.email, 68 | u.profile_picture, 69 | u.updated_at as user_updated, 70 | sa.profile_picture as social_picture, 71 | sa.updated_at as last_login 72 | FROM users u 73 | JOIN social_accounts sa ON u.id = sa.user_id 74 | WHERE u.email = 'gjovanovic.st@gmail.com'; 75 | 76 | -- last_login should be recent if you just logged in 77 | -- Pictures should match if sync worked 78 | ``` 79 | 80 | ## 🚨 Troubleshooting 81 | 82 | **Data not updating?** 83 | 1. ✓ Restart application with new code 84 | 2. ✓ Check logs for errors 85 | 3. ✓ Verify provider actually has new data 86 | 4. ✓ See [TROUBLESHOOTING_SOCIAL_LOGIN.md](TROUBLESHOOTING_SOCIAL_LOGIN.md) 87 | 88 | ## 📚 Full Documentation 89 | 90 | - **Summary:** [docs/PROFILE_SYNC_SUMMARY.md](docs/PROFILE_SYNC_SUMMARY.md) 91 | - **Detailed:** [docs/PROFILE_SYNC_ON_LOGIN.md](docs/PROFILE_SYNC_ON_LOGIN.md) 92 | - **Troubleshooting:** [TROUBLESHOOTING_SOCIAL_LOGIN.md](TROUBLESHOOTING_SOCIAL_LOGIN.md) 93 | 94 | --- 95 | 96 | **TL;DR:** Change your social profile → Login → Data updates automatically! 🎯 97 | 98 | -------------------------------------------------------------------------------- /internal/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gjovanovicst/auth_api/internal/redis" 8 | "github.com/gjovanovicst/auth_api/pkg/jwt" 9 | ) 10 | 11 | // AuthMiddleware authenticates requests using JWT 12 | func AuthMiddleware() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | authHeader := c.GetHeader("Authorization") 15 | if authHeader == "" { 16 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) 17 | return 18 | } 19 | 20 | tokenString := authHeader 21 | if len(authHeader) > 7 && authHeader[:7] == "Bearer " { 22 | tokenString = authHeader[7:] 23 | } 24 | 25 | // Parse and validate JWT 26 | claims, err := jwt.ParseToken(tokenString) 27 | if err != nil { 28 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) 29 | return 30 | } 31 | 32 | // Check Redis blacklists only if Redis is available 33 | if redis.Rdb != nil { 34 | // Check if the specific access token is blacklisted 35 | blacklisted, err := redis.IsAccessTokenBlacklisted(tokenString) 36 | if err != nil { 37 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Token validation error"}) 38 | return 39 | } 40 | if blacklisted { 41 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token has been revoked"}) 42 | return 43 | } 44 | 45 | // Check if all tokens for this user are blacklisted (e.g., after password change) 46 | userBlacklisted, err := redis.IsUserTokensBlacklisted(claims.UserID) 47 | if err != nil { 48 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Token validation error"}) 49 | return 50 | } 51 | if userBlacklisted { 52 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "All user tokens have been revoked"}) 53 | return 54 | } 55 | } else { 56 | // Redis not available - log warning in production, but allow for testing 57 | // In production, this should be treated as an error 58 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Token validation service unavailable"}) 59 | return 60 | } 61 | 62 | c.Set("userID", claims.UserID) 63 | c.Next() 64 | } 65 | } 66 | 67 | // AuthorizeRole checks if the user has the required role 68 | func AuthorizeRole(requiredRole string) gin.HandlerFunc { 69 | return func(c *gin.Context) { 70 | // Assuming userID is already set by AuthMiddleware 71 | _, exists := c.Get("userID") 72 | if !exists { 73 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "User ID not found in context"}) 74 | return 75 | } 76 | 77 | // TODO: Fetch user roles from database or claims 78 | // For demonstration, let's assume a simple check 79 | // if userHasRole(userID.(string), requiredRole) { 80 | // c.Next() 81 | // } else { 82 | // c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden"}) 83 | // } 84 | 85 | // For now, just proceed if authenticated 86 | c.Next() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/user/repository.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/gjovanovicst/auth_api/pkg/models" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type Repository struct { 9 | DB *gorm.DB 10 | } 11 | 12 | func NewRepository(db *gorm.DB) *Repository { 13 | return &Repository{DB: db} 14 | } 15 | 16 | func (r *Repository) CreateUser(user *models.User) error { 17 | return r.DB.Create(user).Error 18 | } 19 | 20 | func (r *Repository) GetUserByEmail(email string) (*models.User, error) { 21 | var user models.User 22 | err := r.DB.Where("email = ?", email).First(&user).Error 23 | return &user, err 24 | } 25 | 26 | func (r *Repository) GetUserByID(id string) (*models.User, error) { 27 | var user models.User 28 | err := r.DB.Preload("SocialAccounts").Where("id = ?", id).First(&user).Error 29 | return &user, err 30 | } 31 | 32 | func (r *Repository) UpdateUser(user *models.User) error { 33 | return r.DB.Save(user).Error 34 | } 35 | 36 | func (r *Repository) UpdateUserPassword(userID, hashedPassword string) error { 37 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Update("password_hash", hashedPassword).Error 38 | } 39 | 40 | func (r *Repository) UpdateUserEmailVerified(userID string, verified bool) error { 41 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Update("email_verified", verified).Error 42 | } 43 | 44 | // 2FA related methods 45 | 46 | // Enable2FA enables 2FA for a user and stores the secret and recovery codes 47 | func (r *Repository) Enable2FA(userID, secret, recoveryCodes string) error { 48 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ 49 | "two_fa_enabled": true, 50 | "two_fa_secret": secret, 51 | "two_fa_recovery_codes": recoveryCodes, 52 | }).Error 53 | } 54 | 55 | // Disable2FA disables 2FA for a user 56 | func (r *Repository) Disable2FA(userID string) error { 57 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ 58 | "two_fa_enabled": false, 59 | "two_fa_secret": "", 60 | "two_fa_recovery_codes": nil, 61 | }).Error 62 | } 63 | 64 | // UpdateRecoveryCodes updates the recovery codes for a user 65 | func (r *Repository) UpdateRecoveryCodes(userID, recoveryCodes string) error { 66 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Update("two_fa_recovery_codes", recoveryCodes).Error 67 | } 68 | 69 | // DeleteUser deletes a user and all related data (cascade) 70 | func (r *Repository) DeleteUser(userID string) error { 71 | return r.DB.Where("id = ?", userID).Delete(&models.User{}).Error 72 | } 73 | 74 | // UpdateUserProfile updates user profile fields (name, first_name, last_name, profile_picture, locale) 75 | func (r *Repository) UpdateUserProfile(userID string, updates map[string]interface{}) error { 76 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error 77 | } 78 | 79 | // UpdateUserEmail updates user email and sets email_verified to false 80 | func (r *Repository) UpdateUserEmail(userID, newEmail string) error { 81 | return r.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ 82 | "email": newEmail, 83 | "email_verified": false, 84 | }).Error 85 | } -------------------------------------------------------------------------------- /internal/log/repository.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gjovanovicst/auth_api/pkg/models" 7 | "github.com/google/uuid" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Repository struct { 12 | DB *gorm.DB 13 | } 14 | 15 | func NewRepository(db *gorm.DB) *Repository { 16 | return &Repository{DB: db} 17 | } 18 | 19 | // ListUserActivityLogs retrieves activity logs for a specific user with pagination and filtering 20 | func (r *Repository) ListUserActivityLogs(userID uuid.UUID, page, limit int, eventType string, startDate, endDate *time.Time) ([]models.ActivityLog, int64, error) { 21 | var logs []models.ActivityLog 22 | var totalCount int64 23 | 24 | // Build the base query 25 | query := r.DB.Where("user_id = ?", userID) 26 | 27 | // Apply event type filter if provided 28 | if eventType != "" { 29 | query = query.Where("event_type = ?", eventType) 30 | } 31 | 32 | // Apply date range filters if provided 33 | if startDate != nil { 34 | query = query.Where("timestamp >= ?", startDate) 35 | } 36 | if endDate != nil { 37 | // Add 1 day to end date to include the entire end date 38 | endOfDay := endDate.Add(24 * time.Hour) 39 | query = query.Where("timestamp < ?", endOfDay) 40 | } 41 | 42 | // Get total count for pagination 43 | if err := query.Model(&models.ActivityLog{}).Count(&totalCount).Error; err != nil { 44 | return nil, 0, err 45 | } 46 | 47 | // Apply pagination and ordering 48 | offset := (page - 1) * limit 49 | if err := query.Order("timestamp DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil { 50 | return nil, 0, err 51 | } 52 | 53 | return logs, totalCount, nil 54 | } 55 | 56 | // ListAllActivityLogs retrieves activity logs for all users (admin functionality) with pagination and filtering 57 | func (r *Repository) ListAllActivityLogs(page, limit int, eventType string, startDate, endDate *time.Time) ([]models.ActivityLog, int64, error) { 58 | var logs []models.ActivityLog 59 | var totalCount int64 60 | 61 | // Build the base query 62 | query := r.DB.Model(&models.ActivityLog{}) 63 | 64 | // Apply event type filter if provided 65 | if eventType != "" { 66 | query = query.Where("event_type = ?", eventType) 67 | } 68 | 69 | // Apply date range filters if provided 70 | if startDate != nil { 71 | query = query.Where("timestamp >= ?", startDate) 72 | } 73 | if endDate != nil { 74 | // Add 1 day to end date to include the entire end date 75 | endOfDay := endDate.Add(24 * time.Hour) 76 | query = query.Where("timestamp < ?", endOfDay) 77 | } 78 | 79 | // Get total count for pagination 80 | if err := query.Count(&totalCount).Error; err != nil { 81 | return nil, 0, err 82 | } 83 | 84 | // Apply pagination and ordering 85 | offset := (page - 1) * limit 86 | if err := query.Order("timestamp DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil { 87 | return nil, 0, err 88 | } 89 | 90 | return logs, totalCount, nil 91 | } 92 | 93 | // GetActivityLogByID retrieves a specific activity log by ID 94 | func (r *Repository) GetActivityLogByID(id uuid.UUID) (*models.ActivityLog, error) { 95 | var log models.ActivityLog 96 | if err := r.DB.Where("id = ?", id).First(&log).Error; err != nil { 97 | return nil, err 98 | } 99 | return &log, nil 100 | } 101 | -------------------------------------------------------------------------------- /.cursor/rules/api-development.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: API development 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # API Development Patterns 7 | 8 | ## Route Organization in Main 9 | Routes are defined in [cmd/api/main.go](mdc:cmd/api/main.go) with clear groupings: 10 | 11 | ### Public Routes 12 | - No authentication required 13 | - Registration, login, password reset 14 | - Social OAuth callbacks 15 | 16 | ### Protected Routes 17 | - Require JWT authentication via `middleware.AuthMiddleware()` 18 | - User profile, logout, 2FA management 19 | - Activity logs 20 | 21 | ### Admin Routes 22 | - Protected with auth middleware 23 | - Future role-based access control 24 | 25 | ## Adding New API Endpoints 26 | 27 | ### 1. Create DTOs 28 | Define request/response structures in `pkg/dto/`: 29 | ```go 30 | type CreateSomethingRequest struct { 31 | Name string `json:"name" validate:"required" example:"Sample Name"` 32 | Description string `json:"description,omitempty" example:"Sample description"` 33 | } 34 | 35 | type SomethingResponse struct { 36 | ID uint `json:"id" example:"1"` 37 | Name string `json:"name" example:"Sample Name"` 38 | CreatedAt string `json:"created_at" example:"2023-01-01T00:00:00Z"` 39 | } 40 | ``` 41 | 42 | ### 2. Add Handler with Swagger Annotations 43 | ```go 44 | // CreateSomething creates a new something 45 | // @Summary Create something 46 | // @Description Create a new something with the provided data 47 | // @Tags something 48 | // @Accept json 49 | // @Produce json 50 | // @Param request body dto.CreateSomethingRequest true "Something data" 51 | // @Success 201 {object} dto.APIResponse{data=dto.SomethingResponse} 52 | // @Failure 400 {object} dto.APIResponse 53 | // @Security ApiKeyAuth 54 | // @Router /something [post] 55 | func (h *Handler) CreateSomething(c *gin.Context) { 56 | // Implementation 57 | } 58 | ``` 59 | 60 | ### 3. Register Route 61 | Add to appropriate group in [cmd/api/main.go](mdc:cmd/api/main.go): 62 | ```go 63 | protected.POST("/something", handler.CreateSomething) 64 | ``` 65 | 66 | ### 4. Regenerate Documentation 67 | ```bash 68 | make swag-init 69 | ``` 70 | 71 | ## Authentication Patterns 72 | 73 | ### JWT Middleware 74 | - Applied to protected routes in [cmd/api/main.go](mdc:cmd/api/main.go) 75 | - Validates JWT tokens and extracts user information 76 | - Sets user context for handlers 77 | 78 | ### 2FA Flow 79 | - Temporary tokens for 2FA verification 80 | - Separate verification endpoint 81 | - Recovery code support 82 | 83 | ### Social Authentication 84 | - OAuth2 flow with state verification 85 | - Provider-specific callbacks 86 | - User linking/creation logic 87 | 88 | ## Response Format Standards 89 | 90 | ### Success Response 91 | ```go 92 | { 93 | "success": true, 94 | "data": {...} 95 | } 96 | ``` 97 | 98 | ### Error Response 99 | ```go 100 | { 101 | "success": false, 102 | "error": "descriptive error message" 103 | } 104 | ``` 105 | 106 | ## Input Validation 107 | - Use `go-playground/validator` tags in DTOs 108 | - Validate in handlers before service calls 109 | - Return appropriate HTTP status codes 110 | 111 | ## Activity Logging 112 | - Log security-relevant events 113 | - Include user ID, IP address, user agent 114 | - Use structured logging format 115 | - Examples: login attempts, password changes, 2FA events 116 | 117 | ## Database Operations 118 | - Use GORM models from `pkg/models/` 119 | - Repository pattern for data access 120 | - Transaction support for multi-step operations 121 | - Proper error handling and logging 122 | 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gjovanovicst/auth_api 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.10.1 9 | github.com/go-playground/validator/v10 v10.26.0 10 | github.com/go-redis/redis/v8 v8.11.5 11 | github.com/golang-jwt/jwt/v5 v5.2.2 12 | github.com/google/uuid v1.6.0 13 | github.com/joho/godotenv v1.5.1 14 | github.com/spf13/viper v1.20.1 15 | github.com/swaggo/files v1.0.1 16 | github.com/swaggo/gin-swagger v1.6.0 17 | github.com/swaggo/swag v1.16.4 18 | golang.org/x/crypto v0.39.0 19 | golang.org/x/oauth2 v0.25.0 20 | gopkg.in/mail.v2 v2.3.1 21 | gorm.io/driver/postgres v1.5.9 22 | gorm.io/gorm v1.25.12 23 | ) 24 | 25 | require ( 26 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 27 | filippo.io/edwards25519 v1.1.0 // indirect 28 | github.com/KyleBanks/depth v1.2.1 // indirect 29 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 30 | github.com/bytedance/sonic v1.13.3 // indirect 31 | github.com/bytedance/sonic/loader v0.2.4 // indirect 32 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 33 | github.com/cloudwego/base64x v0.1.5 // indirect 34 | github.com/cloudwego/iasm v0.2.0 // indirect 35 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 36 | github.com/fsnotify/fsnotify v1.8.0 // indirect 37 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 38 | github.com/gin-contrib/cors v1.7.6 // indirect 39 | github.com/gin-contrib/sse v1.1.0 // indirect 40 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 41 | github.com/go-openapi/jsonreference v0.21.0 // indirect 42 | github.com/go-openapi/spec v0.21.0 // indirect 43 | github.com/go-openapi/swag v0.23.1 // indirect 44 | github.com/go-playground/locales v0.14.1 // indirect 45 | github.com/go-playground/universal-translator v0.18.1 // indirect 46 | github.com/go-sql-driver/mysql v1.8.1 // indirect 47 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 48 | github.com/goccy/go-json v0.10.5 // indirect 49 | github.com/jackc/pgpassfile v1.0.0 // indirect 50 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 51 | github.com/jackc/pgx/v5 v5.6.0 // indirect 52 | github.com/jackc/puddle/v2 v2.2.1 // indirect 53 | github.com/jinzhu/inflection v1.0.0 // indirect 54 | github.com/jinzhu/now v1.1.5 // indirect 55 | github.com/josharian/intern v1.0.0 // indirect 56 | github.com/json-iterator/go v1.1.12 // indirect 57 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 58 | github.com/leodido/go-urn v1.4.0 // indirect 59 | github.com/mailru/easyjson v0.9.0 // indirect 60 | github.com/mattn/go-isatty v0.0.20 // indirect 61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 62 | github.com/modern-go/reflect2 v1.0.2 // indirect 63 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 64 | github.com/pquerna/otp v1.5.0 // indirect 65 | github.com/sagikazarmark/locafero v0.7.0 // indirect 66 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect 67 | github.com/sourcegraph/conc v0.3.0 // indirect 68 | github.com/spf13/afero v1.12.0 // indirect 69 | github.com/spf13/cast v1.7.1 // indirect 70 | github.com/spf13/pflag v1.0.6 // indirect 71 | github.com/subosito/gotenv v1.6.0 // indirect 72 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 73 | github.com/ugorji/go/codec v1.3.0 // indirect 74 | go.uber.org/atomic v1.9.0 // indirect 75 | go.uber.org/multierr v1.9.0 // indirect 76 | golang.org/x/arch v0.18.0 // indirect 77 | golang.org/x/net v0.41.0 // indirect 78 | golang.org/x/sync v0.15.0 // indirect 79 | golang.org/x/sys v0.33.0 // indirect 80 | golang.org/x/text v0.26.0 // indirect 81 | golang.org/x/tools v0.34.0 // indirect 82 | google.golang.org/protobuf v1.36.6 // indirect 83 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 84 | gopkg.in/yaml.v3 v3.0.1 // indirect 85 | gorm.io/datatypes v1.2.5 // indirect 86 | gorm.io/driver/mysql v1.5.6 // indirect 87 | ) 88 | -------------------------------------------------------------------------------- /docs/implementation/IMPLEMENTATION_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Token Validation Endpoint Implementation Summary 2 | 3 | ## ✅ **Successfully Implemented Alternative 2: Dedicated Token Validation Endpoint** 4 | 5 | ### **What Was Added to Auth API** 6 | 7 | #### 1. **New Handler Method** 8 | - **File**: `internal/user/handler.go` 9 | - **Method**: `ValidateToken(c *gin.Context)` 10 | - **Purpose**: Validates JWT tokens for external services 11 | - **Features**: 12 | - Uses existing `AuthMiddleware` for full validation 13 | - Includes Redis blacklist checks 14 | - Returns lightweight response with essential user data 15 | - Proper Swagger documentation 16 | 17 | #### 2. **New Route** 18 | - **File**: `cmd/api/main.go` 19 | - **Endpoint**: `GET /auth/validate` 20 | - **Protection**: Uses `AuthMiddleware()` for full JWT validation 21 | - **Location**: Added to protected routes section 22 | 23 | #### 3. **Updated Documentation** 24 | - **File**: `docs/API.md` - Added endpoint documentation 25 | - **File**: `README.md` - Added to User Management section 26 | - **Swagger**: Generated updated documentation with `make swag-init` 27 | 28 | ### **Endpoint Details** 29 | 30 | ```bash 31 | GET /auth/validate 32 | Authorization: Bearer 33 | ``` 34 | 35 | **Success Response (200):** 36 | ```json 37 | { 38 | "valid": true, 39 | "userID": "uuid-here", 40 | "email": "user@example.com" 41 | } 42 | ``` 43 | 44 | **Error Response (401):** 45 | ```json 46 | { 47 | "error": "Invalid or expired token" 48 | } 49 | ``` 50 | 51 | ### **What Was Updated in Permisio API Code** 52 | 53 | #### 1. **Auth Service** 54 | - **File**: `pemis-api-code-examples.md` 55 | - **Method**: `ValidateToken()` updated to call `/auth/validate` instead of `/profile` 56 | - **Response**: Uses new `ValidationResponse` model 57 | 58 | #### 2. **Models** 59 | - **Added**: `ValidationResponse` struct to match endpoint response 60 | - **Updated**: `UserData` model (removed name field - not available in User model) 61 | 62 | #### 3. **Documentation** 63 | - **Updated**: Authentication strategy description 64 | - **Updated**: Flow diagrams and examples 65 | - **Updated**: Configuration examples 66 | 67 | ### **Benefits of This Implementation** 68 | 69 | ✅ **Dedicated Purpose**: Endpoint specifically designed for token validation 70 | ✅ **Lightweight**: Returns only essential validation data 71 | ✅ **Full Security**: Uses existing `AuthMiddleware` with Redis blacklist checks 72 | ✅ **Clean API**: Separates validation from profile data retrieval 73 | ✅ **External Service Friendly**: Perfect for microservice architecture 74 | 75 | ### **Usage from Permisio API** 76 | 77 | The Permisio API now calls: 78 | ```go 79 | GET http://localhost:8080/auth/validate 80 | Authorization: Bearer 81 | ``` 82 | 83 | Instead of the `/profile` endpoint, providing: 84 | - Faster response (no full profile data) 85 | - Clear separation of concerns 86 | - Dedicated endpoint for external service authentication 87 | 88 | ### **Testing the New Endpoint** 89 | 90 | You can test the new endpoint using curl: 91 | 92 | ```bash 93 | # Test with valid token 94 | curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ 95 | http://localhost:8080/auth/validate 96 | 97 | # Expected response: 98 | # {"valid":true,"userID":"uuid","email":"user@example.com"} 99 | ``` 100 | 101 | ### **Files Modified** 102 | 103 | 1. **Auth API:** 104 | - `internal/user/handler.go` - Added `ValidateToken` method 105 | - `cmd/api/main.go` - Added route 106 | - `docs/API.md` - Updated documentation 107 | - `README.md` - Updated endpoint list 108 | - `docs/` - Regenerated Swagger documentation 109 | 110 | 2. **Permisio API (code examples):** 111 | - `pemis-api-code-examples.md` - Updated auth service and models 112 | 113 | The implementation is complete and ready for use! 🚀 -------------------------------------------------------------------------------- /docs/features/QUICK_REFERENCE_SOCIAL_DATA.md: -------------------------------------------------------------------------------- 1 | # Quick Reference: Social Login Data Storage 2 | 3 | ## Summary 4 | 5 | Enhanced user and social account models to store complete profile data from social login providers. 6 | 7 | ## What Changed? 8 | 9 | ### User Model - New Fields 10 | ```go 11 | Name string // Full name 12 | FirstName string // First name 13 | LastName string // Last name 14 | ProfilePicture string // Picture URL 15 | Locale string // Language/locale 16 | ``` 17 | 18 | ### Social Account Model - New Fields 19 | ```go 20 | Email string // Email from provider 21 | Name string // Name from provider 22 | FirstName string // First name 23 | LastName string // Last name 24 | ProfilePicture string // Picture URL 25 | Username string // GitHub login, etc. 26 | Locale string // Locale 27 | RawData datatypes.JSON // Complete provider response (JSONB) 28 | ``` 29 | 30 | ### New Repository Method 31 | ```go 32 | func (r *Repository) UpdateUser(user *models.User) error 33 | ``` 34 | 35 | ## Data Captured by Provider 36 | 37 | | Provider | Key Fields Captured | 38 | |----------|-------------------| 39 | | **Google** | id, email, verified_email, name, given_name, family_name, picture, locale | 40 | | **Facebook** | id, email, name, first_name, last_name, picture.data.url, locale | 41 | | **GitHub** | id, login, email, name, avatar_url, bio, location, company | 42 | 43 | ## Migration 44 | 45 | - **Type:** GORM AutoMigrate (automatic) 46 | - **Execution:** Runs on application startup 47 | - **Impact:** Adds 5 columns to `users`, 8 columns to `social_accounts` 48 | - **Breaking:** No - all fields nullable and backward compatible 49 | 50 | ## Files Modified 51 | 52 | 1. `pkg/models/user.go` - User model 53 | 2. `pkg/models/social_account.go` - Social account model 54 | 3. `internal/social/service.go` - Provider handlers 55 | 4. `internal/user/repository.go` - UpdateUser method 56 | 57 | ## Testing Quick Commands 58 | 59 | ```bash 60 | # Build application 61 | go build -o auth_api cmd/api/main.go 62 | 63 | # Run application (migration runs automatically) 64 | ./auth_api 65 | 66 | # Test social login 67 | # 1. Visit: http://localhost:8080/auth/google/login 68 | # 2. Complete OAuth flow 69 | # 3. Get profile: curl -H "Authorization: Bearer {token}" http://localhost:8080/profile 70 | 71 | # Check database 72 | psql -U user -d authdb -c "SELECT name, first_name, profile_picture FROM users WHERE email='test@gmail.com';" 73 | ``` 74 | 75 | ## API Response Example 76 | 77 | ```json 78 | { 79 | "id": "...", 80 | "email": "john@gmail.com", 81 | "name": "John Doe", 82 | "first_name": "John", 83 | "last_name": "Doe", 84 | "profile_picture": "https://...", 85 | "locale": "en", 86 | "social_accounts": [ 87 | { 88 | "provider": "google", 89 | "email": "john@gmail.com", 90 | "name": "John Doe", 91 | "raw_data": { /* complete Google response */ } 92 | } 93 | ] 94 | } 95 | ``` 96 | 97 | ## Key Behaviors 98 | 99 | 1. **New User:** All fields populated from social provider 100 | 2. **Linking Social Account:** Only updates empty user fields 101 | 3. **Existing Login:** No data changes on re-authentication 102 | 103 | ## Security Notes 104 | 105 | - Profile pictures: URLs only (not downloaded) 106 | - Raw data: Exposed in API (consider hiding if needed) 107 | - Tokens: Still hidden from JSON responses 108 | - All new fields: Nullable and optional 109 | 110 | ## Documentation 111 | 112 | - Full Details: [docs/SOCIAL_LOGIN_DATA_STORAGE.md](SOCIAL_LOGIN_DATA_STORAGE.md) 113 | - Migration: [docs/migrations/MIGRATION_SOCIAL_LOGIN_DATA.md](migrations/MIGRATION_SOCIAL_LOGIN_DATA.md) 114 | - Changelog: [CHANGELOG.md](../CHANGELOG.md) 115 | 116 | -------------------------------------------------------------------------------- /.cursor/rules/security-patterns.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Security patterns 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Security Patterns and Best Practices 7 | 8 | ## Authentication Flow 9 | 10 | ### JWT Token Management 11 | - Access tokens: Short-lived (15 minutes default) 12 | - Refresh tokens: Long-lived (720 hours default) 13 | - Token rotation on refresh 14 | - Redis-based session management 15 | 16 | ### Two-Factor Authentication (2FA) 17 | - TOTP (Time-based One-Time Password) implementation 18 | - QR code generation for authenticator apps 19 | - Recovery codes for backup access 20 | - Secure secret generation and storage 21 | 22 | ### Social Authentication 23 | - OAuth2 flow with state verification 24 | - Secure provider callback handling 25 | - User account linking/creation 26 | - Scope validation 27 | 28 | ## Security Configuration 29 | 30 | ### Security Scanning 31 | Configuration in [.gosec.json](mdc:.gosec.json): 32 | - Medium severity and confidence thresholds 33 | - Excludes vendor and node_modules directories 34 | - Includes test files in scanning 35 | - JSON output format for CI/CD integration 36 | 37 | ### Environment Security 38 | - Never expose sensitive data in responses 39 | - Use environment variables for secrets 40 | - Hash passwords with bcrypt 41 | - Secure session management with Redis 42 | 43 | ## Authorization Patterns 44 | 45 | ### Middleware Protection 46 | - JWT validation in `middleware.AuthMiddleware()` 47 | - User context extraction 48 | - Route-level protection in [cmd/api/main.go](mdc:cmd/api/main.go) 49 | 50 | ### Role-Based Access Control (Future) 51 | - Admin routes prepared for role checking 52 | - Extensible middleware pattern 53 | - User role management ready 54 | 55 | ## Input Validation and Sanitization 56 | 57 | ### Request Validation 58 | - Use `go-playground/validator` tags in DTOs 59 | - Validate all user inputs before processing 60 | - Sanitize data before database operations 61 | - Parameterized queries to prevent SQL injection 62 | 63 | ### Rate Limiting (Recommended) 64 | - Implement rate limiting for authentication endpoints 65 | - Protect against brute force attacks 66 | - Monitor suspicious activity patterns 67 | 68 | ## Security Event Logging 69 | 70 | ### Activity Logging System 71 | - Log all authentication events 72 | - Track user activities with context 73 | - Include IP address, user agent, timestamps 74 | - Pagination and filtering for security analysis 75 | 76 | ### Security-Relevant Events 77 | - Login attempts (success/failure) 78 | - Password changes 79 | - 2FA enable/disable 80 | - Account modifications 81 | - Suspicious activity detection 82 | 83 | ## Data Protection 84 | 85 | ### Password Security 86 | - Bcrypt hashing with appropriate cost 87 | - Password complexity requirements 88 | - Secure password reset flow 89 | - Protection against timing attacks 90 | 91 | ### Sensitive Data Handling 92 | - Never log sensitive information 93 | - Secure token storage and transmission 94 | - Proper secret rotation procedures 95 | - Clean error messages (no information leakage) 96 | 97 | ## Network Security 98 | 99 | ### HTTPS Requirements 100 | - Enforce HTTPS in production 101 | - Secure cookie settings 102 | - CORS policy implementation 103 | - Security headers configuration 104 | 105 | ### Database Security 106 | - Connection pooling with limits 107 | - Secure connection strings 108 | - Database access control 109 | - Regular security updates 110 | 111 | ## Vulnerability Management 112 | 113 | ### Security Scanning Tools 114 | - gosec for Go-specific security issues 115 | - nancy for vulnerability scanning 116 | - Regular dependency updates 117 | - Automated security checks in CI/CD 118 | 119 | ### Security Testing 120 | - Include security test cases 121 | - Test authentication flows 122 | - Validate authorization controls 123 | - Test input validation boundaries 124 | 125 | ## Production Security Checklist 126 | 127 | ### Deployment Security 128 | - Environment variable protection 129 | - Secure container images 130 | - Network isolation 131 | - Monitoring and alerting 132 | - Regular security audits 133 | - Incident response procedures 134 | 135 | -------------------------------------------------------------------------------- /internal/social/oauth_state.go: -------------------------------------------------------------------------------- 1 | package social 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "net/url" 9 | "strings" 10 | "time" 11 | 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | // OAuthState represents the data stored in OAuth state parameter 16 | type OAuthState struct { 17 | RedirectURI string `json:"redirect_uri"` 18 | Nonce string `json:"nonce"` 19 | Timestamp time.Time `json:"timestamp"` 20 | } 21 | 22 | // generateRandomString generates a cryptographically secure random string 23 | func generateRandomString(length int) (string, error) { 24 | bytes := make([]byte, length) 25 | if _, err := rand.Read(bytes); err != nil { 26 | return "", err 27 | } 28 | return base64.URLEncoding.EncodeToString(bytes)[:length], nil 29 | } 30 | 31 | // IsAllowedRedirectURI validates if the redirect URI is allowed 32 | func IsAllowedRedirectURI(redirectURI string) bool { 33 | if redirectURI == "" { 34 | return false 35 | } 36 | 37 | // Parse the URL to validate it 38 | parsedURL, err := url.Parse(redirectURI) 39 | if err != nil { 40 | return false 41 | } 42 | 43 | // Get allowed domains from environment 44 | allowedDomains := viper.GetStringSlice("ALLOWED_REDIRECT_DOMAINS") 45 | if len(allowedDomains) == 0 { 46 | // Default allowed domains for development 47 | allowedDomains = []string{ 48 | "localhost:3000", 49 | "localhost:5173", 50 | "localhost:8080", 51 | "127.0.0.1:3000", 52 | "127.0.0.1:5173", 53 | "127.0.0.1:8080", 54 | } 55 | } 56 | 57 | // Check if the host is in the allowed list 58 | host := parsedURL.Host 59 | for _, allowedDomain := range allowedDomains { 60 | if host == allowedDomain { 61 | return true 62 | } 63 | // Allow subdomains if domain starts with a dot (e.g., ".example.com") 64 | if strings.HasPrefix(allowedDomain, ".") && strings.HasSuffix(host, allowedDomain) { 65 | return true 66 | } 67 | } 68 | 69 | return false 70 | } 71 | 72 | // CreateOAuthState creates a secure state parameter with redirect URI 73 | func CreateOAuthState(redirectURI string) (string, error) { 74 | // Validate redirect URI 75 | if !IsAllowedRedirectURI(redirectURI) { 76 | return "", fmt.Errorf("redirect URI not allowed: %s", redirectURI) 77 | } 78 | 79 | // Generate a random nonce 80 | nonce, err := generateRandomString(16) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | // Create state object 86 | state := OAuthState{ 87 | RedirectURI: redirectURI, 88 | Nonce: nonce, 89 | Timestamp: time.Now(), 90 | } 91 | 92 | // Encode state as JSON 93 | stateJSON, err := json.Marshal(state) 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | // Base64 encode the state 99 | encodedState := base64.URLEncoding.EncodeToString(stateJSON) 100 | return encodedState, nil 101 | } 102 | 103 | // ParseOAuthState parses and validates the OAuth state parameter 104 | func ParseOAuthState(encodedState string) (*OAuthState, error) { 105 | // Base64 decode 106 | stateJSON, err := base64.URLEncoding.DecodeString(encodedState) 107 | if err != nil { 108 | return nil, fmt.Errorf("invalid state encoding: %v", err) 109 | } 110 | 111 | // Parse JSON 112 | var state OAuthState 113 | if err := json.Unmarshal(stateJSON, &state); err != nil { 114 | return nil, fmt.Errorf("invalid state format: %v", err) 115 | } 116 | 117 | // Validate timestamp (state should not be older than 1 hour) 118 | if time.Since(state.Timestamp) > time.Hour { 119 | return nil, fmt.Errorf("state has expired") 120 | } 121 | 122 | // Validate redirect URI again 123 | if !IsAllowedRedirectURI(state.RedirectURI) { 124 | return nil, fmt.Errorf("redirect URI not allowed: %s", state.RedirectURI) 125 | } 126 | 127 | return &state, nil 128 | } 129 | 130 | // GetDefaultRedirectURI returns the default redirect URI for fallback 131 | func GetDefaultRedirectURI() string { 132 | defaultURI := viper.GetString("DEFAULT_REDIRECT_URI") 133 | if defaultURI == "" { 134 | // Default fallback for development 135 | defaultURI = "http://localhost:5173/auth/callback" 136 | } 137 | return defaultURI 138 | } 139 | -------------------------------------------------------------------------------- /test_logout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test script for the logout functionality 4 | # This script demonstrates how to use the /logout endpoint 5 | 6 | echo "🔐 Testing Logout Functionality" 7 | echo "================================" 8 | 9 | # Colors for output 10 | GREEN='\033[0;32m' 11 | RED='\033[0;31m' 12 | YELLOW='\033[1;33m' 13 | NC='\033[0m' # No Color 14 | 15 | # Base URL 16 | BASE_URL="http://localhost:8080" 17 | 18 | echo "" 19 | echo "📝 Step 1: Register a test user" 20 | REGISTER_RESPONSE=$(curl -s -X POST $BASE_URL/register \ 21 | -H "Content-Type: application/json" \ 22 | -d '{ 23 | "email": "logout_test@example.com", 24 | "password": "password123" 25 | }') 26 | 27 | echo "Register Response: $REGISTER_RESPONSE" 28 | 29 | echo "" 30 | echo "🔑 Step 2: Login to get tokens" 31 | LOGIN_RESPONSE=$(curl -s -X POST $BASE_URL/login \ 32 | -H "Content-Type: application/json" \ 33 | -d '{ 34 | "email": "logout_test@example.com", 35 | "password": "password123" 36 | }') 37 | 38 | echo "Login Response: $LOGIN_RESPONSE" 39 | 40 | # Extract tokens from response (assuming successful login without 2FA) 41 | ACCESS_TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"access_token":"[^"]*' | cut -d'"' -f4) 42 | REFRESH_TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"refresh_token":"[^"]*' | cut -d'"' -f4) 43 | 44 | if [ -z "$ACCESS_TOKEN" ] || [ -z "$REFRESH_TOKEN" ]; then 45 | echo -e "${RED}❌ Failed to extract tokens from login response${NC}" 46 | echo "This might be because:" 47 | echo " - User already exists (try deleting from database)" 48 | echo " - Email verification is required" 49 | echo " - 2FA is enabled for this user" 50 | echo " - Server is not running" 51 | exit 1 52 | fi 53 | 54 | echo "" 55 | echo -e "${GREEN}✅ Successfully extracted tokens${NC}" 56 | echo "Access Token: ${ACCESS_TOKEN:0:20}..." 57 | echo "Refresh Token: ${REFRESH_TOKEN:0:20}..." 58 | 59 | echo "" 60 | echo "👤 Step 3: Test protected endpoint (profile) before logout" 61 | PROFILE_RESPONSE=$(curl -s -X GET $BASE_URL/profile \ 62 | -H "Authorization: Bearer $ACCESS_TOKEN") 63 | 64 | echo "Profile Response: $PROFILE_RESPONSE" 65 | 66 | if echo $PROFILE_RESPONSE | grep -q "error"; then 67 | echo -e "${YELLOW}⚠️ Profile request failed (expected if user doesn't exist in DB)${NC}" 68 | else 69 | echo -e "${GREEN}✅ Profile request successful${NC}" 70 | fi 71 | 72 | echo "" 73 | echo "🚪 Step 4: Logout using both refresh and access tokens" 74 | LOGOUT_RESPONSE=$(curl -s -X POST $BASE_URL/logout \ 75 | -H "Content-Type: application/json" \ 76 | -H "Authorization: Bearer $ACCESS_TOKEN" \ 77 | -d "{ 78 | \"refresh_token\": \"$REFRESH_TOKEN\", 79 | \"access_token\": \"$ACCESS_TOKEN\" 80 | }") 81 | 82 | echo "Logout Response: $LOGOUT_RESPONSE" 83 | 84 | if echo $LOGOUT_RESPONSE | grep -q "Successfully logged out"; then 85 | echo -e "${GREEN}✅ Logout successful!${NC}" 86 | else 87 | echo -e "${RED}❌ Logout failed${NC}" 88 | exit 1 89 | fi 90 | 91 | echo "" 92 | echo "🔍 Step 5: Try to access protected endpoint after logout (should fail)" 93 | PROFILE_AFTER_LOGOUT=$(curl -s -X GET $BASE_URL/profile \ 94 | -H "Authorization: Bearer $ACCESS_TOKEN") 95 | 96 | echo "Profile After Logout Response: $PROFILE_AFTER_LOGOUT" 97 | 98 | if echo $PROFILE_AFTER_LOGOUT | grep -q "error"; then 99 | echo -e "${GREEN}✅ Access token correctly blacklisted!${NC}" 100 | else 101 | echo -e "${RED}❌ Access token was not properly blacklisted${NC}" 102 | fi 103 | 104 | echo "" 105 | echo "🔍 Step 6: Try to use refresh token after logout (should fail)" 106 | REFRESH_AFTER_LOGOUT=$(curl -s -X POST $BASE_URL/refresh-token \ 107 | -H "Content-Type: application/json" \ 108 | -d "{ 109 | \"refresh_token\": \"$REFRESH_TOKEN\" 110 | }") 111 | 112 | echo "Refresh After Logout Response: $REFRESH_AFTER_LOGOUT" 113 | 114 | if echo $REFRESH_AFTER_LOGOUT | grep -q "error"; then 115 | echo -e "${GREEN}✅ Refresh token correctly revoked!${NC}" 116 | else 117 | echo -e "${RED}❌ Refresh token was not properly revoked${NC}" 118 | fi 119 | 120 | echo "" 121 | echo "🎉 Logout functionality test completed!" 122 | echo "" 123 | echo "Summary:" 124 | echo "- ✅ User can logout successfully" 125 | echo "- ✅ Access token is blacklisted after logout" 126 | echo "- ✅ Refresh token is revoked after logout" 127 | echo "- ✅ Logout requires authentication (access token)" 128 | echo "- ✅ Logout endpoint returns proper success message" 129 | -------------------------------------------------------------------------------- /internal/user/service_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gjovanovicst/auth_api/internal/email" 7 | "github.com/gjovanovicst/auth_api/pkg/errors" 8 | "github.com/spf13/viper" 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | // Setup test configuration 14 | viper.Set("JWT_SECRET", "testsecret") 15 | viper.Set("ACCESS_TOKEN_EXPIRATION_MINUTES", 15) 16 | viper.Set("REFRESH_TOKEN_EXPIRATION_HOURS", 720) 17 | 18 | m.Run() 19 | } 20 | 21 | func TestPasswordHashing(t *testing.T) { 22 | password := "testpassword123" 23 | 24 | // Test bcrypt hashing 25 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 26 | if err != nil { 27 | t.Fatalf("Expected no error hashing password, got %v", err) 28 | } 29 | 30 | // Verify password 31 | bcryptErr := bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) 32 | if bcryptErr != nil { 33 | t.Fatalf("Expected password hash to match original password, got error: %v", bcryptErr) 34 | } 35 | 36 | // Test wrong password 37 | wrongPassword := "wrongpassword" 38 | bcryptErr = bcrypt.CompareHashAndPassword(hashedPassword, []byte(wrongPassword)) 39 | if bcryptErr == nil { 40 | t.Fatal("Expected error for wrong password, got nil") 41 | } 42 | } 43 | 44 | func TestServiceCreation(t *testing.T) { 45 | // Test that service can be created without errors 46 | repo := &Repository{} 47 | emailService := email.NewService() 48 | service := NewService(repo, emailService) 49 | 50 | if service == nil { 51 | t.Fatal("Expected service to be created, got nil") 52 | } 53 | 54 | if service.Repo != repo { 55 | t.Fatal("Expected service to have correct repository") 56 | } 57 | 58 | if service.EmailService != emailService { 59 | t.Fatal("Expected service to have correct email service") 60 | } 61 | } 62 | 63 | func TestAppErrorCreation(t *testing.T) { 64 | // Test custom error creation 65 | err := errors.NewAppError(errors.ErrUnauthorized, "Test error message") 66 | 67 | if err == nil { 68 | t.Fatal("Expected error to be created, got nil") 69 | } 70 | 71 | // The Code field contains the HTTP status code, not the error type 72 | if err.Code != 401 { // http.StatusUnauthorized 73 | t.Fatalf("Expected HTTP status code 401, got %d", err.Code) 74 | } 75 | 76 | if err.Message != "Test error message" { 77 | t.Fatalf("Expected error message 'Test error message', got '%s'", err.Message) 78 | } 79 | } 80 | 81 | func TestPasswordStrengthRequirements(t *testing.T) { 82 | // Test various password scenarios 83 | testCases := []struct { 84 | password string 85 | minLength int 86 | shouldPass bool 87 | }{ 88 | {"short", 8, false}, 89 | {"password123", 8, true}, 90 | {"verylongpasswordthatshoulddefinitelypass", 8, true}, 91 | {"", 8, false}, 92 | {"1234567", 8, false}, 93 | {"12345678", 8, true}, 94 | } 95 | 96 | for _, tc := range testCases { 97 | isValid := len(tc.password) >= tc.minLength 98 | if isValid != tc.shouldPass { 99 | t.Fatalf("Password '%s' with min length %d: expected %t, got %t", 100 | tc.password, tc.minLength, tc.shouldPass, isValid) 101 | } 102 | } 103 | } 104 | 105 | func TestEmailValidation(t *testing.T) { 106 | // Test basic email format validation (simplified) 107 | testCases := []struct { 108 | email string 109 | isValid bool 110 | }{ 111 | {"test@example.com", true}, 112 | {"user@domain.org", true}, 113 | {"invalid-email", false}, 114 | {"@domain.com", false}, 115 | {"user@", false}, 116 | {"", false}, 117 | {"user.name@domain.co.uk", true}, 118 | } 119 | 120 | for _, tc := range testCases { 121 | // Improved email validation 122 | hasAt := false 123 | hasDot := false 124 | atIndex := -1 125 | 126 | for i, char := range tc.email { 127 | if char == '@' { 128 | if hasAt { // Multiple @ symbols 129 | hasAt = false 130 | break 131 | } 132 | hasAt = true 133 | atIndex = i 134 | } 135 | if char == '.' && hasAt && i > atIndex { 136 | hasDot = true 137 | } 138 | } 139 | 140 | // Must have @ and ., @ cannot be first or last, must have text after @ 141 | isValid := hasAt && hasDot && len(tc.email) > 0 && atIndex > 0 && atIndex < len(tc.email)-1 142 | 143 | if isValid != tc.isValid { 144 | t.Fatalf("Email '%s': expected %t, got %t", tc.email, tc.isValid, isValid) 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /internal/email/service.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | "gopkg.in/mail.v2" 9 | ) 10 | 11 | type Service struct { 12 | // Configuration for SMTP server 13 | } 14 | 15 | func NewService() *Service { 16 | return &Service{} 17 | } 18 | 19 | func (s *Service) SendVerificationEmail(toEmail, token string) error { 20 | fmt.Printf("DEBUG: SendVerificationEmail called with email: %s, token: %s\n", toEmail, token) 21 | 22 | from := viper.GetString("EMAIL_FROM") 23 | subject := "Verify Your Email Address" 24 | body := fmt.Sprintf("Please verify your email address by clicking on the link: http://localhost:8080/verify-email?token=%s", token) 25 | 26 | // Check if we're in development mode (no real SMTP configured) 27 | emailHost := viper.GetString("EMAIL_HOST") 28 | fmt.Printf("DEBUG: Email host configured as: %s\n", emailHost) 29 | 30 | if emailHost == "" || emailHost == "smtp.example.com" { 31 | // Development mode - log email instead of sending 32 | log.Printf("=== EMAIL VERIFICATION (DEVELOPMENT MODE) ===") 33 | log.Printf("To: %s", toEmail) 34 | log.Printf("From: %s", from) 35 | log.Printf("Subject: %s", subject) 36 | log.Printf("Body: %s", body) 37 | log.Printf("=== Verification link: http://localhost:8080/verify-email?token=%s ===", token) 38 | log.Printf("=== EMAIL END ===") 39 | return nil 40 | } 41 | 42 | // Production mode - send actual email 43 | m := mail.NewMessage() 44 | m.SetHeader("From", from) 45 | m.SetHeader("To", toEmail) 46 | m.SetHeader("Subject", subject) 47 | m.SetBody("text/plain", body) 48 | 49 | d := mail.NewDialer( 50 | emailHost, 51 | viper.GetInt("EMAIL_PORT"), 52 | viper.GetString("EMAIL_USERNAME"), 53 | viper.GetString("EMAIL_PASSWORD"), 54 | ) 55 | 56 | if err := d.DialAndSend(m); err != nil { 57 | log.Printf("Failed to send verification email to %s: %v", toEmail, err) 58 | // Fallback to development mode - log the email instead of failing 59 | log.Printf("=== EMAIL VERIFICATION (FALLBACK - SMTP FAILED) ===") 60 | log.Printf("To: %s", toEmail) 61 | log.Printf("From: %s", from) 62 | log.Printf("Subject: %s", subject) 63 | log.Printf("Body: %s", body) 64 | log.Printf("=== Verification link: http://localhost:8080/verify-email?token=%s ===", token) 65 | log.Printf("=== EMAIL END ===") 66 | log.Printf("Note: Check server logs for the verification link above since email delivery failed") 67 | return nil // Don't fail registration just because email failed 68 | } 69 | log.Printf("Verification email sent to %s", toEmail) 70 | return nil 71 | } 72 | 73 | func (s *Service) SendPasswordResetEmail(toEmail, resetLink string) error { 74 | from := viper.GetString("EMAIL_FROM") 75 | subject := "Password Reset Request" 76 | body := fmt.Sprintf("You requested a password reset. Please click on the link to reset your password: %s", resetLink) 77 | 78 | // Check if we're in development mode (no real SMTP configured) 79 | emailHost := viper.GetString("EMAIL_HOST") 80 | if emailHost == "" || emailHost == "smtp.example.com" { 81 | // Development mode - log email instead of sending 82 | log.Printf("=== PASSWORD RESET EMAIL (DEVELOPMENT MODE) ===") 83 | log.Printf("To: %s", toEmail) 84 | log.Printf("From: %s", from) 85 | log.Printf("Subject: %s", subject) 86 | log.Printf("Body: %s", body) 87 | log.Printf("=== Reset link: %s ===", resetLink) 88 | log.Printf("=== EMAIL END ===") 89 | return nil 90 | } 91 | 92 | // Production mode - send actual email 93 | m := mail.NewMessage() 94 | m.SetHeader("From", from) 95 | m.SetHeader("To", toEmail) 96 | m.SetHeader("Subject", subject) 97 | m.SetBody("text/plain", body) 98 | 99 | d := mail.NewDialer( 100 | emailHost, 101 | viper.GetInt("EMAIL_PORT"), 102 | viper.GetString("EMAIL_USERNAME"), 103 | viper.GetString("EMAIL_PASSWORD"), 104 | ) 105 | 106 | if err := d.DialAndSend(m); err != nil { 107 | log.Printf("Failed to send password reset email to %s: %v", toEmail, err) 108 | // Fallback to development mode - log the email instead of failing 109 | log.Printf("=== PASSWORD RESET EMAIL (FALLBACK - SMTP FAILED) ===") 110 | log.Printf("To: %s", toEmail) 111 | log.Printf("From: %s", from) 112 | log.Printf("Subject: %s", subject) 113 | log.Printf("Body: %s", body) 114 | log.Printf("=== Reset link: %s ===", resetLink) 115 | log.Printf("=== EMAIL END ===") 116 | log.Printf("Note: Check server logs for the reset link above since email delivery failed") 117 | return nil // Don't fail the operation just because email failed 118 | } 119 | log.Printf("Password reset email sent to %s", toEmail) 120 | return nil 121 | } -------------------------------------------------------------------------------- /docs/migrations/README_SMART_LOGGING.md: -------------------------------------------------------------------------------- 1 | # Activity Log Smart Logging Migration 2 | 3 | ## Overview 4 | 5 | This migration adds professional activity logging features to reduce database bloat while maintaining security audit capabilities. 6 | 7 | ## What Changed 8 | 9 | ### New Fields Added to `activity_logs` Table 10 | 11 | 1. **severity** (VARCHAR(20), NOT NULL, DEFAULT 'INFORMATIONAL') 12 | - Values: CRITICAL, IMPORTANT, INFORMATIONAL 13 | - Categorizes events by security importance 14 | 15 | 2. **expires_at** (TIMESTAMP WITH TIME ZONE, NULLABLE) 16 | - Automatic expiration timestamp based on retention policies 17 | - Enables efficient cleanup of old logs 18 | 19 | 3. **is_anomaly** (BOOLEAN, NOT NULL, DEFAULT false) 20 | - Flags logs created due to anomaly detection 21 | - Helps identify security-relevant events 22 | 23 | ### New Indexes 24 | 25 | - `idx_activity_logs_expires` - Speeds up cleanup queries 26 | - `idx_activity_logs_cleanup` - Composite index for batch cleanup 27 | - `idx_activity_logs_user_timestamp` - Optimizes user activity pattern queries 28 | 29 | ## How to Apply 30 | 31 | ### Using psql (Manual) 32 | 33 | ```bash 34 | psql -U your_user -d your_database -f migrations/20240103_add_activity_log_smart_fields.sql 35 | ``` 36 | 37 | ### Using Golang (Programmatic) 38 | 39 | The migration will be applied automatically when the application starts if using GORM AutoMigrate on the updated `ActivityLog` model. 40 | 41 | ## Rollback 42 | 43 | If you need to rollback this migration: 44 | 45 | ```bash 46 | psql -U your_user -d your_database -f migrations/20240103_add_activity_log_smart_fields_rollback.sql 47 | ``` 48 | 49 | ## Expected Impact 50 | 51 | ### Database Size Reduction 52 | - **Before**: All events logged indefinitely (TOKEN_REFRESH, PROFILE_ACCESS, etc.) 53 | - **After**: 80-95% reduction through: 54 | - Disabled high-frequency events (TOKEN_REFRESH, PROFILE_ACCESS) by default 55 | - Automatic cleanup based on retention policies 56 | - Anomaly-based conditional logging 57 | 58 | ### Retention Policies 59 | - **Critical Events**: 365 days (LOGIN, PASSWORD_CHANGE, 2FA_ENABLE, etc.) 60 | - **Important Events**: 180 days (EMAIL_VERIFY, SOCIAL_LOGIN, etc.) 61 | - **Informational Events**: 90 days (TOKEN_REFRESH, PROFILE_ACCESS if enabled) 62 | 63 | ## Configuration 64 | 65 | Set these environment variables to customize behavior: 66 | 67 | ```bash 68 | # Enable/disable cleanup (default: true) 69 | LOG_CLEANUP_ENABLED=true 70 | 71 | # Cleanup interval (default: 24h) 72 | LOG_CLEANUP_INTERVAL=24h 73 | 74 | # Batch size for cleanup operations (default: 1000) 75 | LOG_CLEANUP_BATCH_SIZE=1000 76 | 77 | # Disable specific events (comma-separated) 78 | LOG_DISABLED_EVENTS=TOKEN_REFRESH,PROFILE_ACCESS 79 | 80 | # Enable anomaly detection (default: true) 81 | LOG_ANOMALY_DETECTION_ENABLED=true 82 | 83 | # Custom retention periods (in days) 84 | LOG_RETENTION_CRITICAL=365 85 | LOG_RETENTION_IMPORTANT=180 86 | LOG_RETENTION_INFORMATIONAL=90 87 | ``` 88 | 89 | ## Verification 90 | 91 | After applying the migration, verify the changes: 92 | 93 | ```sql 94 | -- Check new columns exist 95 | \d activity_logs 96 | 97 | -- Check indexes 98 | \di activity_logs* 99 | 100 | -- Verify severity distribution 101 | SELECT severity, COUNT(*) 102 | FROM activity_logs 103 | GROUP BY severity; 104 | 105 | -- Check expiration dates are set 106 | SELECT 107 | severity, 108 | COUNT(*) as total, 109 | COUNT(expires_at) as with_expiration, 110 | MIN(expires_at) as earliest_expiration, 111 | MAX(expires_at) as latest_expiration 112 | FROM activity_logs 113 | GROUP BY severity; 114 | ``` 115 | 116 | ## Troubleshooting 117 | 118 | ### Migration Fails 119 | 120 | If the migration fails due to existing data: 121 | 122 | 1. Backup your database first 123 | 2. Check for NULL values or invalid data 124 | 3. Run the migration steps individually to identify the issue 125 | 126 | ### Performance Issues 127 | 128 | If cleanup is slow: 129 | 130 | 1. Adjust `LOG_CLEANUP_BATCH_SIZE` to a smaller value 131 | 2. Increase `LOG_CLEANUP_INTERVAL` to reduce frequency 132 | 3. Consider running cleanup during off-peak hours 133 | 134 | ### Too Much Data Being Deleted 135 | 136 | If cleanup is too aggressive: 137 | 138 | 1. Increase retention periods via environment variables 139 | 2. Change event severities in code to keep them longer 140 | 3. Disable cleanup temporarily while investigating 141 | 142 | ## Support 143 | 144 | For issues or questions, refer to: 145 | - Main documentation: `docs/API.md` 146 | - Implementation plan: `professional.plan.md` 147 | - Code: `internal/log/` and `internal/config/logging.go` 148 | 149 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Authentication API 2 | 3 | Thank you for your interest in contributing! We welcome pull requests, issues, and suggestions to make this project better. 4 | 5 | ## How to Contribute 6 | 7 | 1. **Fork the repository** and create your branch from `main` or `develop`. 8 | 2. **Clone your fork** and set up the project locally (see README for instructions). 9 | 3. **Create a descriptive branch name** (e.g., `feature/social-login`, `fix/email-verification-bug`). 10 | 4. **Make your changes** with clear, concise commits. 11 | 5. **Test your changes** using `make test` or `go test ./...`. 12 | 6. **Lint and format** your code: `make fmt` and `make lint`. 13 | 7. **Push to your fork** and open a pull request (PR) against the `develop` branch. 14 | 8. **Describe your PR** clearly, referencing any related issues. 15 | 16 | ## Code Style 17 | - Follow Go best practices and idioms. 18 | - Use `go fmt` for formatting. 19 | - Write clear, descriptive commit messages. 20 | - Add/update tests for new features or bug fixes. 21 | 22 | ## Database Migrations 23 | 24 | ### When to Create a Migration 25 | 26 | Create a migration when making: 27 | - Database schema changes (add/remove/modify tables/columns) 28 | - Data transformations 29 | - Index changes 30 | - Constraint modifications 31 | - Any breaking database changes 32 | 33 | ### How to Create a Migration 34 | 35 | **1. Use the Template** 36 | ```bash 37 | # Copy migration template 38 | timestamp=$(date +%Y%m%d_%H%M%S) 39 | cp migrations/TEMPLATE.md migrations/${timestamp}_your_description.md 40 | ``` 41 | 42 | **2. Create SQL Files** 43 | 44 | Forward migration (`migrations/YYYYMMDD_HHMMSS_description.sql`): 45 | ```sql 46 | BEGIN; 47 | -- Your changes here 48 | ALTER TABLE table_name ADD COLUMN new_column VARCHAR(100); 49 | COMMIT; 50 | ``` 51 | 52 | Rollback migration (`migrations/YYYYMMDD_HHMMSS_description_rollback.sql`): 53 | ```sql 54 | BEGIN; 55 | -- Reverse your changes 56 | ALTER TABLE table_name DROP COLUMN new_column; 57 | COMMIT; 58 | ``` 59 | 60 | **3. Test Thoroughly** 61 | ```bash 62 | # Test on local database 63 | psql -U postgres -d auth_db_test -f migrations/YYYYMMDD_description.sql 64 | 65 | # Verify changes 66 | psql -U postgres -d auth_db_test -c "\d table_name" 67 | 68 | # Test rollback 69 | psql -U postgres -d auth_db_test -f migrations/YYYYMMDD_description_rollback.sql 70 | ``` 71 | 72 | **4. Document the Migration** 73 | 74 | Fill out the migration documentation file: 75 | - What changed and why 76 | - Impact assessment 77 | - Breaking changes (if any) 78 | - Verification steps 79 | 80 | **5. Update Documentation** 81 | 82 | - [ ] [migrations/MIGRATIONS_LOG.md](migrations/MIGRATIONS_LOG.md) 83 | - [ ] [MIGRATIONS.md](docs/migrations/MIGRATIONS.md) (if user-facing) 84 | - [ ] [BREAKING_CHANGES.md](BREAKING_CHANGES.md) (if breaking) 85 | - [ ] [UPGRADE_GUIDE.md](UPGRADE_GUIDE.md) (if version upgrade) 86 | - [ ] [CHANGELOG.md](CHANGELOG.md) 87 | 88 | ### Migration Checklist 89 | 90 | Before submitting PR with migration: 91 | 92 | - [ ] Forward migration SQL created 93 | - [ ] Rollback migration SQL created 94 | - [ ] Migration documentation completed 95 | - [ ] Tested locally (apply + rollback) 96 | - [ ] Tested with realistic data volume 97 | - [ ] All documentation files updated 98 | - [ ] Tests added/updated for schema changes 99 | - [ ] PR labeled with "migration" 100 | - [ ] Breaking changes clearly marked (if any) 101 | 102 | ### Breaking Changes 103 | 104 | If your migration is breaking: 105 | 106 | 1. **Document extensively** in [BREAKING_CHANGES.md](BREAKING_CHANGES.md) 107 | 2. **Provide migration path** for users 108 | 3. **Bump version appropriately** (major version for breaking changes) 109 | 4. **Add to [UPGRADE_GUIDE.md](UPGRADE_GUIDE.md)** with step-by-step instructions 110 | 5. **Consider deprecation first** instead of immediate removal 111 | 112 | **Semver Guidelines:** 113 | - **Major (2.0.0):** Breaking database or API changes 114 | - **Minor (1.1.0):** New features, backward compatible 115 | - **Patch (1.0.1):** Bug fixes, backward compatible 116 | 117 | ### Resources 118 | 119 | - [migrations/README.md](migrations/README.md) - Detailed migration guide 120 | - [migrations/TEMPLATE.md](migrations/TEMPLATE.md) - Migration template 121 | - [MIGRATIONS.md](docs/migrations/MIGRATIONS.md) - User migration guide 122 | - [BREAKING_CHANGES.md](BREAKING_CHANGES.md) - Breaking changes tracker 123 | 124 | ## Reporting Issues 125 | - Use the GitHub Issues tab. 126 | - Provide as much detail as possible (steps to reproduce, logs, screenshots). 127 | 128 | ## Code of Conduct 129 | Please be respectful and inclusive. See `CODE_OF_CONDUCT.md` for details. 130 | 131 | --- 132 | Thank you for helping make this project better! 133 | -------------------------------------------------------------------------------- /docs/guides/multi-app-oauth-config.md: -------------------------------------------------------------------------------- 1 | # Multi-Application OAuth Configuration 2 | 3 | ## Overview 4 | The authentication API now supports multiple applications using the same OAuth endpoints with different redirect URIs. This allows you to have multiple frontends (e.g., admin panel, user portal, mobile app) that all use the same authentication backend. 5 | 6 | ## Configuration 7 | 8 | ### Environment Variables 9 | 10 | Add these variables to your `.env` file: 11 | 12 | ```env 13 | # Multi-Application Support 14 | # Comma-separated list of allowed redirect domains for OAuth callbacks 15 | ALLOWED_REDIRECT_DOMAINS=localhost:3000,localhost:5173,localhost:8080,example.com,.example.com 16 | 17 | # Default redirect URI when none is specified 18 | DEFAULT_REDIRECT_URI=http://localhost:5173/auth/callback 19 | 20 | # Production frontend URL for CORS 21 | FRONTEND_URL=https://your-production-domain.com 22 | ``` 23 | 24 | ### Domain Configuration 25 | 26 | #### Development 27 | For local development, include all your local ports: 28 | ```env 29 | ALLOWED_REDIRECT_DOMAINS=localhost:3000,localhost:5173,localhost:8080,127.0.0.1:3000,127.0.0.1:5173 30 | ``` 31 | 32 | #### Production 33 | For production, specify your allowed domains: 34 | ```env 35 | # Allow specific domains 36 | ALLOWED_REDIRECT_DOMAINS=myapp.com,admin.myapp.com 37 | 38 | # Allow all subdomains with dot notation 39 | ALLOWED_REDIRECT_DOMAINS=.myapp.com,anotherapp.com 40 | ``` 41 | 42 | ## Usage 43 | 44 | ### Frontend Integration 45 | 46 | #### 1. Basic Usage (Default Redirect) 47 | ```javascript 48 | // Will redirect to DEFAULT_REDIRECT_URI after OAuth 49 | window.location.href = '/auth/google/login'; 50 | ``` 51 | 52 | #### 2. Custom Redirect URI 53 | ```javascript 54 | // Will redirect to specified URI after OAuth 55 | const redirectUri = 'https://admin.myapp.com/auth/callback'; 56 | window.location.href = `/auth/google/login?redirect_uri=${encodeURIComponent(redirectUri)}`; 57 | ``` 58 | 59 | #### 3. Handling Callbacks 60 | Your frontend should handle the callback URL parameters: 61 | 62 | ```javascript 63 | // In your /auth/callback route 64 | const urlParams = new URLSearchParams(window.location.search); 65 | 66 | if (urlParams.get('error')) { 67 | // Handle error 68 | console.error('OAuth error:', urlParams.get('error')); 69 | } else if (urlParams.get('access_token')) { 70 | // Handle success 71 | const accessToken = urlParams.get('access_token'); 72 | const refreshToken = urlParams.get('refresh_token'); 73 | const provider = urlParams.get('provider'); // 'google', 'facebook', or 'github' 74 | 75 | // Store tokens and redirect to app 76 | localStorage.setItem('access_token', accessToken); 77 | localStorage.setItem('refresh_token', refreshToken); 78 | window.location.href = '/dashboard'; 79 | } 80 | ``` 81 | 82 | ### API Endpoints 83 | 84 | All OAuth providers support the same pattern: 85 | 86 | - **Google**: 87 | - Login: `GET /auth/google/login?redirect_uri=...` 88 | - Callback: `GET /auth/google/callback` 89 | 90 | - **Facebook**: 91 | - Login: `GET /auth/facebook/login?redirect_uri=...` 92 | - Callback: `GET /auth/facebook/callback` 93 | 94 | - **GitHub**: 95 | - Login: `GET /auth/github/login?redirect_uri=...` 96 | - Callback: `GET /auth/github/callback` 97 | 98 | ## Security Features 99 | 100 | ### 1. Domain Whitelist 101 | - Only domains in `ALLOWED_REDIRECT_DOMAINS` are accepted 102 | - Prevents open redirect vulnerabilities 103 | - Supports subdomain wildcards with dot notation 104 | 105 | ### 2. Secure State Management 106 | - OAuth state parameter contains encrypted redirect URI 107 | - Includes timestamp to prevent replay attacks 108 | - Cryptographically secure random nonce generation 109 | 110 | ### 3. CORS Configuration 111 | - Automatic CORS headers for allowed domains 112 | - Supports credentials for secure cookie handling 113 | - Production-ready with configurable origins 114 | 115 | ## Examples 116 | 117 | ### Multi-Domain Setup 118 | ```env 119 | # Support multiple applications 120 | ALLOWED_REDIRECT_DOMAINS=app.mycompany.com,admin.mycompany.com,mobile.mycompany.com 121 | DEFAULT_REDIRECT_URI=https://app.mycompany.com/auth/callback 122 | ``` 123 | 124 | ### Development Setup 125 | ```env 126 | # Support local development 127 | ALLOWED_REDIRECT_DOMAINS=localhost:3000,localhost:5173,localhost:8080 128 | DEFAULT_REDIRECT_URI=http://localhost:5173/auth/callback 129 | ``` 130 | 131 | ### Production with Subdomains 132 | ```env 133 | # Allow all subdomains of mycompany.com 134 | ALLOWED_REDIRECT_DOMAINS=.mycompany.com,mycompany.com 135 | DEFAULT_REDIRECT_URI=https://mycompany.com/auth/callback 136 | ``` 137 | 138 | ## Error Handling 139 | 140 | The API will redirect to the frontend with error parameters: 141 | 142 | - `?error=invalid_state` - Invalid or expired OAuth state 143 | - `?error=authorization_code_missing` - OAuth provider didn't return code 144 | - `?error=missing_state` - OAuth state parameter missing 145 | - `?error=Invalid%20redirect%20URI` - Redirect URI not in whitelist 146 | 147 | ## Migration Guide 148 | 149 | ### From Single Application 150 | If you were using the hardcoded redirect URI: 151 | 152 | 1. Add the environment variables to your `.env` file 153 | 2. Your existing setup will continue to work with default values 154 | 3. Optionally, start using the `redirect_uri` parameter for flexibility 155 | 156 | ### For New Applications 157 | 1. Set up the environment variables 158 | 2. Add your domain to `ALLOWED_REDIRECT_DOMAINS` 159 | 3. Use the `redirect_uri` parameter in your OAuth login URLs -------------------------------------------------------------------------------- /docs/implementation_phases/Phase_8_Two_Factor_Authentication_Implementation.md: -------------------------------------------------------------------------------- 1 | ## Phase 8: Two-Factor Authentication (2FA) Implementation 2 | 3 | This phase details the integration of Two-Factor Authentication (2FA) into the RESTful API, providing an additional layer of security for user accounts. 2FA typically involves a second verification step beyond the traditional password, such as a code from an authenticator app (TOTP) or an SMS code. 4 | 5 | ### 8.1 2FA Methods 6 | 7 | We will primarily focus on Time-based One-Time Passwords (TOTP) using authenticator applications (e.g., Google Authenticator, Authy) due to their security and widespread adoption. SMS-based 2FA can be considered as an alternative or additional option, but TOTP is generally preferred for its independence from mobile network issues and lower cost. 8 | 9 | ### 8.2 Database Model Updates for 2FA 10 | 11 | To support 2FA, the `User` model will need to be updated to store 2FA-related information. 12 | 13 | **Updated `User` Model Fields:** 14 | 15 | | Field Name | Data Type | Description | Constraints/Notes | 16 | |-----------------|-----------------|---------------------------------------------------|--------------------------------------------------| 17 | | `TwoFAEnabled` | `boolean` | Indicates if 2FA is enabled for the user. | Default to `false` | 18 | | `TwoFASecret` | `string` | Base32 encoded secret key for TOTP. | Encrypted storage recommended, Nullable | 19 | | `TwoFARecoveryCodes` | `string[]` | List of one-time recovery codes. | Encrypted storage recommended, Nullable | 20 | 21 | **GORM Model Definition (GoLang - additions to existing `User` struct):** 22 | 23 | ```go 24 | type User struct { 25 | // ... existing fields 26 | TwoFAEnabled bool `gorm:"default:false" json:"two_fa_enabled"` 27 | TwoFASecret string `json:"-"` // Stored encrypted, not exposed via JSON 28 | TwoFARecoveryCodes []string `gorm:"type:text[]" json:"-"` // Stored encrypted, not exposed via JSON 29 | } 30 | ``` 31 | 32 | ### 8.3 2FA Enrollment Process 33 | 34 | Users will need to enroll in 2FA. This process typically involves generating a secret, displaying a QR code, and verifying the setup. 35 | 36 | **Process Flow:** 37 | 1. **Generate 2FA Secret:** When a user initiates 2FA setup, generate a new TOTP secret key (e.g., using `github.com/pquerna/otp/totp`). 38 | 2. **Store Secret (Temporarily):** Store this secret temporarily in Redis or a session, associated with the user. 39 | 3. **Generate QR Code:** Create a provisioning URI and generate a QR code image from it. The URI includes the secret, user email, and issuer name. 40 | 4. **Display QR Code:** Return the QR code image (or its URL) and the secret key to the frontend. 41 | 5. **User Scans/Enters:** The user scans the QR code with their authenticator app or manually enters the secret. 42 | 6. **Verify 2FA Setup:** The user enters a TOTP code from their app. The backend verifies this code against the temporarily stored secret. 43 | 7. **Finalize Enrollment:** If verification is successful, save the `TwoFASecret` to the `User` model in the database, set `TwoFAEnabled` to `true`, and generate recovery codes. Invalidate the temporary secret in Redis/session. 44 | 45 | **Example Endpoints:** 46 | - `POST /2fa/generate`: Generates a 2FA secret and QR code. 47 | - `POST /2fa/verify-setup`: Verifies the initial 2FA setup with a TOTP code. 48 | - `POST /2fa/enable`: Enables 2FA for the user after successful verification. 49 | - `POST /2fa/disable`: Disables 2FA for the user (requires password and/or TOTP code). 50 | - `GET /2fa/recovery-codes`: Generates and displays new recovery codes (requires password and/or TOTP code). 51 | 52 | ### 8.4 2FA Login Process 53 | 54 | Once 2FA is enabled, the login process will require an additional step. 55 | 56 | **Process Flow:** 57 | 1. **Initial Login:** User provides email and password. Backend verifies credentials. 58 | 2. **Check 2FA Status:** If credentials are valid and `TwoFAEnabled` is `true` for the user, the backend returns a response indicating that 2FA is required (e.g., HTTP 202 Accepted with a temporary token or session ID). 59 | 3. **Request 2FA Code:** Frontend prompts the user for their TOTP code. 60 | 4. **Verify 2FA Code:** User submits the TOTP code. Backend verifies the code against the stored `TwoFASecret`. 61 | 5. **Issue JWTs:** If the TOTP code is valid, issue the access and refresh tokens. 62 | 63 | **Example Endpoints:** 64 | - `POST /login`: (Modified) Handles initial email/password login. If 2FA is enabled, returns a specific status/message. 65 | - `POST /2fa/login-verify`: Verifies the TOTP code during login. 66 | 67 | ### 8.5 Libraries and Tools 68 | 69 | - **TOTP Generation/Validation:** `github.com/pquerna/otp/totp` 70 | - **Base32 Encoding:** Go's `encoding/base32` package. 71 | - **QR Code Generation:** `github.com/skip2/go-qrcode` (for generating QR code images). 72 | 73 | ### 8.6 Security Considerations 74 | 75 | - **Secret Storage:** The `TwoFASecret` and `TwoFARecoveryCodes` MUST be stored encrypted in the database. 76 | - **Rate Limiting:** Implement rate limiting on 2FA verification attempts to prevent brute-force attacks. 77 | - **Recovery Codes:** Ensure recovery codes are generated securely, displayed to the user only once, and handled with extreme care. They should be single-use. 78 | - **User Experience:** Provide clear instructions and feedback to the user throughout the 2FA enrollment and login processes. 79 | 80 | This phase will significantly enhance the security posture of the application by adding robust Two-Factor Authentication capabilities. 81 | 82 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: [main, develop] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | postgres: 16 | image: postgres:15 17 | env: 18 | POSTGRES_PASSWORD: postgres 19 | POSTGRES_DB: auth_test 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | ports: 26 | - 5435:5432 27 | 28 | redis: 29 | image: redis:7 30 | options: >- 31 | --health-cmd "redis-cli ping" 32 | --health-interval 10s 33 | --health-timeout 5s 34 | --health-retries 5 35 | ports: 36 | - 6381:6379 37 | 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v4 41 | 42 | - name: Set up Go 43 | uses: actions/setup-go@v4 44 | with: 45 | go-version: "1.23" 46 | 47 | - name: Cache Go modules 48 | uses: actions/cache@v3 49 | with: 50 | path: ~/go/pkg/mod 51 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 52 | restore-keys: | 53 | ${{ runner.os }}-go- 54 | 55 | - name: Download dependencies 56 | run: go mod download 57 | 58 | - name: Run unit tests 59 | env: 60 | JWT_SECRET: testsecret 61 | ACCESS_TOKEN_EXPIRATION_MINUTES: 15 62 | REFRESH_TOKEN_EXPIRATION_HOURS: 720 63 | DB_HOST: localhost 64 | DB_PORT: 5435 65 | DB_USER: postgres 66 | DB_PASSWORD: postgres 67 | DB_NAME: auth_test 68 | REDIS_ADDR: localhost:6381 69 | REDIS_PASSWORD: "" 70 | REDIS_DB: 0 71 | run: go test -v -race -coverprofile=coverage.out ./... 72 | 73 | - name: Upload coverage to Codecov 74 | uses: codecov/codecov-action@v3 75 | with: 76 | file: ./coverage.out 77 | flags: unittests 78 | name: codecov-umbrella 79 | 80 | build: 81 | name: Build 82 | runs-on: ubuntu-latest 83 | needs: test 84 | 85 | steps: 86 | - name: Checkout code 87 | uses: actions/checkout@v4 88 | 89 | - name: Set up Go 90 | uses: actions/setup-go@v4 91 | with: 92 | go-version: "1.23" 93 | 94 | - name: Build application 95 | run: go build -v ./cmd/api 96 | 97 | - name: Build Docker image 98 | run: docker build -t auth-api:${{ github.sha }} . 99 | 100 | - name: Save Docker image 101 | run: docker save auth-api:${{ github.sha }} | gzip > auth-api.tar.gz 102 | 103 | - name: Upload Docker image artifact 104 | if: ${{ !env.ACT }} 105 | uses: actions/upload-artifact@v4 106 | with: 107 | name: docker-image 108 | path: auth-api.tar.gz 109 | 110 | security-scan: 111 | name: Security Scan 112 | runs-on: ubuntu-latest 113 | needs: test 114 | 115 | steps: 116 | - name: Checkout code 117 | uses: actions/checkout@v4 118 | 119 | - name: Set up Go 120 | uses: actions/setup-go@v4 121 | with: 122 | go-version: "1.23" 123 | 124 | - name: Run Gosec Security Scanner 125 | run: | 126 | go install github.com/securego/gosec/v2/cmd/gosec@latest 127 | gosec ./... 128 | 129 | - name: Run Nancy vulnerability scanner 130 | if: ${{ !env.ACT }} 131 | continue-on-error: true # Nancy requires OSS Index authentication, allow failure 132 | run: | 133 | go install github.com/sonatype-nexus-community/nancy@latest 134 | go list -json -deps ./... | nancy sleuth || echo "⚠️ Nancy scan skipped - requires OSS Index authentication. To enable, add NANCY_TOKEN secret." 135 | 136 | deploy-staging: 137 | name: Deploy to Staging 138 | runs-on: ubuntu-latest 139 | needs: [test, build, security-scan] 140 | if: github.ref == 'refs/heads/develop' 141 | environment: staging 142 | 143 | steps: 144 | - name: Checkout code 145 | uses: actions/checkout@v4 146 | 147 | - name: Download Docker image artifact 148 | if: ${{ !env.ACT }} 149 | uses: actions/download-artifact@v4 150 | with: 151 | name: docker-image 152 | 153 | - name: Load Docker image 154 | if: ${{ !env.ACT }} 155 | run: docker load < auth-api.tar.gz 156 | 157 | - name: Deploy to staging 158 | run: | 159 | echo "Deploying to staging environment..." 160 | # Here you would add your actual deployment commands 161 | # For example, using kubectl, docker-compose, or cloud provider CLI 162 | echo "Deployment completed successfully" 163 | 164 | deploy-production: 165 | name: Deploy to Production 166 | runs-on: ubuntu-latest 167 | needs: [test, build, security-scan] 168 | if: github.ref == 'refs/heads/main' 169 | environment: production 170 | 171 | steps: 172 | - name: Checkout code 173 | uses: actions/checkout@v4 174 | 175 | - name: Download Docker image artifact 176 | if: ${{ !env.ACT }} 177 | uses: actions/download-artifact@v4 178 | with: 179 | name: docker-image 180 | 181 | - name: Load Docker image 182 | if: ${{ !env.ACT }} 183 | run: docker load < auth-api.tar.gz 184 | 185 | - name: Deploy to production 186 | run: | 187 | echo "Deploying to production environment..." 188 | # Here you would add your actual deployment commands 189 | # For example, using kubectl, docker-compose, or cloud provider CLI 190 | echo "Production deployment completed successfully" 191 | -------------------------------------------------------------------------------- /docs/features/SMART_LOGGING_QUICK_REFERENCE.md: -------------------------------------------------------------------------------- 1 | # Activity Logging Quick Reference 2 | 3 | ## TL;DR 4 | 5 | The new activity logging system reduces database bloat by 80-95% while maintaining security audit capabilities. High-frequency events are disabled by default but logged when anomalies detected. 6 | 7 | ## Quick Start 8 | 9 | ### Zero Configuration Required 10 | The system works out-of-the-box with smart defaults: 11 | - ✅ Critical security events always logged 12 | - ❌ High-frequency events (TOKEN_REFRESH, PROFILE_ACCESS) disabled 13 | - ✅ Anomalies automatically detected and logged 14 | - ✅ Old logs automatically cleaned up 15 | 16 | ### Migration 17 | 18 | ```bash 19 | # Apply database migration 20 | psql -U user -d database -f migrations/20240103_add_activity_log_smart_fields.sql 21 | 22 | # Restart application 23 | # That's it! 24 | ``` 25 | 26 | ## Event Categories 27 | 28 | | Category | Retention | Examples | Default | 29 | |----------|-----------|----------|---------| 30 | | **Critical** | 365 days | LOGIN, PASSWORD_CHANGE, 2FA_ENABLE | Always logged | 31 | | **Important** | 180 days | EMAIL_VERIFY, SOCIAL_LOGIN | Always logged | 32 | | **Informational** | 90 days | TOKEN_REFRESH, PROFILE_ACCESS | Anomaly only | 33 | 34 | ## Common Configurations 35 | 36 | ### High Security 37 | ```bash 38 | LOG_TOKEN_REFRESH=true 39 | LOG_SAMPLE_TOKEN_REFRESH=0.1 40 | LOG_RETENTION_CRITICAL=730 41 | ``` 42 | 43 | ### High Performance 44 | ```bash 45 | LOG_DISABLED_EVENTS=TOKEN_REFRESH,PROFILE_ACCESS,EMAIL_VERIFY 46 | LOG_RETENTION_CRITICAL=180 47 | LOG_CLEANUP_INTERVAL=12h 48 | ``` 49 | 50 | ### Development 51 | ```bash 52 | LOG_TOKEN_REFRESH=true 53 | LOG_PROFILE_ACCESS=true 54 | LOG_CLEANUP_ENABLED=false 55 | ``` 56 | 57 | ### GDPR Minimal 58 | ```bash 59 | LOG_DISABLED_EVENTS=TOKEN_REFRESH,PROFILE_ACCESS 60 | LOG_ANOMALY_DETECTION_ENABLED=false 61 | LOG_RETENTION_CRITICAL=90 62 | ``` 63 | 64 | ## Key Environment Variables 65 | 66 | | Variable | Default | Purpose | 67 | |----------|---------|---------| 68 | | `LOG_CLEANUP_ENABLED` | `true` | Enable automatic cleanup | 69 | | `LOG_CLEANUP_INTERVAL` | `24h` | How often cleanup runs | 70 | | `LOG_ANOMALY_DETECTION_ENABLED` | `true` | Enable anomaly detection | 71 | | `LOG_ANOMALY_NEW_IP` | `true` | Log on new IP address | 72 | | `LOG_RETENTION_CRITICAL` | `365` | Days to keep critical logs | 73 | | `LOG_RETENTION_IMPORTANT` | `180` | Days to keep important logs | 74 | | `LOG_RETENTION_INFORMATIONAL` | `90` | Days to keep informational logs | 75 | | `LOG_TOKEN_REFRESH` | `false` | Log token refresh events | 76 | | `LOG_PROFILE_ACCESS` | `false` | Log profile access events | 77 | 78 | ## Anomaly Detection 79 | 80 | Automatically logs when: 81 | - ✅ User accesses from new IP address 82 | - ✅ User uses new device/browser 83 | - ⚙️ Optional: Unusual time access 84 | - ⚙️ Optional: Geographic location change 85 | 86 | ## Database Schema Changes 87 | 88 | New fields added to `activity_logs`: 89 | - `severity` (VARCHAR) - CRITICAL, IMPORTANT, INFORMATIONAL 90 | - `expires_at` (TIMESTAMP) - Automatic expiration date 91 | - `is_anomaly` (BOOLEAN) - True if logged due to anomaly 92 | 93 | ## Quick Checks 94 | 95 | ### Verify Migration Applied 96 | ```sql 97 | \d activity_logs 98 | -- Should show severity, expires_at, is_anomaly columns 99 | ``` 100 | 101 | ### Check Configuration Working 102 | ```sql 103 | -- Count by severity 104 | SELECT severity, COUNT(*) FROM activity_logs GROUP BY severity; 105 | 106 | -- Check expirations set 107 | SELECT severity, MIN(expires_at), MAX(expires_at) 108 | FROM activity_logs GROUP BY severity; 109 | 110 | -- Check anomaly detection 111 | SELECT COUNT(*) FROM activity_logs WHERE is_anomaly = true; 112 | ``` 113 | 114 | ### Monitor Cleanup 115 | ```sql 116 | -- Logs ready for cleanup 117 | SELECT COUNT(*) FROM activity_logs WHERE expires_at < NOW(); 118 | 119 | -- Should decrease after cleanup runs 120 | ``` 121 | 122 | ## Troubleshooting 123 | 124 | ### Database still growing fast? 125 | 1. Check `LOG_TOKEN_REFRESH=false` and `LOG_PROFILE_ACCESS=false` 126 | 2. Verify cleanup enabled: `LOG_CLEANUP_ENABLED=true` 127 | 3. Check retention periods not too long 128 | 129 | ### Not enough audit data? 130 | 1. Enable specific events: `LOG_TOKEN_REFRESH=true` 131 | 2. Use sampling: `LOG_SAMPLE_TOKEN_REFRESH=0.01` (1%) 132 | 3. Verify anomaly detection enabled 133 | 134 | ### Cleanup not working? 135 | 1. Check application logs for errors 136 | 2. Verify database permissions 137 | 3. Try manual cleanup: 138 | ```sql 139 | DELETE FROM activity_logs WHERE expires_at < NOW(); 140 | ``` 141 | 142 | ## Expected Results 143 | 144 | ### Before 145 | - TOKEN_REFRESH: ~2M logs/month for 1000 users 146 | - PROFILE_ACCESS: ~500K logs/month 147 | - All logs kept forever 148 | - Database: Rapid growth 149 | 150 | ### After 151 | - TOKEN_REFRESH: Only anomalies (~100-1000/month) 152 | - PROFILE_ACCESS: Only anomalies (~50-500/month) 153 | - Critical events: Full logging 154 | - Automatic cleanup: Regular size 155 | - Database: 80-95% reduction 156 | 157 | ## Documentation 158 | 159 | - **Full Guide**: `docs/ACTIVITY_LOGGING_GUIDE.md` 160 | - **Environment Vars**: `docs/ENV_VARIABLES.md` 161 | - **Implementation**: `docs/SMART_LOGGING_IMPLEMENTATION.md` 162 | - **Migration**: `migrations/README_SMART_LOGGING.md` 163 | - **API Docs**: `docs/API.md` 164 | 165 | ## Support 166 | 167 | Configuration not working? 168 | 1. Check application startup logs 169 | 2. Verify environment variables loaded 170 | 3. Review `docs/ACTIVITY_LOGGING_GUIDE.md` 171 | 4. Check code: `internal/log/` and `internal/config/logging.go` 172 | 173 | --- 174 | 175 | **Remember**: The system is designed to work with zero configuration. Only customize if you have specific requirements! 176 | 177 | -------------------------------------------------------------------------------- /docs/implementation_phases/Phase_9_User_Activity_Logs_Implementation.md: -------------------------------------------------------------------------------- 1 | ## Phase 9: User Activity Logs Implementation 2 | 3 | This phase focuses on implementing a robust and scalable system for tracking user activity within the application. User activity logs are crucial for security auditing, compliance, debugging, and understanding user behavior. The design will prioritize efficiency, data integrity, and smart retention policies to manage storage effectively. 4 | 5 | ### 9.1 Database Choice for Activity Logs 6 | 7 | While PostgreSQL is used for core user data, user activity logs, being time-series data with high write volumes and often sequential reads, can benefit from specialized database solutions. For optimal performance and scalability, a dedicated time-series database or a highly optimized relational table is recommended. 8 | 9 | **Option 1: PostgreSQL (Optimized Table)** 10 | 11 | If keeping the technology stack lean is a priority, PostgreSQL can still be a viable option with proper indexing and partitioning. This approach leverages existing infrastructure and knowledge. 12 | 13 | **Pros:** 14 | - No new database technology to manage. 15 | - Familiarity with GORM for interaction. 16 | 17 | **Cons:** 18 | - May require manual partitioning for very high volumes. 19 | - Querying large historical datasets can be slower than specialized solutions. 20 | 21 | **Optimization Strategies for PostgreSQL:** 22 | - **Indexing:** Create indexes on `UserID`, `Timestamp`, and `EventType` for efficient querying. 23 | - **Partitioning:** Implement time-based table partitioning (e.g., by month or year) to improve query performance and facilitate easier data archival/deletion. 24 | - **Denormalization:** Store all necessary context directly in the log entry to avoid joins during common queries. 25 | 26 | **Option 2: Time-Series Database (e.g., InfluxDB, TimescaleDB)** 27 | 28 | Time-series databases are purpose-built for handling large volumes of timestamped data, offering superior write and query performance for logging scenarios. 29 | 30 | **Pros:** 31 | - High write throughput and optimized storage for time-series data. 32 | - Fast queries for time-based ranges and aggregations. 33 | - Built-in data retention policies and downsampling capabilities. 34 | 35 | **Cons:** 36 | - Introduces a new database technology to the stack, increasing operational complexity. 37 | - Requires a separate client library and potentially a different ORM/data access pattern. 38 | 39 | **Recommendation:** For this project, given the existing PostgreSQL setup, we will initially proceed with an **optimized PostgreSQL table** for user activity logs. This minimizes additional complexity while still allowing for good performance with proper design. If log volumes become exceptionally high (e.g., millions of events per day), migrating to a dedicated time-series database can be considered as a future enhancement. 40 | 41 | ### 9.2 Activity Log Schema 42 | 43 | The `ActivityLog` model will capture essential details about each user action. 44 | 45 | | Field Name | Data Type | Description | Constraints/Notes | 46 | |-----------------|-----------------|---------------------------------------------------|--------------------------------------------------| 47 | | `ID` | `UUID` | Unique identifier for the log entry. | Primary Key, Auto-generated, Indexed | 48 | | `UserID` | `UUID` | ID of the user performing the action. | Indexed, Foreign Key to `User` (optional, for performance) | 49 | | `EventType` | `string` | Type of activity (e.g., `LOGIN`, `LOGOUT`, `PASSWORD_CHANGE`, `2FA_ENABLE`). | Indexed | 50 | | `Timestamp` | `time.Time` | When the activity occurred. | Indexed, Primary for time-series queries | 51 | | `IPAddress` | `string` | IP address from which the action originated. | | 52 | | `UserAgent` | `string` | User-Agent string of the client. | | 53 | | `Details` | `jsonb` | JSONB field for flexible, structured additional details (e.g., `{"old_email": "a@b.com", "new_email": "x@y.com"}`). | | 54 | 55 | **GORM Model Definition (GoLang):** 56 | 57 | ```go 58 | type ActivityLog struct { 59 | ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` 60 | UserID uuid.UUID `gorm:"index" json:"user_id"` // Consider not making it a foreign key constraint for performance if high volume 61 | EventType string `gorm:"index;not null" json:"event_type"` 62 | Timestamp time.Time `gorm:"index;not null" json:"timestamp"` 63 | IPAddress string `json:"ip_address"` 64 | UserAgent string `json:"user_agent"` 65 | Details json.RawMessage `gorm:"type:jsonb" json:"details"` // Use json.RawMessage for flexible JSONB 66 | } 67 | ``` 68 | 69 | ### 9.3 Log Ingestion and Asynchronous Logging 70 | 71 | To avoid impacting the performance of critical API operations, activity logging should be asynchronous. 72 | 73 | **Mechanism:** 74 | 1. **Log Service:** Create a dedicated `internal/log/service.go` that exposes a method like `LogActivity(userID, eventType, ipAddress, userAgent, details)`. This service will be responsible for writing logs. 75 | 2. **Go Routines and Channels:** When an event occurs (e.g., user login), the handler or service will send the log data to a channel. A separate Go routine will continuously read from this channel and write the logs to the database. 76 | 3. **Error Handling:** Implement robust error handling for logging. If a log cannot be written, it should ideally be retried or sent to an error queue/dead-letter queue to prevent data loss, without blocking the main request flow. 77 | 78 | **Example `internal/log/service.go`:** 79 | 80 | ```go 81 | package log 82 | 83 | import ( 84 | "context" 85 | "encoding/json" 86 | "log" 87 | 88 | 89 | -------------------------------------------------------------------------------- /pkg/jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | // Setup test configuration 12 | viper.Set("JWT_SECRET", "testsecret") 13 | viper.Set("ACCESS_TOKEN_EXPIRATION_MINUTES", 15) 14 | viper.Set("REFRESH_TOKEN_EXPIRATION_HOURS", 720) 15 | 16 | m.Run() 17 | } 18 | 19 | func TestGenerateAccessToken(t *testing.T) { 20 | userID := "test-user-id" 21 | 22 | token, err := GenerateAccessToken(userID) 23 | if err != nil { 24 | t.Fatalf("Expected no error, got %v", err) 25 | } 26 | 27 | if token == "" { 28 | t.Fatal("Expected token to be generated, got empty string") 29 | } 30 | 31 | // Verify token can be parsed 32 | claims, err := ParseToken(token) 33 | if err != nil { 34 | t.Fatalf("Expected token to be parseable, got error: %v", err) 35 | } 36 | 37 | if claims.UserID != userID { 38 | t.Fatalf("Expected user ID %s, got %s", userID, claims.UserID) 39 | } 40 | 41 | // Check that token has been issued recently (within last minute) 42 | if time.Since(claims.IssuedAt.Time) > time.Minute { 43 | t.Fatal("Token seems to have been issued too long ago") 44 | } 45 | } 46 | 47 | func TestGenerateRefreshToken(t *testing.T) { 48 | userID := "test-user-id" 49 | 50 | token, err := GenerateRefreshToken(userID) 51 | if err != nil { 52 | t.Fatalf("Expected no error, got %v", err) 53 | } 54 | 55 | if token == "" { 56 | t.Fatal("Expected token to be generated, got empty string") 57 | } 58 | 59 | // Verify token can be parsed 60 | claims, err := ParseToken(token) 61 | if err != nil { 62 | t.Fatalf("Expected token to be parseable, got error: %v", err) 63 | } 64 | 65 | if claims.UserID != userID { 66 | t.Fatalf("Expected user ID %s, got %s", userID, claims.UserID) 67 | } 68 | 69 | // Check that token has been issued recently (within last minute) 70 | if time.Since(claims.IssuedAt.Time) > time.Minute { 71 | t.Fatal("Token seems to have been issued too long ago") 72 | } 73 | } 74 | 75 | func TestParseTokenValid(t *testing.T) { 76 | userID := "test-user-id" 77 | 78 | // Generate a token first 79 | token, err := GenerateAccessToken(userID) 80 | if err != nil { 81 | t.Fatalf("Failed to generate token: %v", err) 82 | } 83 | 84 | // Parse the token 85 | claims, err := ParseToken(token) 86 | if err != nil { 87 | t.Fatalf("Expected no error parsing valid token, got %v", err) 88 | } 89 | 90 | if claims.UserID != userID { 91 | t.Fatalf("Expected user ID %s, got %s", userID, claims.UserID) 92 | } 93 | 94 | // Check that token has been issued recently (within last minute) 95 | if time.Since(claims.IssuedAt.Time) > time.Minute { 96 | t.Fatal("Token seems to have been issued too long ago") 97 | } 98 | } 99 | 100 | func TestParseTokenInvalid(t *testing.T) { 101 | invalidToken := "invalid.token.here" 102 | 103 | _, err := ParseToken(invalidToken) 104 | if err == nil { 105 | t.Fatal("Expected error parsing invalid token, got nil") 106 | } 107 | } 108 | 109 | func TestParseTokenEmpty(t *testing.T) { 110 | _, err := ParseToken("") 111 | if err == nil { 112 | t.Fatal("Expected error parsing empty token, got nil") 113 | } 114 | } 115 | 116 | func TestParseTokenExpired(t *testing.T) { 117 | // Set very short expiration for test 118 | viper.Set("ACCESS_TOKEN_EXPIRATION_MINUTES", 0) // This should create an already expired token 119 | 120 | userID := "test-user-id" 121 | token, err := GenerateAccessToken(userID) 122 | if err != nil { 123 | t.Fatalf("Failed to generate token: %v", err) 124 | } 125 | 126 | // Wait a moment to ensure expiration 127 | time.Sleep(time.Second) 128 | 129 | _, err = ParseToken(token) 130 | if err == nil { 131 | t.Fatal("Expected error parsing expired token, got nil") 132 | } 133 | 134 | // Reset for other tests 135 | viper.Set("ACCESS_TOKEN_EXPIRATION_MINUTES", 15) 136 | } 137 | 138 | func TestGenerateTokenWithEmptyUserID(t *testing.T) { 139 | // Our implementation allows empty user IDs, so this should succeed 140 | token, err := GenerateAccessToken("") 141 | if err != nil { 142 | t.Fatalf("Expected no error generating token with empty user ID, got %v", err) 143 | } 144 | 145 | if token == "" { 146 | t.Fatal("Expected token to be generated even with empty user ID") 147 | } 148 | 149 | // Verify we can parse it back 150 | claims, err := ParseToken(token) 151 | if err != nil { 152 | t.Fatalf("Expected to be able to parse token with empty user ID, got error: %v", err) 153 | } 154 | 155 | if claims.UserID != "" { 156 | t.Fatalf("Expected empty user ID in claims, got %s", claims.UserID) 157 | } 158 | } 159 | 160 | func TestTokenTypeDifferentiation(t *testing.T) { 161 | userID := "test-user-id" 162 | 163 | accessToken, err := GenerateAccessToken(userID) 164 | if err != nil { 165 | t.Fatalf("Failed to generate access token: %v", err) 166 | } 167 | 168 | refreshToken, err := GenerateRefreshToken(userID) 169 | if err != nil { 170 | t.Fatalf("Failed to generate refresh token: %v", err) 171 | } 172 | 173 | accessClaims, err := ParseToken(accessToken) 174 | if err != nil { 175 | t.Fatalf("Failed to parse access token: %v", err) 176 | } 177 | 178 | refreshClaims, err := ParseToken(refreshToken) 179 | if err != nil { 180 | t.Fatalf("Failed to parse refresh token: %v", err) 181 | } 182 | 183 | // Check that both tokens have been issued recently 184 | if time.Since(accessClaims.IssuedAt.Time) > time.Minute { 185 | t.Fatal("Access token seems to have been issued too long ago") 186 | } 187 | 188 | if time.Since(refreshClaims.IssuedAt.Time) > time.Minute { 189 | t.Fatal("Refresh token seems to have been issued too long ago") 190 | } 191 | 192 | // Tokens should be different 193 | if accessToken == refreshToken { 194 | t.Fatal("Access and refresh tokens should be different") 195 | } 196 | } -------------------------------------------------------------------------------- /docs/implementation/VERSION_BUMP_1.1.0.md: -------------------------------------------------------------------------------- 1 | # Version Bump to 1.1.0 2 | 3 | ## Release Date 4 | 2024-12-04 5 | 6 | ## Version Information 7 | - **Previous Version**: 1.0.0 8 | - **New Version**: 1.1.0 9 | - **Release Type**: Minor Release (new features, backward compatible) 10 | 11 | ## Files Updated 12 | 13 | ### 1. Core Application Files 14 | - ✅ `cmd/api/main.go` - Updated Swagger version annotation from `1.0` to `1.1.0` 15 | 16 | ### 2. Documentation Files 17 | - ✅ `docs/docs.go` - Regenerated with version 1.1.0 18 | - ✅ `docs/swagger.json` - Updated version field to 1.1.0 19 | - ✅ `docs/swagger.yaml` - Updated version field to 1.1.0 20 | - ✅ `README.md` - Added CI/CD features and commands section 21 | - ✅ `CHANGELOG.md` - Added comprehensive v1.1.0 release notes 22 | 23 | ### 3. Configuration Files 24 | - ✅ `.github/workflows/ci.yml` - Updated with act-compatible configuration (ports and conditional artifacts) 25 | - ✅ `internal/middleware/auth_test.go` - Updated to support environment variable configuration 26 | - ✅ `internal/log/service.go` - Added security exception comment 27 | 28 | ### 4. Files NOT Changed (No Version References) 29 | - ❌ `Dockerfile` - No version reference (builds from source) 30 | - ❌ `docker-compose.yml` - No application version tag (uses local build) 31 | - ❌ `go.mod` - Module path unchanged 32 | 33 | ## What's New in 1.1.0 34 | 35 | ### CI/CD Infrastructure 36 | 1. **GitHub Actions Workflow** 37 | - Complete CI/CD pipeline with test, build, and security-scan jobs 38 | - Automated testing with PostgreSQL and Redis services 39 | - Docker image building and artifact management 40 | 41 | 2. **Local Testing with Act** 42 | - Full compatibility with `nektos/act` for local CI testing 43 | - Smart port configuration to avoid conflicts (PostgreSQL: 5435, Redis: 6381) 44 | - Conditional artifact handling (skips upload/download for local runs) 45 | 46 | 3. **Security Scanning** 47 | - Gosec security scanner integration (0 issues) 48 | - Nancy vulnerability scanner (optional, requires OSS Index authentication) 49 | - Nancy configured with `continue-on-error` to not block CI if authentication fails 50 | - Proper security exception documentation 51 | 52 | ### Test Infrastructure Improvements 53 | 1. **Environment Variable Support** 54 | - Tests now read from CI environment using `viper.AutomaticEnv()` 55 | - Proper defaults with `viper.SetDefault()` allowing env override 56 | - Improved test reliability in CI environments 57 | 58 | 2. **Redis Connection Handling** 59 | - Better error handling for Redis availability 60 | - Tests properly skip when Redis is unavailable 61 | - Configurable Redis connection parameters 62 | 63 | ### Documentation Updates 64 | 1. **README.md Enhancements** 65 | - Added CI/CD to Developer Experience features 66 | - New CI/CD Commands section with act usage examples 67 | - Installation guide for act 68 | 69 | 2. **CHANGELOG.md** 70 | - Comprehensive release notes for v1.1.0 71 | - Clear separation between v1.0.0 and v1.1.0 changes 72 | - Detailed descriptions of fixes and improvements 73 | 74 | ## Breaking Changes 75 | **None** - This is a fully backward-compatible release. 76 | 77 | ## Migration Required 78 | **No** - No database migrations or configuration changes required. 79 | 80 | ## Docker Image Tagging 81 | The application uses local build in docker-compose and does not use versioned tags. 82 | If you publish to a registry, you should tag as: 83 | ```bash 84 | docker tag auth-api:latest your-registry/auth-api:1.1.0 85 | docker tag auth-api:latest your-registry/auth-api:latest 86 | docker push your-registry/auth-api:1.1.0 87 | docker push your-registry/auth-api:latest 88 | ``` 89 | 90 | ## Testing Verification 91 | 92 | ### All CI Jobs Passing ✅ 93 | 1. **Test Job** - All tests passing with proper environment configuration 94 | 2. **Build Job** - Go build and Docker image build successful 95 | 3. **Security-Scan Job** - Gosec security scan passing with 0 issues 96 | 97 | ### Local Testing with Act 98 | ```bash 99 | # Run all jobs locally 100 | act -j test --container-architecture linux/amd64 101 | act -j build --container-architecture linux/amd64 102 | act -j security-scan --container-architecture linux/amd64 103 | 104 | # List all available jobs 105 | act -l 106 | ``` 107 | 108 | ## Release Checklist 109 | 110 | - [x] Update version in `cmd/api/main.go` 111 | - [x] Regenerate Swagger documentation 112 | - [x] Update `CHANGELOG.md` with release notes 113 | - [x] Update `README.md` with new features 114 | - [x] Verify all CI jobs passing 115 | - [x] Test locally with act 116 | - [x] Review security scan results 117 | - [ ] Create git tag: `git tag -a v1.1.0 -m "Release v1.1.0"` 118 | - [ ] Push tag: `git push origin v1.1.0` 119 | - [ ] Create GitHub release with CHANGELOG notes 120 | - [ ] Build and tag Docker images (if publishing) 121 | 122 | ## Post-Release Actions 123 | 124 | 1. **Git Tagging** 125 | ```bash 126 | git add . 127 | git commit -m "chore(release): bump version to 1.1.0" 128 | git tag -a v1.1.0 -m "Release version 1.1.0 - CI/CD Improvements" 129 | git push origin main 130 | git push origin v1.1.0 131 | ``` 132 | 133 | 2. **GitHub Release** 134 | - Create a new release on GitHub 135 | - Use tag v1.1.0 136 | - Copy release notes from CHANGELOG.md 137 | - Attach any built artifacts if needed 138 | 139 | 3. **Docker Registry** (if applicable) 140 | ```bash 141 | docker build -t auth-api:1.1.0 -t auth-api:latest . 142 | docker tag auth-api:1.1.0 your-registry/auth-api:1.1.0 143 | docker push your-registry/auth-api:1.1.0 144 | docker push your-registry/auth-api:latest 145 | ``` 146 | 147 | ## Notes 148 | 149 | - All changes are backward compatible 150 | - No database migrations required 151 | - No breaking changes to API endpoints 152 | - All existing functionality preserved 153 | - CI/CD improvements enhance developer experience without affecting runtime behavior 154 | 155 | -------------------------------------------------------------------------------- /docs/implementation/SWAGGER_UPDATE_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Swagger Documentation Update 2 | 3 | ## ✅ Swagger Updated Successfully 4 | 5 | The Swagger documentation has been regenerated to reflect the new profile data structure. 6 | 7 | ### Command Used 8 | ```bash 9 | make swag-init 10 | ``` 11 | 12 | ### Files Updated 13 | - ✅ `docs/docs.go` - Generated Go documentation 14 | - ✅ `docs/swagger.json` - JSON schema 15 | - ✅ `docs/swagger.yaml` - YAML schema 16 | 17 | --- 18 | 19 | ## 📊 New Swagger Schema 20 | 21 | ### `dto.UserResponse` (Updated) 22 | 23 | Now includes all profile fields: 24 | 25 | ```yaml 26 | dto.UserResponse: 27 | properties: 28 | id: 29 | type: string 30 | email: 31 | type: string 32 | email_verified: 33 | type: boolean 34 | name: # ✨ NEW 35 | type: string 36 | first_name: # ✨ NEW 37 | type: string 38 | last_name: # ✨ NEW 39 | type: string 40 | profile_picture: # ✨ NEW 41 | type: string 42 | locale: # ✨ NEW 43 | type: string 44 | two_fa_enabled: 45 | type: boolean 46 | created_at: 47 | type: string 48 | updated_at: 49 | type: string 50 | social_accounts: # ✨ NEW (array) 51 | type: array 52 | items: 53 | $ref: '#/definitions/dto.SocialAccountResponse' 54 | ``` 55 | 56 | ### `dto.SocialAccountResponse` (New Type) 57 | 58 | Complete social account information: 59 | 60 | ```yaml 61 | dto.SocialAccountResponse: 62 | properties: 63 | id: 64 | type: string 65 | provider: # google, facebook, github 66 | type: string 67 | provider_user_id: 68 | type: string 69 | email: 70 | type: string 71 | name: 72 | type: string 73 | first_name: 74 | type: string 75 | last_name: 76 | type: string 77 | profile_picture: 78 | type: string 79 | username: # GitHub login, etc. 80 | type: string 81 | locale: 82 | type: string 83 | created_at: 84 | type: string 85 | updated_at: 86 | type: string 87 | ``` 88 | 89 | --- 90 | 91 | ## 🌐 Swagger UI 92 | 93 | ### Accessing Swagger UI 94 | 95 | Once the application is running: 96 | ``` 97 | http://localhost:8080/swagger/index.html 98 | ``` 99 | 100 | ### Profile Endpoint Documentation 101 | 102 | **GET /profile** 103 | 104 | **Response 200 (application/json):** 105 | ```json 106 | { 107 | "id": "string", 108 | "email": "string", 109 | "email_verified": true, 110 | "name": "string", 111 | "first_name": "string", 112 | "last_name": "string", 113 | "profile_picture": "string", 114 | "locale": "string", 115 | "two_fa_enabled": true, 116 | "created_at": "string", 117 | "updated_at": "string", 118 | "social_accounts": [ 119 | { 120 | "id": "string", 121 | "provider": "string", 122 | "provider_user_id": "string", 123 | "email": "string", 124 | "name": "string", 125 | "first_name": "string", 126 | "last_name": "string", 127 | "profile_picture": "string", 128 | "username": "string", 129 | "locale": "string", 130 | "created_at": "string", 131 | "updated_at": "string" 132 | } 133 | ] 134 | } 135 | ``` 136 | 137 | --- 138 | 139 | ## 🔍 Verification 140 | 141 | ### Check Swagger Definitions 142 | 143 | ```bash 144 | # Check UserResponse definition 145 | grep -A 30 "dto.UserResponse:" docs/swagger.yaml 146 | 147 | # Check SocialAccountResponse definition 148 | grep -A 15 "dto.SocialAccountResponse:" docs/swagger.yaml 149 | ``` 150 | 151 | ### Test in Swagger UI 152 | 153 | 1. Start application: `./auth_api.exe` 154 | 2. Visit: `http://localhost:8080/swagger/index.html` 155 | 3. Find **User** section 156 | 4. Click on **GET /profile** 157 | 5. Click "Try it out" 158 | 6. Paste your Bearer token 159 | 7. Execute 160 | 8. See complete response with all new fields ✅ 161 | 162 | --- 163 | 164 | ## 📝 Example Response in Swagger 165 | 166 | When you test `/profile` in Swagger UI, you'll see: 167 | 168 | ```json 169 | { 170 | "id": "a65aec73-3c91-450c-b51f-a49391d6c3ba", 171 | "email": "gjovanovic.st@gmail.com", 172 | "email_verified": true, 173 | "name": "Goran Jovanovic", 174 | "first_name": "Goran", 175 | "last_name": "Jovanovic", 176 | "profile_picture": "https://lh3.googleusercontent.com/a/...", 177 | "locale": "en", 178 | "two_fa_enabled": true, 179 | "created_at": "2025-07-31T22:34:10+00:00", 180 | "updated_at": "2025-11-08T17:35:55+00:00", 181 | "social_accounts": [ 182 | { 183 | "id": "...", 184 | "provider": "google", 185 | "provider_user_id": "...", 186 | "email": "gjovanovic.st@gmail.com", 187 | "name": "Goran Jovanovic", 188 | "first_name": "Goran", 189 | "last_name": "Jovanovic", 190 | "profile_picture": "https://lh3.googleusercontent.com/a/...", 191 | "locale": "en", 192 | "created_at": "2025-11-08T17:35:55+00:00", 193 | "updated_at": "2025-11-08T17:35:55+00:00" 194 | } 195 | ] 196 | } 197 | ``` 198 | 199 | --- 200 | 201 | ## ✅ What Was Generated 202 | 203 | Swag tool generated/updated: 204 | 205 | 1. **`dto.UserResponse`** - With 12 fields (was 6) 206 | 2. **`dto.SocialAccountResponse`** - New type definition 207 | 3. **Profile endpoint** - Updated response schema 208 | 4. **All referenced DTOs** - Properly linked 209 | 210 | --- 211 | 212 | ## 🎯 Summary 213 | 214 | | Item | Status | 215 | |------|--------| 216 | | Swagger regenerated | ✅ | 217 | | `UserResponse` updated | ✅ (6 → 12 fields) | 218 | | `SocialAccountResponse` added | ✅ (new type) | 219 | | Profile endpoint schema | ✅ Updated | 220 | | docs/swagger.yaml | ✅ Updated | 221 | | docs/swagger.json | ✅ Updated | 222 | | docs/docs.go | ✅ Updated | 223 | 224 | --- 225 | 226 | ## 📚 Related Files 227 | 228 | - Swagger Annotations: `internal/user/handler.go` (lines 254-262) 229 | - DTO Definitions: `pkg/dto/auth.go` (lines 78-108) 230 | - Swagger Output: `docs/swagger.yaml`, `docs/swagger.json`, `docs/docs.go` 231 | 232 | --- 233 | 234 | **Swagger is now fully updated and reflects the new profile structure!** 🎉 235 | 236 | Test it at: `http://localhost:8080/swagger/index.html` 237 | 238 | -------------------------------------------------------------------------------- /docs/guides/NANCY_SETUP.md: -------------------------------------------------------------------------------- 1 | # Nancy Vulnerability Scanner Setup 2 | 3 | ## Overview 4 | 5 | Nancy is an optional vulnerability scanner from Sonatype that checks your Go dependencies against the OSS Index for known security vulnerabilities. It's integrated into the CI/CD pipeline but requires authentication to function. 6 | 7 | ## Current Configuration 8 | 9 | Nancy is configured with `continue-on-error: true` in the GitHub Actions workflow, meaning: 10 | - ✅ The CI pipeline will **not fail** if Nancy authentication is missing 11 | - ✅ Gosec security scanner still runs and provides security scanning 12 | - ⚠️ Nancy will show a warning but won't block your builds 13 | 14 | ## Why Nancy Requires Authentication 15 | 16 | Nancy connects to the [Sonatype OSS Index](https://ossindex.sonatype.org/) which requires a free account to prevent abuse and rate limiting. 17 | 18 | ## How to Enable Nancy (Optional) 19 | 20 | If you want to enable full vulnerability scanning with Nancy, follow these steps: 21 | 22 | ### 1. Create a Free OSS Index Account 23 | 24 | 1. Go to https://ossindex.sonatype.org/ 25 | 2. Click "Sign Up" and create a free account 26 | 3. Verify your email address 27 | 28 | ### 2. Get Your API Token 29 | 30 | 1. Log in to OSS Index 31 | 2. Go to your account settings 32 | 3. Generate an API token 33 | 4. Copy your username and token 34 | 35 | ### 3. Add GitHub Secrets 36 | 37 | Add the following secrets to your GitHub repository: 38 | 39 | 1. Go to your repository on GitHub 40 | 2. Navigate to **Settings** → **Secrets and variables** → **Actions** 41 | 3. Click **New repository secret** 42 | 4. Add these two secrets: 43 | - Name: `NANCY_USERNAME`, Value: your OSS Index username 44 | - Name: `NANCY_TOKEN`, Value: your OSS Index API token 45 | 46 | ### 4. Update the Workflow (Optional) 47 | 48 | If you added the secrets, you can update `.github/workflows/ci.yml` to use them: 49 | 50 | ```yaml 51 | - name: Run Nancy vulnerability scanner 52 | if: ${{ !env.ACT }} 53 | continue-on-error: true 54 | env: 55 | NANCY_USERNAME: ${{ secrets.NANCY_USERNAME }} 56 | NANCY_TOKEN: ${{ secrets.NANCY_TOKEN }} 57 | run: | 58 | go install github.com/sonatype-nexus-community/nancy@latest 59 | if [ -n "$NANCY_TOKEN" ]; then 60 | go list -json -deps ./... | nancy sleuth --username "$NANCY_USERNAME" --token "$NANCY_TOKEN" 61 | else 62 | go list -json -deps ./... | nancy sleuth || echo "⚠️ Nancy scan skipped - requires OSS Index authentication." 63 | fi 64 | ``` 65 | 66 | ## Local Usage 67 | 68 | To run Nancy locally: 69 | 70 | ### Without Authentication (Limited) 71 | ```bash 72 | go install github.com/sonatype-nexus-community/nancy@latest 73 | go list -json -deps ./... | nancy sleuth 74 | ``` 75 | 76 | ### With Authentication (Recommended) 77 | ```bash 78 | # Set environment variables 79 | export NANCY_USERNAME="your_username" 80 | export NANCY_TOKEN="your_token" 81 | 82 | # Run Nancy 83 | go list -json -deps ./... | nancy sleuth --username "$NANCY_USERNAME" --token "$NANCY_TOKEN" 84 | ``` 85 | 86 | Or add to your `.env` file (don't commit this): 87 | ```bash 88 | NANCY_USERNAME=your_username 89 | NANCY_TOKEN=your_token 90 | ``` 91 | 92 | ## Alternative: Use Make Command 93 | 94 | The Makefile includes a vulnerability scan command: 95 | 96 | ```bash 97 | make vulnerability-scan 98 | ``` 99 | 100 | This will attempt to run Nancy. If you have credentials configured, it will use them. 101 | 102 | ## What Nancy Checks 103 | 104 | Nancy scans your Go dependencies (`go.mod` and transitive dependencies) against the OSS Index database for: 105 | - Known CVEs (Common Vulnerabilities and Exposures) 106 | - Security advisories 107 | - Vulnerability severity scores 108 | - Affected version ranges 109 | - Remediation recommendations 110 | 111 | ## Current Security Coverage 112 | 113 | Even without Nancy, your CI pipeline includes: 114 | - ✅ **Gosec** - Static analysis security scanner for Go code 115 | - ✅ **Go Module Checksums** - Ensures dependency integrity 116 | - ✅ **Unit Tests** - Including security-focused tests 117 | - ✅ **Code Review** - Manual security review process 118 | 119 | Nancy adds an additional layer by checking for known vulnerabilities in dependencies. 120 | 121 | ## Troubleshooting 122 | 123 | ### "401 Unauthorized" Error 124 | This means Nancy couldn't authenticate with OSS Index. Either: 125 | - You haven't set up credentials (this is fine - it won't break CI) 126 | - Your credentials are incorrect 127 | - Your API token has expired 128 | 129 | ### Rate Limiting 130 | Without authentication, OSS Index has strict rate limits. If you hit them: 131 | - Wait a few minutes and try again 132 | - Consider setting up authentication (free and unlimited) 133 | 134 | ### Nancy Installation Issues 135 | If Nancy fails to install: 136 | ```bash 137 | # Clear Go module cache 138 | go clean -modcache 139 | 140 | # Reinstall 141 | go install github.com/sonatype-nexus-community/nancy@latest 142 | ``` 143 | 144 | ## Recommendations 145 | 146 | ### For Open Source Projects 147 | - Authentication is optional but recommended 148 | - Use GitHub secrets to protect credentials 149 | - Document in your CONTRIBUTING.md if you require Nancy to pass 150 | 151 | ### For Private/Enterprise Projects 152 | - **Strongly recommended** to set up authentication 153 | - Consider making Nancy a required check (remove `continue-on-error`) 154 | - Regular vulnerability scanning is a security best practice 155 | 156 | ### For Personal Projects 157 | - Authentication is optional 158 | - Gosec provides good security coverage without it 159 | - Enable Nancy if you want comprehensive dependency scanning 160 | 161 | ## Resources 162 | 163 | - [Nancy GitHub Repository](https://github.com/sonatype-nexus-community/nancy) 164 | - [OSS Index](https://ossindex.sonatype.org/) 165 | - [Sonatype Documentation](https://ossindex.sonatype.org/doc/rest) 166 | - [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) 167 | 168 | ## Summary 169 | 170 | Nancy is **optional** and **non-blocking** in this project's CI pipeline. The security-scan job will pass regardless of Nancy's status. If you want full vulnerability scanning, follow the setup steps above. Otherwise, Gosec provides excellent security scanning for your code. 171 | 172 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/joho/godotenv" 9 | "github.com/spf13/viper" 10 | 11 | _ "github.com/gjovanovicst/auth_api/docs" // docs is generated by Swag CLI 12 | "github.com/gjovanovicst/auth_api/internal/database" 13 | "github.com/gjovanovicst/auth_api/internal/email" 14 | logService "github.com/gjovanovicst/auth_api/internal/log" 15 | "github.com/gjovanovicst/auth_api/internal/middleware" 16 | "github.com/gjovanovicst/auth_api/internal/redis" 17 | "github.com/gjovanovicst/auth_api/internal/social" 18 | "github.com/gjovanovicst/auth_api/internal/twofa" 19 | "github.com/gjovanovicst/auth_api/internal/user" 20 | swaggerFiles "github.com/swaggo/files" 21 | ginSwagger "github.com/swaggo/gin-swagger" 22 | ) 23 | 24 | // @title Authentication and Authorization API 25 | // @version 1.1.0 26 | // @description This is a sample authentication and authorization API built with Go and Gin. 27 | // @termsOfService http://swagger.io/terms/ 28 | 29 | // @contact.name API Support 30 | // @contact.url http://www.swagger.io/support 31 | // @contact.email support@swagger.io 32 | 33 | // @license.name Apache 2.0 34 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 35 | 36 | // @host localhost:8080 37 | // @BasePath / 38 | 39 | // @securityDefinitions.apikey ApiKeyAuth 40 | // @in header 41 | // @name Authorization 42 | // @description Type "Bearer" + your JWT token 43 | 44 | func main() { 45 | // Load environment variables from .env file 46 | if err := godotenv.Load(); err != nil { 47 | log.Println("No .env file found, relying on environment variables") 48 | } 49 | 50 | // Initialize Viper for configuration management 51 | viper.AutomaticEnv() // Read environment variables 52 | viper.SetDefault("PORT", "8080") 53 | viper.SetDefault("ACCESS_TOKEN_EXPIRATION_MINUTES", 15) 54 | viper.SetDefault("REFRESH_TOKEN_EXPIRATION_HOURS", 720) 55 | 56 | // Connect to database 57 | database.ConnectDatabase() 58 | 59 | // Connect to Redis 60 | redis.ConnectRedis() 61 | 62 | // Run database migrations 63 | database.MigrateDatabase() 64 | 65 | // Initialize Activity Log Service 66 | logService.InitializeLogService() 67 | 68 | // Initialize Activity Log Cleanup Service 69 | cleanupService := logService.InitializeCleanupService(database.DB) 70 | if cleanupService != nil { 71 | // Ensure graceful shutdown of cleanup service 72 | defer cleanupService.Shutdown() 73 | } 74 | 75 | // Initialize Services and Handlers 76 | userRepo := user.NewRepository(database.DB) 77 | socialRepo := social.NewRepository(database.DB) 78 | logRepo := logService.NewRepository(database.DB) 79 | emailService := email.NewService() 80 | userService := user.NewService(userRepo, emailService) 81 | socialService := social.NewService(userRepo, socialRepo) 82 | twofaService := twofa.NewService(userRepo) 83 | logQueryService := logService.NewQueryService(logRepo) 84 | userHandler := user.NewHandler(userService) 85 | socialHandler := social.NewHandler(socialService) 86 | twofaHandler := twofa.NewHandler(twofaService) 87 | logHandler := logService.NewHandler(logQueryService) 88 | 89 | // Setup Gin Router 90 | r := gin.Default() 91 | 92 | // Add CORS middleware 93 | r.Use(middleware.CORSMiddleware()) 94 | 95 | // Public routes 96 | public := r.Group("/") 97 | { 98 | public.POST("/register", userHandler.Register) 99 | public.POST("/login", userHandler.Login) 100 | public.POST("/refresh-token", userHandler.RefreshToken) 101 | public.POST("/forgot-password", userHandler.ForgotPassword) 102 | public.POST("/reset-password", userHandler.ResetPassword) 103 | public.GET("/verify-email", userHandler.VerifyEmail) 104 | // 2FA login verification (public because it needs temp token) 105 | public.POST("/2fa/login-verify", twofaHandler.VerifyLogin) 106 | } 107 | 108 | // Social authentication routes 109 | auth := r.Group("/auth") 110 | { 111 | // Google OAuth2 112 | auth.GET("/google/login", socialHandler.GoogleLogin) 113 | auth.GET("/google/callback", socialHandler.GoogleCallback) 114 | 115 | // Facebook OAuth2 116 | auth.GET("/facebook/login", socialHandler.FacebookLogin) 117 | auth.GET("/facebook/callback", socialHandler.FacebookCallback) 118 | 119 | // GitHub OAuth2 120 | auth.GET("/github/login", socialHandler.GithubLogin) 121 | auth.GET("/github/callback", socialHandler.GithubCallback) 122 | } 123 | 124 | // Protected routes (require JWT authentication) 125 | protected := r.Group("/") 126 | protected.Use(middleware.AuthMiddleware()) 127 | { 128 | // User profile routes 129 | protected.GET("/profile", userHandler.GetProfile) 130 | protected.PUT("/profile", userHandler.UpdateProfile) 131 | protected.DELETE("/profile", userHandler.DeleteAccount) 132 | protected.PUT("/profile/email", userHandler.UpdateEmail) 133 | protected.PUT("/profile/password", userHandler.UpdatePassword) 134 | 135 | // Auth routes 136 | protected.GET("/auth/validate", userHandler.ValidateToken) 137 | protected.POST("/logout", userHandler.Logout) 138 | 139 | // 2FA management routes 140 | protected.POST("/2fa/generate", twofaHandler.Generate2FA) 141 | protected.POST("/2fa/verify-setup", twofaHandler.VerifySetup) 142 | protected.POST("/2fa/enable", twofaHandler.Enable2FA) 143 | protected.POST("/2fa/disable", twofaHandler.Disable2FA) 144 | protected.POST("/2fa/recovery-codes", twofaHandler.GenerateRecoveryCodes) 145 | 146 | // Activity log routes 147 | protected.GET("/activity-logs", logHandler.GetUserActivityLogs) 148 | protected.GET("/activity-logs/event-types", logHandler.GetEventTypes) 149 | protected.GET("/activity-logs/:id", logHandler.GetActivityLogByID) 150 | } 151 | 152 | // Admin routes (for future role-based access control) 153 | admin := r.Group("/admin") 154 | admin.Use(middleware.AuthMiddleware()) 155 | // TODO: Add admin role check middleware 156 | { 157 | admin.GET("/activity-logs", logHandler.GetAllActivityLogs) 158 | } 159 | 160 | // Add Swagger UI endpoint 161 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 162 | 163 | // Start the server 164 | port := viper.GetString("PORT") 165 | log.Printf("Server starting on port %s", port) 166 | if err := r.Run(fmt.Sprintf(":%s", port)); err != nil { 167 | log.Fatalf("Server failed to start: %v", err) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /internal/log/handler.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gjovanovicst/auth_api/pkg/dto" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type Handler struct { 12 | QueryService *QueryService 13 | } 14 | 15 | func NewHandler(queryService *QueryService) *Handler { 16 | return &Handler{QueryService: queryService} 17 | } 18 | 19 | // @Summary Get user activity logs 20 | // @Description Retrieve the authenticated user's activity logs with pagination and filtering 21 | // @Tags Activity Logs 22 | // @Security ApiKeyAuth 23 | // @Produce json 24 | // @Param page query int false "Page number (default: 1)" minimum(1) 25 | // @Param limit query int false "Items per page (default: 20, max: 100)" minimum(1) maximum(100) 26 | // @Param event_type query string false "Filter by event type" 27 | // @Param start_date query string false "Start date filter (YYYY-MM-DD)" 28 | // @Param end_date query string false "End date filter (YYYY-MM-DD)" 29 | // @Success 200 {object} dto.ActivityLogListResponse 30 | // @Failure 400 {object} dto.ErrorResponse 31 | // @Failure 401 {object} dto.ErrorResponse 32 | // @Failure 500 {object} dto.ErrorResponse 33 | // @Router /activity-logs [get] 34 | func (h *Handler) GetUserActivityLogs(c *gin.Context) { 35 | // Get user ID from authentication middleware 36 | userID, exists := c.Get("userID") 37 | if !exists { 38 | c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Error: "User ID not found in context"}) 39 | return 40 | } 41 | 42 | // Parse query parameters 43 | var req dto.ActivityLogListRequest 44 | if err := c.ShouldBindQuery(&req); err != nil { 45 | c.JSON(http.StatusBadRequest, dto.ErrorResponse{Error: err.Error()}) 46 | return 47 | } 48 | 49 | // Parse user ID 50 | userUUID, err := uuid.Parse(userID.(string)) 51 | if err != nil { 52 | c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Error: "Invalid user ID"}) 53 | return 54 | } 55 | 56 | // Get activity logs 57 | response, appErr := h.QueryService.ListUserActivityLogs(userUUID, req) 58 | if appErr != nil { 59 | c.JSON(appErr.Code, dto.ErrorResponse{Error: appErr.Message}) 60 | return 61 | } 62 | 63 | c.JSON(http.StatusOK, response) 64 | } 65 | 66 | // @Summary Get activity log by ID 67 | // @Description Retrieve a specific activity log by ID (users can only access their own logs) 68 | // @Tags Activity Logs 69 | // @Security ApiKeyAuth 70 | // @Produce json 71 | // @Param id path string true "Activity Log ID" 72 | // @Success 200 {object} dto.ActivityLogResponse 73 | // @Failure 400 {object} dto.ErrorResponse 74 | // @Failure 401 {object} dto.ErrorResponse 75 | // @Failure 403 {object} dto.ErrorResponse 76 | // @Failure 404 {object} dto.ErrorResponse 77 | // @Failure 500 {object} dto.ErrorResponse 78 | // @Router /activity-logs/{id} [get] 79 | func (h *Handler) GetActivityLogByID(c *gin.Context) { 80 | // Get user ID from authentication middleware 81 | userID, exists := c.Get("userID") 82 | if !exists { 83 | c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Error: "User ID not found in context"}) 84 | return 85 | } 86 | 87 | // Parse log ID from URL 88 | logIDStr := c.Param("id") 89 | logID, err := uuid.Parse(logIDStr) 90 | if err != nil { 91 | c.JSON(http.StatusBadRequest, dto.ErrorResponse{Error: "Invalid activity log ID"}) 92 | return 93 | } 94 | 95 | // Parse user ID 96 | userUUID, err := uuid.Parse(userID.(string)) 97 | if err != nil { 98 | c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Error: "Invalid user ID"}) 99 | return 100 | } 101 | 102 | // Get activity log 103 | response, appErr := h.QueryService.GetActivityLogByID(logID, userUUID) 104 | if appErr != nil { 105 | c.JSON(appErr.Code, dto.ErrorResponse{Error: appErr.Message}) 106 | return 107 | } 108 | 109 | c.JSON(http.StatusOK, response) 110 | } 111 | 112 | // @Summary Get all activity logs (Admin) 113 | // @Description Retrieve all users' activity logs with pagination and filtering (admin access required) 114 | // @Tags Activity Logs 115 | // @Security ApiKeyAuth 116 | // @Produce json 117 | // @Param page query int false "Page number (default: 1)" minimum(1) 118 | // @Param limit query int false "Items per page (default: 20, max: 100)" minimum(1) maximum(100) 119 | // @Param event_type query string false "Filter by event type" 120 | // @Param start_date query string false "Start date filter (YYYY-MM-DD)" 121 | // @Param end_date query string false "End date filter (YYYY-MM-DD)" 122 | // @Success 200 {object} dto.ActivityLogListResponse 123 | // @Failure 400 {object} dto.ErrorResponse 124 | // @Failure 401 {object} dto.ErrorResponse 125 | // @Failure 403 {object} dto.ErrorResponse 126 | // @Failure 500 {object} dto.ErrorResponse 127 | // @Router /admin/activity-logs [get] 128 | func (h *Handler) GetAllActivityLogs(c *gin.Context) { 129 | // Note: In a real application, you would check for admin role here 130 | // For now, this is a placeholder for future role-based access control 131 | 132 | // Parse query parameters 133 | var req dto.ActivityLogListRequest 134 | if err := c.ShouldBindQuery(&req); err != nil { 135 | c.JSON(http.StatusBadRequest, dto.ErrorResponse{Error: err.Error()}) 136 | return 137 | } 138 | 139 | // Get activity logs 140 | response, appErr := h.QueryService.ListAllActivityLogs(req) 141 | if appErr != nil { 142 | c.JSON(appErr.Code, dto.ErrorResponse{Error: appErr.Message}) 143 | return 144 | } 145 | 146 | c.JSON(http.StatusOK, response) 147 | } 148 | 149 | // @Summary Get available event types 150 | // @Description Retrieve list of available activity log event types for filtering 151 | // @Tags Activity Logs 152 | // @Security ApiKeyAuth 153 | // @Produce json 154 | // @Success 200 {object} map[string][]string 155 | // @Failure 401 {object} dto.ErrorResponse 156 | // @Router /activity-logs/event-types [get] 157 | func (h *Handler) GetEventTypes(c *gin.Context) { 158 | eventTypes := []string{ 159 | EventLogin, 160 | EventLogout, 161 | EventRegister, 162 | EventPasswordChange, 163 | EventPasswordReset, 164 | EventEmailVerify, 165 | EventEmailChange, 166 | Event2FAEnable, 167 | Event2FADisable, 168 | Event2FALogin, 169 | EventTokenRefresh, 170 | EventSocialLogin, 171 | EventProfileAccess, 172 | EventProfileUpdate, 173 | EventAccountDeletion, 174 | EventRecoveryCodeUsed, 175 | EventRecoveryCodeGen, 176 | } 177 | 178 | c.JSON(http.StatusOK, gin.H{ 179 | "event_types": eventTypes, 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /pkg/dto/auth.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // RegisterRequest represents the request payload for user registration 4 | type RegisterRequest struct { 5 | Email string `json:"email" validate:"required,email"` 6 | Password string `json:"password" validate:"required,min=8"` 7 | } 8 | 9 | // LoginRequest represents the request payload for user login 10 | type LoginRequest struct { 11 | Email string `json:"email" validate:"required,email"` 12 | Password string `json:"password" validate:"required"` 13 | } 14 | 15 | // RefreshTokenRequest represents the request payload for token refresh 16 | type RefreshTokenRequest struct { 17 | RefreshToken string `json:"refresh_token" validate:"required"` 18 | } 19 | 20 | // LogoutRequest represents the request payload for user logout 21 | type LogoutRequest struct { 22 | RefreshToken string `json:"refresh_token" validate:"required"` 23 | AccessToken string `json:"access_token" validate:"required"` 24 | } 25 | 26 | // ForgotPasswordRequest represents the request payload for forgot password 27 | type ForgotPasswordRequest struct { 28 | Email string `json:"email" validate:"required,email"` 29 | } 30 | 31 | // ResetPasswordRequest represents the request payload for password reset 32 | type ResetPasswordRequest struct { 33 | Token string `json:"token" validate:"required"` 34 | NewPassword string `json:"new_password" validate:"required,min=8"` 35 | } 36 | 37 | // LoginResponse represents the response payload for successful login 38 | type LoginResponse struct { 39 | AccessToken string `json:"access_token"` 40 | RefreshToken string `json:"refresh_token"` 41 | } 42 | 43 | // TwoFARequiredResponse represents response when 2FA is required during login 44 | type TwoFARequiredResponse struct { 45 | Message string `json:"message"` 46 | TempToken string `json:"temp_token"` 47 | } 48 | 49 | // TwoFAVerifyRequest represents the request payload for TOTP verification 50 | type TwoFAVerifyRequest struct { 51 | Code string `json:"code" validate:"required"` 52 | } 53 | 54 | // TwoFALoginRequest represents the request payload for 2FA login verification 55 | type TwoFALoginRequest struct { 56 | TempToken string `json:"temp_token" validate:"required"` 57 | Code string `json:"code,omitempty"` 58 | RecoveryCode string `json:"recovery_code,omitempty"` 59 | } 60 | 61 | // TwoFADisableRequest represents the request payload for disabling 2FA 62 | type TwoFADisableRequest struct { 63 | Code string `json:"code" validate:"required"` 64 | } 65 | 66 | // TwoFAEnableResponse represents the response when 2FA is enabled 67 | type TwoFAEnableResponse struct { 68 | Message string `json:"message"` 69 | RecoveryCodes []string `json:"recovery_codes"` 70 | } 71 | 72 | // TwoFARecoveryCodesResponse represents the response for new recovery codes 73 | type TwoFARecoveryCodesResponse struct { 74 | Message string `json:"message"` 75 | RecoveryCodes []string `json:"recovery_codes"` 76 | } 77 | 78 | // SocialAccountResponse represents social account data in user profile 79 | type SocialAccountResponse struct { 80 | ID string `json:"id"` 81 | Provider string `json:"provider"` 82 | ProviderUserID string `json:"provider_user_id"` 83 | Email string `json:"email,omitempty"` 84 | Name string `json:"name,omitempty"` 85 | FirstName string `json:"first_name,omitempty"` 86 | LastName string `json:"last_name,omitempty"` 87 | ProfilePicture string `json:"profile_picture,omitempty"` 88 | Username string `json:"username,omitempty"` 89 | Locale string `json:"locale,omitempty"` 90 | CreatedAt string `json:"created_at"` 91 | UpdatedAt string `json:"updated_at"` 92 | } 93 | 94 | // UserResponse represents the user data in responses 95 | type UserResponse struct { 96 | ID string `json:"id"` 97 | Email string `json:"email"` 98 | EmailVerified bool `json:"email_verified"` 99 | Name string `json:"name,omitempty"` 100 | FirstName string `json:"first_name,omitempty"` 101 | LastName string `json:"last_name,omitempty"` 102 | ProfilePicture string `json:"profile_picture,omitempty"` 103 | Locale string `json:"locale,omitempty"` 104 | TwoFAEnabled bool `json:"two_fa_enabled"` 105 | CreatedAt string `json:"created_at"` 106 | UpdatedAt string `json:"updated_at"` 107 | SocialAccounts []SocialAccountResponse `json:"social_accounts,omitempty"` 108 | } 109 | 110 | // ErrorResponse represents a standard error response 111 | type ErrorResponse struct { 112 | Error string `json:"error"` 113 | } 114 | 115 | // MessageResponse represents a standard message response 116 | type MessageResponse struct { 117 | Message string `json:"message"` 118 | } 119 | 120 | // UpdateProfileRequest represents the request payload for profile update 121 | type UpdateProfileRequest struct { 122 | Name string `json:"name,omitempty" validate:"omitempty,min=1,max=100" example:"John Doe"` 123 | FirstName string `json:"first_name,omitempty" validate:"omitempty,min=1,max=50" example:"John"` 124 | LastName string `json:"last_name,omitempty" validate:"omitempty,min=1,max=50" example:"Doe"` 125 | ProfilePicture string `json:"profile_picture,omitempty" validate:"omitempty,url" example:"https://example.com/avatar.jpg"` 126 | Locale string `json:"locale,omitempty" validate:"omitempty,min=2,max=10" example:"en-US"` 127 | } 128 | 129 | // UpdateEmailRequest represents the request payload for email update 130 | type UpdateEmailRequest struct { 131 | Email string `json:"email" validate:"required,email" example:"newemail@example.com"` 132 | Password string `json:"password" validate:"required" example:"currentpassword123"` 133 | } 134 | 135 | // UpdatePasswordRequest represents the request payload for password update 136 | type UpdatePasswordRequest struct { 137 | CurrentPassword string `json:"current_password" validate:"required" example:"oldpassword123"` 138 | NewPassword string `json:"new_password" validate:"required,min=8" example:"newpassword123"` 139 | } 140 | 141 | // DeleteAccountRequest represents the request payload for account deletion 142 | type DeleteAccountRequest struct { 143 | Password string `json:"password" validate:"required" example:"password123"` 144 | ConfirmDeletion bool `json:"confirm_deletion" validate:"required,eq=true" example:"true"` 145 | } 146 | -------------------------------------------------------------------------------- /docs/implementation_phases/Phase_6_Testing_and_Deployment_Strategy.md: -------------------------------------------------------------------------------- 1 | ## Phase 7: Testing and Deployment Strategy 2 | 3 | This phase outlines the testing and deployment strategies for the GoLang authentication and authorization RESTful API. Comprehensive testing ensures the reliability, security, and performance of the application, while a well-defined deployment strategy facilitates efficient and consistent deployment to production environments. 4 | 5 | ### 7.1 Testing Strategy 6 | 7 | Testing will be conducted at multiple levels to ensure the quality and correctness of the API. 8 | 9 | 1. **Unit Tests:** 10 | - **Purpose:** To test individual functions, methods, and components in isolation. 11 | - **Scope:** Business logic (services), utility functions (e.g., password hashing, JWT generation/parsing), and repository methods. 12 | - **Tools:** Go's built-in `testing` package. 13 | - **Best Practices:** 14 | - Write tests for all critical paths and edge cases. 15 | - Use mock objects or interfaces for external dependencies (e.g., database, Redis, email service) to ensure true isolation. 16 | - Ensure high code coverage for core logic. 17 | 18 | 2. **Integration Tests:** 19 | - **Purpose:** To test the interaction between different components (e.g., handler-service-repository-database flow). 20 | - **Scope:** API endpoints, database interactions, Redis interactions, and social OAuth flows. 21 | - **Tools:** Go's `testing` package, `httptest` for HTTP requests, and potentially a test database (e.g., Dockerized PostgreSQL and Redis for testing). 22 | - **Best Practices:** 23 | - Use a clean test database for each test run to ensure repeatable results. 24 | - Test full request-response cycles for API endpoints. 25 | - Verify data persistence and retrieval. 26 | 27 | 3. **End-to-End (E2E) Tests (Optional but Recommended):** 28 | - **Purpose:** To simulate real user scenarios and test the entire application flow from client to server. 29 | - **Scope:** Full authentication and authorization flows, including social logins and email verification. 30 | - **Tools:** Could involve a separate testing framework or custom Go scripts that interact with the deployed API. 31 | - **Best Practices:** 32 | - Test critical user journeys. 33 | - Ensure all components are working together correctly. 34 | 35 | 4. **Security Testing:** 36 | - **Purpose:** To identify vulnerabilities and ensure the API is secure against common attacks. 37 | - **Scope:** Authentication mechanisms, authorization checks, input validation, token handling. 38 | - **Methods:** Manual penetration testing, automated security scanners, code reviews for security best practices. 39 | - **Focus Areas:** JWT validation, password hashing, OAuth2 state parameter validation, rate limiting, email verification token validity. 40 | 41 | ### 7.2 Deployment Strategy 42 | 43 | The API will be containerized using Docker for consistent and portable deployment across different environments. Kubernetes or a similar container orchestration platform is recommended for production deployment. 44 | 45 | 1. **Dockerization:** 46 | - A `Dockerfile` will be created to build a lightweight Docker image of the Go application. 47 | - The Dockerfile will include steps for building the Go binary, copying necessary files, and setting up the entry point. 48 | - **Example `Dockerfile`:** 49 | ```dockerfile 50 | # Use the official Golang image as a base image 51 | FROM golang:1.22-alpine AS builder 52 | 53 | # Set the current working directory inside the container 54 | WORKDIR /app 55 | 56 | # Copy go.mod and go.sum files and download dependencies 57 | COPY go.mod go.sum ./ 58 | RUN go mod download 59 | 60 | # Copy the source code into the container 61 | COPY . . 62 | 63 | # Build the Go application 64 | RUN go build -o /go-auth-api ./cmd/api 65 | 66 | # Use a minimal image for the final stage 67 | FROM alpine:latest 68 | 69 | # Install ca-certificates for HTTPS connections 70 | RUN apk --no-cache add ca-certificates 71 | 72 | # Set the current working directory inside the container 73 | WORKDIR /root/ 74 | 75 | # Copy the built binary from the builder stage 76 | COPY --from=builder /go-auth-api . 77 | 78 | # Expose the port the application runs on 79 | EXPOSE 8080 80 | 81 | # Run the application 82 | CMD ["./go-auth-api"] 83 | ``` 84 | 85 | 2. **Environment Configuration:** 86 | - All sensitive configurations (database credentials, API keys, JWT secrets, Redis connection details) will be managed via environment variables. 87 | - For local development, a `.env` file can be used with `godotenv`. 88 | - For production, environment variables will be injected by the deployment environment (e.g., Kubernetes Secrets, Docker Compose `.env` files, CI/CD pipelines). 89 | 90 | 3. **Database and Redis Setup:** 91 | - PostgreSQL and Redis instances will be deployed separately from the Go application. 92 | - For development, Docker Compose can be used to spin up local instances of PostgreSQL and Redis. 93 | - For production, managed services (e.g., AWS RDS, Google Cloud SQL for PostgreSQL; AWS ElastiCache, Google Cloud Memorystore for Redis) are recommended for scalability, reliability, and ease of management. 94 | 95 | 4. **CI/CD Pipeline (Conceptual):** 96 | - Automate the build, test, and deployment process using a CI/CD pipeline (e.g., GitHub Actions, GitLab CI/CD, Jenkins). 97 | - **Steps:** 98 | - **Code Commit:** Developer pushes code to the repository. 99 | - **Build:** CI/CD pipeline triggers, builds the Docker image. 100 | - **Test:** Runs unit and integration tests. 101 | - **Image Push:** If tests pass, pushes the Docker image to a container registry (e.g., Docker Hub, Google Container Registry). 102 | - **Deployment:** Deploys the new image to the staging or production environment (e.g., using Kubernetes manifests or Docker Compose files). 103 | 104 | 5. **Monitoring and Logging:** 105 | - Integrate logging libraries (e.g., Go's `log` package, or a more advanced structured logger like `logrus` or `zap`) to capture application logs. 106 | - Forward logs to a centralized logging system (e.g., ELK stack, Grafana Loki, cloud logging services). 107 | - Implement monitoring for API performance, error rates, and resource utilization (e.g., Prometheus and Grafana). 108 | 109 | This comprehensive testing and deployment strategy ensures that the API is robust, secure, and ready for production use. 110 | 111 | -------------------------------------------------------------------------------- /.cursor/rules/code-patterns.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Code patterns 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Go Code Patterns and Conventions 7 | 8 | ## Architecture Pattern: Repository-Service-Handler 9 | 10 | ### Repository Layer 11 | ```go 12 | type Repository interface { 13 | Create(entity *Entity) error 14 | GetByID(id uint) (*Entity, error) 15 | Update(entity *Entity) error 16 | Delete(id uint) error 17 | } 18 | 19 | type repository struct { 20 | db *gorm.DB 21 | } 22 | 23 | func NewRepository(db *gorm.DB) Repository { 24 | return &repository{db: db} 25 | } 26 | ``` 27 | 28 | ### Service Layer 29 | ```go 30 | type Service interface { 31 | BusinessOperation(input Input) (*Output, error) 32 | } 33 | 34 | type service struct { 35 | repo Repository 36 | // other dependencies 37 | } 38 | 39 | func NewService(repo Repository) Service { 40 | return &service{repo: repo} 41 | } 42 | ``` 43 | 44 | ### Handler Layer 45 | ```go 46 | type Handler struct { 47 | service Service 48 | } 49 | 50 | func NewHandler(service Service) *Handler { 51 | return &Handler{service: service} 52 | } 53 | 54 | // @Summary Operation description 55 | // @Router /endpoint [method] 56 | func (h *Handler) HandlerMethod(c *gin.Context) { 57 | // Request binding 58 | // Validation 59 | // Service call 60 | // Response 61 | } 62 | ``` 63 | 64 | ## Dependency Injection Pattern 65 | 66 | ### Constructor Functions 67 | All components use constructor functions with dependency injection: 68 | ```go 69 | func NewService(repo Repository, emailService EmailService) Service { 70 | return &service{ 71 | repo: repo, 72 | emailService: emailService, 73 | } 74 | } 75 | ``` 76 | 77 | ### Main Function Organization 78 | In [cmd/api/main.go](mdc:cmd/api/main.go): 79 | 1. Initialize infrastructure (DB, Redis) 80 | 2. Create repositories 81 | 3. Create services with dependencies 82 | 4. Create handlers 83 | 5. Setup routes 84 | 85 | ## Error Handling Patterns 86 | 87 | ### Custom Error Types 88 | Define in `pkg/errors/`: 89 | ```go 90 | type AuthError struct { 91 | Code string 92 | Message string 93 | Err error 94 | } 95 | 96 | func (e *AuthError) Error() string { 97 | return e.Message 98 | } 99 | ``` 100 | 101 | ### Error Response Pattern 102 | ```go 103 | if err != nil { 104 | c.JSON(http.StatusBadRequest, gin.H{ 105 | "success": false, 106 | "error": err.Error(), 107 | }) 108 | return 109 | } 110 | ``` 111 | 112 | ### Success Response Pattern 113 | ```go 114 | c.JSON(http.StatusOK, gin.H{ 115 | "success": true, 116 | "data": result, 117 | }) 118 | ``` 119 | 120 | ## Database Patterns 121 | 122 | ### GORM Models 123 | Located in `pkg/models/`: 124 | ```go 125 | type User struct { 126 | ID uint `gorm:"primaryKey" json:"id"` 127 | Email string `gorm:"unique;not null" json:"email"` 128 | Password string `gorm:"not null" json:"-"` 129 | CreatedAt time.Time `json:"created_at"` 130 | UpdatedAt time.Time `json:"updated_at"` 131 | } 132 | ``` 133 | 134 | ### Repository Methods 135 | ```go 136 | func (r *repository) GetByEmail(email string) (*models.User, error) { 137 | var user models.User 138 | if err := r.db.Where("email = ?", email).First(&user).Error; err != nil { 139 | return nil, err 140 | } 141 | return &user, nil 142 | } 143 | ``` 144 | 145 | ## DTO (Data Transfer Object) Patterns 146 | 147 | ### Request DTOs 148 | ```go 149 | type LoginRequest struct { 150 | Email string `json:"email" validate:"required,email" example:"user@example.com"` 151 | Password string `json:"password" validate:"required,min=8" example:"password123"` 152 | } 153 | ``` 154 | 155 | ### Response DTOs 156 | ```go 157 | type UserResponse struct { 158 | ID uint `json:"id" example:"1"` 159 | Email string `json:"email" example:"user@example.com"` 160 | CreatedAt string `json:"created_at" example:"2023-01-01T00:00:00Z"` 161 | } 162 | ``` 163 | 164 | ## Swagger Documentation Pattern 165 | 166 | ### Handler Annotations 167 | ```go 168 | // @Summary User login 169 | // @Description Authenticate user with email and password 170 | // @Tags authentication 171 | // @Accept json 172 | // @Produce json 173 | // @Param request body dto.LoginRequest true "Login credentials" 174 | // @Success 200 {object} dto.APIResponse{data=dto.LoginResponse} 175 | // @Failure 400 {object} dto.APIResponse 176 | // @Router /login [post] 177 | ``` 178 | 179 | ## Configuration Pattern 180 | 181 | ### Environment Variables 182 | Using Viper in [cmd/api/main.go](mdc:cmd/api/main.go): 183 | ```go 184 | viper.AutomaticEnv() 185 | viper.SetDefault("PORT", "8080") 186 | viper.SetDefault("ACCESS_TOKEN_EXPIRATION_MINUTES", 15) 187 | ``` 188 | 189 | ## Testing Patterns 190 | 191 | ### Table-Driven Tests 192 | ```go 193 | func TestService_Method(t *testing.T) { 194 | tests := []struct { 195 | name string 196 | input Input 197 | want Output 198 | wantErr bool 199 | }{ 200 | // test cases 201 | } 202 | 203 | for _, tt := range tests { 204 | t.Run(tt.name, func(t *testing.T) { 205 | // test implementation 206 | }) 207 | } 208 | } 209 | ``` 210 | 211 | ### Mock Interfaces 212 | Use interfaces for all dependencies to enable mocking in tests. 213 | 214 | ## Security Code Patterns 215 | 216 | ### Password Hashing 217 | ```go 218 | func HashPassword(password string) (string, error) { 219 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 220 | return string(bytes), err 221 | } 222 | ``` 223 | 224 | ### JWT Token Generation 225 | ```go 226 | func GenerateToken(userID uint, tokenType string) (string, error) { 227 | claims := jwt.MapClaims{ 228 | "user_id": userID, 229 | "type": tokenType, 230 | "exp": time.Now().Add(expiration).Unix(), 231 | } 232 | // token generation logic 233 | } 234 | ``` 235 | 236 | ## Activity Logging Pattern 237 | 238 | ### Log Service Usage 239 | ```go 240 | logService.LogActivity(logService.ActivityLogParams{ 241 | UserID: userID, 242 | EventType: "login_success", 243 | Description: "User logged in successfully", 244 | IPAddress: c.ClientIP(), 245 | UserAgent: c.GetHeader("User-Agent"), 246 | }) 247 | ``` 248 | 249 | ## File Organization Rules 250 | 251 | ### Package Structure 252 | - One responsibility per package 253 | - Internal packages in `internal/` 254 | - Shared packages in `pkg/` 255 | - Feature-based organization 256 | 257 | ### File Naming 258 | - `handler.go` for HTTP handlers 259 | - `service.go` for business logic 260 | - `repository.go` for data access 261 | - `models.go` for database models 262 | - `dto.go` for data transfer objects 263 | 264 | -------------------------------------------------------------------------------- /docs/features/SMART_LOGGING_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Smart Activity Logging - Implementation Summary 2 | 3 | ## ✅ Completed Implementation 4 | 5 | Your authentication API now has a **professional, production-ready activity logging system** that reduced your database by **98.13%**! 6 | 7 | --- 8 | 9 | ## 📊 Results Achieved 10 | 11 | ### Before Smart Logging: 12 | - **91,485 logs** in 4 months 13 | - **95% were PROFILE_ACCESS** (noise) 14 | - **3% were TOKEN_REFRESH** (noise) 15 | - Database growing rapidly 16 | - Hard to find security events 17 | 18 | ### After Smart Logging: 19 | - **1,712 critical logs** remaining (98.13% reduction!) 20 | - **0 PROFILE_ACCESS logs** (all deleted) 21 | - **0 TOKEN_REFRESH logs** (all deleted) 22 | - Only security-relevant events remain 23 | - Automatic cleanup every 24 hours 24 | 25 | --- 26 | 27 | ## 🎯 What Changed 28 | 29 | ### 1. Event Categorization 30 | Events now classified by importance: 31 | - **Critical** (365 days): LOGIN, PASSWORD_CHANGE, 2FA changes 32 | - **Important** (180 days): EMAIL_VERIFY, SOCIAL_LOGIN 33 | - **Informational** (90 days): TOKEN_REFRESH, PROFILE_ACCESS (disabled) 34 | 35 | ### 2. Anomaly Detection 36 | High-frequency events only logged when unusual: 37 | - New IP address detected 38 | - New device/browser detected 39 | - Unusual access patterns 40 | 41 | ### 3. Automatic Cleanup 42 | Background service runs daily: 43 | - Deletes expired logs automatically 44 | - Batch processing (no table locks) 45 | - Graceful shutdown handling 46 | 47 | ### 4. Smart Defaults 48 | Works perfectly out-of-the-box: 49 | - TOKEN_REFRESH: Disabled (would create 1M+ logs/month) 50 | - PROFILE_ACCESS: Disabled (would create 500K+ logs/month) 51 | - Anomaly detection: Enabled 52 | - Cleanup: Runs daily 53 | 54 | --- 55 | 56 | ## 📁 Files Created 57 | 58 | ### Core Implementation 59 | 1. `internal/config/logging.go` - Configuration system 60 | 2. `internal/log/anomaly.go` - Anomaly detection engine 61 | 3. `internal/log/cleanup.go` - Automatic cleanup service 62 | 63 | ### Database 64 | 4. `migrations/20240103_add_activity_log_smart_fields.sql` - Migration 65 | 5. `migrations/20240103_add_activity_log_smart_fields_rollback.sql` - Rollback 66 | 6. `migrations/README_SMART_LOGGING.md` - Migration guide 67 | 68 | ### Documentation 69 | 7. `docs/ACTIVITY_LOGGING_GUIDE.md` - Complete guide 70 | 8. `docs/ENV_VARIABLES.md` - All variables 71 | 9. `docs/SMART_LOGGING_IMPLEMENTATION.md` - Implementation details 72 | 10. `docs/SMART_LOGGING_QUICK_REFERENCE.md` - Quick reference 73 | 11. `docs/QUICK_SETUP_LOGGING.md` - Quick .env setup 74 | 12. `COPY_TO_ENV.txt` - Copy-paste for .env 75 | 13. `IMPLEMENTATION_COMPLETE.md` - Completion summary 76 | 14. `SMART_LOGGING_SUMMARY.md` - This file 77 | 78 | ### Utilities 79 | 15. `scripts/cleanup_activity_logs.sql` - Manual cleanup SQL 80 | 16. `scripts/cleanup_activity_logs.sh` - Interactive cleanup script 81 | 82 | ### Updated Files 83 | 17. `pkg/models/activity_log.go` - Added severity, expires_at, is_anomaly 84 | 18. `internal/log/service.go` - Smart logging logic 85 | 19. `cmd/api/main.go` - Cleanup service integration 86 | 20. `docs/API.md` - Updated documentation 87 | 21. `README.md` - Updated features 88 | 22. `CHANGELOG.md` - Version history 89 | 90 | --- 91 | 92 | ## 🚀 Running Status 93 | 94 | **Application is LIVE with smart logging!** 95 | 96 | ``` 97 | ✅ Database connected 98 | ✅ Redis connected 99 | ✅ Migrations applied 100 | ✅ Cleanup service initialized (interval: 24h) 101 | ✅ Cleanup worker started 102 | ✅ Server running on port 8080 103 | ✅ Initial cleanup completed (35 logs deleted) 104 | ``` 105 | 106 | --- 107 | 108 | ## 📝 Configuration 109 | 110 | ### Quick Setup (Optional) 111 | 112 | Add to your `.env` file: 113 | ```bash 114 | LOG_CLEANUP_ENABLED=true 115 | LOG_CLEANUP_INTERVAL=24h 116 | LOG_ANOMALY_DETECTION_ENABLED=true 117 | LOG_RETENTION_CRITICAL=365 118 | LOG_RETENTION_IMPORTANT=180 119 | LOG_RETENTION_INFORMATIONAL=90 120 | ``` 121 | 122 | ### No Configuration Needed! 123 | The system works perfectly with defaults. Only configure if you need to customize. 124 | 125 | --- 126 | 127 | ## 🎓 Key Learnings 128 | 129 | ### What We Discovered 130 | 1. **95% of logs were PROFILE_ACCESS** - pure noise 131 | 2. **3% were TOKEN_REFRESH** - also noise 132 | 3. Only **2% were actual security events** - the signal we need 133 | 134 | ### The Solution 135 | - Disable high-frequency events by default 136 | - Use anomaly detection to catch important patterns 137 | - Automatic cleanup based on event importance 138 | - Result: 98% less data, 100% of security value 139 | 140 | --- 141 | 142 | ## 🔮 Future Recommendations 143 | 144 | ### When to Adjust Settings 145 | 146 | **High Security Environment:** 147 | ```bash 148 | LOG_TOKEN_REFRESH=true 149 | LOG_SAMPLE_TOKEN_REFRESH=0.1 # Log 10% 150 | LOG_RETENTION_CRITICAL=730 # 2 years 151 | ``` 152 | 153 | **High Traffic / Low Storage:** 154 | ```bash 155 | LOG_RETENTION_CRITICAL=180 156 | LOG_CLEANUP_INTERVAL=12h 157 | ``` 158 | 159 | **Development:** 160 | ```bash 161 | LOG_TOKEN_REFRESH=true 162 | LOG_PROFILE_ACCESS=true 163 | LOG_CLEANUP_ENABLED=false 164 | ``` 165 | 166 | --- 167 | 168 | ## 📞 Support 169 | 170 | ### Documentation 171 | - **Quick Start**: `COPY_TO_ENV.txt` 172 | - **Setup Guide**: `docs/QUICK_SETUP_LOGGING.md` 173 | - **Full Guide**: `docs/ACTIVITY_LOGGING_GUIDE.md` 174 | - **Reference**: `docs/SMART_LOGGING_QUICK_REFERENCE.md` 175 | 176 | ### Code 177 | - Configuration: `internal/config/logging.go` 178 | - Anomaly Detection: `internal/log/anomaly.go` 179 | - Cleanup Service: `internal/log/cleanup.go` 180 | - Model: `pkg/models/activity_log.go` 181 | 182 | --- 183 | 184 | ## ✨ Success Metrics 185 | 186 | | Metric | Achievement | 187 | |--------|-------------| 188 | | Database Reduction | **98.13%** ✅ | 189 | | Logs Deleted | **89,773** ✅ | 190 | | Remaining Logs | **1,712** ✅ | 191 | | Security Events Kept | **100%** ✅ | 192 | | Noise Removed | **99.98%** ✅ | 193 | | Auto-Cleanup | **Active** ✅ | 194 | | Zero Config Needed | **Yes** ✅ | 195 | 196 | --- 197 | 198 | ## 🎉 Conclusion 199 | 200 | Your authentication API now has **enterprise-grade activity logging** that: 201 | 202 | ✅ **Reduces database bloat by 98%** 203 | ✅ **Maintains complete security audit trail** 204 | ✅ **Detects suspicious patterns automatically** 205 | ✅ **Cleans itself up automatically** 206 | ✅ **Works perfectly with zero configuration** 207 | ✅ **Is fully customizable when needed** 208 | 209 | **Status: PRODUCTION READY** 🚀 210 | 211 | --- 212 | 213 | *Implementation completed: December 3, 2025* 214 | *Total implementation time: ~3 hours* 215 | *Lines of code: 2,500+* 216 | *Documentation pages: 14* 217 | *Database reduction: 98.13%* 218 | *Test status: All working ✅* 219 | 220 | -------------------------------------------------------------------------------- /internal/log/query_service.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "time" 7 | 8 | "github.com/gjovanovicst/auth_api/pkg/dto" 9 | "github.com/gjovanovicst/auth_api/pkg/errors" 10 | "github.com/gjovanovicst/auth_api/pkg/models" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | type QueryService struct { 15 | Repo *Repository 16 | } 17 | 18 | func NewQueryService(repo *Repository) *QueryService { 19 | return &QueryService{Repo: repo} 20 | } 21 | 22 | // ListUserActivityLogs retrieves activity logs for a specific user with pagination and filtering 23 | func (s *QueryService) ListUserActivityLogs(userID uuid.UUID, req dto.ActivityLogListRequest) (*dto.ActivityLogListResponse, *errors.AppError) { 24 | // Set default values 25 | if req.Page <= 0 { 26 | req.Page = 1 27 | } 28 | if req.Limit <= 0 { 29 | req.Limit = 20 // Default page size 30 | } 31 | if req.Limit > 100 { 32 | req.Limit = 100 // Maximum page size 33 | } 34 | 35 | // Parse date filters 36 | var startDate, endDate *time.Time 37 | var err error 38 | 39 | if req.StartDate != "" { 40 | if parsed, parseErr := time.Parse("2006-01-02", req.StartDate); parseErr != nil { 41 | return nil, errors.NewAppError(errors.ErrBadRequest, "Invalid start_date format. Use YYYY-MM-DD") 42 | } else { 43 | startDate = &parsed 44 | } 45 | } 46 | 47 | if req.EndDate != "" { 48 | if parsed, parseErr := time.Parse("2006-01-02", req.EndDate); parseErr != nil { 49 | return nil, errors.NewAppError(errors.ErrBadRequest, "Invalid end_date format. Use YYYY-MM-DD") 50 | } else { 51 | endDate = &parsed 52 | } 53 | } 54 | 55 | // Validate date range 56 | if startDate != nil && endDate != nil && startDate.After(*endDate) { 57 | return nil, errors.NewAppError(errors.ErrBadRequest, "start_date cannot be after end_date") 58 | } 59 | 60 | // Get logs from repository 61 | logs, totalCount, err := s.Repo.ListUserActivityLogs(userID, req.Page, req.Limit, req.EventType, startDate, endDate) 62 | if err != nil { 63 | return nil, errors.NewAppError(errors.ErrInternal, "Failed to retrieve activity logs") 64 | } 65 | 66 | // Convert to response format 67 | responseData := make([]dto.ActivityLogResponse, len(logs)) 68 | for i, log := range logs { 69 | responseData[i] = s.convertToResponse(log) 70 | } 71 | 72 | // Calculate pagination metadata 73 | totalPages := int(math.Ceil(float64(totalCount) / float64(req.Limit))) 74 | hasNext := req.Page < totalPages 75 | hasPrevious := req.Page > 1 76 | 77 | pagination := dto.PaginationResponse{ 78 | Page: req.Page, 79 | Limit: req.Limit, 80 | TotalRecords: totalCount, 81 | TotalPages: totalPages, 82 | HasNext: hasNext, 83 | HasPrevious: hasPrevious, 84 | } 85 | 86 | return &dto.ActivityLogListResponse{ 87 | Data: responseData, 88 | Pagination: pagination, 89 | }, nil 90 | } 91 | 92 | // ListAllActivityLogs retrieves activity logs for all users (admin functionality) 93 | func (s *QueryService) ListAllActivityLogs(req dto.ActivityLogListRequest) (*dto.ActivityLogListResponse, *errors.AppError) { 94 | // Set default values 95 | if req.Page <= 0 { 96 | req.Page = 1 97 | } 98 | if req.Limit <= 0 { 99 | req.Limit = 20 // Default page size 100 | } 101 | if req.Limit > 100 { 102 | req.Limit = 100 // Maximum page size 103 | } 104 | 105 | // Parse date filters 106 | var startDate, endDate *time.Time 107 | var err error 108 | 109 | if req.StartDate != "" { 110 | if parsed, parseErr := time.Parse("2006-01-02", req.StartDate); parseErr != nil { 111 | return nil, errors.NewAppError(errors.ErrBadRequest, "Invalid start_date format. Use YYYY-MM-DD") 112 | } else { 113 | startDate = &parsed 114 | } 115 | } 116 | 117 | if req.EndDate != "" { 118 | if parsed, parseErr := time.Parse("2006-01-02", req.EndDate); parseErr != nil { 119 | return nil, errors.NewAppError(errors.ErrBadRequest, "Invalid end_date format. Use YYYY-MM-DD") 120 | } else { 121 | endDate = &parsed 122 | } 123 | } 124 | 125 | // Validate date range 126 | if startDate != nil && endDate != nil && startDate.After(*endDate) { 127 | return nil, errors.NewAppError(errors.ErrBadRequest, "start_date cannot be after end_date") 128 | } 129 | 130 | // Get logs from repository 131 | logs, totalCount, err := s.Repo.ListAllActivityLogs(req.Page, req.Limit, req.EventType, startDate, endDate) 132 | if err != nil { 133 | return nil, errors.NewAppError(errors.ErrInternal, "Failed to retrieve activity logs") 134 | } 135 | 136 | // Convert to response format 137 | responseData := make([]dto.ActivityLogResponse, len(logs)) 138 | for i, log := range logs { 139 | responseData[i] = s.convertToResponse(log) 140 | } 141 | 142 | // Calculate pagination metadata 143 | totalPages := int(math.Ceil(float64(totalCount) / float64(req.Limit))) 144 | hasNext := req.Page < totalPages 145 | hasPrevious := req.Page > 1 146 | 147 | pagination := dto.PaginationResponse{ 148 | Page: req.Page, 149 | Limit: req.Limit, 150 | TotalRecords: totalCount, 151 | TotalPages: totalPages, 152 | HasNext: hasNext, 153 | HasPrevious: hasPrevious, 154 | } 155 | 156 | return &dto.ActivityLogListResponse{ 157 | Data: responseData, 158 | Pagination: pagination, 159 | }, nil 160 | } 161 | 162 | // GetActivityLogByID retrieves a specific activity log by ID 163 | func (s *QueryService) GetActivityLogByID(id uuid.UUID, requestingUserID uuid.UUID) (*dto.ActivityLogResponse, *errors.AppError) { 164 | log, err := s.Repo.GetActivityLogByID(id) 165 | if err != nil { 166 | return nil, errors.NewAppError(errors.ErrNotFound, "Activity log not found") 167 | } 168 | 169 | // Check if the requesting user has permission to view this log 170 | // Users can only view their own logs unless they're admin (this would need additional role checking) 171 | if log.UserID != requestingUserID { 172 | return nil, errors.NewAppError(errors.ErrForbidden, "Access denied to this activity log") 173 | } 174 | 175 | response := s.convertToResponse(*log) 176 | return &response, nil 177 | } 178 | 179 | // convertToResponse converts a models.ActivityLog to dto.ActivityLogResponse 180 | func (s *QueryService) convertToResponse(log models.ActivityLog) dto.ActivityLogResponse { 181 | var details interface{} 182 | if len(log.Details) > 0 { 183 | // Try to unmarshal the JSON details into a generic interface 184 | if err := json.Unmarshal(log.Details, &details); err != nil { 185 | // If unmarshaling fails, return empty object 186 | details = map[string]interface{}{} 187 | } 188 | } else { 189 | details = map[string]interface{}{} 190 | } 191 | 192 | return dto.ActivityLogResponse{ 193 | ID: log.ID.String(), 194 | UserID: log.UserID.String(), 195 | EventType: log.EventType, 196 | Timestamp: log.Timestamp.Format(time.RFC3339), 197 | IPAddress: log.IPAddress, 198 | UserAgent: log.UserAgent, 199 | Details: details, 200 | } 201 | } 202 | --------------------------------------------------------------------------------