diff --git a/.env.example b/.env.example index 8c89096..db46c22 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,8 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60 # ── Ports ───────────────────────────────────────────────────────────────────── # Port exposed on the host for the Next.js frontend FRONTEND_PORT=3000 + +# ── Docker Hub images (optional — leave blank to build locally) ─────────────── +# Set these on the target machine so docker compose pull works without building +# BACKEND_IMAGE=your_dockerhub_username/lms-backend:latest +# FRONTEND_IMAGE=your_dockerhub_username/lms-frontend:latest diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index e660f2e..9493258 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.orm import Session +import os from ..database import get_db from ..dependencies import get_current_user @@ -16,6 +17,7 @@ router = APIRouter(prefix="/auth", tags=["auth"]) _COOKIE_NAME = "access_token" _COOKIE_MAX_AGE = ACCESS_TOKEN_EXPIRE_MINUTES * 60 # seconds +_COOKIE_SECURE = os.getenv("COOKIE_SECURE", "false").lower() == "true" def _set_auth_cookie(response: Response, user_id: int) -> None: @@ -24,7 +26,7 @@ def _set_auth_cookie(response: Response, user_id: int) -> None: key=_COOKIE_NAME, value=token, httponly=True, - secure=True, # send only over HTTPS (Nginx handles TLS in prod) + secure=_COOKIE_SECURE, samesite="lax", max_age=_COOKIE_MAX_AGE, path="/", diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..45eab80 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Pull LMS images from GHCR and start the stack on a new machine. +# Usage: ./deploy.sh +# Requires: .env file with BACKEND_IMAGE, FRONTEND_IMAGE, POSTGRES_PASSWORD, JWT_SECRET_KEY + +set -e + +cd "$(dirname "$0")" + +if [ ! -f .env ]; then + echo "❌ .env not found. Copy .env.example → .env and fill in the values." + exit 1 +fi + +source .env + +if [ -z "$POSTGRES_PASSWORD" ] || [ "$POSTGRES_PASSWORD" = "change_me_strong_password" ]; then + echo "❌ Set POSTGRES_PASSWORD in .env" + exit 1 +fi + +if [ -z "$JWT_SECRET_KEY" ] || [ "$JWT_SECRET_KEY" = "change_me_generate_with_secrets_token_hex_32" ]; then + echo "❌ Set JWT_SECRET_KEY in .env" + echo " Generate: python3 -c \"import secrets; print(secrets.token_hex(32))\"" + exit 1 +fi + +# Login GHCR if token is provided +if [ -n "$GITHUB_TOKEN" ] && [ -n "$GITHUB_USER" ]; then + echo "🔐 Logging in to ghcr.io..." + echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USER" --password-stdin +fi + +echo "📦 Pulling images from GHCR..." +docker compose pull + +echo "🚀 Starting stack..." +docker compose up -d + +echo "" +echo "✅ Running! Open http://localhost:${FRONTEND_PORT:-3000}" +docker compose ps diff --git a/docker-compose.yml b/docker-compose.yml index 1112add..0fa17ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: # ── FastAPI backend ─────────────────────────────────────────────────────────── backend: + image: ${BACKEND_IMAGE:-lms-backend:latest} build: context: ./backend dockerfile: Dockerfile @@ -42,6 +43,7 @@ services: # ── Next.js frontend ────────────────────────────────────────────────────────── frontend: + image: ${FRONTEND_IMAGE:-lms-frontend:latest} build: context: ./frontend dockerfile: Dockerfile diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 9e80154..0e1deff 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -27,6 +27,7 @@ ENV NODE_ENV=production COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/next.config.js ./ EXPOSE 3000 diff --git a/frontend/src/types/modules.d.ts b/frontend/src/types/modules.d.ts new file mode 100644 index 0000000..eb152f0 --- /dev/null +++ b/frontend/src/types/modules.d.ts @@ -0,0 +1,13 @@ +declare module "fabric" { + export const fabric: any; +} + +declare module "pdfjs-dist" { + export const GlobalWorkerOptions: { + workerSrc: string; + }; + export const version: string; + export function getDocument(params: any): { + promise: Promise; + }; +} diff --git a/push-ghcr.sh b/push-ghcr.sh new file mode 100755 index 0000000..03dd5ee --- /dev/null +++ b/push-ghcr.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Push LMS images to GitHub Container Registry (GHCR) +# Usage: ./push-ghcr.sh [tag] +# Reads GITHUB_USER and GITHUB_TOKEN from .env + +set -e + +cd "$(dirname "$0")" + +# Load .env +if [ -f .env ]; then + export $(grep -v '^#' .env | grep -E 'GITHUB_USER|GITHUB_TOKEN' | xargs) +fi + +TAG="${1:-latest}" + +GITHUB_USER="${GITHUB_USER:?GITHUB_USER not set in .env}" +GITHUB_TOKEN="${GITHUB_TOKEN:?GITHUB_TOKEN not set in .env}" + +BACKEND_IMAGE="ghcr.io/${GITHUB_USER}/lms-backend:${TAG}" +FRONTEND_IMAGE="ghcr.io/${GITHUB_USER}/lms-frontend:${TAG}" + +echo "========================================" +echo " LMS → GHCR Push" +echo " Backend : $BACKEND_IMAGE" +echo " Frontend: $FRONTEND_IMAGE" +echo "========================================" +echo "" + +# ── 1. Login to GHCR ────────────────────────────────────────────────────────── +echo "🔐 Logging in to ghcr.io ..." +echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USER" --password-stdin + +# ── 2. Build images ─────────────────────────────────────────────────────────── +echo "" +echo "🔨 Building images..." +cd "$(dirname "$0")" +docker compose build + +# ── 3. Tag for GHCR ─────────────────────────────────────────────────────────── +echo "" +echo "🏷 Tagging images..." +docker tag lms-backend:latest "$BACKEND_IMAGE" +docker tag lms-frontend:latest "$FRONTEND_IMAGE" + +# ── 4. Push ─────────────────────────────────────────────────────────────────── +echo "" +echo "⬆ Pushing to GHCR..." +docker push "$BACKEND_IMAGE" +docker push "$FRONTEND_IMAGE" + +echo "" +echo "✅ Done! Images pushed:" +echo " $BACKEND_IMAGE" +echo " $FRONTEND_IMAGE" +echo "" +echo "📋 Add these to .env on the target machine:" +echo " BACKEND_IMAGE=$BACKEND_IMAGE" +echo " FRONTEND_IMAGE=$FRONTEND_IMAGE" +echo "" +echo "🚀 On the target machine:" +echo " docker compose pull && docker compose up -d" diff --git a/readme.md b/readme.md index 10fadba..f3f2846 100644 --- a/readme.md +++ b/readme.md @@ -51,4 +51,86 @@ Please use the following technologies for this project: Let's build this step-by-step to avoid context limits. 1. First, analyze this spec and confirm you understand. 2. Provide the database schema models (SQLAlchemy or Prisma). -3. Wait for my confirmation before writing the Backend API routes. \ No newline at end of file +3. Wait for my confirmation before writing the Backend API routes. + +--- + +## 7. CI/CD — Push lên GitHub Container Registry (GHCR) + +### 7.1. Yêu cầu + +- **Docker** đã cài và đang chạy +- **GitHub Personal Access Token (PAT)** với quyền `write:packages` và `read:packages` + - Tạo tại: https://github.com/settings/tokens → *Generate new token (classic)* +- File `.env` đã được cấu hình (xem `.env.example`) + +### 7.2. Cấu hình `.env` + +Thêm các dòng sau vào file `.env` (file này đã được gitignore, **không commit**): + +```env +GITHUB_USER= +GITHUB_TOKEN= +BACKEND_IMAGE=ghcr.io//lms-backend:latest +FRONTEND_IMAGE=ghcr.io//lms-frontend:latest +``` + +### 7.3. Build & Push lên GHCR + +```bash +chmod +x push-ghcr.sh +./push-ghcr.sh +``` + +Script sẽ tự động: +1. Đọc `GITHUB_USER` và `GITHUB_TOKEN` từ `.env` +2. Đăng nhập vào `ghcr.io` +3. Build cả hai image (`lms-backend`, `lms-frontend`) bằng `docker compose build` +4. Tag và push lên GHCR + +Để push với tag cụ thể (ví dụ: version): +```bash +./push-ghcr.sh v1.0.0 +``` + +### 7.4. Deploy trên máy khác + +Trên máy đích (server, VPS, máy tính khác): + +```bash +# 1. Copy các file cần thiết +scp docker-compose.yml .env.example deploy.sh user@server:/opt/lms/ +ssh user@server + +# 2. Tạo .env từ example +cd /opt/lms +cp .env.example .env +# Điền các giá trị: POSTGRES_PASSWORD, JWT_SECRET_KEY, GITHUB_USER, GITHUB_TOKEN, +# BACKEND_IMAGE, FRONTEND_IMAGE + +# 3. Chạy deploy +chmod +x deploy.sh +./deploy.sh +``` + +Script `deploy.sh` sẽ: +1. Kiểm tra `.env` hợp lệ +2. Đăng nhập GHCR (nếu có token) +3. Pull image từ GHCR +4. Khởi động toàn bộ stack: `db`, `backend`, `frontend` + +### 7.5. Kiểm tra packages trên GitHub + +Sau khi push, image sẽ xuất hiện tại: +``` +https://github.com/?tab=packages +``` + +> **Lưu ý:** Mặc định packages ở chế độ **Private**. Để máy khác pull mà không cần token, vào +> GitHub → Packages → tên package → *Package settings* → đổi visibility sang **Public**. + +### 7.6. Sinh JWT Secret Key + +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` \ No newline at end of file