Initial commit: Portugais App
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
const express = require('express');
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3007;
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'portugais-secret-key-2026-change-me';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Pierre2026!';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// ===== DATABASE =====
|
||||
const db = new Database('/data/portugais.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 TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
is_admin INTEGER DEFAULT 0,
|
||||
avatar TEXT DEFAULT '🇵🇹',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS points (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
points INTEGER DEFAULT 0,
|
||||
activity TEXT,
|
||||
activity_type TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_progress (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
exercises_done INTEGER DEFAULT 0,
|
||||
games_played INTEGER DEFAULT 0,
|
||||
points_earned INTEGER DEFAULT 0,
|
||||
streak INTEGER DEFAULT 0,
|
||||
UNIQUE(user_id, date),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
phrase TEXT NOT NULL,
|
||||
UNIQUE(user_id, phrase),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
`);
|
||||
|
||||
// Create default admin
|
||||
const existingAdmin = db.prepare('SELECT id FROM users WHERE username = ?').get('Pierre');
|
||||
if (!existingAdmin) {
|
||||
const hash = bcrypt.hashSync(ADMIN_PASSWORD, 10);
|
||||
db.prepare('INSERT INTO users (username, password, display_name, is_admin, avatar) VALUES (?, ?, ?, ?, ?)').run('Pierre', hash, 'Pierre', 1, '👨💼');
|
||||
console.log('Admin user Pierre created');
|
||||
}
|
||||
|
||||
// ===== AUTH MIDDLEWARE =====
|
||||
function authMiddleware(req, res, next) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Token requis' });
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const user = db.prepare('SELECT id, username, display_name, is_admin, avatar FROM users WHERE id = ?').get(decoded.userId);
|
||||
if (!user) return res.status(401).json({ error: 'Utilisateur introuvable' });
|
||||
req.user = user;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Token invalide' });
|
||||
}
|
||||
}
|
||||
|
||||
function adminMiddleware(req, res, next) {
|
||||
if (!req.user.is_admin) return res.status(403).json({ error: 'Accès admin requis' });
|
||||
next();
|
||||
}
|
||||
|
||||
// ===== AUTH ROUTES =====
|
||||
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)) {
|
||||
return res.status(401).json({ error: 'Identifiant ou mot de passe incorrect' });
|
||||
}
|
||||
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '30d' });
|
||||
res.json({
|
||||
token,
|
||||
user: { id: user.id, username: user.username, display_name: user.display_name, is_admin: user.is_admin, avatar: user.avatar }
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/register', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const { username, password, display_name, avatar } = req.body;
|
||||
if (!username || !password || !display_name) {
|
||||
return res.status(400).json({ error: 'Tous les champs sont requis' });
|
||||
}
|
||||
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (existing) return res.status(400).json({ error: 'Ce nom d\'utilisateur existe déjà' });
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const result = db.prepare('INSERT INTO users (username, password, display_name, avatar) VALUES (?, ?, ?, ?)').run(username, hash, display_name, avatar || '🇵🇹');
|
||||
res.json({ id: result.lastInsertRowid, username, display_name, avatar: avatar || '🇵🇹' });
|
||||
});
|
||||
|
||||
app.get('/api/me', authMiddleware, (req, res) => {
|
||||
res.json({ user: req.user });
|
||||
});
|
||||
|
||||
// ===== ADMIN ROUTES =====
|
||||
app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const users = db.prepare(`
|
||||
SELECT u.id, u.username, u.display_name, u.is_admin, u.avatar, u.created_at,
|
||||
COALESCE(SUM(p.points), 0) as total_points
|
||||
FROM users u
|
||||
LEFT JOIN points p ON p.user_id = u.id
|
||||
GROUP BY u.id
|
||||
ORDER BY total_points DESC
|
||||
`).all();
|
||||
res.json({ users });
|
||||
});
|
||||
|
||||
app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
if (userId === req.user.id) return res.status(400).json({ error: 'Vous ne pouvez pas vous supprimer' });
|
||||
db.prepare('DELETE FROM favorites WHERE user_id = ?').run(userId);
|
||||
db.prepare('DELETE FROM daily_progress WHERE user_id = ?').run(userId);
|
||||
db.prepare('DELETE FROM points WHERE user_id = ?').run(userId);
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.put('/api/admin/users/:id/toggle-admin', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
const user = db.prepare('SELECT is_admin FROM users WHERE id = ?').get(userId);
|
||||
if (!user) return res.status(404).json({ error: 'Utilisateur introuvable' });
|
||||
db.prepare('UPDATE users SET is_admin = ? WHERE id = ?').run(user.is_admin ? 0 : 1, userId);
|
||||
res.json({ is_admin: !user.is_admin });
|
||||
});
|
||||
|
||||
// ===== POINTS ROUTES =====
|
||||
app.post('/api/points/add', authMiddleware, (req, res) => {
|
||||
const { points, activity, activity_type } = req.body;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
db.prepare('INSERT INTO points (user_id, points, activity, activity_type) VALUES (?, ?, ?, ?)').run(req.user.id, points, activity, activity_type);
|
||||
|
||||
const existing = db.prepare('SELECT * FROM daily_progress WHERE user_id = ? AND date = ?').get(req.user.id, today);
|
||||
if (existing) {
|
||||
db.prepare('UPDATE daily_progress SET exercises_done = exercises_done + ?, points_earned = points_earned + ? WHERE user_id = ? AND date = ?').run(activity_type === 'exercise' ? 1 : 0, points, req.user.id, today);
|
||||
} else {
|
||||
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
|
||||
const yesterdayProgress = db.prepare('SELECT streak FROM daily_progress WHERE user_id = ? AND date = ?').get(req.user.id, yesterday);
|
||||
const newStreak = (yesterdayProgress && yesterdayProgress.streak > 0) ? yesterdayProgress.streak + 1 : 1;
|
||||
db.prepare('INSERT INTO daily_progress (user_id, date, exercises_done, games_played, points_earned, streak) VALUES (?, ?, ?, ?, ?, ?)').run(req.user.id, today, activity_type === 'exercise' ? 1 : 0, activity_type === 'game' ? 1 : 0, points, newStreak);
|
||||
}
|
||||
|
||||
const total = db.prepare('SELECT COALESCE(SUM(points), 0) as total FROM points WHERE user_id = ?').get(req.user.id);
|
||||
res.json({ total_points: total.total });
|
||||
});
|
||||
|
||||
app.get('/api/points/leaderboard', authMiddleware, (req, res) => {
|
||||
const leaderboard = db.prepare(`
|
||||
SELECT u.id, u.display_name, u.avatar,
|
||||
COALESCE(SUM(p.points), 0) as total_points
|
||||
FROM users u
|
||||
LEFT JOIN points p ON p.user_id = u.id
|
||||
GROUP BY u.id
|
||||
ORDER BY total_points DESC
|
||||
LIMIT 20
|
||||
`).all();
|
||||
res.json({ leaderboard });
|
||||
});
|
||||
|
||||
app.get('/api/points/me', authMiddleware, (req, res) => {
|
||||
const total = db.prepare('SELECT COALESCE(SUM(points), 0) as total FROM points WHERE user_id = ?').get(req.user.id);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const progress = db.prepare('SELECT * FROM daily_progress WHERE user_id = ? AND date = ?').get(req.user.id, today);
|
||||
const recentPoints = db.prepare('SELECT * FROM points WHERE user_id = ? ORDER BY created_at DESC LIMIT 10').all(req.user.id);
|
||||
res.json({ total_points: total.total, today: progress || { exercises_done: 0, games_played: 0, points_earned: 0, streak: 0 }, recent: recentPoints });
|
||||
});
|
||||
|
||||
app.get('/api/points/history', authMiddleware, (req, res) => {
|
||||
const history = db.prepare(`
|
||||
SELECT date, exercises_done, games_played, points_earned, streak
|
||||
FROM daily_progress WHERE user_id = ?
|
||||
ORDER BY date DESC LIMIT 30
|
||||
`).all(req.user.id);
|
||||
res.json({ history });
|
||||
});
|
||||
|
||||
// ===== FAVORITES =====
|
||||
app.get('/api/favorites', authMiddleware, (req, res) => {
|
||||
const favs = db.prepare('SELECT phrase FROM favorites WHERE user_id = ?').all(req.user.id);
|
||||
res.json({ favorites: favs.map(f => f.phrase) });
|
||||
});
|
||||
|
||||
app.post('/api/favorites/toggle', authMiddleware, (req, res) => {
|
||||
const { phrase } = req.body;
|
||||
const existing = db.prepare('SELECT id FROM favorites WHERE user_id = ? AND phrase = ?').get(req.user.id, phrase);
|
||||
if (existing) {
|
||||
db.prepare('DELETE FROM favorites WHERE id = ?').run(existing.id);
|
||||
res.json({ favorited: false });
|
||||
} else {
|
||||
db.prepare('INSERT INTO favorites (user_id, phrase) VALUES (?, ?)').run(req.user.id, phrase);
|
||||
res.json({ favorited: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== SPA FALLBACK =====
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Portugais app running on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user