Skip to content

Commit d21b53a

Browse files
authored
Merge pull request #27 from RustyRory/feat/26-update-frontend-actions
feat(docker): fixes #26 - update frontend
2 parents e8e6f91 + b7e9782 commit d21b53a

8 files changed

Lines changed: 299 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Changelog
22

3+
## [0.3.0] — 2026-04-27
4+
5+
### Phase 3 — Actions Docker + Authentification
6+
7+
#### Added
8+
9+
- `POST /api/container/restart` : redémarre un container par nom
10+
- `POST /api/container/stop` : arrête un container par nom
11+
- `POST /api/container/start` : démarre un container arrêté par nom
12+
- Authentification par session (`express-session`) — login/password via variables d'environnement (`AUTH_USER`, `AUTH_PASS`, `SESSION_SECRET`)
13+
- `POST /auth/login` : création de session, durée 8h
14+
- `POST /auth/logout` : destruction de session
15+
- Middleware `requireAuth` protégeant toutes les routes `/api/*`
16+
- Page de login (`login.html`) avec formulaire et gestion d'erreur
17+
- Page publique `home.html` : liste des 5 applications avec liens directs, lien "Monitoring" discret en footer
18+
- Routing conditionnel sur `GET /` : affiche `home.html` si non connecté, le dashboard si connecté
19+
- Boutons `Restart` / `Stop` sur les cards containers running, `Start` sur les cards exited
20+
- Désactivation des boutons pendant l'action en cours
21+
- Redirection automatique vers `/login.html` si la session expire (réponse 401)
22+
- Bouton "Déconnexion" dans le header
23+
24+
#### Changed
25+
26+
- `app.js` : gestion du 401 sur `fetchStatus` avec redirection vers la page de login
27+
- `server.js` : `requireAuth` appliqué uniquement sur les routes `/api/*`, `GET /` sert la page publique ou le dashboard selon la session
28+
- Tests : variables d'environnement d'auth isolées via `process.env` avant import pour éviter la pollution par le `.env` local
29+
30+
---
31+
332
## [0.2.0] — 2026-04-27
433

534
### Phase 2 — Monitoring HTTP des applications web

vps-monitor-app/api/server.js

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { checkWebsites } from './services/http.js';
88

99
const __dirname = dirname(fileURLToPath(import.meta.url));
1010

11-
const PORT = process.env.PORT || 3000;
12-
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
13-
const AUTH_USER = process.env.AUTH_USER || 'admin';
14-
const AUTH_PASS = process.env.AUTH_PASS || 'admin';
11+
const PORT = process.env.PORT || 3000;
12+
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
13+
const AUTH_USER = process.env.AUTH_USER || 'admin';
14+
const AUTH_PASS = process.env.AUTH_PASS || 'admin';
1515
const SESSION_SECRET = process.env.SESSION_SECRET || 'change-me';
1616

1717
const app = express();
@@ -24,7 +24,14 @@ app.use(session({
2424
cookie: { httpOnly: true, maxAge: 8 * 60 * 60 * 1000 },
2525
}));
2626

27-
// --- Auth routes (publiques) ---
27+
// --- Routes publiques ---
28+
29+
app.get('/', (req, res) => {
30+
if (req.session.authenticated) {
31+
return res.sendFile(join(__dirname, '../public/index.html'));
32+
}
33+
res.sendFile(join(__dirname, '../public/home.html'));
34+
});
2835

