feat: initial project structure — Express/TS/TypeORM + React/TS + Docker + Gitea CI
Some checks failed
CI/CD — Build & Deploy / Build (push) Failing after 1m47s
CI/CD — Build & Deploy / Deploy to VPS (push) Has been skipped

This commit is contained in:
2026-03-14 04:13:58 +01:00
commit 4a3be2a323
18 changed files with 396 additions and 0 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
MYSQL_ROOT_PASSWORD=changeme
DB_NAME=originsdigital
DB_USER=originsdigital
DB_PASSWORD=changeme
JWT_SECRET=changeme

View 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
View 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
View 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
View 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"
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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,
},
},
},
});