Techadamia Backend Documentation

Code Review Checklist

This checklist guides reviewers through the security-relevant surface of the backend. All items below have been implemented and tested.

It has two parts: the production-hardening controls (transport, sessions, uploads, config) and the application layer added afterwards (accounts, profiles, social, messaging), whose risks are mostly authorization and information disclosure rather than infrastructure.

Tip

Each item points at the source location to review and what to confirm. For the narrative behind these controls, read SECURITY.md; for the function-level map, CODE_DOCUMENTATION.md.

Security Features Review

✅ CORS Protection

  • Review: internal/middleware/middleware.go - CORSMiddleware()
  • Check: Explicit origin allowlist enforced
  • Check: Preflight OPTIONS requests handled
  • Config: .env.example - CORS_ALLOWED_ORIGINS

✅ Rate Limiting

  • Review: internal/middleware/middleware.go - RateLimitMiddleware()
  • Review: internal/middleware/ratelimit_redis.go - Redis implementation
  • Check: In-memory eviction working (per-window cleanup)
  • Check: Redis Lua script atomicity
  • Config: .env.example - Rate limit specs

✅ Request ID Tracking

  • Review: internal/utils/request_id.go - UUID generation
  • Review: internal/middleware/middleware.go - RequestIDMiddleware()
  • Check: Request ID in all log entries
  • Check: X-Request-Id header in responses

✅ Access Logging

  • Review: internal/middleware/middleware.go - AccessLogMiddleware()
  • Check: Structured logs include method, path, status, latency, IP, request ID
  • Check: No duplicate logging (gin.Default() → gin.New())

✅ Security Headers

  • Review: internal/middleware/middleware.go - SecurityHeadersMiddleware()
  • Check: X-Content-Type-Options: nosniff
  • Check: X-Frame-Options: DENY
  • Check: Strict-Transport-Security (HSTS) builder
  • Check: Referrer-Policy configured
  • Check: Permissions-Policy configured

✅ CSRF Protection

  • Review: internal/middleware/middleware.go - CSRFMiddleware()
  • Review: internal/api/handlers.go - Token generation in Login(), removal in Logout()
  • Check: POST/PUT/DELETE require CSRF token
  • Check: GET/HEAD/OPTIONS bypass CSRF
  • Check: Bearer auth bypasses CSRF

✅ File Upload Security

  • Review: internal/api/handlers.go - UploadMedia()
  • Check: Size limit enforced with http.MaxBytesReader
  • Check: MIME type allowlist validated
  • Check: Magic bytes detected with http.DetectContentType()
  • Check: Filenames sanitized (UUID-based storage)
  • Check: Failed uploads cleaned up

✅ Session Security

  • Review: internal/service/auth.go - Login() method
  • Check: Prior sessions deleted in transaction on login
  • Check: Session rotation atomic (DeleteAllUserSessions + AddSession)
  • Check: Session cookies have HttpOnly, Secure, SameSite flags
  • Review: Cookie configuration in internal/api/handlers.go - sessionCookieConfig()

✅ Graceful Shutdown

  • Review: cmd/main.go - Signal handling and server.Shutdown()
  • Check: SIGTERM/SIGINT handled
  • Check: In-flight requests complete before exit
  • Check: Timeout prevents hanging

✅ Health Endpoints

  • Review: internal/api/handlers.go - /health and /ready endpoints
  • Check: /health always returns 200
  • Check: /ready returns 200 when initialized, 503 during startup
  • Check: /ready does DB ping

✅ Error Normalization

  • Review: internal/api/handlers.go - Error mapping
  • Check: PostgreSQL 23505 (unique) → 400
  • Check: PostgreSQL 23503 (FK) → 400
  • Check: PostgreSQL 23514 (check) → 400
  • Check: No stack traces sent to clients
  • Check: User-friendly error messages

✅ Environment Validation

  • Review: cmd/main.go - loadConfig() function
  • Check: Required vars validated: DATABASE_URL, APP_PORT, MEDIA_STORAGE_DIR, SESSION_COOKIE_SECURE
  • Check: Format validation: HSTS_MAX_AGE, TRUSTED_PROXIES CIDR, MIME types
  • Check: Logical validation: Production + insecure cookies = fail
  • Check: File permissions: MEDIA_STORAGE_DIR writable
  • Check: External service validation: RATE_LIMIT_REDIS_URL format

✅ Trusted Proxies

  • Review: internal/api/routes.go - SetTrustedProxies()
  • Review: cmd/main.go - validateTrustedProxies()
  • Check: IP addresses and CIDR blocks accepted
  • Check: X-Forwarded-For trusted only from configured proxies

✅ Session Cleanup

  • Review: cmd/main.go - startSessionCleanup() job
  • Review: internal/service/auth.go - DeleteExpiredSessions()
  • Check: Cleanup runs on startup
  • Check: Periodic job (configurable interval)
  • Check: SQL query in internal/sql/query.sql

