feat(sprint1-step2): core economy TS + useEconomy hook (lazy calc) + 13 tests vitest
This commit is contained in:
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
78
Backend/src/controllers/authControllers.js
Normal file
78
Backend/src/controllers/authControllers.js
Normal 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 };
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
57
Backend/src/middlewares/verifyOAuth.js
Normal file
57
Backend/src/middlewares/verifyOAuth.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user