feat(sprint1-step2): core economy TS + useEconomy hook (lazy calc) + 13 tests vitest

This commit is contained in:
2026-03-17 06:36:51 +01:00
parent c414cf2d07
commit c69da320cc
13 changed files with 2627 additions and 174 deletions

View File

@@ -30,10 +30,10 @@ const cors = require("cors");
app.use(
cors({
origin: [
process.env.FRONTEND_URL, // keep this one, after checking the value in `backend/.env`
"http://mysite.com",
"http://another-domain.com",
process.env.FRONTEND_URL,
process.env.SUPER_OAUTH_URL,
],
credentials: true,
})
);

View File

@@ -0,0 +1,78 @@
const jwt = require("jsonwebtoken");
const tables = require("../tables");
const secretKey = process.env.APP_SECRET;
/**
* GET /api/auth/callback?code=<token>
*
* Reçoit le token SuperOAuth depuis le frontend après redirect OAuth.
* Valide auprès de SuperOAuth, résout ou crée le user local, retourne un JWT local.
*/
const callback = async (req, res) => {
const { code } = req.query;
if (!code) {
return res.status(400).json({ message: "Missing OAuth code." });
}
const superOAuthUrl = process.env.SUPER_OAUTH_URL;
if (!superOAuthUrl) {
return res.status(500).json({ message: "Auth service not configured." });
}
try {
const response = await fetch(`${superOAuthUrl}/api/v1/auth/token/validate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: code }),
});
const data = await response.json();
if (!response.ok || !data.data?.valid || !data.data?.user) {
return res.status(401).json({ message: "Invalid OAuth token." });
}
if (!data.data.user.isActive) {
return res.status(401).json({ message: "Account is disabled." });
}
const { id: superOAuthId, email, nickname } = data.data.user;
let localUser = await tables.users.getBySuperOAuthId(superOAuthId);
if (!localUser) {
// Premier login OAuth — créer le compte local
const insertId = await tables.users.create({
nickname: nickname ?? email?.split("@")[0] ?? `user_${Date.now()}`,
mail: email ?? `${superOAuthId}@oauth.local`,
password: "", // pas de password local — auth via SuperOAuth uniquement
tetardcoin: 1000,
});
await tables.users.linkSuperOAuth(insertId, superOAuthId);
localUser = await tables.users.read(insertId);
}
const token = jwt.sign({ user: localUser.id }, secretKey, { expiresIn: "7d" });
return res.status(200).json({
message: "Connexion réussie",
user: localUser,
token,
});
} catch (err) {
console.error("auth/callback — error", err);
return res.status(500).json({ message: "Internal server error." });
}
};
/**
* POST /api/auth/logout
* Stateless — le token JWT est géré côté client.
*/
const logout = (_req, res) => {
return res.status(200).json({ message: "Déconnexion réussie." });
};
module.exports = { callback, logout };

View File

@@ -410,6 +410,27 @@ const destroy = async (req, res) => {
};
const updateCoins = async (req, res) => {
const userId = req.params.id;
const { tetardcoin } = req.body;
if (tetardcoin === undefined || typeof tetardcoin !== "number") {
return res.status(400).json({ message: "tetardcoin (number) requis." });
}
try {
const affectedRows = await tables.users.edit(userId, { tetardcoin });
if (affectedRows === 0) {
return res.status(404).json({ message: "Utilisateur non trouvé." });
}
return res.status(200).json({ tetardcoin });
} catch (err) {
return res.status(500).json({ message: "Erreur lors de la mise à jour.", error: err.message });
}
};
module.exports = {
browse,
read,
@@ -419,4 +440,5 @@ module.exports = {
login,
forgottenPassword,
resetPassword,
updateCoins,
};

View File

@@ -0,0 +1,57 @@
const tables = require("../tables");
/**
* Middleware verifyOAuth — Token Introspection via SuperOAuth.
*
* Flow :
* 1. Extraire le token du header x-auth-token
* 2. Appeler SuperOAuth POST /api/v1/auth/token/validate
* 3. Résoudre l'utilisateur local par super_oauth_id
* 4. req.user = localUser.id (integer) — verifySelf inchangé
*/
const verifyOAuth = async (req, res, next) => {
const token = req.header("x-auth-token");
if (!token) {
return res.status(401).json({ message: "Access denied. No token provided." });
}
const superOAuthUrl = process.env.SUPER_OAUTH_URL;
if (!superOAuthUrl) {
console.error("verifyOAuth — SUPER_OAUTH_URL not configured");
return res.status(500).json({ message: "Auth service not configured." });
}
try {
const response = await fetch(`${superOAuthUrl}/api/v1/auth/token/validate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const data = await response.json();
if (!response.ok || !data.data?.valid || !data.data?.user) {
return res.status(401).json({ message: "Invalid or expired token." });
}
if (!data.data.user.isActive) {
return res.status(401).json({ message: "Account is disabled." });
}
const superOAuthId = data.data.user.id;
const localUser = await tables.users.getBySuperOAuthId(superOAuthId);
if (!localUser) {
return res.status(401).json({ message: "Account not linked. Please log in via SuperOAuth." });
}
req.user = localUser.id;
return next();
} catch (err) {
console.error("verifyOAuth — auth service unreachable", err);
return res.status(500).json({ message: "Authentication service unreachable." });
}
};
module.exports = verifyOAuth;

View File

@@ -8,8 +8,9 @@ const router = express.Router();
// Import Controllers
const userControllers = require("./controllers/userControllers");
const authControllers = require("./controllers/authControllers");
const verifyToken = require("./middlewares/verifyToken");
const verifyOAuth = require("./middlewares/verifyOAuth");
// Vérifie que le token appartient au même utilisateur que :id
const verifySelf = (req, res, next) => {
@@ -19,7 +20,11 @@ const verifySelf = (req, res, next) => {
return next();
};
// User management
// Auth SuperOAuth
router.get("/auth/callback", authControllers.callback);
router.post("/auth/logout", authControllers.logout);
// User management (auth locale — conservée pendant migration)
router.get("/users", verifyToken, userControllers.browse);
router.get("/users/:id", verifyToken, verifySelf, userControllers.read);
router.get("/users/:id/field", verifyToken, verifySelf, userControllers.read);
@@ -28,6 +33,9 @@ router.post("/users", userControllers.add);
router.delete("/users/:id", verifyToken, verifySelf, userControllers.destroy);
router.post("/login", userControllers.login);
// Sync game state — SuperOAuth uniquement
router.patch("/users/:id/coins", verifyOAuth, verifySelf, userControllers.updateCoins);
/* ************************************************************************* */