Application Layer Review

✅ Auth Token Handling (password reset, email verification)

  • Review: internal/service/tokens.go - newAuthToken(), hashToken()
  • Check: Only the SHA-256 digest is stored; the raw token never hits the database
  • Check: Tokens are 32 bytes from crypto/rand
  • Check: TTLs enforced in the query, not just at issue time (passwordResetTTL 1h, emailVerificationTTL 24h)
  • Check: Issuing a new token invalidates the outstanding one
  • Check: DeleteExpiredAuthTokens is wired into the cleanup sweep in cmd/main.go

✅ Account Enumeration Safety

  • Review: internal/api/authflows.go - all four handlers
  • Check: /password-reset/request returns an identical response for known and unknown emails
  • Check: /verify-email/resend returns identically for unknown, already-verified, suspended, and deleted accounts (ResolveUnverifiedUser)
  • Check: No timing shortcut leaks existence (the work is the same on both paths)
  • Check: Email send failures are logged, never surfaced to the caller
  • Check: These routes sit behind the login rate limiter in routes.go

✅ Session Revocation on Credential Change

  • Review: internal/service/profile.go - ChangePassword()
  • Review: internal/service/tokens.go - ConfirmPasswordReset()
  • Check: Both delete all sessions for the account, in the same transaction as the password write
  • Check: Both burn outstanding reset tokens
  • Check: ChangePassword verifies the current password before doing anything
  • Check: The caller receives a fresh session so they are not logged out of their own request

✅ Optional Authentication

  • Review: internal/middleware/middleware.go - OptionalAuthMiddleware()
  • Check: A missing, malformed, expired, or non-active session falls through unauthenticated rather than erroring
  • Check: It never grants a role — it only populates viewer context
  • Check: No route relies on it for authorization (only GET /posts/:slug and GET /users/:user, for liked_by_me / following)

✅ Comment Authorization & Moderation

  • Review: internal/service/comments.go
  • Check: UpdateComment is author-only; admins do not get edit rights
  • Check: DeleteComment allows admins to delete any comment, everyone else only their own
  • Check: Hidden and deleted comments are excluded from ListPostComments
  • Check: A missing, hidden, or already-deleted comment returns ErrCommentNotFound rather than distinguishing the cases
  • Check: ParentID is validated to belong to the same post (ErrInvalidParentComment)
  • Check: Body bounded by ValidateCommentBody (non-blank, ≤ 5000)

✅ Direct Message Authorization

  • Review: internal/api/messages.go, internal/service/messages.go
  • Check: MessagesWith only returns messages where the caller is sender or recipient
  • Check: MarkMessagesRead is scoped to the caller as recipient
  • Check: MessageResponse.Mine is derived per viewer rather than exposing raw participant IDs
  • Check: Self-messaging is rejected in the service and the schema
  • Check: Body bounded by ValidateMessageBody (non-blank, ≤ 2000)

