Skip to content
This repository was archived by the owner on Aug 14, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9fc858c
sync dev to staging (#118)
ukemeikot May 18, 2026
6bcf9e2
Chore/merge dev to staging (#122)
Taterbro May 18, 2026
7a0abe1
syncing dev branch to staging (#128)
ukemeikot May 19, 2026
d7f60ab
sync dev to staging (#130)
ukemeikot May 19, 2026
3c799b2
sync dev to staging (#131)
ukemeikot May 19, 2026
5ce86a0
Mergin-dev-to-staging (#132)
Gospelmairo May 19, 2026
f692b4f
chore: sync dev to staging (catches YAML duplicate-tags fix) (#133)
ukemeikot May 19, 2026
d4d6156
fix(api): remove duplicate 'tags' keys breaking Swagger UI (#129) (#134)
ukemeikot May 19, 2026
19350e5
Merge remote-tracking branch 'origin/dev' into chore/sync-dev-to-stag…
May 19, 2026
2a7d0ac
Merge pull request #136 from hngprojects/chore/sync-dev-to-staging-tr…
ukemeikot May 19, 2026
537a13a
chore: add observability setup to project - with relevant documentati…
ukemeikot May 19, 2026
52c5774
Merge branch 'dev' into staging
May 19, 2026
cafbdd7
Merge pull request #139 from hngprojects/chore/sync-dev-to-staging-ot…
ukemeikot May 19, 2026
d757816
Feat/add dev scripts (#125) (#142)
ukemeikot May 19, 2026
1b15312
Chore/sync dev to staging admin trainer (#146)
ukemeikot May 19, 2026
68b3d82
Chore/sync dev to staging 2 (#150)
ukemeikot May 19, 2026
1d90c68
Chore/sync dev to staging 2 (#151)
ukemeikot May 19, 2026
47304db
fix(server): remove erroneous seed-script env check from cmd/server
May 19, 2026
9a46a57
Merge pull request #152 from hngprojects/hotfix/remove-bogus-cmd-serv…
ukemeikot May 19, 2026
e310e43
Merge pull request #155 from hngprojects/dev
ukemeikot May 19, 2026
bd6d322
syncing dev to staging (#159)
ukemeikot May 20, 2026
49cf1c4
sync dev to staging for uuid fix (#162)
ukemeikot May 20, 2026
721e63f
sync dev to staging (#164)
ukemeikot May 20, 2026
eca6e4e
sync dev to staging (#166)
ukemeikot May 20, 2026
28776ef
Syncing Dev to Staging (#171)
Gospelmairo May 20, 2026
19253a0
Merge branch 'dev' into staging
May 20, 2026
b124308
Merge pull request #173 from hngprojects/chore/sync-dev-to-staging-bo…
ukemeikot May 20, 2026
ce63800
Merge pull request #175 from hngprojects/dev
ukemeikot May 20, 2026
0cb80bc
Merge pull request #179 from hngprojects/dev
ukemeikot May 20, 2026
26a26ee
syncing dev to staging (#187)
ukemeikot May 21, 2026
d3f2f81
Chore/sync dev to staging trainer setup (#191)
ukemeikot May 21, 2026
3b52753
Merge dev to staging (#193)
Gospelmairo May 21, 2026
e3bba79
Chore/sync dev to staging validate token (#198)
ukemeikot May 22, 2026
f66f4d2
Chore/sync dev to staging availability bulk (#200)
ukemeikot May 22, 2026
46f6712
Chore/sync dev to staging admin list clients (#203)
ukemeikot May 22, 2026
9053eae
sync dev to staging
May 22, 2026
231bb0a
Merge pull request #206 from hngprojects/chore/sync-dev-to-staging-lo…
ukemeikot May 22, 2026
f0698b3
sync dev to staging (#211)
ukemeikot May 22, 2026
35c93fe
sync dev to staging (#213)
ukemeikot May 22, 2026
2cc28ef
Chore/sync dev to staging trainers me (#215)
ukemeikot May 23, 2026
cc5d3a7
syncing dev to staging (#217)
ukemeikot May 23, 2026
4a0f947
Merge pull request #218 from hngprojects/dev
ukemeikot May 23, 2026
cbccdfe
feat(trainers): add POST /trainers/resend-setup for re-issuing the in…
ukemeikot May 23, 2026
12d05af
sync dev to staging (#223)
ukemeikot May 23, 2026
2a9bfe8
Merge dev to stagging (#225)
Gospelmairo May 23, 2026
1742221
Chore/sync dev to staging media (#235)
ukemeikot May 25, 2026
740bb12
Chore/sync dev to staging zoom activities (#250)
ukemeikot May 26, 2026
ed85616
Chore/sync dev to staging notifications iap fix (#256)
ukemeikot May 27, 2026
7f41990
Chore/sync dev to staging notifications reminders (#268)
ukemeikot May 28, 2026
bd62dff
Merge pull request #269 from hngprojects/dev
ukemeikot May 28, 2026
2e607d2
Merge pull request #271 from hngprojects/dev
ukemeikot May 28, 2026
611522b
feat(settings): admin Settings page endpoints + client /categories (#…
ukemeikot May 28, 2026
ea89790
Sync dev to stagging (#275)
Gospelmairo May 29, 2026
9837598
Merge dev to stagging 2 (#277)
Gospelmairo May 29, 2026
4a57b2b
Merge remote-tracking branch 'origin/dev' into chore/sync-dev-to-stag…
May 29, 2026
1d2b538
Merge pull request #281 from hngprojects/chore/sync-dev-to-staging-se…
ukemeikot May 29, 2026
f593889
sync dev to staging (#284)
ukemeikot May 29, 2026
43af8ba
Chore/sync dev to staging 2 (#288)
Gospelmairo May 29, 2026
eb05fea
Sync dev to stagging (#289)
Gospelmairo May 29, 2026
5b556a6
Chore/sync dev to staging trainer admin actions (#295)
ukemeikot May 30, 2026
32de3d4
Sync dev to stagging (#298)
Gospelmairo May 30, 2026
d86f426
Chore/sync dev to staging auth hotfix (#302)
ukemeikot May 30, 2026
f1eedcd
sync dev to staging (#303)
ukemeikot May 30, 2026
3c03e5c
Chore/sync dev to staging (#325)
Taterbro Jun 4, 2026
dd8fcea
Chore/sync dev to staging meet accounts (#332)
ukemeikot Jun 4, 2026
99db521
Chore/synnc dev to staging (#339)
Taterbro Jun 5, 2026
af6df8d
Chore/dev to staging (#345)
Gospelmairo Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 132 additions & 46 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,128 +1,214 @@
# ── Runtime ──────────────────────────────────────────────────────────────────
APP_ENV=development
PORT=8080
LOG_LEVEL=info

# DevOps(observability setup)
SERVICE_NAME=personal-trainer-be
OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=<observability-server-private-ip-or-dns>:4317
LOG_FORMAT=json

# Frontend URL for CORS (e.g., http://localhost:3000)
FRONTEND_URL=http://localhost:3000
API_BASE_URL=

# ── Observability ────────────────────────────────────────────────────────────
OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=<observability-server-private-ip-or-dns>:4317
LOG_FORMAT=json

# ── Data stores ──────────────────────────────────────────────────────────────
# Postgres connection string
# Example: postgres://user:pass@localhost:5432/personal_trainer?sslmode=disable
DATABASE_URL=postgres://postgres:postgres@localhost:5432/personal_trainer?sslmode=disable

# SMTP email settings (leave empty in development — emails are logged to console instead)
# Redis connection string
REDIS_URL=redis://localhost:6379

# ── Core auth ────────────────────────────────────────────────────────────────
# Server-side signing key for OUR JWTs (access + refresh tokens). Generate
# any 32-byte random string. NEVER share between environments.
JWT_SECRET=

# ── Email ────────────────────────────────────────────────────────────────────
# SMTP — leave empty in development; emails are logged to console instead.
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=

# Resend transactional email (preferred over SMTP when both are set).
# Leave empty in development — emails are logged to console instead.
# Get an API key from https://resend.com/api-keys
RESEND_API_KEY=
RESEND_FROM=fitcal@hng14.com
RESEND_FROM=fitcall@hng14.com


# Google OAuth2 credentials
# Get these from https://console.cloud.google.com → APIs & Services → Credentials
# ── Social sign-in: Google ───────────────────────────────────────────────────
# Web OAuth credentials from https://console.cloud.google.com → APIs & Services → Credentials
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URL=http://localhost:8080/api/v1/auth/google/callback

# Google mobile sign-in (POST /auth/google/mobile).
# Each native client registers its own OAuth client ID in Google Cloud Console.
# The backend accepts ID tokens whose `aud` matches any of these IDs (plus the
# web GOOGLE_CLIENT_ID above). Leave empty if you don't support that platform.
# Mobile sign-in (POST /auth/google/mobile). Each native client registers
# its own OAuth client ID in Google Cloud Console. The backend accepts ID
# tokens whose `aud` matches any of these IDs (plus the web GOOGLE_CLIENT_ID
# above). Leave empty if you don't support that platform.
GOOGLE_ANDROID_CLIENT_ID=
GOOGLE_IOS_CLIENT_ID=

# Redis connection string
REDIS_URL=redis://localhost:6379

JWT_SECRET=

# Zoom Server-to-Server OAuth credentials (for auto-generating meeting links).
# Used when ZOOM_MEETING_HOST=org (default) and as the fallback for trainers
# who haven't connected their own Zoom yet under ZOOM_MEETING_HOST=trainer.
# Get these from https://marketplace.zoom.us → Develop → Build App → Server-to-Server OAuth
# ── Social sign-in: Apple ────────────────────────────────────────────────────
# Sign in with Apple (POST /auth/apple). Single endpoint serves iOS native
# (AuthenticationServices), Android REST, and web "Sign in with Apple JS".
#
# Comma-separated list of bundle IDs / Apple Services IDs accepted as the
# `aud` claim on Apple's identity token:
# - iOS native flow → your iOS app bundle id (e.g. com.fitcal.app)
# - Web flow → the Services ID from Apple Developer → Identifiers
# - Android REST → uses the Services ID
#
# Leave empty to fall back to APPLE_BUNDLE_ID below (the IAP one). For a
# single-platform deploy that's all you need.
#
# Apple's public keys (JWKS) are fetched automatically from
# https://appleid.apple.com/auth/keys — there is no client secret to set
# on the server side.
APPLE_SIGN_IN_BUNDLE_IDS=

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove duplicated APPLE_SIGN_IN_BUNDLE_IDS to avoid silent override.

APPLE_SIGN_IN_BUNDLE_IDS is declared twice (Line 71 and Line 200). In .env parsing, the later key wins, so a value set in the first occurrence can be unintentionally overridden by the second (often blank), breaking Apple sign-in audience validation. Keep exactly one declaration and one canonical section.

Also applies to: 200-200

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example at line 71, There are duplicate declarations of the environment
variable APPLE_SIGN_IN_BUNDLE_IDS which causes the later (often blank) entry to
silently override the earlier one; remove the redundant declaration so only a
single canonical APPLE_SIGN_IN_BUNDLE_IDS entry remains in the .env.example,
keep the intended value/comment in the chosen section, and ensure any related
documentation or example comments reference that single declaration to prevent
accidental overrides.


# ── Zoom ─────────────────────────────────────────────────────────────────────
# Server-to-Server OAuth (auto-generates meeting links). Used when
# ZOOM_MEETING_HOST=org (default) and as the fallback for trainers who
# haven't connected their own Zoom under ZOOM_MEETING_HOST=trainer.
# Source: https://marketplace.zoom.us → Develop → Build App → Server-to-Server OAuth
ZOOM_ACCOUNT_ID=
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=

# Per-trainer Zoom OAuth (Marketplace app type: "OAuth"). When set, trainers
# can connect their own Zoom account at /trainers/me/zoom/connect and meetings
# are hosted under their account instead of the org one. Required scopes on
# the Zoom app: meeting:write, meeting:read, user:read.
# Per-trainer Zoom OAuth (Marketplace app type "OAuth"). When set, trainers
# can connect their own Zoom account at /trainers/me/zoom/connect and
# meetings are hosted under their account instead of the org one.
# Required scopes on the Zoom app: meeting:write, meeting:read, user:read.
ZOOM_OAUTH_CLIENT_ID=
ZOOM_OAUTH_CLIENT_SECRET=
ZOOM_OAUTH_REDIRECT_URL=http://localhost:8080/api/v1/trainers/me/zoom/callback

# Base64-encoded 32-byte AES-256 key used to encrypt the trainers' OAuth
# tokens at rest. Generate with: openssl rand -base64 32
# Required iff you set ZOOM_OAUTH_CLIENT_ID — without it the OAuth pipeline
# Required iff ZOOM_OAUTH_CLIENT_ID is set — without it the OAuth pipeline
# stays disabled and the selector silently falls back to the org account.
ZOOM_TOKEN_ENCRYPTION_KEY=

# Zoom Meeting SDK credentials (Marketplace app type: "Meeting SDK"). Used to
# Meeting SDK credentials (Marketplace app type "Meeting SDK"). Used to
# sign the short-lived JWT the mobile/web Zoom SDK needs to join a call
# in-app via GET /sessions/{id}/join-info. Without these the endpoint 503s
# and clients fall back to the raw join_url.
ZOOM_SDK_KEY=
ZOOM_SDK_SECRET=

# Feature flags for the rollout. Default values keep the previous behaviour.
# Feature flags for the rollout. Defaults keep the existing behaviour.
# org | link → existing flow: org account hosts, email link opens Zoom
# trainer | link → trainer hosts (org fallback), email link opens Zoom
# org | sdk → org hosts, email link opens FitCall app and joins via SDK
# trainer | sdk → trainer hosts, in-app SDK joins end-to-end
ZOOM_MEETING_HOST=org
ZOOM_JOIN_MODE=link

# Universal-link domain used in the email "Join" URL when ZOOM_JOIN_MODE=sdk.
# Must match the Associated Domains entry on iOS + the intent-filter host on
# Android. The backend serves the AASA + assetlinks.json files generated from
# the IOS_*/ANDROID_* vars below.
# ── Google Meet (org-account only) ───────────────────────────────────────────
# Meet rooms are minted by a single Workspace user (e.g. meet-bot@yourdomain)
# rather than per-trainer OAuth. One refresh token in env, many bookings per
# day — Google's `spaces.create` API has no per-creator concurrency cap.
#
# Setup is a one-time operation per environment; see docs/MEET_INTEGRATION.md
# for the full walkthrough. Quick version:
#
# 1. Sign up for Google Workspace (any tier; Business Starter is enough).
# Create a dedicated user, e.g. meet-bot@yourdomain.
# 2. In console.cloud.google.com:
# - Enable the Google Meet REST API
# - Create an OAuth Client ID (type: Web application)
# - Add `http://localhost:8765/callback` to Authorized redirect URIs
# - On the OAuth consent screen, add the scope
# https://www.googleapis.com/auth/meetings.space.created
# 3. Run the bootstrap helper to get the refresh token:
# MEET_OAUTH_CLIENT_ID=<id> MEET_OAUTH_CLIENT_SECRET=<secret> \
# go run ./cmd/meet-bootstrap
# Sign in as meet-bot@yourdomain in the browser, paste the printed
# refresh token into MEET_REFRESH_TOKEN below.
# 4. Flip MEET_ENABLED=true and restart.
#
# Until MEET_ENABLED=true, the FE hides the Meet platform option and any
# inbound booking with session_platform=google_meet returns 503.

# Master switch. Keep false until the refresh token below is provisioned.
MEET_ENABLED=false

# OAuth client credentials from Google Cloud Console → Credentials.
MEET_OAUTH_CLIENT_ID=
MEET_OAUTH_CLIENT_SECRET=

# Long-lived refresh token for the Workspace user. Generated via the
# bootstrap helper (see above). Treat as a secret — leaking it lets
# anyone mint Meet rooms on your org account until you revoke it from
# https://myaccount.google.com/permissions on the meet-bot account.
MEET_REFRESH_TOKEN=

# Email of the Workspace user the refresh token belongs to. Only used
# for log lines so operators chasing "why are Meet rooms failing" can
# tell which mailbox to re-auth.
MEET_HOST_EMAIL=meet-bot@yourdomain

# ── Universal links (mobile deep linking) ────────────────────────────────────
# Used in the email "Join" URL when ZOOM_JOIN_MODE=sdk. Must match the
# Associated Domains entry on iOS + the intent-filter host on Android. The
# backend serves the AASA + assetlinks.json files generated from the
# IOS_*/ANDROID_* vars below.
UNIVERSAL_LINK_DOMAIN=
IOS_APP_BUNDLE_ID=
IOS_APP_TEAM_ID=
ANDROID_APP_PACKAGE=
ANDROID_APP_SHA256=

# MinIO (S3-compatible) for avatar storage.
# - Local dev: docker compose brings up a MinIO at localhost:9000 with the
# credentials below as defaults. The bucket is created at startup if missing.
# - Production: MINIO_ENDPOINT to your managed S3/MinIO host, set
# ── Storage (MinIO / S3) ─────────────────────────────────────────────────────
# - Local dev: docker compose brings up a MinIO at localhost:9000 with these
# credentials. The bucket is created at startup if missing.
# - Production: point MINIO_ENDPOINT at your managed S3/MinIO host, set
# MINIO_USE_SSL=true, and MINIO_PUBLIC_BASE_URL to whatever CDN/host serves
# the bucket publicly (e.g. https://cdn.fitcall.me/fitcall-avatars).
# Leave MINIO_ENDPOINT blank in environments where avatar upload is not needed —
# the endpoint will return 503 instead of panicking.
# Leave MINIO_ENDPOINT blank where avatar upload isn't needed — the
# endpoint returns 503 instead of panicking.
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=fitcall-avatars
MINIO_USE_SSL=false
MINIO_PUBLIC_BASE_URL=http://localhost:9000/fitcall-avatars

# ── Push notifications (Firebase Cloud Messaging) ────────────────────────────
FCM_CREDENTIALS_JSON=
FCM_PROJECT_ID=

# ── In-App Purchases (Apple + Google) ────────────────────────────────────────
# Set to true in development to skip real Apple/Google verification calls.
# ── In-App Purchases (Apple App Store + Google Play) ─────────────────────────
# Set to true in development to skip real Apple/Google receipt verification.
# MUST be false in staging and production.
IAP_SKIP_VERIFICATION=true

# Apple App Store — get from App Store Connect → Apps → App Information → Shared Secret
APPLE_SHARED_SECRET=
APPLE_BUNDLE_ID=com.fitcal.app

# Google Play — service account JSON from Google Play Console → Setup → API access
# Paste the entire contents of the service account key JSON file here (single line),
# or set to the path of the JSON file.
# ── Sign in with Apple ───────────────────────────────────────────────────────
# Comma-separated list of bundle IDs / Apple Services IDs accepted as the
# `aud` claim on the identity token. iOS native flow → iOS app bundle id;
# web "Sign in with Apple JS" → the Services ID configured in the Apple
# Developer portal. Leave empty to fall back to APPLE_BUNDLE_ID above.
# Server endpoint: POST /auth/apple.
APPLE_SIGN_IN_BUNDLE_IDS=

# Google Play — service account JSON from Google Play Console → Setup → API access.
# Accepts EITHER format (auto-detected by the leading character):
# - Raw JSON (one line, starts with `{`). Flatten with:
# jq -c . service-account.json
# - Base64-encoded JSON (anything else). Convenient for Docker Compose,
# k8s ConfigMaps, or anywhere quoting raw JSON is awkward. Encode with:
# PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes("service-account.json"))
# Bash: base64 -w0 service-account.json
#
# Whichever you choose, the server needs the embedded RSA private key live —
# it cannot work from a file path or a hash.
GOOGLE_PACKAGE_NAME=com.fitcal.app
GOOGLE_SERVICE_ACCOUNT_JSON=
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ coverage.txt
.env.local
*.env

# Secrets / service-account credential files. Patterns cover the
# default names Firebase / Google Cloud emit when you download keys.
# If you ever need a credential file in-repo (e.g. a test fixture),
# explicitly un-ignore it with `!path/to/file.json`.
google-services.json
*firebase-adminsdk*.json
*-service-account*.json
*serviceaccount*.json

# IDE / editor
.idea/
.vscode/
Expand Down
15 changes: 15 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Fingerprints of historical findings that have been reviewed and
# confirmed non-secret. Format: one fingerprint per line, as emitted
# by `gitleaks git . --verbose`. Add a comment above each entry
# explaining WHY the value is safe so a future reader doesn't have to
# re-investigate.
#
# Adding a fingerprint here suppresses the warning permanently —
# only use this for genuine false positives (test fixtures, OpenAPI
# example values, etc.). For real leaked secrets, rotate first.

# Test-only JWT secret used to satisfy auth.GenerateJWTToken's
# JWT_SECRET-must-be-set check inside the Apple Sign In handler unit
# tests. Never reaches production; the test file shortens it to
# "test-secret" on later commits, matching other auth tests.
b32b79c2b3b771512416267fe562665c2f6c1aec:internal/auth/apple_test.go:generic-api-key:112
Loading
Loading