chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:11 +08:00
commit 94d5d7e16e
605 changed files with 80879 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
PORT=4001
CLIENT_ORIGIN=http://localhost:4000
JWT_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
ACCESS_TTL_SEC=900
REFRESH_SECRET=dev-super-refresh-secret-change-me
REFRESH_TTL_SEC=604800
+107
View File
@@ -0,0 +1,107 @@
{
"users": [
{
"id": 1,
"email": "john@example.com",
"password": "12345678",
"name": "John Doe",
"provider": "email",
"google_sub": null,
"created_at": "2024-01-01T10:00:00.000Z",
"updated_at": "2024-01-01T10:00:00.000Z"
},
{
"id": 2,
"email": "google.user@example.com",
"password": "FP8-BZu57tq6dbxily42G",
"name": "Google User",
"provider": "google",
"google_sub": "mock-google-sub-123456",
"created_at": "2025-10-13T13:47:58.962Z",
"updated_at": "2025-10-13T13:55:36.888Z"
}
],
"revoked_tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInR5cCI6InJlZnJlc2giLCJqdGkiOiI4OU53cTNLTzQ3Y29PMkJORXRKSUYiLCJpYXQiOjE3NjAzNjMxMTMsImV4cCI6MTc2MDk2NzkxM30.0H2qIVgn1zqqqwPskQqiC9g8WxsSf3153RhjJRzaBRM",
"revoked_at": "2025-10-13T13:45:18.141Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInR5cCI6ImFjY2VzcyIsImlhdCI6MTc2MDM2MzExOCwiZXhwIjoxNzYwMzY0MDE4fQ.FYCDr4SKIW5QdlJTMzQiskE6S9PrEmQLQr3sqhss2Ac",
"revoked_at": "2025-10-13T13:47:52.407Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInR5cCI6InJlZnJlc2giLCJqdGkiOiJsdVgwZ1VqOUdiV1lsWGw4NzNxQW4iLCJpYXQiOjE3NjAzNjMxMTgsImV4cCI6MTc2MDk2NzkxOH0.S6dq8u02xUrsQ4Y-cj94_tUHq_uhXH5DWXFxVpTgptk",
"revoked_at": "2025-10-13T13:47:52.408Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInR5cCI6InJlZnJlc2giLCJqdGkiOiIwZ1NNa0dXRUV3ZFUzRFRadGU0MlkiLCJpYXQiOjE3NjAzNjM0NjYsImV4cCI6MTc2MDk2ODI2Nn0.vuC674JAaT5IQI8Fqc4DRK9n3VQsQjGC6Z3Ds-kb8Ik",
"revoked_at": "2025-10-13T13:51:13.439Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInR5cCI6ImFjY2VzcyIsImlhdCI6MTc2MDM2MzQ3MywiZXhwIjoxNzYwMzY0MzczfQ.irc95PuNnmUcEYsZTX--_SriXyD98KHtQ_VBgn5Walk",
"revoked_at": "2025-10-13T13:51:14.470Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInR5cCI6InJlZnJlc2giLCJqdGkiOiJlN09WM056YU9TWHVtS1NjTVRpM2UiLCJpYXQiOjE3NjAzNjM0NzMsImV4cCI6MTc2MDk2ODI3M30.CeFZZ3lZktww4fm5NbWpMuU7y5_K7LxMyxoTbv7NldA",
"revoked_at": "2025-10-13T13:51:14.470Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiSDh0aEVSendvNjhyOGZfVDJ5aXp1IiwiaWF0IjoxNzYwMzYzNzM2LCJleHAiOjE3NjA5Njg1MzZ9.TanDpShEyP8YqwbLSB-_eg4sReXka_KC2ulQikeBTx0",
"revoked_at": "2025-10-13T13:55:43.888Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiT2d1M2ViV0xEQ0JIMzNjRGRISmo5IiwiaWF0IjoxNzYwMzYzNzQzLCJleHAiOjE3NjA5Njg1NDN9.CHqEkcxp1uLztk30MQ9abqv2_q7elOK8RGrj07T92PU",
"revoked_at": "2025-10-13T13:55:46.670Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiYTluNFpZSWpGd2FKRk5CS0hKNWFBIiwiaWF0IjoxNzYwMzYzNzQ2LCJleHAiOjE3NjA5Njg1NDZ9.H4Ka8kV9L-ekVCp63Kf0FolYpITSp08hKnRNy2lAWwY",
"revoked_at": "2025-10-13T13:55:49.862Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiM3o3Q3BtM2s5OVpQZkFWdGVVaEJPIiwiaWF0IjoxNzYwMzYzNzQ5LCJleHAiOjE3NjA5Njg1NDl9.eJVL1480PoxpDJ7qW_JRluYd6QUGHc1IOZiaN8JZmGE",
"revoked_at": "2025-10-13T13:57:34.751Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiSG9ncVhwTXBBaHhuV1lWZW5ORXJxIiwiaWF0IjoxNzYwMzYzODU0LCJleHAiOjE3NjA5Njg2NTR9.ynyXvZbeB2NUFgzT3Yhoo5hVMf2TZJshzAXxr0fQOPc",
"revoked_at": "2025-10-13T14:14:48.532Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiN0JoVG9xd2ozTTdDRFYxWVY3el9kIiwiaWF0IjoxNzYwMzY0ODg4LCJleHAiOjE3NjA5Njk2ODh9.-9Ji9DNPyM-RbVxx4VERSiLIeEBEMeafdshvB-r4y-0",
"revoked_at": "2025-10-13T14:21:50.105Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoieFVSMlZQVWNkRzVJdlRNbG9TWUtFIiwiaWF0IjoxNzYwMzY1MzEwLCJleHAiOjE3NjA5NzAxMTB9.MnruPjtk8nAACyWc6YL3vK1VnQrZl_9EV_gQpOCv1p0",
"revoked_at": "2025-10-13T14:23:27.771Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiSUNsS0JQSDZqbmRad1YzS0Z0Z256IiwiaWF0IjoxNzYwMzY1NDA3LCJleHAiOjE3NjA5NzAyMDd9.TwFjyIyE6ezXPy8A_ymsy5IGG7KUfiUWMjaolNeir5g",
"revoked_at": "2025-10-13T14:23:29.904Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiSmxSRWYyMnowbThDbXIzWFByT0J4IiwiaWF0IjoxNzYwMzY1NDA5LCJleHAiOjE3NjA5NzAyMDl9.LkmqBa7wzfza0_Yp3NKXrq3G3hc0B4em6MdsY_C3Il8",
"revoked_at": "2025-10-13T14:23:31.169Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoic2RVczNHTmdLMHdITjNzbTExZG1vIiwiaWF0IjoxNzYwMzY1NDExLCJleHAiOjE3NjA5NzAyMTF9.V7RvJToV3aJlzV2kPtPkuzy_96qissNcgwz8j4Uw7OY",
"revoked_at": "2025-10-13T14:23:32.221Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiZ2dudHBWM0JjZFJVVmtqaGc2dmdnIiwiaWF0IjoxNzYwMzY1NDEyLCJleHAiOjE3NjA5NzAyMTJ9.rG9zKWot4Z4he5ddN9L7ir95EOLW2r7Tk505KBJ3gwY",
"revoked_at": "2025-10-13T14:23:33.468Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiQ2RDX2hxZFB4SnJJSl9oak9pX2FyIiwiaWF0IjoxNzYwMzY1NDEzLCJleHAiOjE3NjA5NzAyMTN9.k5JxsrxGyd_c9Gn0FrNYaFHkKz7Rdi_G58xKJ8CJMso",
"revoked_at": "2025-10-13T14:40:58.867Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJhY2Nlc3MiLCJpYXQiOjE3NjAzNjY0NTgsImV4cCI6MTc2MDM2NzM1OH0.lI7QFYFiTTIRqn4vfQMmpCVRJbtRVsPpQ5gukoZ2LO8",
"revoked_at": "2025-10-13T14:40:59.484Z"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImVtYWlsIjoiZ29vZ2xlLnVzZXJAZXhhbXBsZS5jb20iLCJ0eXAiOiJyZWZyZXNoIiwianRpIjoiZ1FOTkhTWFYwV0pURFFDZHdhdm1vIiwiaWF0IjoxNzYwMzY2NDU4LCJleHAiOjE3NjA5NzEyNTh9.cLP_lTXsygnodOnw06XELcfeCOnimq-mwTvSDDBndrM",
"revoked_at": "2025-10-13T14:40:59.490Z"
}
],
"password_resets": []
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "mock-auth-api",
"private": true,
"type": "module",
"scripts": {
"start": "node server.js",
"start:watch": "NODE_ENV=development nodemon server.js"
},
"dependencies": {
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dayjs": "^1.11.10",
"dotenv": "^16.4.5",
"json-server": "^0.17.4",
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
"lowdb": "^6.1.1",
"nanoid": "^5.0.7"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"/api/v1/auth/*": "/$1",
"/api/v1/auth/me": "/me",
"/api/v1/auth/register": "/register",
"/api/v1/auth/login": "/login",
"/api/v1/auth/google": "/google",
"/api/v1/auth/logout": "/logout",
"/api/v1/auth/refresh": "/refresh",
"/api/v1/auth/forgot-password": "/forgot-password",
"/api/v1/auth/reset-password": "/reset-password"
}
+383
View File
@@ -0,0 +1,383 @@
import "dotenv/config";
import jsonServer from "json-server";
import cors from "cors";
import cookieParser from 'cookie-parser';
import jwt from "jsonwebtoken";
import { nanoid } from "nanoid";
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
// -------------------- Env & Defaults --------------------
const {
JWT_SECRET = process.env.JWT_SECRET,
ACCESS_TTL_SEC = process.env.ACCESS_TTL_SEC, // 15 minutes
REFRESH_TTL_SEC = process.env.REFRESH_TTL_SEC, // 7 days
CLIENT_ORIGIN = process.env.CLIENT_ORIGIN,
PORT = "4001", // match your NEXT_PUBLIC_API_URL host:port
} = process.env;
const ACCESS_TTL = parseInt(ACCESS_TTL_SEC, 10);
const REFRESH_TTL = parseInt(REFRESH_TTL_SEC, 10);
// -------------------- App & DB Setup --------------------
const app = jsonServer.create();
const router = jsonServer.router("db.json");
const middlewares = jsonServer.defaults({ noCors: true });
const defaultData = {
users: [],
revoked_tokens: [],
password_resets: [],
};
const adapter = new JSONFile("db.json");
const db = new Low(adapter, defaultData);
await db.read();
db.data.users ??= [];
db.data.revoked_tokens ??= [];
db.data.password_resets ??= [];
// -------------------- CORS (with credentials) --------------------
const corsOptions = {
origin: CLIENT_ORIGIN, // must be explicit when using credentials
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
};
app.use(cors(corsOptions));
app.use(
cors({
origin: CLIENT_ORIGIN, // must be the exact origin when using credentials
credentials: true,
methods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS'],
allowedHeaders: ['Content-Type','Authorization'],
})
);
app.options('*', cors({ origin: CLIENT_ORIGIN, credentials: true }));
app.use(cookieParser());
// -------------------- Middleware --------------------
app.use(jsonServer.bodyParser);
app.use(middlewares);
// Optional request log to debug routes
app.use((req, _res, next) => {
console.log(req.method, req.url);
next();
});
// -------------------- Helpers --------------------
function signAccessToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email, typ: "access" },
JWT_SECRET,
{ expiresIn: ACCESS_TTL }
);
}
function signRefreshToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email, typ: "refresh", jti: nanoid() },
JWT_SECRET,
{ expiresIn: REFRESH_TTL }
);
}
function setRefreshCookie(res, token, maxAgeMs) {
res.cookie('refresh_token', token, {
httpOnly: true,
sameSite: 'lax',
secure: false, // true in production if using HTTPS
maxAge: maxAgeMs,
path: '/', // adjust if needed
});
}
function sanitizeUser(u) {
const { password, ...safe } = u;
return safe;
}
function authz(req) {
const h = req.headers.authorization || "";
const token = h.startsWith("Bearer ") ? h.slice(7) : null;
if (!token) return null;
try {
const payload = jwt.verify(token, JWT_SECRET);
return payload;
} catch {
return null;
}
}
async function getUserByEmail(email) {
await db.read();
return db.data.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
}
async function getUserById(id) {
await db.read();
return db.data.users.find((u) => u.id === id);
}
async function createUser({
email,
password,
name,
provider = "email",
google_sub = null,
}) {
await db.read();
const nextId = db.data.users.length
? Math.max(...db.data.users.map((u) => u.id)) + 1
: 1;
const now = new Date().toISOString();
const newUser = {
id: nextId,
email,
password, // ⚠️ mock only (no hashing)
name,
provider,
google_sub,
created_at: now,
updated_at: now,
};
db.data.users.push(newUser);
await db.write();
return newUser;
}
async function upsertGoogleUser({ email, name, google_sub }) {
await db.read();
let user =
db.data.users.find((u) => u.google_sub === google_sub) ||
db.data.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
if (user) {
user.google_sub = google_sub;
user.provider = "google";
user.name = user.name || name;
user.updated_at = new Date().toISOString();
await db.write();
return user;
}
return createUser({
email,
password: nanoid(),
name,
provider: "google",
google_sub,
});
}
async function revokeToken(token) {
await db.read();
db.data.revoked_tokens.push({
token,
revoked_at: new Date().toISOString(),
});
await db.write();
}
async function isRevoked(token) {
await db.read();
return !!db.data.revoked_tokens.find((t) => t.token === token);
}
// -------------------- AUTH Routes --------------------
// POST /api/v1/auth/register
app.post(["/api/v1/auth/register", "/register"], async (req, res) => {
const { email, password, name } = req.body || {};
if (!email || !password)
return res
.status(422)
.json({ message: "email și password sunt obligatorii" });
const existing = await getUserByEmail(email);
if (existing) return res.status(409).json({ message: "Email deja folosit" });
const user = await createUser({ email, password, name });
const access_token = signAccessToken(user);
const refresh_token = signRefreshToken(user);
setRefreshCookie(res, refresh_token, REFRESH_TTL * 1000);
res.json({ user: sanitizeUser(user), access_token, refresh_token });
});
// POST /api/v1/auth/login
app.post(["/api/v1/auth/login", "/login"], async (req, res) => {
const { email, password } = req.body || {};
const user = await getUserByEmail(email || "");
if (!user || user.password !== password)
return res.status(401).json({ message: "Credențiale invalide" });
const access_token = signAccessToken(user);
const refresh_token = signRefreshToken(user);
setRefreshCookie(res, refresh_token, REFRESH_TTL * 1000);
res.json({ user: sanitizeUser(user), access_token, refresh_token });
});
// POST /api/v1/auth/google (MOCK: sets refresh cookie like classic login)
app.post(["/api/v1/auth/google", "/google"], async (req, res) => {
const { token } = req.body || {};
if (!token) return res.status(422).json({ message: "token lipsă" });
let payload;
try {
payload = typeof token === "string" ? JSON.parse(token) : token;
} catch {
return res
.status(400)
.json({ message: "token invalid (mock așteaptă JSON cu {email,name,sub})" });
}
const { email, name, sub } = payload || {};
if (!email || !sub) {
return res
.status(400)
.json({ message: "token mock trebuie să conțină email și sub" });
}
// create/update Google user
const user = await upsertGoogleUser({
email,
name: name || email.split("@")[0],
google_sub: sub,
});
// issue tokens
const access_token = signAccessToken(user);
const refresh_token = signRefreshToken(user);
// ✅ set refresh cookie exactly like the classic login
setRefreshCookie(res, refresh_token, REFRESH_TTL * 1000);
// return same shape as login (keeps parity with Rails response)
return res.json({
user: sanitizeUser(user),
access_token,
refresh_token,
});
});
// POST /api/v1/auth/logout — clear refresh cookie & revoke tokens
app.post(["/api/v1/auth/logout", "/logout"], async (req, res) => {
const h = req.headers.authorization || "";
const at = h.startsWith("Bearer ") ? h.slice(7) : null;
if (at) await revokeToken(at);
const rt = req.cookies?.refresh_token;
if (rt) await revokeToken(rt);
res.clearCookie("refresh_token", { httpOnly: true, sameSite: "lax", secure: false, path: "/" });
return res.json({ success: true });
});
// POST /api/v1/auth/refresh — cookie-based (no body)
app.post(["/api/v1/auth/refresh", "/refresh"], async (req, res) => {
const refresh_token = req.cookies?.refresh_token;
if (!refresh_token) return res.status(401).json({ message: "No refresh token" });
try {
const payload = jwt.verify(refresh_token, JWT_SECRET);
if (payload.typ !== "refresh") return res.status(401).json({ message: "tip token greșit" });
if (await isRevoked(refresh_token)) return res.status(401).json({ message: "refresh token revocat" });
const user = await getUserById(payload.sub);
if (!user) return res.status(401).json({ message: "user inexistent" });
await revokeToken(refresh_token); // rotate
const new_access = signAccessToken(user);
const new_refresh = signRefreshToken(user);
setRefreshCookie(res, new_refresh, REFRESH_TTL * 1000);
return res.json({ access_token: new_access }); // only access in body
} catch {
return res.status(401).json({ message: "refresh token invalid sau expirat" });
}
});
// GET /api/v1/auth/me
app.get(["/api/v1/auth/me", "/me"], async (req, res) => {
const payload = authz(req);
if (!payload || payload.typ !== "access")
return res.status(401).json({ message: "Unauthorized" });
if (await isRevoked((req.headers.authorization || "").slice(7))) {
return res.status(401).json({ message: "Token revocat" });
}
const user = await getUserById(payload.sub);
if (!user) return res.status(404).json({ message: "User not found" });
res.json({ user: sanitizeUser(user) });
});
// -------------------- Password Reset (mock) --------------------
app.post(
["/api/v1/auth/forgot-password", "/forgot-password"],
async (req, res) => {
const { email } = req.body || {};
const user = email ? await getUserByEmail(email) : null;
if (!user)
return res.json({
message: "Dacă emailul există, vei primi un link de resetare",
});
const token = nanoid();
await db.read();
db.data.password_resets.push({
id: nanoid(),
user_id: user.id,
token,
created_at: new Date().toISOString(),
consumed_at: null,
});
await db.write();
// In mock, return token for testing the UI
res.json({ message: "Reset email sent (mock)", token });
}
);
app.post(
["/api/v1/auth/reset-password", "/reset-password"],
async (req, res) => {
const { token, password } = req.body || {};
if (!token || !password)
return res
.status(422)
.json({ message: "token și password sunt obligatorii" });
await db.read();
const prIndex = db.data.password_resets.findIndex(
(r) => r.token === token && !r.consumed_at
);
if (prIndex === -1)
return res.status(400).json({ message: "token invalid sau folosit" });
const pr = db.data.password_resets[prIndex];
const user = await getUserById(pr.user_id);
if (!user) return res.status(404).json({ message: "user inexistent" });
user.password = password;
user.updated_at = new Date().toISOString();
db.data.password_resets[prIndex].consumed_at = new Date().toISOString();
await db.write();
res.json({ message: "Parola a fost actualizată" });
}
);
// -------------------- Mount json-server router for other CRUD --------------------
app.use("/api", router);
// -------------------- Start --------------------
app.listen(Number(PORT), () => {
console.log(`Mock AUTH API on http://localhost:${PORT}`);
});