From 4a3be2a32388ca10ea42cd6c9b9193233b79fcf4 Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Sat, 14 Mar 2026 04:13:58 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20project=20structure=20?= =?UTF-8?q?=E2=80=94=20Express/TS/TypeORM=20+=20React/TS=20+=20Docker=20+?= =?UTF-8?q?=20Gitea=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 +++ .gitea/workflows/deploy.yml | 51 +++++++++++++++++++++++++++++++ .gitignore | 19 ++++++++++++ backend/Dockerfile | 18 +++++++++++ backend/package.json | 34 +++++++++++++++++++++ backend/src/config/data-source.ts | 16 ++++++++++ backend/src/index.ts | 29 ++++++++++++++++++ backend/tsconfig.json | 18 +++++++++++ docker-compose.prod.yml | 29 ++++++++++++++++++ docker-compose.yml | 47 ++++++++++++++++++++++++++++ frontend/Dockerfile | 18 +++++++++++ frontend/index.html | 12 ++++++++ frontend/nginx.conf | 22 +++++++++++++ frontend/package.json | 24 +++++++++++++++ frontend/src/App.tsx | 10 ++++++ frontend/src/main.tsx | 9 ++++++ frontend/tsconfig.json | 20 ++++++++++++ frontend/vite.config.ts | 15 +++++++++ 18 files changed, 396 insertions(+) create mode 100644 .env.example create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/package.json create mode 100644 backend/src/config/data-source.ts create mode 100644 backend/src/index.ts create mode 100644 backend/tsconfig.json create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package.json create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6132e07 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +MYSQL_ROOT_PASSWORD=changeme +DB_NAME=originsdigital +DB_USER=originsdigital +DB_PASSWORD=changeme +JWT_SECRET=changeme diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..8ad9366 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,51 @@ +name: CI/CD — Build & Deploy + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: | + backend/package-lock.json + frontend/package-lock.json + + - name: Install & build backend + working-directory: backend + run: | + npm ci + npm run build + + - name: Install & build frontend + working-directory: frontend + run: | + npm ci + npm run build + + deploy: + name: Deploy to VPS + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + cd /home/tetardtek/github/originsdigital + git pull origin main + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b4db9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Env +.env +.env.local +.env.*.local + +# Dependencies +node_modules/ +backend/node_modules/ +frontend/node_modules/ + +# Build +backend/dist/ +frontend/dist/ + +# Docker +*.log + +# OS +.DS_Store diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f8d55f4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:22-alpine AS production + +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=builder /app/dist ./dist + +EXPOSE 4000 + +CMD ["node", "dist/index.js"] diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..d1ffcaf --- /dev/null +++ b/backend/package.json @@ -0,0 +1,34 @@ +{ + "name": "originsdigital-backend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typeorm": "ts-node -e \"require('typeorm/cli')\"", + "migration:generate": "npm run typeorm -- migration:generate", + "migration:run": "npm run typeorm -- migration:run", + "migration:revert": "npm run typeorm -- migration:revert" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.18.3", + "jsonwebtoken": "^9.0.2", + "mysql2": "^3.9.3", + "reflect-metadata": "^0.2.2", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.12.2", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.3" + } +} diff --git a/backend/src/config/data-source.ts b/backend/src/config/data-source.ts new file mode 100644 index 0000000..97d4d02 --- /dev/null +++ b/backend/src/config/data-source.ts @@ -0,0 +1,16 @@ +import "reflect-metadata"; +import { DataSource } from "typeorm"; + +export const AppDataSource = new DataSource({ + type: "mysql", + host: process.env.DB_HOST ?? "mysql", + port: parseInt(process.env.DB_PORT ?? "3306"), + username: process.env.DB_USER ?? "originsdigital", + password: process.env.DB_PASSWORD ?? "", + database: process.env.DB_NAME ?? "originsdigital", + synchronize: false, + logging: process.env.NODE_ENV === "development", + entities: ["src/entities/**/*.ts"], + migrations: ["src/migrations/**/*.ts"], + subscribers: [], +}); diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..2ff113a --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,29 @@ +import "reflect-metadata"; +import express from "express"; +import cors from "cors"; +import dotenv from "dotenv"; +import { AppDataSource } from "./config/data-source"; + +dotenv.config(); + +const app = express(); +const PORT = parseInt(process.env.PORT ?? "4000"); + +app.use(cors()); +app.use(express.json()); + +app.get("/api/health", (_req, res) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); +}); + +AppDataSource.initialize() + .then(() => { + console.log("Database connected"); + app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + }); + }) + .catch((err) => { + console.error("Database connection failed:", err); + process.exit(1); + }); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..02bf912 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..7683ed7 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,29 @@ +# Prod — MySQL géré par mysql-prod existant sur le VPS (172.17.0.1:3306) +# Usage : docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +services: + mysql: + # On ne démarre pas de container MySQL en prod + deploy: + replicas: 0 + + backend: + build: ./backend + restart: unless-stopped + environment: + NODE_ENV: production + PORT: 4000 + DB_HOST: 172.17.0.1 + DB_PORT: 3306 + DB_NAME: ${DB_NAME:-originsdigital} + DB_USER: ${DB_USER:-originsdigital} + DB_PASSWORD: ${DB_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + extra_hosts: + - "host.docker.internal:172.17.0.1" + ports: [] + + frontend: + build: ./frontend + restart: unless-stopped + ports: + - "127.0.0.1:3007:80" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c82e4ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + mysql: + image: mysql:8 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${DB_NAME:-originsdigital} + MYSQL_USER: ${DB_USER:-originsdigital} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - mysql_data:/var/lib/mysql + ports: + - "127.0.0.1:3308:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: ./backend + restart: unless-stopped + environment: + NODE_ENV: development + PORT: 4000 + DB_HOST: mysql + DB_PORT: 3306 + DB_NAME: ${DB_NAME:-originsdigital} + DB_USER: ${DB_USER:-originsdigital} + DB_PASSWORD: ${DB_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + depends_on: + mysql: + condition: service_healthy + ports: + - "127.0.0.1:4000:4000" + + frontend: + build: ./frontend + restart: unless-stopped + depends_on: + - backend + ports: + - "127.0.0.1:3007:80" + +volumes: + mysql_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..3c30233 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +# Build stage +FROM node:22-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Production stage — nginx sert le build + proxy /api vers backend +FROM nginx:alpine AS production + +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6c3188c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + OriginsDigital + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..8320ddc --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # React SPA — renvoie index.html pour toutes les routes frontend + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy /api vers le backend Express + location /api/ { + proxy_pass http://backend:4000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..47ef41d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "originsdigital-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.4.3", + "vite": "^5.2.8" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..ac600d4 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,10 @@ +function App() { + return ( +
+

OriginsDigital — v2

+

Refonte en cours.

+
+ ); +} + +export default App; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..77d159f --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..e9bb9e8 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + "/api": { + target: "http://localhost:4000", + changeOrigin: true, + }, + }, + }, +});