first commit
This commit is contained in:
4
backend/.dockerignore
Normal file
4
backend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.env*
|
||||
*.log
|
||||
.DS_Store
|
||||
41
backend/Dockerfile
Normal file
41
backend/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# ╔══════════════════════════════════════════════╗
|
||||
# ║ Backend Dockerfile – multi-stage ║
|
||||
# ╚══════════════════════════════════════════════╝
|
||||
|
||||
# ── Stage 1: deps (incl. native build tools) ─────
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# ── Stage 2: development (hot reload via --watch) ─
|
||||
FROM deps AS development
|
||||
COPY . .
|
||||
EXPOSE 3001
|
||||
CMD ["node", "--watch", "src/server.js"]
|
||||
|
||||
# ── Stage 3: productie-deps (apart gebouwd) ───────
|
||||
FROM deps AS prod-deps
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
# ── Stage 4: production (minimale image) ──────────
|
||||
FROM node:22-alpine AS production
|
||||
LABEL maintainer="VibeFinance"
|
||||
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=prod-deps /app/node_modules ./node_modules
|
||||
COPY package.json ./
|
||||
COPY src/ ./src/
|
||||
|
||||
RUN chown -R appuser:appgroup /app
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3001/api/health || exit 1
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
27
backend/package.json
Normal file
27
backend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "vibefinance-backend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "node --watch src/server.js",
|
||||
"lint": "eslint src --ext .js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.13.3",
|
||||
"pino": "^9.0.0",
|
||||
"pino-http": "^10.0.0",
|
||||
"pino-pretty": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0"
|
||||
}
|
||||
}
|
||||
11
backend/src/config/index.js
Normal file
11
backend/src/config/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import "dotenv/config";
|
||||
|
||||
export const config = {
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
PORT: parseInt(process.env.PORT || "3001", 10),
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN || "http://localhost",
|
||||
JWT_SECRET: process.env.JWT_SECRET || "change-me-in-production",
|
||||
JWT_EXPIRES: process.env.JWT_EXPIRES || "7d",
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
};
|
||||
30
backend/src/constants/initialData.js
Normal file
30
backend/src/constants/initialData.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export const INITIAL_DATA = {
|
||||
eigenVermogen: {
|
||||
doel: 500000,
|
||||
categories: [
|
||||
{ id: "c1", naam: "Aandelen", color: "#3b82f6", items: [] },
|
||||
{ id: "c2", naam: "Crypto", color: "#8b5cf6", items: [] },
|
||||
{ id: "c3", naam: "Commodities", color: "#f59e0b", items: [] },
|
||||
{ id: "c4", naam: "Vastgoed", color: "#10b981", items: [] },
|
||||
{ id: "c5", naam: "Crowdfunding", color: "#06b6d4", items: [] },
|
||||
{ id: "c6", naam: "Te Besteden", color: "#64748b", items: [] },
|
||||
],
|
||||
},
|
||||
schuld: {
|
||||
initieleSchuld: 303000,
|
||||
schuldHistory: [],
|
||||
blokken: [
|
||||
{ id: "s1", naam: "Hypotheek", color: "#ef4444", items: [
|
||||
{ id: "s1i1", naam: "Leningdeel 1", waarde: 0 },
|
||||
{ id: "s1i2", naam: "Leningdeel 2", waarde: 0 },
|
||||
{ id: "s1i3", naam: "Leningdeel 3", waarde: 0 },
|
||||
{ id: "s1i4", naam: "Leningdeel 4", waarde: 0 },
|
||||
]},
|
||||
{ id: "s2", naam: "Belastingdienst", color: "#f97316", items: [
|
||||
{ id: "s2i1", naam: "Kinderopvangtoeslag", waarde: 0 },
|
||||
{ id: "s2i2", naam: "Sub-categorie 2", waarde: 0 },
|
||||
]},
|
||||
],
|
||||
},
|
||||
evVoortgang: [],
|
||||
};
|
||||
33
backend/src/db.js
Normal file
33
backend/src/db.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import pg from "pg";
|
||||
import { config } from "./config/index.js";
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
export const db = new Pool({
|
||||
connectionString: config.DATABASE_URL,
|
||||
});
|
||||
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
naam TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
wachtwoord TEXT NOT NULL,
|
||||
rol TEXT NOT NULL DEFAULT 'Viewer',
|
||||
actief BOOLEAN NOT NULL DEFAULT true,
|
||||
aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_data (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.query(`
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
`);
|
||||
|
||||
await db.query(`
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT;
|
||||
`);
|
||||
9
backend/src/logger.js
Normal file
9
backend/src/logger.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import pino from "pino";
|
||||
import { config } from "./config/index.js";
|
||||
|
||||
export const logger = pino({
|
||||
level: config.NODE_ENV === "production" ? "info" : "debug",
|
||||
...(config.NODE_ENV !== "production" && {
|
||||
transport: { target: "pino-pretty", options: { colorize: true } },
|
||||
}),
|
||||
});
|
||||
22
backend/src/middleware/auth.js
Normal file
22
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { config } from "../config/index.js";
|
||||
|
||||
export function authenticate(req, res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Niet ingelogd." });
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(header.slice(7), config.JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: "Sessie verlopen. Log opnieuw in." });
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAdmin(req, res, next) {
|
||||
if (req.user?.rol !== "Admin") {
|
||||
return res.status(403).json({ error: "Geen toegang." });
|
||||
}
|
||||
next();
|
||||
}
|
||||
6
backend/src/middleware/errorHandler.js
Normal file
6
backend/src/middleware/errorHandler.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export function errorHandler(err, _req, res, _next) {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = status < 500 ? err.message : "Er is een interne fout opgetreden.";
|
||||
if (status >= 500) console.error("[ERROR]", err);
|
||||
res.status(status).json({ error: message });
|
||||
}
|
||||
135
backend/src/routes/auth.js
Normal file
135
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Router } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { config } from "../config/index.js";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { userStore } from "../stores/userStore.js";
|
||||
import { db } from "../db.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/** POST /api/auth/register */
|
||||
router.post("/register", async (req, res, next) => {
|
||||
try {
|
||||
const { naam, email, wachtwoord } = req.body;
|
||||
if (!naam || !email || !wachtwoord)
|
||||
return res.status(400).json({ error: "Naam, e-mail en wachtwoord zijn verplicht." });
|
||||
if (wachtwoord.length < 8)
|
||||
return res.status(400).json({ error: "Wachtwoord moet minimaal 8 tekens zijn." });
|
||||
|
||||
const existing = await userStore.findByEmail(email.toLowerCase().trim());
|
||||
if (existing)
|
||||
return res.status(409).json({ error: "E-mailadres is al in gebruik." });
|
||||
|
||||
const { rows: countRows } = await db.query("SELECT COUNT(*) FROM users");
|
||||
const isFirst = parseInt(countRows[0].count) === 0;
|
||||
|
||||
const hash = await bcrypt.hash(wachtwoord, 12);
|
||||
const user = await userStore.create({
|
||||
naam: naam.trim(),
|
||||
email: email.toLowerCase().trim(),
|
||||
wachtwoord: hash,
|
||||
rol: isFirst ? "Admin" : "Viewer",
|
||||
actief: true,
|
||||
});
|
||||
|
||||
const token = jwt.sign(
|
||||
{ sub: user.id, naam: user.naam, email: user.email, rol: user.rol },
|
||||
config.JWT_SECRET,
|
||||
{ expiresIn: config.JWT_EXPIRES }
|
||||
);
|
||||
|
||||
res.status(201).json({ token, user: { id: user.id, naam: user.naam, email: user.email, rol: user.rol, avatar: null } });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
/** POST /api/auth/login */
|
||||
router.post("/login", async (req, res, next) => {
|
||||
try {
|
||||
const { email, wachtwoord } = req.body;
|
||||
if (!email || !wachtwoord)
|
||||
return res.status(400).json({ error: "E-mail en wachtwoord zijn verplicht." });
|
||||
|
||||
const user = await userStore.findByEmail(email.toLowerCase().trim());
|
||||
if (!user || !await bcrypt.compare(wachtwoord, user.wachtwoord))
|
||||
return res.status(401).json({ error: "Ongeldig e-mailadres of wachtwoord." });
|
||||
|
||||
if (!user.actief)
|
||||
return res.status(403).json({ error: "Account is inactief." });
|
||||
|
||||
const token = jwt.sign(
|
||||
{ sub: user.id, naam: user.naam, email: user.email, rol: user.rol },
|
||||
config.JWT_SECRET,
|
||||
{ expiresIn: config.JWT_EXPIRES }
|
||||
);
|
||||
|
||||
const fullUser = await userStore.findById(user.id);
|
||||
res.json({ token, user: { id: user.id, naam: user.naam, email: user.email, rol: user.rol, avatar: fullUser?.avatar ?? null } });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
/** GET /api/auth/me */
|
||||
router.get("/me", authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const user = await userStore.findById(req.user.sub);
|
||||
res.json({ user: { id: user.id, naam: user.naam, email: user.email, rol: user.rol, avatar: user?.avatar ?? null } });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
/** PATCH /api/auth/me — update naam / email */
|
||||
router.patch("/me", authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { naam, email } = req.body;
|
||||
if (!naam?.trim() || !email?.trim())
|
||||
return res.status(400).json({ error: "Naam en e-mail zijn verplicht." });
|
||||
|
||||
const conflict = await userStore.findByEmail(email.toLowerCase().trim());
|
||||
if (conflict && conflict.id !== req.user.sub)
|
||||
return res.status(409).json({ error: "E-mailadres is al in gebruik." });
|
||||
|
||||
const updated = await userStore.update(req.user.sub, { naam: naam.trim(), email: email.toLowerCase().trim() });
|
||||
const token = jwt.sign(
|
||||
{ sub: updated.id, naam: updated.naam, email: updated.email, rol: updated.rol },
|
||||
config.JWT_SECRET,
|
||||
{ expiresIn: config.JWT_EXPIRES }
|
||||
);
|
||||
res.json({ token, user: { id: updated.id, naam: updated.naam, email: updated.email, rol: updated.rol, avatar: updated.avatar ?? null } });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
/** POST /api/auth/me/password — wijzig wachtwoord */
|
||||
router.post("/me/password", authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { huidig, nieuw: nieuwWw } = req.body;
|
||||
if (!huidig || !nieuwWw)
|
||||
return res.status(400).json({ error: "Huidig en nieuw wachtwoord zijn verplicht." });
|
||||
if (nieuwWw.length < 8)
|
||||
return res.status(400).json({ error: "Nieuw wachtwoord moet minimaal 8 tekens zijn." });
|
||||
|
||||
const user = await userStore.findById(req.user.sub);
|
||||
if (!user || !await bcrypt.compare(huidig, user.wachtwoord))
|
||||
return res.status(401).json({ error: "Huidig wachtwoord is onjuist." });
|
||||
|
||||
const hash = await bcrypt.hash(nieuwWw, 12);
|
||||
await userStore.update(req.user.sub, { wachtwoord: hash });
|
||||
res.json({ message: "Wachtwoord gewijzigd." });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
/** PATCH /api/auth/me/avatar — sla avatar op als base64 */
|
||||
router.patch("/me/avatar", authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { avatar } = req.body;
|
||||
// avatar mag null zijn (verwijderen) of een base64 data-URL
|
||||
if (avatar !== null && avatar !== undefined && typeof avatar !== "string")
|
||||
return res.status(400).json({ error: "Ongeldig avatar formaat." });
|
||||
|
||||
await userStore.update(req.user.sub, { avatar: avatar ?? null });
|
||||
res.json({ avatar: avatar ?? null });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
/** POST /api/auth/logout */
|
||||
router.post("/logout", authenticate, (_req, res) => res.json({ message: "Uitgelogd." }));
|
||||
|
||||
export default router;
|
||||
12
backend/src/routes/data.js
Normal file
12
backend/src/routes/data.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { dataStore } from "../stores/dataStore.js";
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticate);
|
||||
|
||||
router.get("/", async (req, res, next) => { try { res.json(await dataStore.load(req.user.sub)); } catch (e) { next(e); } });
|
||||
router.put("/", async (req, res, next) => { try { res.json(await dataStore.save(req.user.sub, req.body)); } catch (e) { next(e); } });
|
||||
router.delete("/", async (req, res, next) => { try { await dataStore.reset(req.user.sub); res.json({ message: "Reset." }); } catch (e) { next(e); } });
|
||||
|
||||
export default router;
|
||||
7
backend/src/routes/health.js
Normal file
7
backend/src/routes/health.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
const START = Date.now();
|
||||
router.get("/", (_req, res) => {
|
||||
res.json({ status: "ok", uptime: Math.floor((Date.now() - START) / 1000) });
|
||||
});
|
||||
export default router;
|
||||
42
backend/src/routes/users.js
Normal file
42
backend/src/routes/users.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Router } from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { authenticate, requireAdmin } from "../middleware/auth.js";
|
||||
import { userStore } from "../stores/userStore.js";
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticate);
|
||||
|
||||
const safe = ({ wachtwoord, ...u }) => ({ ...u, aangemaakt: u.aangemaakt ?? null });
|
||||
|
||||
router.get("/", requireAdmin, async (_req, res, next) => {
|
||||
try { res.json((await userStore.list()).map(safe)); } catch (e) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/", requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { naam, email, wachtwoord, rol = "Viewer" } = req.body;
|
||||
if (!naam || !email || !wachtwoord) return res.status(400).json({ error: "Naam, e-mail en wachtwoord zijn verplicht." });
|
||||
const hash = await bcrypt.hash(wachtwoord, 12);
|
||||
const u = await userStore.create({ naam, email: email.toLowerCase(), wachtwoord: hash, rol, actief: true });
|
||||
res.status(201).json(safe(u));
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
router.patch("/:id", requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const patch = { ...req.body };
|
||||
if (patch.wachtwoord) {
|
||||
patch.wachtwoord = await bcrypt.hash(patch.wachtwoord, 12);
|
||||
}
|
||||
const u = await userStore.update(req.params.id, patch);
|
||||
if (!u) return res.status(404).json({ error: "Niet gevonden." });
|
||||
res.json(safe(u));
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
router.delete("/:id", requireAdmin, async (req, res, next) => {
|
||||
try { await userStore.remove(req.params.id); res.json({ message: "Verwijderd." }); }
|
||||
catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
61
backend/src/server.js
Normal file
61
backend/src/server.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import helmet from "helmet";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import cookieParser from "cookie-parser";
|
||||
import pino from "pino";
|
||||
import pinoHttp from "pino-http";
|
||||
import { config } from "./config/index.js";
|
||||
import authRouter from "./routes/auth.js";
|
||||
import dataRouter from "./routes/data.js";
|
||||
import usersRouter from "./routes/users.js";
|
||||
import healthRouter from "./routes/health.js";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
|
||||
export const logger = pino({
|
||||
level: config.NODE_ENV === "production" ? "info" : "debug",
|
||||
...(config.NODE_ENV !== "production" && {
|
||||
transport: { target: "pino-pretty", options: { colorize: true } },
|
||||
}),
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────────────────
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: config.CORS_ORIGIN, credentials: true }));
|
||||
app.set("trust proxy", 1);
|
||||
|
||||
// ── Rate limiting ─────────────────────────────────────────────────────────────
|
||||
app.use("/api/auth", rateLimit({ windowMs: 15 * 60 * 1000, max: 20, standardHeaders: true, legacyHeaders: false }));
|
||||
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false }));
|
||||
|
||||
// ── Parsing ───────────────────────────────────────────────────────────────────
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// ── Logging ───────────────────────────────────────────────────────────────────
|
||||
if (config.NODE_ENV !== "test") {
|
||||
app.use(pinoHttp({ logger }));
|
||||
}
|
||||
|
||||
// ── Routes ────────────────────────────────────────────────────────────────────
|
||||
app.use("/api/health", healthRouter);
|
||||
app.use("/api/auth", authRouter);
|
||||
app.use("/api/data", dataRouter);
|
||||
app.use("/api/users", usersRouter);
|
||||
|
||||
// ── 404 voor overige /api/* ───────────────────────────────────────────────────
|
||||
app.use("/api/*", (_req, res) => res.status(404).json({ error: "Endpoint niet gevonden." }));
|
||||
|
||||
// ── Error handler ─────────────────────────────────────────────────────────────
|
||||
app.use(errorHandler);
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
if (config.NODE_ENV !== "test") {
|
||||
app.listen(config.PORT, () => {
|
||||
logger.info(`VibeFinance API gestart op http://localhost:${config.PORT} [${config.NODE_ENV}]`);
|
||||
});
|
||||
}
|
||||
|
||||
export default app;
|
||||
22
backend/src/stores/dataStore.js
Normal file
22
backend/src/stores/dataStore.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { db } from "../db.js";
|
||||
import { INITIAL_DATA } from "../constants/initialData.js";
|
||||
|
||||
export const dataStore = {
|
||||
async load(userId) {
|
||||
const { rows } = await db.query("SELECT data FROM user_data WHERE user_id = $1", [userId]);
|
||||
return rows[0] ? JSON.parse(rows[0].data) : structuredClone(INITIAL_DATA);
|
||||
},
|
||||
async save(userId, payload) {
|
||||
await db.query(
|
||||
"INSERT INTO user_data (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2",
|
||||
[userId, JSON.stringify(payload)]
|
||||
);
|
||||
return payload;
|
||||
},
|
||||
async reset(userId) {
|
||||
await db.query(
|
||||
"INSERT INTO user_data (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2",
|
||||
[userId, JSON.stringify(structuredClone(INITIAL_DATA))]
|
||||
);
|
||||
},
|
||||
};
|
||||
43
backend/src/stores/userStore.js
Normal file
43
backend/src/stores/userStore.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* PostgreSQL-backed user store.
|
||||
*/
|
||||
import { db } from "../db.js";
|
||||
|
||||
const toUser = (row) => row ? { ...row } : null;
|
||||
|
||||
export const userStore = {
|
||||
async list() {
|
||||
const { rows } = await db.query("SELECT * FROM users");
|
||||
return rows.map(toUser);
|
||||
},
|
||||
async findByEmail(email) {
|
||||
const { rows } = await db.query("SELECT * FROM users WHERE email = $1", [email]);
|
||||
return toUser(rows[0]);
|
||||
},
|
||||
async findById(id) {
|
||||
const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [id]);
|
||||
return toUser(rows[0]);
|
||||
},
|
||||
async create(data) {
|
||||
const u = { id: `u${Date.now()}`, ...data };
|
||||
await db.query(
|
||||
"INSERT INTO users (id, naam, email, wachtwoord, rol, actief) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
[u.id, u.naam, u.email, u.wachtwoord, u.rol ?? "Viewer", u.actief ?? true]
|
||||
);
|
||||
return u;
|
||||
},
|
||||
async update(id, patch) {
|
||||
const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [id]);
|
||||
const user = toUser(rows[0]);
|
||||
if (!user) return null;
|
||||
const u = { ...user, ...patch };
|
||||
await db.query(
|
||||
"UPDATE users SET naam=$1, email=$2, wachtwoord=$3, rol=$4, actief=$5, avatar=$6 WHERE id=$7",
|
||||
[u.naam, u.email, u.wachtwoord, u.rol, u.actief, u.avatar ?? null, id]
|
||||
);
|
||||
return u;
|
||||
},
|
||||
async remove(id) {
|
||||
await db.query("DELETE FROM users WHERE id = $1", [id]);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user