Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LT4C Backend

Full-stack ready NestJS service providing dual authentication via email/password and Discord OAuth2, built on Fastify with Prisma/PostgreSQL and fully dockerized for one-command startup.

Features

  • Dual auth: Local (email + bcrypt) and Discord OAuth2 with automatic linking/creation.
  • Secure tokens: JWT access (15m) + rotating refresh tokens (7d) stored as hashed entries with revocation.
  • HttpOnly cookies: Access + refresh tokens delivered via secure cookies with domain + SameSite enforcement.
  • Prisma/PostgreSQL: Strong relational schema tracking credentials, OAuth identities, and refresh tokens.
  • Fastify hardening: Helmet, strict CORS allowlist, rate limiting, logging, and validation pipes.
  • Docker-first: docker compose up -d builds & runs API + Postgres, runs migrations + seed automatically.
  • Testing: Unit coverage for auth helpers and e2e probe for the health endpoint.

Email Verification System

  • Architecture: auth.<domain> renders /auth/verify/:code, calls api.<domain> only (GET /auth/email-verifications/:code/status, POST /.../confirm, POST /auth/email-verifications/resend, POST /auth/request-email-verification). No frontend DB access.
  • Token model: 256-bit URL-safe token, stored as tokenHash = HMAC-SHA256(token, EMAIL_VERIFICATION_TOKEN_SECRET), one-time use, TTL (EMAIL_VERIFICATION_TTL_MINUTES, default 30). Tokens revoked on resend; attempts are rate-limited per email/IP and tracked in EmailVerificationAttempt.
  • Queue + delivery: Emails are enqueued in EmailJob (encrypted payload AES-256-GCM, key derived from the token secret) and processed every 20s with exponential backoff + DLQ after EMAIL_QUEUE_MAX_ATTEMPTS. Template ships HTML + plain text with CTA + fallback URL, sender EMAIL_FROM.
  • DB: EmailVerificationToken, EmailVerificationAttempt, and EmailJob tables plus User.emailVerifiedAt.
  • Config knobs: APP_DOMAIN, AUTH_BASE_URL, API_BASE_URL, ALLOWED_ORIGINS (must include AUTH_BASE_URL), EMAIL_FROM, EMAIL_FROM_NAME, EMAIL_REPLY_TO, MAIL_PROVIDER=smtp, SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, EMAIL_VERIFICATION_TOKEN_SECRET, EMAIL_VERIFICATION_TTL_MINUTES, EMAIL_VERIFICATION_REQUEST_LIMIT_PER_EMAIL, EMAIL_VERIFICATION_REQUEST_LIMIT_PER_IP, EMAIL_VERIFICATION_STATUS_LIMIT_PER_IP, EMAIL_VERIFICATION_CONFIRM_LIMIT_PER_IP, EMAIL_VERIFICATION_RATE_WINDOW_MINUTES, EMAIL_QUEUE_MAX_ATTEMPTS, EMAIL_QUEUE_BACKOFF_SECONDS.
  • DNS checklist: publish SPF (v=spf1 include:<mailer> -all), DKIM (CNAME from provider), DMARC (v=DMARC1; p=quarantine; rua=mailto:dmarc@<domain>), and align EMAIL_FROM with the signed domain. Ensure PTR/rDNS + TLS supported by SMTP provider. Monitor bounces and complaints at the provider dashboard.
  • Ops runbook: Inspect queue with SELECT id,status,attempts,lastError FROM "EmailJob" ORDER BY createdAt DESC LIMIT 20;. Jobs stuck in DEAD should be triaged (bad creds or blocked domain) before manual requeue (UPDATE "EmailJob" SET status='PENDING', attempts=0, nextRunAt=NOW() WHERE id='<id>';). Rate-limit violations return HTTP 429; do not loosen limits without enabling additional abuse controls.
  • Frontend sample (React-ish pseudo-code):
    const api = import.meta.env.VITE_API_BASE_URL;
    const code = useParams().code;
    const [status, setStatus] = useState<'loading'|'valid'|'expired'|'used'|'invalid'|'revoked'>('loading');
    useEffect(() => {
      fetch(`${api}/auth/email-verifications/${code}/status`)
        .then((res) => res.json())
        .then((data) => setStatus(data.status))
        .catch(() => setStatus('invalid'));
    }, [code]);
    const confirm = () => fetch(`${api}/auth/email-verifications/${code}/confirm`, { method: 'POST' });
    const resend = () =>
      fetch(`${api}/auth/email-verifications/resend`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: session.user.email }), // or userId from auth session
      });

