chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user