2936
app.post('/auth/login', (req, res) => {
3037
const { username, password } = req.body;
@@ -40,28 +47,20 @@ app.post('/auth/logout', (req, res) => {
4047
res.json({ ok: true });
4148
});
4249

43-
// --- Middleware auth ---
50+
// Fichiers statiques (style.css, app.js, login.html, home.html…)
51+
// index.html n'est pas accessible directement — servi uniquement via GET /
52+
app.use(express.static(join(__dirname, '../public')));
53+
54+
// --- Middleware auth pour l'API ---
4455

4556
function requireAuth(req, res, next) {
4657
if (req.session.authenticated) return next();
47-
if (req.path.startsWith('/api/')) return res.status(401).json({ error: 'Non authentifié' });
48-
res.redirect('/login.html');
58+
res.status(401).json({ error: 'Non authentifié' });
4959
}
5060

51-
// Fichiers statiques publics (login.html, style.css)
52-
app.use(express.static(join(__dirname, '../public')));
53-
54-
// Toutes les routes suivantes sont protégées
55-
app.use(requireAuth);
56-
57-
// Redirige / vers index.html (déjà servi en statique, mais protégé via middleware)
58-
app.get('/', (_req, res) => {
59-
res.sendFile(join(__dirname, '../public/index.html'));
60-
});
61-
62-
// --- API ---
61+
// --- API (protégée) ---
6362

64-
app.get('/api/status', async (_req, res) => {
63+
app.get('/api/status', requireAuth, async (_req, res) => {
6564
try {
6665
const [containers, websites] = await Promise.all([
6766
getContainers(),
@@ -81,7 +80,7 @@ app.get('/api/status', async (_req, res) => {
8180
}
8281
});
8382

84-
app.post('/api/container/restart', async (req, res) => {
83+
app.post('/api/container/restart', requireAuth, async (req, res) => {
8584
const { name } = req.body;
8685
if (!name) return res.status(400).json({ error: 'name requis' });
8786
try {
@@ -92,7 +91,7 @@ app.post('/api/container/restart', async (req, res) => {
9291
}
9392
});
9493

95-
app.post('/api/container/stop', async (req, res) => {
94+
app.post('/api/container/stop', requireAuth, async (req, res) => {
9695
const { name } = req.body;
9796
if (!name) return res.status(400).json({ error: 'name requis' });
9897
try {
@@ -103,7 +102,7 @@ app.post('/api/container/stop', async (req, res) => {
103102
}
104103
});
105104

106-
app.post('/api/container/start', async (req, res) => {
105+
app.post('/api/container/start', requireAuth, async (req, res) => {
107106
const { name } = req.body;
108107
if (!name) return res.status(400).json({ error: 'name requis' });
109108
try {

vps-monitor-app/api/server.test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { jest } from '@jest/globals';
22

3+
// Définis avant l'import pour que dotenv ne les écrase pas
4+
process.env.AUTH_USER = 'admin';
5+
process.env.AUTH_PASS = 'testpass';
6+
process.env.SESSION_SECRET = 'test-secret';
7+
38
jest.unstable_mockModule('./services/docker.js', () => ({
49
getContainers: jest.fn().mockResolvedValue([
510
{ name: 'app1', status: 'running', image: 'img', ports: ['3000'], uptime: 'Up 1 hour' },
@@ -18,7 +23,7 @@ jest.unstable_mockModule('./services/http.js', () => ({
1823
const { default: app } = await import('./server.js');
1924
const { default: request } = await import('supertest');
2025

21-
const CREDENTIALS = { username: 'admin', password: 'admin' };
26+
const CREDENTIALS = { username: 'admin', password: 'testpass' };
2227

2328
describe('Auth', () => {
2429
it('refuse sans session', async () => {

vps-monitor-app/eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ export default [
4242
document: 'readonly',
4343
setInterval: 'readonly',
4444
console: 'readonly',
45+
window: 'readonly',
4546
},
4647
},
4748
rules: {
48-
'no-unused-vars': 'warn',
49+
'no-unused-vars': ['warn', { varsIgnorePattern: '^(containerAction|logout)$' }],
4950
},
5051
},
5152
];

vps-monitor-app/public/app.js

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,60 @@ const REFRESH_INTERVAL = 5000;
22

33
async function fetchStatus() {
44
const res = await fetch('/api/status');
5+
if (res.status === 401) {
6+
window.location.href = '/login.html';
7+
return null;
8+
}
59
return res.json();
610
}
711

12+
async function containerAction(action, name) {
13+
const btn = document.querySelector(`[data-name="${name}"][data-action="${action}"]`);
14+
if (btn) {
15+
btn.disabled = true;
16+
btn.textContent = '...';
17+
}
18+
19+
await fetch(`/api/container/${action}`, {
20+
method: 'POST',
21+
headers: { 'Content-Type': 'application/json' },
22+
body: JSON.stringify({ name }),
23+
});
24+
25+
await refresh();
26+
}
27+
28+
async function logout() {
29+
await fetch('/auth/logout', { method: 'POST' });
30+
window.location.href = '/login.html';
31+
}
32+
833
function renderContainers(containers) {
934
const el = document.getElementById('containers');
10-
el.innerHTML = containers.map((c) => `
11-
<div class="card">
12-
<div class="card-header">
13-
<span class="card-name">${c.name}</span>
14-
<span class="dot ${c.status}" title="${c.status}"></span>
35+
el.innerHTML = containers.map((c) => {
36+
const isRunning = c.status === 'running';
37+
return `
38+
<div class="card">
39+
<div class="card-header">
40+
<span class="card-name">${c.name}</span>
41+
<span class="dot ${c.status}" title="${c.status}"></span>
42+
</div>
43+
<div class="card-meta">
44+
<div>${c.image}</div>
45+
<div>${c.uptime}</div>
46+
<div>${c.ports.join(', ') || '—'}</div>
47+
</div>
48+
<div class="card-actions">
49+
${isRunning ? `
50+
<button data-name="${c.name}" data-action="restart" onclick="containerAction('restart', '${c.name}')">Restart</button>
51+
<button data-name="${c.name}" data-action="stop" onclick="containerAction('stop', '${c.name}')">Stop</button>
52+
` : `
53+
<button data-name="${c.name}" data-action="start" onclick="containerAction('start', '${c.name}')">Start</button>
54+
`}
55+
</div>
1556
</div>
16-
<div class="card-meta">
17-
<div>${c.image}</div>
18-
<div>${c.uptime}</div>
19-
<div>${c.ports.join(', ') || '—'}</div>
20-
</div>
21-
</div>
22-
`).join('');
57+
`;
58+
}).join('');
2359
}
2460

2561
function renderWebsites(websites) {
@@ -58,6 +94,7 @@ function renderSummary(containers, websites) {
5894
async function refresh() {
5995
try {
6096
const data = await fetchStatus();
97+
if (!data) return;
6198
renderGlobalStatus(data.globalStatus);
6299
renderSummary(data.containers, data.websites);
63100
renderContainers(data.containers);

vps-monitor-app/public/home.html

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<!DOCTYPE html>
2+
<html lang="fr">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>VPS — B3 Dev</title>
7+
<style>
8+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9+
10+
body {
11+
font-family: 'Segoe UI', Arial, sans-serif;
12+
background: #0f172a;
13+
color: #e2e8f0;
14+
min-height: 100vh;
15+
display: flex;
16+
flex-direction: column;
17+
align-items: center;
18+
justify-content: center;
19+
padding: 40px 20px;
20+
}
21+
22+
header {
23+
text-align: center;
24+
margin-bottom: 48px;
25+
}
26+
27+
header h1 {
28+
font-size: 1.75rem;
29+
font-weight: 700;
30+
letter-spacing: 0.05em;
31+
color: #f1f5f9;
32+
}
33+
34+
header p {
35+
margin-top: 8px;
36+
font-size: 0.875rem;
37+
color: #64748b;
38+
}
39+
40+
.grid {
41+
display: grid;
42+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
43+
gap: 16px;
44+
width: 100%;
45+
max-width: 760px;
46+
}
47+
48+
a.card {
49+
display: flex;
50+
flex-direction: column;
51+
gap: 6px;
52+
padding: 20px 24px;
53+
background: #1e293b;
54+
border: 1px solid #334155;
55+
border-radius: 12px;
56+
text-decoration: none;
57+
color: #e2e8f0;
58+
transition: background 0.15s, border-color 0.15s, transform 0.15s;
59+
}
60+
61+
a.card:hover {
62+
background: #273548;
63+
border-color: #4f6a8a;
64+
transform: translateY(-2px);
65+
}
66+
67+
a.card .label {
68+
font-size: 1rem;
69+
font-weight: 600;
70+
color: #f1f5f9;
71+
}
72+
73+
a.card .url {
74+
font-size: 0.75rem;
75+
color: #64748b;
76+
font-family: monospace;
77+
}
78+
79+
footer {
80+
margin-top: 48px;
81+
display: flex;
82+
flex-direction: column;
83+
align-items: center;
84+
gap: 12px;
85+
}
86+
87+
footer span {
88+
font-size: 0.75rem;
89+
color: #334155;
90+
}
91+
92+
footer a.monitoring-link {
93+
font-size: 0.75rem;
94+
color: #475569;
95+
text-decoration: none;
96+
border: 1px solid #1e293b;
97+
border-radius: 6px;
98+
padding: 0.3rem 0.8rem;
99+
transition: color 0.15s, border-color 0.15s;
100+
}
101+
102+
footer a.monitoring-link:hover {
103+
color: #94a3b8;
104+
border-color: #334155;
105+
}
106+
</style>
107+
</head>
108+
<body>
109+
110+
<header>
111+
<h1>B3 Dev — Applications</h1>
112+
<p>VPS 78.138.58.95</p>
113+
</header>
114+
115+
<div class="grid">
116+
<a class="card" href="/cinemap/">
117+
<span class="label">CineMap — TP Laravel</span>
118+
<span class="url">/cinemap/</span>
119+
</a>
120+
<a class="card" href="/collegelaboussole/">
121+
<span class="label">College La Boussole</span>
122+
<span class="url">/collegelaboussole/</span>
123+
</a>
124+
<a class="card" href="/saintbarthvolley/">
125+
<span class="label">SaintBarth Volley</span>
126+
<span class="url">/saintbarthvolley/</span>
127+
</a>
128+
<a class="card" href="/lucky7/">
129+
<span class="label">Lucky7</span>
130+
<span class="url">/lucky7/</span>
131+
</a>
132+
<a class="card" href="/B3dev-TP_VUE/">
133+
<span class="label">TP VUE</span>
134+
<span class="url">/B3dev-TP_VUE/</span>
135+
</a>
136+
</div>
137+
138+
<footer>
139+
<span>B3 Dev · 2026</span>
140+
<a class="monitoring-link" href="/login.html">Monitoring</a>
141+
</footer>
142+
143+
</body>
144+
</html>

vps-monitor-app/public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<h1>VPS Monitor</h1>
1212
<div id="global-status" class="badge">--</div>
1313
<div id="summary" class="summary"></div>
14+
<button class="logout-btn" onclick="logout()">Déconnexion</button>
1415
</header>
1516

1617
<main>

0 commit comments

Comments
 (0)