feat: initial project structure — Express/TS/TypeORM + React/TS + Docker + Gitea CI
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
MYSQL_ROOT_PASSWORD=changeme
|
||||||
|
DB_NAME=originsdigital
|
||||||
|
DB_USER=originsdigital
|
||||||
|
DB_PASSWORD=changeme
|
||||||
|
JWT_SECRET=changeme
|
||||||
51
.gitea/workflows/deploy.yml
Normal file
51
.gitea/workflows/deploy.yml
Normal file
@@ -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
|
||||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -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
|
||||||
18
backend/Dockerfile
Normal file
18
backend/Dockerfile
Normal file
@@ -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"]
|
||||||
34
backend/package.json
Normal file
34
backend/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/src/config/data-source.ts
Normal file
16
backend/src/config/data-source.ts
Normal file
@@ -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: [],
|
||||||
|
});
|
||||||
29
backend/src/index.ts
Normal file
29
backend/src/index.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
18
backend/tsconfig.json
Normal file
18
backend/tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
29
docker-compose.prod.yml
Normal file
29
docker-compose.prod.yml
Normal file
@@ -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"
|
||||||
47
docker-compose.yml
Normal file
47
docker-compose.yml
Normal file
@@ -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:
|
||||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@@ -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;"]
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>OriginsDigital</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
frontend/nginx.conf
Normal file
22
frontend/nginx.conf
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/App.tsx
Normal file
10
frontend/src/App.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>OriginsDigital — v2</h1>
|
||||||
|
<p>Refonte en cours.</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
9
frontend/src/main.tsx
Normal file
9
frontend/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
15
frontend/vite.config.ts
Normal file
15
frontend/vite.config.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user