Getting Started

  1. Copy environment variables and fill the Discord credentials:
    cp .env.example .env
  2. Build and launch the stack:
    docker compose up -d
    • The API listens on http://localhost:3000.
    • Prisma migrations and the seed user (test@example.com / Password123!) run automatically on first boot.

Verifying the Stack

  • Health: curl http://localhost:3000/health
  • Register:
    curl -X POST http://localhost:3000/auth/register \
      -H "content-type: application/json" \
      -d '{"email":"u@ex.com","password":"Password123!","name":"U"}' -i
  • Login:
    curl -X POST http://localhost:3000/auth/login \
      -H "content-type: application/json" \
      -d '{"email":"u@ex.com","password":"Password123!"}' -i
  • Refresh:
    curl -X POST http://localhost:3000/auth/refresh -H "cookie: refresh_token=..." -i
  • Me:
    curl http://localhost:3000/me -H "cookie: access_token=..."
  • Discord OAuth: open http://localhost:3000/auth/discord in a browser and complete the consent screen.

Project Layout

apps/api        # NestJS application source
prisma          # Prisma schema, migrations, seed script
docker          # Container entrypoint

Key modules:

  • src/config: Zod-backed config loader + typed helpers.
  • src/auth: Controllers, strategies (local, JWT, refresh, Discord), guards, DTOs, token helpers.
  • src/users: User service + /me endpoint.
  • src/health: Simple health check.
  • src/prisma: Prisma service with shutdown hooks.

Tooling & Scripts

Command Description
pnpm start:dev Run Nest with live reload
pnpm build Compile TypeScript to dist/
pnpm test Run unit tests
pnpm test:e2e Run e2e suite
pnpm prisma:migrate Run Prisma migrations locally
pnpm prisma:seed Seed database locally

Automation Scripts

The scripts/ folder contains helpers that wrap the local stack setup and Helm deployment so you can bootstrap everything with a single command. See scripts/README.md for details.

Scripts Quick Reference

  • scripts/setup.sh: copies .env, installs dependencies, waits for Postgres, runs Prisma migrations/seeds, and brings up the API container via docker compose.
  • scripts/deploy.sh: runs helm upgrade --install with either the values.k3s.yaml overlay (dev) or values.eks.yaml (staging/prod); it expects an image reference and exposes --namespace/--external-db.
  • scripts/run-all.sh: runs ./scripts/setup.sh followed by ./scripts/deploy.sh, so a single invocation such as
    ./scripts/run-all.sh dev ghcr.io/my-org/lt4c-backend:sha-1234
    boots the stack and deploys the latest signed image.

Docker Notes

  • apps/api/Dockerfile is a multi-stage build that installs deps, builds the app, and ships a minimal runtime.
  • docker/entrypoint.sh runs prisma migrate deploy, seeds idempotently, and starts the server.
  • Volume pgdata persists database state between restarts.

LT4C Daemon Integration

  • Box provisioning (POST /admin/boxes, POST /boxes) and runtime APIs talk to the LT4C daemons using per-node credentials. Each node stores its daemon API base URL and bearer token in the database; admins can rotate them through the node management endpoints without restarting the API.
  • Environment knobs:
    • LT4C_DEFAULT_IMAGE: Docker image pushed to every daemon when provisioning new boxes.
    • LT4C_TIMEOUT_MS / LT4C_PROVISION_TIMEOUT_MS: HTTP timeout + overall poll timeout while waiting for the daemon to reach RUNNING.
    • LT4C_USER_AGENT: Optional identifier injected into daemon requests.
  • When the daemon reports anything other than RUNNING, the backend tears down the remote box and returns a 4xx so callers can retry safely.
  • Users (and admins) can supply softwareProfiles when creating boxes. Supported presets: nodejs20, nodejs22, python, nginx, java. The selection is persisted in the DB and forwarded to the daemon via metadata for automated bootstrapping.

Billing & Notifications

  • Khi tạo box hoặc gia hạn, backend tự động trừ ZCoin của người dùng bằng product.monthlyPrice * số-tháng. Nếu provisioning/gia hạn thất bại, hệ thống hoàn xu ngay lập tức.
  • Boxes quá hạn > 3 ngày sẽ bị dọn tự động: backend xóa VM khỏi daemon, xóa bản ghi trong DB và gửi thông báo vào /notifications.
  • Endpoint /notifications cho phép người dùng xem lịch sử thông báo/auto-cleanup và đánh dấu đã đọc.

Production Considerations

  • Set COOKIE_DOMAIN to your real domain and adjust ALLOWED_ORIGINS for all frontends.
  • Replace the dev JWT secrets with strong values and rotate regularly.
  • Use HTTPS everywhere so Secure cookies are honored.
  • Configure monitoring around /health and add alerts on DB or rate-limit saturation.

