Initial commit: charles.secourit.fr - App de suivi maintenance 360 Partners

This commit is contained in:
2026-08-24 12:12:51 +00:00
commit c7109893b2
21 changed files with 704 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
RUN mkdir -p data
EXPOSE 3008
CMD ["node", "src/index.js"]
+15
View File
@@ -0,0 +1,15 @@
{
"name": "charles-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"express": "^4.21.0",
"better-sqlite3": "^11.6.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"cors": "^2.8.5"
}
}
+22
View File
@@ -0,0 +1,22 @@
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'charles-secourit-2026-secret-key';
function generateToken(user) {
return jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
}
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Token manquant' });
try {
req.user = jwt.verify(token, JWT_SECRET);
next();
} catch { return res.status(401).json({ error: 'Token invalide' }); }
}
function adminOnly(req, res, next) {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Acces refuse' });
next();
}
module.exports = { generateToken, authMiddleware, adminOnly };
+65
View File
@@ -0,0 +1,65 @@
const Database = require('better-sqlite3');
const path = require('path');
const bcrypt = require('bcryptjs');
const dbDir = path.join(__dirname, '..', 'data');
const db = new Database(path.join(dbDir, 'charles.db'));
db.pragma('journal_mode = WAL');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'client'))
);
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
company_name TEXT NOT NULL,
parent_id INTEGER,
sort_order INTEGER DEFAULT 0,
has_cloud INTEGER DEFAULT 0,
cloud_details TEXT DEFAULT '',
has_website INTEGER DEFAULT 0,
website_wordpress INTEGER DEFAULT 0,
website_managed_by TEXT DEFAULT '',
website_details TEXT DEFAULT '',
has_emails INTEGER DEFAULT 0,
email_count INTEGER DEFAULT 0,
email_addresses TEXT DEFAULT '[]',
has_team INTEGER DEFAULT 0,
team_members TEXT DEFAULT '[]',
notes TEXT DEFAULT '',
monthly_price REAL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (parent_id) REFERENCES domains(id)
);
`);
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count;
if (userCount === 0) {
const insert = db.prepare('INSERT INTO users (username, password_hash, name, role) VALUES (?, ?, ?, ?)');
insert.run('pierre', bcrypt.hashSync('Charles2026!', 10), 'Pierre', 'admin');
insert.run('charles', bcrypt.hashSync('360partners!', 10), 'Charles', 'client');
}
const domainCount = db.prepare('SELECT COUNT(*) as count FROM domains').get().count;
if (domainCount === 0) {
const insert = db.prepare('INSERT INTO domains (name, company_name, parent_id, sort_order) VALUES (?, ?, ?, ?)');
const result = insert.run('360-partners.fr', '360 Partners', null, 0);
const pid = result.lastInsertRowid;
insert.run('monsieur-edgar.fr', 'Monsieur Edgar', pid, 1);
insert.run('monsieur-edgar.com', 'Monsieur Edgar', pid, 2);
insert.run('qwarter.fr', 'Qwarter', pid, 3);
insert.run('agenceredo.fr', 'Agence Redo', pid, 4);
insert.run('hub-grade.com', 'HUB-GRADE', pid, 5);
insert.run('momentz.online', 'MOMENTZ', pid, 6);
insert.run('momentz.paris', 'MOMENTZ', pid, 7);
}
module.exports = db;
+80
View File
@@ -0,0 +1,80 @@
const express = require('express');
const cors = require('cors');
const bcrypt = require('bcryptjs');
const db = require('./db');
const { generateToken, authMiddleware, adminOnly } = require('./auth');
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
return res.status(401).json({ error: 'Identifiants incorrects' });
}
const token = generateToken(user);
res.json({ token, user: { id: user.id, name: user.name, role: user.role } });
});
app.get('/api/domains', authMiddleware, (req, res) => {
const domains = db.prepare('SELECT * FROM domains ORDER BY sort_order, id').all();
const parent = domains.find(d => !d.parent_id);
const children = domains.filter(d => d.parent_id);
if (req.user.role === 'client') {
domains.forEach(d => { delete d.monthly_price; });
}
res.json({ parent, children });
});
app.get('/api/domains/:id', authMiddleware, (req, res) => {
const domain = db.prepare('SELECT * FROM domains WHERE id = ?').get(req.params.id);
if (!domain) return res.status(404).json({ error: 'Domaine non trouve' });
if (req.user.role === 'client') delete domain.monthly_price;
res.json(domain);
});
app.put('/api/domains/:id', authMiddleware, (req, res) => {
const domain = db.prepare('SELECT * FROM domains WHERE id = ?').get(req.params.id);
if (!domain) return res.status(404).json({ error: 'Domaine non trouve' });
const fields = [
'has_cloud', 'cloud_details', 'has_website', 'website_wordpress',
'website_managed_by', 'website_details', 'has_emails', 'email_count',
'email_addresses', 'has_team', 'team_members', 'notes'
];
if (req.user.role === 'admin') fields.push('monthly_price');
const updates = {};
fields.forEach(f => { if (req.body[f] !== undefined) updates[f] = req.body[f]; });
updates.updated_at = new Date().toISOString();
const setClause = Object.keys(updates).map(k => k + ' = ?').join(', ');
const values = Object.values(updates);
values.push(req.params.id);
db.prepare('UPDATE domains SET ' + setClause + ' WHERE id = ?').run(...values);
const updated = db.prepare('SELECT * FROM domains WHERE id = ?').get(req.params.id);
if (req.user.role === 'client') delete updated.monthly_price;
res.json(updated);
});
app.post('/api/domains', authMiddleware, adminOnly, (req, res) => {
const { name, company_name, parent_id } = req.body;
if (!name || !company_name) return res.status(400).json({ error: 'Nom requis' });
const maxOrder = db.prepare('SELECT COALESCE(MAX(sort_order), 0) as m FROM domains').get().m;
const result = db.prepare('INSERT INTO domains (name, company_name, parent_id, sort_order) VALUES (?, ?, ?, ?)')
.run(name, company_name, parent_id || null, maxOrder + 1);
res.json(db.prepare('SELECT * FROM domains WHERE id = ?').get(result.lastInsertRowid));
});
app.delete('/api/domains/:id', authMiddleware, adminOnly, (req, res) => {
db.prepare('DELETE FROM domains WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
app.get('/api/health', (req, res) => res.json({ ok: true }));
const PORT = process.env.PORT || 3008;
app.listen(PORT, '0.0.0.0', () => console.log('Charles backend on port ' + PORT));