✅ Notification Scoping

  • Review: internal/service/notifications.go
  • Check: MarkNotificationRead is scoped by userID, so one user cannot mark another's notification read
  • Check: Notifications are persisted before publish, so a dropped live event is never a lost notification
  • Check: Emitters are fire-and-forget — a notification failure never fails the originating request
  • Check: NotifyFollowersOfNewPost resolves the owner from the post, not the caller (an admin publishing someone else's draft attributes correctly)

✅ SSE Stream (GET /events)

  • Review: internal/api/notifications.go - Events()
  • Review: internal/realtime/hub.go
  • Check: The subscription's cancel is deferred, so a dropped connection cannot leak a subscriber
  • Check: Publish never blocks — a full buffer drops the event rather than stalling the writer
  • Check: The stream is behind RequireAuthMiddleware and only ever receives events addressed to that user
  • Check: Write deadline cleared, heartbeat present, X-Accel-Buffering: no set
  • Check: The single-instance limitation is understood before scaling out (see IMPLEMENTATION_SUMMARY.md)

✅ Role Model (reader / author / admin)

  • Review: internal/api/routes.go - group nesting
  • Check: Readers reach social and messaging routes but not the author group (post create/update/delete)
  • Check: Media upload is intentionally open to all active accounts (avatars); attaching media to a post still requires the author role
  • Check: POST /register rejects any role other than author or reader
  • Check: Readers cannot self-promote — role is never read from user input after registration

✅ Profile & Avatar Handling

  • Review: internal/api/profile.go - UpdateMe()
  • Check: Avatar media must be owned by the caller (no pointing at someone else's upload)
  • Check: PublicProfileResponse omits email, phone, and account status
  • Check: Free-text fields bounded by ValidateProfileFields
  • Check: resolveProfileTarget accepts a username or UUID without leaking which users exist beyond a 404

✅ Outbound Email

  • Review: internal/mail/mail.go
  • Check: Send logs the subject but never the body (bodies carry tokens)
  • Check: STARTTLS for standard ports, implicit TLS for 465
  • Check: nil mailer is safe everywhere (Enabled() false, Send a no-op)
  • Check: Deployment configures SMTP — without it, readers activate without verifying their address

✅ Search Input

  • Review: internal/api/social.go - SearchPosts(), and the SearchPosts query
  • Check: The query text goes through the parameterized full-text query, never string interpolation
  • Check: Published posts only — drafts must not be reachable via search
  • Check: Results paginated with the shared limit cap

Configuration Review

  • .env.example - All environment variables documented
  • Check: Required vs optional clearly marked
  • Check: Examples provided for each variable
  • Check: Defaults documented where applicable

Documentation Review

  • SECURITY.md - Complete security guide

    • Each feature explained with code locations
    • Verification steps provided
    • Troubleshooting included
  • DEPLOYMENT.md - Production deployment guide

    • Prerequisites listed
    • Setup instructions clear
    • Reverse proxy examples (nginx)
    • Rate limiting strategy explained
    • Health checks documented
  • IMPLEMENTATION_SUMMARY.md - Implementation overview

    • Checklist items documented
    • Files created/modified listed
    • Known limitations stated
    • Production checklist provided
  • API_REFERENCE.md - Every route in routes.go has an entry, and no entry describes a route that no longer exists

  • CODE_DOCUMENTATION.md - New packages and exported symbols itemized

  • DEVELOPMENT.md - Local setup still reproduces from a clean checkout

Code Quality Checks

  • Go formatting: go fmt ./... passes
  • Builds successfully: go build -o app ./cmd/main.go
  • Tests pass: go test -p 1 ./... with a disposable TEST_DATABASE_URL (see DEVELOPMENT.md)
  • Dependencies: go mod tidy is clean

Security Audit

  • SQL Injection: Parameterized queries via sqlc, including full-text search ✓
  • Authentication: Session-based with rotation ✓
  • Authorization: Role-based middleware, plus per-row ownership checks on comments, messages, notifications, and avatars ✓
  • Input Validation: File uploads, env vars, and every free-text body (comment, message, bio, display name) ✓
  • Output Encoding: JSON safe encoding; search excerpts use markdown emphasis rather than HTML ✓
  • Cryptography: bcrypt for passwords, secure random for session/CSRF/auth tokens, SHA-256 at rest for auth tokens ✓
  • Information Disclosure: Account-flow endpoints are enumeration-safe; public profiles omit email/phone/status ✓
  • Error Handling: No stack traces to clients ✓
  • Logging: Structured logs with context; email bodies (which carry tokens) are never logged ✓
  • CSRF: Cookie-based token validation ✓
  • CORS: Explicit origin allowlist ✓

Integration Points

  • Redis: Optional for multi-instance rate limiting

    • Configuration validation ✓
    • Lua script syntax correct ✓
    • Error handling if Redis unavailable ✓
  • PostgreSQL: Database queries

    • Session management queries ✓
    • sqlc regeneration ✓
    • Transaction safety ✓

Deployment Readiness

  • Can be built: go build succeeds ✓
  • Environment variables comprehensive ✓
  • Reverse proxy documentation included ✓
  • Health checks documented ✓
  • Monitoring guidance provided ✓
  • Backup strategy documented (ops responsibility) ✓
  • Troubleshooting guide complete ✓

Sign-Off

  • Reviewer Name: _____________________
  • Date: _____________________
  • Approved for Production: [ ] Yes [ ] No
  • Comments: _____________________

Notes for Reviewers

  1. Rate Limiting Strategy: In-memory works for single-instance; Redis needed for multi-instance.
  2. CSRF Protection: Cookie-based; works with browser clients. Bearer tokens bypass CSRF.
  3. Session Rotation: Atomic transaction prevents race conditions.
  4. Error Messages: User-friendly for client, detailed in server logs with request ID.
  5. Trusted Proxies: Critical for accurate client IP detection and rate limiting.
  6. File Uploads: 4-layer validation (size, MIME header, magic bytes, cleanup).
  7. Uniform Responses Are Deliberate: The account-flow endpoints return the same body whether or not an account matched. If a change makes one of them more informative — a different message, status, or latency — that is a regression, not an improvement.
  8. Idempotent Social Actions: Like/unlike and follow/unfollow return whether anything changed rather than erroring on a repeat. Do not "fix" a double-tap into a 409.
  9. Realtime Is Best-Effort: Every pushed event exists as a row first. A dropped SSE event is acceptable; a missing notification row is not.
  10. SMTP Is Not Optional in Production: With it unconfigured, reader accounts activate without proving they own the address.

See SECURITY.md for detailed security documentation.

Generated from REVIEW_CHECKLIST.md on 2026-08-17 23:37 CEST. Edit the Markdown source and re-run task docs to update.