Testing

  • Unit: apps/api/src/auth/utils/token.util.spec.ts
  • E2E: apps/api/test/health.e2e-spec.ts

VPS Bootstrap Script

Need a one-shot deployment onto a vanilla Debian/Ubuntu VPS? Use deploy/scripts/bootstrap-vps.sh. It:

  • Installs Docker + Compose, git, curl, and prerequisites.
  • Creates a dedicated lt4c system user, clones this repository (defaults to https://github.com/lt4c/lt4c-backend.git), and writes a production-ready .env.
  • Builds & starts the stack via docker compose, registers a systemd unit for auto-restart, and optionally sets up a self-hosted GitHub Actions runner so CI/CD continues to work even without GitHub-hosted runners.

Usage

Run it directly via curl (or copy it into your automation tooling) with the required secrets exported as environment variables:

curl -fsSL https://raw.githubusercontent.com/lt4c/lt4c-backend/main/deploy/scripts/bootstrap-vps.sh | sudo \
  ACCESS_TOKEN_SECRET='...' \
  REFRESH_TOKEN_SECRET='...' \
  DISCORD_CLIENT_ID='...' \
  DISCORD_CLIENT_SECRET='...' \
  DISCORD_CALLBACK_URL='https://api.example.com/auth/discord/callback' \
  COOKIE_DOMAIN='api.example.com' \
  ALLOWED_ORIGINS='https://app.example.com' \
  bash

Required environment variables

ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_CALLBACK_URL, ALLOWED_ORIGINS, COOKIE_DOMAIN.

Optional environment variables

  • REPO_URL (defaults to this GitHub repo), GIT_REF (main), APP_PORT, NODE_ENV.
  • Database knobs: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD. When omitted, the script generates a random password and keeps everything scoped to the local Postgres container. You can also supply a complete DATABASE_URL to point at an external RDS/VPC instance.
  • .env handling: set PRESERVE_EXISTING_ENV=true to keep the current file untouched on reruns.
  • Self-hosted runner: set INSTALL_GITHUB_RUNNER=true along with GITHUB_REPOSITORY=lt4c/lt4c-backend, GITHUB_RUNNER_TOKEN=<ephemeral-token>, and optional GITHUB_RUNNER_LABELS, GITHUB_RUNNER_NAME, GITHUB_RUNNER_VERSION.
  • Remote image mode: set USE_REMOTE_IMAGE=true and provide API_IMAGE=ghcr.io/<org>/lt4c-backend@sha256:... (or docker.io/<dockerhub-user>/lt4c-backend@sha256:...). If the registry is private, also export REGISTRY_USERNAME, REGISTRY_PASSWORD, and optionally REGISTRY_SERVER before running the script. In this mode the script loads docker-compose.remote.yml, pulls the CI-produced image, and skips local builds entirely, which keeps VPS rollouts aligned with the GitHub Actions pipeline.

After completion, the API is reachable on http://<vps-ip>:${APP_PORT:-3000} and managed through systemctl status lt4c-backend. Re-run the script anytime you need to roll out a new commit; it will pull the latest code, rebuild the images, and restart the stack idempotently.

Happy shipping!

Infrastructure & Environments

  • infra/bootstrap provisions the S3 bucket and DynamoDB lock table that every environment shares for remote state.
  • infra/modules supply reusable building blocks (VPC, EKS, IAM/IRSA, RDS, Secrets Manager) wired together in infra/envs/{dev,staging,prod}.
  • deploy/helm/app is the single chart with a base values.yaml, plus the values.k3s.yaml and values.eks.yaml overlays described below. Bootstrap manifests live in deploy/k8s-bootstrap/ (namespace-app.yaml and the External Secrets/ClusterSecretStore manifest).

Bootstrap & Terraform Flow

  1. Remote state: cd infra/bootstrap && terraform init && terraform apply -var="aws_region=us-east-1" -var="state_bucket_name=lt4c-terraform-state".
  2. Apply the first environment (usually dev) with create_github_oidc_provider=true so the GitHub OIDC provider is created once.
  3. Apply staging/prod pointing the backend at the shared bucket/table, passing github_oidc_provider_arn and github_repository.
  4. Capture outputs (cluster_name, rds_endpoint, ci_role_arn, external_secrets_role_arn, github_oidc_provider_arn) and supply them to GitHub as repo variables/secrets and to the deploy workflow.
  5. Fetch kubeconfig for each cluster once it exists: aws eks update-kubeconfig --region us-east-1 --name lt4c-staging.

Kubernetes Bootstrap

  1. Apply the namespace + External Secrets bootstrap manifests once per cluster:
    kubectl apply -f deploy/k8s-bootstrap/namespace-app.yaml
    kubectl apply -f deploy/k8s-bootstrap/external-secrets.yaml
  2. The namespace ensures the LT4C workload has a predictable home and the bootstrap manifest seeds the ClusterSecretStore that External Secrets uses via IRSA.
  3. Populate AWS Secrets Manager entries for /lt4c/<env>/ACCESS_TOKEN_SECRET, /lt4c/<env>/REFRESH_TOKEN_SECRET, /lt4c/<env>/DISCORD_CLIENT_ID, /lt4c/<env>/DISCORD_CLIENT_SECRET, and /lt4c/<env>/DATABASE_URL.

Deploying to k3s

  • Use values.k3s.yaml to switch the service to NodePort, point the ingress class to nginx, and hint that storage should use the local-path storage class.
  • Secrets live in the config map or SOPS/SealedSecrets, so .Values.externalSecrets.enabled is false and config.data.DATABASE_URL contains the connection string.
  • Provide the workflow with K3S_KUBECONFIG_DEV (base64 kubeconfig) so it can authenticate before running Helm.
  • Manual command:
    helm upgrade --install app deploy/helm/app -f deploy/helm/app/values.yaml -f deploy/helm/app/values.k3s.yaml      --set image.repository=ghcr.io/your-org/lt4c-backend      --set-string image.tag=sha-<commit>      --set-string global.environment=dev
  • GitHub Actions: trigger deploy.yml via workflow_dispatch with environment=dev to reuse the same pipeline used for EKS but skip AWS login.

Deploying to EKS

  • values.eks.yaml enables the ALB ingress, sets the gp3 storage hint, and leaves External Secrets on so AWS Secrets Manager supplies sensitive values.
  • Use the deploy workflow: it assumes the CI role via GitHub OIDC, verifies the Cosign signature, updates the kubeconfig with aws eks update-kubeconfig, runs the Prisma migration job, and executes helm upgrade --install with the digest pin.
  • Always redeploy the signed image.digest from CI so k3s and EKS share the same artifact.

GitHub OIDC Roles

  • Terraform exposes ci_deployer_role_arn and external_secrets_role_arn, plus the GitHub OIDC provider ARN that the workflow needs for IRSA. Use those outputs to populate the repository variables and environments listed later in this file.
  • Create GitHub Environments dev, staging, prod and bind each to the matching AWS role so the workflow can assume it through OIDC.

Cutover from k3s to EKS

  1. Keep the same PostgreSQL instance and Secrets Manager secrets so credentials stay stable.
  2. Deploy the signed sha-<commit> image to EKS via the deploy workflow or helm upgrade using values.eks.yaml and the digest from CI.
  3. Health-check the ALB ingress and gradually shift traffic (DNS weight, health checks) before draining the k3s nodes.
  4. Tear down the k3s cluster once EKS is fully serving production traffic.

CI/CD (GitHub Actions)

  • ci.yml: runs on PR + pushes to main.

    • pnpm lint/typecheck/tests → pnpm audit → OSV scanner → Semgrep SAST.
    • Builds apps/api/Dockerfile, caches layers, produces SBOM (Syft) and Trivy scan (fails on HIGH+).
    • Pushes the signed image to both ghcr.io/<owner>/lt4c-backend and docker.io/<dockerhub-user>/lt4c-backend with tags sha-<commit>, v<package.json>, latest; Cosign signs each registry digest (keyless).
  • deploy.yml: manual (workflow_dispatch) or semver tags (v*).

    • Environments: dev, staging, prod (prod guarded by GitHub Environment approval).
    • Uses GitHub OIDC → AWS IAM role, verifies Cosign signature, runs Prisma migration Job from the Helm template, then helm upgrade --install with digest pinning and --atomic.
    • Records rollouts via kubectl rollout status. Migrations are re-runnable and automatically cleaned up.
  • ssh-deploy.yml: auto-runs after ci succeeds on main (and exposes a workflow_dispatch entry point) to deliver the signed container to the Docker + Compose VPS via SSH.

    • Logs into the host with the configured key, fast-forwards the repo in $SSH_DEPLOY_PATH, exports USE_REMOTE_IMAGE=true + API_IMAGE=ghcr.io/<owner>/lt4c-backend:sha-<commit>, then executes docker compose -f docker-compose.yml [-f docker-compose.remote.yml] pull && up -d --remove-orphans (second file is used automatically when present).
    • Cosign verification happens upstream in ci, so this job simply reuses the attested artifact.
    • Pass dry_run=true when dispatching manually to validate secrets/paths without touching the host.

Required GitHub Repository Variables

Variable Purpose
AWS_REGION e.g., us-east-1
AWS_ACCOUNT_ID Used in docs/scripts
AWS_ROLE_ARN_DEV / AWS_ROLE_ARN_STAGING / AWS_ROLE_ARN_PROD OIDC roles from Terraform (module.iam.ci_deployer_role_arn)
EKS_CLUSTER_NAME_DEV / EKS_CLUSTER_NAME_STAGING / EKS_CLUSTER_NAME_PROD Terraform outputs per env
K8S_NAMESPACE_DEV / K8S_NAMESPACE_STAGING / K8S_NAMESPACE_PROD lt4c-<env>
SSH_DEPLOY_PATH Absolute path on the VPS where this repo lives (e.g., /opt/lt4c-backend).

Required GitHub Secrets

  • DOCKERHUB_USERNAME – Docker Hub namespace that owns lt4c-backend.
  • DOCKERHUB_TOKEN – Docker Hub access token/password with read/write permission (used for docker login + Cosign signing target).
  • SSH_DEPLOY_HOST – Public hostname/IP of the VPS targeted by ssh-deploy.yml.
  • SSH_DEPLOY_USER – SSH user (for example, the lt4c system user created by bootstrap-vps.sh).
  • SSH_DEPLOY_PRIVATE_KEY – Private key paired with the VPS authorized_keys entry for that user.
  • SSH_DEPLOY_PORT – Optional TCP port override (defaults to 22 when omitted).

Grant the repository Environments named dev, staging, prod and map each to the matching AWS role (Actions will pick matrix.environment).

First Deployment Checklist

  1. Bootstrap remote state (infra/bootstrap) with the shared S3 bucket and DynamoDB table.
  2. Apply the dev stack (with create_github_oidc_provider=true) so the GitHub OIDC provider and CI/external-secrets roles exist.
  3. Apply staging/prod stacks, passing the provider ARN and wiring each backend to the same bucket/table while feeding the outputs (cluster_name, ci_role_arn, external_secrets_role_arn, etc.) back into GitHub.
  4. Populate AWS Secrets Manager entries for /lt4c/<env>/ACCESS_TOKEN_SECRET, /lt4c/<env>/REFRESH_TOKEN_SECRET, /lt4c/<env>/DISCORD_CLIENT_ID, /lt4c/<env>/DISCORD_CLIENT_SECRET, and /lt4c/<env>/DATABASE_URL.
  5. Apply the Kubernetes bootstrap manifests once per cluster (kubectl apply -f deploy/k8s-bootstrap/namespace-app.yaml, .../external-secrets.yaml).
  6. Trigger the deploy workflow for dev (or run helm upgrade with values.k3s.yaml), verify /health responds, then promote the signed digest to staging/prod via tagged pushes and manual approvals as needed.

Secrets & Security Guardrails

  • No secrets live in git. Everything comes from AWS Secrets Manager via External Secrets.
  • JWT secrets must be rotated regularly; rotate refresh tokens first, then access tokens, followed by a pod restart (kubectl rollout restart deploy/app -n <ns>).
  • All IAM roles follow least privilege with IRSA + GitHub OIDC; never use static AWS keys in GitHub.
  • Helm chart enforces: runAsNonRoot, readOnlyRootFS, dropped capabilities, resource requests/limits, probes on /health, NetworkPolicy default deny, PDB, configurable HPA, and migration Jobs executed outside the web container.
  • Pod logs are structured JSON to stdout; integrate with Loki/ELK. Metrics/traces ready for Prometheus/OpenTelemetry (add ServiceMonitor/collector as needed).

Promotion Flow

  1. Merge PR → CI builds/signed image (sha-<commit> tag).
  2. Run Deploy targeting dev. Validate behaviour + smoke tests.
  3. Create annotated tag vX.Y.Z → staging deploy auto-runs. Monitor.
  4. Approve prod GitHub Environment, run Deploy with the same digest. Document Helm revision + Actions run ID in the change record.
  5. If a regression is detected, use helm rollback app <revision> -n lt4c-<env> and re-run the migration job if needed (documented in Runbook.md).

For detailed operational procedures (rollbacks, secret rotations, incident flow), see Runbook.md.

About

Backend API của LT4C, xử lý logic nghiệp vụ, xác thực/ủy quyền, kết nối cơ sở dữ liệu và cung cấp endpoint cho các client

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages