Initial Commit
This commit is contained in:
commit
7647bcd52a
13 changed files with 3097 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
todos.db
|
||||
todos.db-shm
|
||||
todos.db-wal
|
||||
.env
|
||||
103
app.js
Normal file
103
app.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// server.js
|
||||
const path = require('path');
|
||||
const Fastify = require('fastify');
|
||||
const formBody = require('@fastify/formbody');
|
||||
const fastifyStatic = require('@fastify/static');
|
||||
const fastifyView = require('@fastify/view');
|
||||
const ejs = require('ejs');
|
||||
const promClient = require('prom-client');
|
||||
const fastifyCookie = require('@fastify/cookie');
|
||||
const fastifySession = require('@fastify/session');
|
||||
|
||||
// --- App Instance ---
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
// --- Plugins ---
|
||||
app.register(formBody);
|
||||
app.register(fastifyCookie);
|
||||
app.register(fastifySession, {
|
||||
secret: 'this_very_long_random_secret_32_chars_minimum!',
|
||||
cookie: { secure: false },
|
||||
saveUninitialized: false,
|
||||
});
|
||||
app.register(fastifyStatic, {
|
||||
root: path.join(__dirname, 'public'),
|
||||
prefix: '/static/',
|
||||
});
|
||||
app.register(fastifyView, {
|
||||
engine: { ejs },
|
||||
root: path.join(__dirname, 'views'),
|
||||
layout: 'layout.ejs',
|
||||
});
|
||||
|
||||
// --- Prometheus Metrics ---
|
||||
const register = new promClient.Registry();
|
||||
promClient.collectDefaultMetrics({ register });
|
||||
|
||||
const counterCreated = new promClient.Counter({
|
||||
name: 'todo_created_total',
|
||||
help: 'Nombre total de todos créés',
|
||||
});
|
||||
const counterDeleted = new promClient.Counter({
|
||||
name: 'todo_deleted_total',
|
||||
help: 'Nombre total de todos supprimés',
|
||||
});
|
||||
const gaugeCount = new promClient.Gauge({
|
||||
name: 'todo_count',
|
||||
help: 'Nombre courant de todos',
|
||||
});
|
||||
register.registerMetric(counterCreated);
|
||||
register.registerMetric(counterDeleted);
|
||||
register.registerMetric(gaugeCount);
|
||||
|
||||
const fastifySwagger = require('@fastify/swagger');
|
||||
const fastifySwaggerUI = require('@fastify/swagger-ui');
|
||||
|
||||
// --- Swagger ---
|
||||
app.register(fastifySwagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
title: 'Todo API',
|
||||
version: '1.0.0',
|
||||
description: 'CRUD de Todo en Fastify (Node.js) + SQLite',
|
||||
},
|
||||
servers: [{ url: 'http://localhost:3000' }],
|
||||
},
|
||||
});
|
||||
app.register(fastifySwaggerUI, {
|
||||
routePrefix: '/api/docs',
|
||||
uiConfig: {
|
||||
docExpansion: 'full',
|
||||
deepLinking: false
|
||||
}
|
||||
});
|
||||
|
||||
// --- Routes ---
|
||||
app.register(require('./routes/api'), {
|
||||
promRegister: register,
|
||||
counterCreated,
|
||||
counterDeleted,
|
||||
gaugeCount,
|
||||
});
|
||||
app.register(require('./routes/ui'), {
|
||||
counterCreated,
|
||||
counterDeleted,
|
||||
gaugeCount,
|
||||
});
|
||||
app.register(require('./routes/other'), {
|
||||
promRegister: register,
|
||||
});
|
||||
|
||||
// --- Start Server ---
|
||||
async function start() {
|
||||
try {
|
||||
await app.ready();
|
||||
app.swagger();
|
||||
await app.listen({ port: 3000, host: 'localhost' });
|
||||
app.log.info('Server listening on http://localhost:3000');
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
start();
|
||||
50
db.js
Normal file
50
db.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// db.js
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const db = new Database('todos.db');
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// Création table si absente
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pseudo TEXT NOT NULL,
|
||||
discord_id TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
users_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (users_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Ajout de l'utilisateur test si absent
|
||||
const userTest = db.prepare('SELECT * FROM users WHERE id = 1').get();
|
||||
if (!userTest) {
|
||||
db.prepare('INSERT INTO users (id, pseudo, discord_id) VALUES (?, ?, ?)').run(1, 'test', 'null id');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// USERS
|
||||
allUsers: () => db.prepare('SELECT * FROM users ORDER BY id DESC').all(),
|
||||
getUserById: (id) => db.prepare('SELECT * FROM users WHERE id = ?').get(id),
|
||||
getUserByDiscordId: (discord_id) => db.prepare('SELECT * FROM users WHERE discord_id = ?').get(discord_id),
|
||||
createUser: (pseudo, discord_id) =>
|
||||
db.prepare('INSERT INTO users (pseudo, discord_id) VALUES (?, ?)').run(pseudo, discord_id),
|
||||
|
||||
// TODOS
|
||||
allTodos: () => db.prepare('SELECT * FROM todos ORDER BY id DESC').all(),
|
||||
getTodo: (id) => db.prepare('SELECT * FROM todos WHERE id = ?').get(id),
|
||||
getTodosByUser: (users_id) => db.prepare('SELECT * FROM todos WHERE users_id = ? ORDER BY id DESC').all(users_id),
|
||||
createTodo: (users_id, title) =>
|
||||
db.prepare('INSERT INTO todos (users_id, title) VALUES (?, ?)').run(users_id, title),
|
||||
updateTodo: (id, { title, completed }) =>
|
||||
db.prepare('UPDATE todos SET title = COALESCE(?, title), completed = COALESCE(?, completed) WHERE id = ?')
|
||||
.run(title ?? null, typeof completed === 'number' ? completed : null, id),
|
||||
removeTodo: (id) => db.prepare('DELETE FROM todos WHERE id = ?').run(id),
|
||||
};
|
||||
50
discord.js
Normal file
50
discord.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const axios = require("axios");
|
||||
const db = require('./db');
|
||||
|
||||
const BOT_ID = "1410578978712060024";
|
||||
const BOT_SECRET = "vhk6jp_jYjvShOqpI8MJ2Efjjm_9Cmyi";
|
||||
const REDIRECT_URI = "http://localhost:3000/auth/discord/callback";
|
||||
|
||||
exports.handleDiscordAuth = async (request, reply) => {
|
||||
const code = request.query.code;
|
||||
if (!code) return reply.code(400).send({ error: "Code de validation manquant" });
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", BOT_ID);
|
||||
params.append("client_secret", BOT_SECRET);
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("code", code);
|
||||
params.append("redirect_uri", REDIRECT_URI);
|
||||
params.append("scope", "identify email");
|
||||
const tokenData = await axios.post(
|
||||
"https://discord.com/api/oauth2/token",
|
||||
params,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}
|
||||
);
|
||||
const accessToken = tokenData.data.access_token;
|
||||
const userResponse = await axios.get("https://discord.com/api/users/@me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
const userData = userResponse.data;
|
||||
if (!userData || !userData.id) {
|
||||
console.error('Réponse Discord inattendue:', userResponse.data);
|
||||
return reply.code(500).send({ error: "Impossible de récupérer les infos utilisateur depuis Discord", details: userResponse.data });
|
||||
}
|
||||
let savedUser = db.getUserByDiscordId(userData.id);
|
||||
if (!savedUser) {
|
||||
db.createUser(userData.username, userData.id);
|
||||
savedUser = db.getUserByDiscordId(userData.id);
|
||||
}
|
||||
request.session.user = savedUser;
|
||||
return reply.redirect('/');
|
||||
} catch (err) {
|
||||
console.error(err.response?.data || err.message);
|
||||
return reply.code(500).send({ error: "Erreur lors de la connexion à Discord" });
|
||||
}
|
||||
};
|
||||
2297
package-lock.json
generated
Normal file
2297
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "totolist",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.lehub.tf:2222/SchoolTask/totolist.git"
|
||||
},
|
||||
"license": "ISC",
|
||||
"author": "",
|
||||
"type": "commonjs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/formbody": "^8.0.2",
|
||||
"@fastify/session": "^11.1.0",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@fastify/swagger": "^9.5.1",
|
||||
"@fastify/swagger-ui": "^5.2.3",
|
||||
"@fastify/view": "^11.1.1",
|
||||
"axios": "^1.11.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"ejs": "^3.1.10",
|
||||
"fastify": "^5.5.0",
|
||||
"nunjucks": "^3.2.4",
|
||||
"prom-client": "^15.1.3"
|
||||
}
|
||||
}
|
||||
242
public/style.css
Normal file
242
public/style.css
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
:root {
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
--primary-bg: #0b0c10;
|
||||
--header-bg: #111217;
|
||||
--border: #1e1f27;
|
||||
--input-bg: #14151c;
|
||||
--input-border: #262837;
|
||||
--button-bg: #1a1c27;
|
||||
--button-border: #2a2d3e;
|
||||
--danger-bg: #2a0f15;
|
||||
--danger-border: #3c1e26;
|
||||
--accent: #9ecbff;
|
||||
--text: #eaeaea;
|
||||
--muted: #8892a6;
|
||||
--done: #8aa;
|
||||
--discord: #5865F2;
|
||||
--discord-hover: #4752C4;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--primary-bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: var(--header-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
header nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
header a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
header a:hover {
|
||||
background: #1a2333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 720px;
|
||||
margin: 24px auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.new form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
button, .discordAuth {
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--button-border);
|
||||
background: var(--button-bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: filter 0.2s, background 0.2s, border 0.2s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button:hover, .discordAuth:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
li > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
span.done {
|
||||
text-decoration: line-through;
|
||||
color: var(--done);
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Discord Auth Button */
|
||||
.discordAuth {
|
||||
background: var(--discord);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 8px 0 #23272a22;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.discordAuth::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: url('https://cdn.jsdelivr.net/gh/edent/SuperTinyIcons/images/svg/discord.svg') no-repeat center/contain;
|
||||
}
|
||||
|
||||
.discordAuth:hover {
|
||||
background: var(--discord-hover);
|
||||
box-shadow: 0 4px 16px 0 #23272a33;
|
||||
}
|
||||
|
||||
.logout {
|
||||
background: #e74c3c;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 8px 0 #e74c3c22;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.logout::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/icons/box-arrow-left.svg') no-repeat center/contain;
|
||||
}
|
||||
|
||||
.logout:hover {
|
||||
background: #c0392b;
|
||||
box-shadow: 0 4px 16px 0 #e74c3c33;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--header-bg);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 16px 24px;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toggle-btn, .delete-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #26323833;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #e74c3c22;
|
||||
}
|
||||
|
||||
.todo-meta {
|
||||
margin-top: 2px;
|
||||
color: var(--muted);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
144
routes/api.js
Normal file
144
routes/api.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
const fastifySwagger = require('@fastify/swagger');
|
||||
const fastifySwaggerUI = require('@fastify/swagger-ui');
|
||||
|
||||
module.exports = async function (app, opts) {
|
||||
const db = require('../db');
|
||||
const promClient = require('prom-client');
|
||||
|
||||
// --- Schémas pour Swagger ---
|
||||
const Todo = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer' },
|
||||
title: { type: 'string' },
|
||||
completed: { type: 'integer', enum: [0, 1] },
|
||||
created_at: { type: 'string' },
|
||||
},
|
||||
};
|
||||
const TodoCreate = {
|
||||
type: 'object',
|
||||
required: ['title'],
|
||||
properties: { title: { type: 'string' } },
|
||||
};
|
||||
const TodoUpdate = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
completed: { type: 'integer', enum: [0, 1] },
|
||||
},
|
||||
};
|
||||
|
||||
// --- Prometheus ---
|
||||
const register = opts.promRegister;
|
||||
const counterCreated = opts.counterCreated;
|
||||
const counterDeleted = opts.counterDeleted;
|
||||
const gaugeCount = opts.gaugeCount;
|
||||
|
||||
function refreshGauge() {
|
||||
try {
|
||||
const count = db.all().length;
|
||||
gaugeCount.set(count);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
app.get('/api/todos', {
|
||||
schema: {
|
||||
response: { 200: { type: 'array', items: Todo } },
|
||||
tags: ['todos'],
|
||||
},
|
||||
}, async () => db.allTodos());
|
||||
|
||||
app.get('/api/todos/:id', {
|
||||
schema: {
|
||||
params: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
||||
response: { 200: Todo, 404: { type: 'object', properties: { message: { type: 'string' } } } },
|
||||
tags: ['todos'],
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const todo = db.getTodo(req.params.id);
|
||||
if (!todo) return reply.code(404).send({ message: 'Not found' });
|
||||
return todo;
|
||||
});
|
||||
|
||||
app.post('/api/todos', {
|
||||
schema: {
|
||||
body: TodoCreate,
|
||||
response: { 201: Todo },
|
||||
tags: ['todos'],
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const { title } = req.body;
|
||||
const users_id = req.session?.user?.id || 1;
|
||||
const result = db.createTodo(users_id, title);
|
||||
counterCreated.inc();
|
||||
refreshGauge();
|
||||
const created = db.getTodo(result.lastInsertRowid);
|
||||
reply.code(201);
|
||||
return created;
|
||||
});
|
||||
|
||||
app.patch('/api/todos/:id', {
|
||||
schema: {
|
||||
params: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
||||
body: TodoUpdate,
|
||||
response: { 200: Todo, 404: { type: 'object', properties: { message: { type: 'string' } } } },
|
||||
tags: ['todos'],
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const id = req.params.id;
|
||||
if (!db.getTodo(id)) return reply.code(404).send({ message: 'Not found' });
|
||||
db.updateTodo(id, req.body || {});
|
||||
const updated = db.getTodo(id);
|
||||
refreshGauge();
|
||||
return updated;
|
||||
});
|
||||
|
||||
app.delete('/api/todos/:id', {
|
||||
schema: {
|
||||
params: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
||||
response: { 204: { type: 'null' } },
|
||||
tags: ['todos'],
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const id = req.params.id;
|
||||
const existing = db.getTodo(id);
|
||||
if (existing) {
|
||||
db.removeTodo(id);
|
||||
counterDeleted.inc();
|
||||
refreshGauge();
|
||||
}
|
||||
reply.code(204).send();
|
||||
});
|
||||
|
||||
// Route pour voir tous les users
|
||||
app.get('/api/users', {
|
||||
schema: {
|
||||
response: {
|
||||
200: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer' },
|
||||
pseudo: { type: 'string' },
|
||||
discord_id: { type: 'string' },
|
||||
created_at: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['users'],
|
||||
}
|
||||
}, async () => db.allUsers());
|
||||
|
||||
app.register(fastifySwagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
title: 'Todo API',
|
||||
version: '1.0.0',
|
||||
description: 'CRUD de Todo en Fastify (Node.js) + SQLite',
|
||||
},
|
||||
servers: [{ url: 'http://localhost:3000' }],
|
||||
},
|
||||
});
|
||||
};
|
||||
26
routes/other.js
Normal file
26
routes/other.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
module.exports = async function (app, opts) {
|
||||
const register = opts.promRegister;
|
||||
|
||||
app.get('/', async (req, reply) => reply.redirect('/ui'));
|
||||
|
||||
app.get('/metrics', async (req, reply) => {
|
||||
reply.header('Content-Type', register.contentType);
|
||||
return register.metrics();
|
||||
});
|
||||
|
||||
// --- Auth Discord ---
|
||||
app.get('/auth/discord', async (req, reply) => {
|
||||
const clientId = '1410578978712060024';
|
||||
const redirectUri = 'http://localhost:3000/auth/discord/callback';
|
||||
const scope = 'identify email';
|
||||
const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}`;
|
||||
reply.redirect(discordAuthUrl);
|
||||
});
|
||||
|
||||
app.get('/auth/discord/callback', require('./../discord').handleDiscordAuth);
|
||||
|
||||
app.get('/logout', async (req, reply) => {
|
||||
req.session.destroy();
|
||||
reply.redirect('/');
|
||||
});
|
||||
};
|
||||
51
routes/ui.js
Normal file
51
routes/ui.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
module.exports = async function (app, opts) {
|
||||
const db = require('../db');
|
||||
const counterCreated = opts.counterCreated;
|
||||
const counterDeleted = opts.counterDeleted;
|
||||
const gaugeCount = opts.gaugeCount;
|
||||
|
||||
function refreshGauge() {
|
||||
try {
|
||||
const count = db.all().length;
|
||||
gaugeCount.set(count);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
app.get('/ui', async (req, reply) => {
|
||||
if (!req.session.user) {
|
||||
return reply.view('login_required.ejs', { user: req.session.user });
|
||||
}
|
||||
const todos = db.getTodosByUser(req.session.user.id);
|
||||
return reply.view('index.ejs', { todos, user: req.session.user });
|
||||
});
|
||||
|
||||
app.post('/ui/todos', async (req, reply) => {
|
||||
const title = (req.body?.title || '').trim();
|
||||
if (title) {
|
||||
db.createTodo(req.session.user.id, title);
|
||||
counterCreated.inc();
|
||||
refreshGauge();
|
||||
}
|
||||
reply.redirect('/ui');
|
||||
});
|
||||
|
||||
app.post('/ui/todos/:id/toggle', async (req, reply) => {
|
||||
const id = Number(req.params.id);
|
||||
const todo = db.getTodo(id);
|
||||
if (todo) {
|
||||
db.updateTodo(id, { completed: todo.completed ? 0 : 1 });
|
||||
refreshGauge();
|
||||
}
|
||||
reply.redirect('/ui');
|
||||
});
|
||||
|
||||
app.post('/ui/todos/:id/delete', async (req, reply) => {
|
||||
const id = Number(req.params.id);
|
||||
if (db.getTodo(id)) {
|
||||
db.removeTodo(id);
|
||||
counterDeleted.inc();
|
||||
refreshGauge();
|
||||
}
|
||||
reply.redirect('/ui');
|
||||
});
|
||||
};
|
||||
52
views/index.ejs
Normal file
52
views/index.ejs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<section class="new">
|
||||
<form method="post" action="/ui/todos">
|
||||
<input type="text" name="title" placeholder="Nouvelle tâche..." required />
|
||||
<button type="submit">Ajouter</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="list">
|
||||
<% if (todos.length === 0) { %>
|
||||
<p>Aucune tâche pour le moment.</p>
|
||||
<% } else { %>
|
||||
<ul>
|
||||
<% todos.forEach(function(t) { %>
|
||||
<li>
|
||||
<form method="post" action="/ui/todos/<%= t.id %>/toggle" style="display:inline;">
|
||||
<button title="Basculer" class="toggle-btn">
|
||||
<% if (t.completed) { %>
|
||||
<!-- Icône coche remplie -->
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="#4CAF50" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="10" fill="#4CAF50"/>
|
||||
<polyline points="6,11 9,14 14,7" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
</svg>
|
||||
<% } else { %>
|
||||
<!-- Icône cercle vide -->
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="#bbb" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9"/>
|
||||
</svg>
|
||||
<% } %>
|
||||
</button>
|
||||
</form>
|
||||
<div class="<%= t.completed ? 'done' : '' %>">
|
||||
<span><%= t.title %></span>
|
||||
<div class="todo-meta">
|
||||
<small>#<%= t.id %> — <%= t.created_at %></small>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/ui/todos/<%= t.id %>/delete" style="display:inline;">
|
||||
<button class="delete-btn" title="Supprimer">
|
||||
<!-- Icône poubelle moderne -->
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/>
|
||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</section>
|
||||
41
views/layout.ejs
Normal file
41
views/layout.ejs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title><%= typeof title !== 'undefined' ? title : "Todo App" %></title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Todo App</h1>
|
||||
<nav>
|
||||
<a href="/ui">UI</a>
|
||||
<a href="/api/docs">Swagger</a>
|
||||
<a href="/metrics">Metrics</a>
|
||||
<% if (!user) { %>
|
||||
<a class="discordAuth" href="/auth/discord">Discord Connect</a>
|
||||
<% } else { %>
|
||||
<a class="logout" href="/logout">Déconnexion</a>
|
||||
<% } %>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<%- body %>
|
||||
</main>
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<span>
|
||||
© <a href="https://mecdu.dev" target="_blank" rel="noopener" class="footer-link">mecdu.dev</a>
|
||||
</span>
|
||||
<% if (user && user.pseudo) { %>
|
||||
<span style="margin-left:16px; color:var(--muted);">
|
||||
|
|
||||
</span>
|
||||
<span style="margin-left:16px; color:var(--accent);">
|
||||
Connecté en tant que <strong><%= user.pseudo %></strong>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
4
views/login_required.ejs
Normal file
4
views/login_required.ejs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div style="text-align:center; margin-top: 60px;">
|
||||
<h2>Il faut que tu te connectes sur Discord, petit fou !</h2>
|
||||
<a class="discordAuth" href="/auth/discord">Connexion Discord</a>
|
||||
</div>
|
||||
Loading…
Add table
Reference in a new issue