server/routes/owner.js (lines 1–274)
- Repository
- wynajem_motorowek
- Path
server/routes/owner.js
const express = require("express");
const crypto = require("crypto");
const { validateRangeInput } = require("../lib/validation");
const ownerSessions = new Map();
const OWNER_SESSION_TTL_MS = 8 * 60 * 60 * 1000;
function ownerCreds() {
return {
login: String(process.env.OWNER_LOGIN || "").trim(),
password: String(process.env.OWNER_PASSWORD || ""),
};
}
function authMiddleware(req, res, next) {
const auth = String(req.headers.authorization || "");
const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
const expiresAt = token ? ownerSessions.get(token) : null;
if (!token || !expiresAt || expiresAt <= Date.now()) {
if (token) ownerSessions.delete(token);
return res.status(401).json({ ok: false, reason: "UNAUTHORIZED" });
}
ownerSessions.set(token, Date.now() + OWNER_SESSION_TTL_MS);
req.ownerSessionToken = token;
next();
}
function toSqlTimestamp(date, minutes) {
const hh = String(Math.floor(minutes / 60)).padStart(2, "0");
const mm = String(minutes % 60).padStart(2, "0");
return `${date} ${hh}:${mm}:00`;
}
module.exports = function ownerRoute({ db }) {
const router = express.Router();
function parseInvoiceData(raw) {
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (_err) {
return { raw: String(raw) };
}
}
router.post("/login", (req, res) => {
const login = String(req.body?.login || "");
const password = String(req.body?.password || "");
const creds = ownerCreds();
if (!creds.login || !creds.password) {
return res.status(503).json({ ok: false, reason: "OWNER_AUTH_NOT_CONFIGURED" });
}
if (login !== creds.login || password !== creds.password) {
return res.status(401).json({ ok: false, reason: "INVALID_CREDENTIALS" });
}
const token = crypto.randomBytes(32).toString("hex");
ownerSessions.set(token, Date.now() + OWNER_SESSION_TTL_MS);
return res.json({ ok: true, token, expiresInSeconds: OWNER_SESSION_TTL_MS / 1000 });
});
router.post("/logout", authMiddleware, (req, res) => {
ownerSessions.delete(req.ownerSessionToken);
res.json({ ok: true });
});
router.use(authMiddleware);
router.get("/reservations", async (req, res, next) => {
try {
const date = String(req.query.date || "").trim();
let where = "";
const params = [];
if (date) {
where = "WHERE rv.start_time >= ? AND rv.start_time < datetime(?, '+1 day')";
params.push(`${date} 00:00:00`, `${date} 00:00:00`);
}
const { rows } = await db.query(
`SELECT rv.id,
r.name AS resource_name,
r.capacity,
rv.start_time,
rv.end_time,
rv.status,
rv.kind,
rv.order_number,
rv.payment_ref,
rv.payment_method,
rv.customer_name,
rv.customer_email,
rv.phone,
rv.people_count,
rv.want_invoice,
rv.invoice_data,
rv.payment_status,
rv.created_at
FROM reservations rv
JOIN resources r ON r.id = rv.resource_id
${where}
ORDER BY rv.start_time DESC`,
params
);
res.json({
ok: true,
reservations: rows.map((row) => ({
id: row.id,
resourceName: row.resource_name,
capacity: Number(row.capacity || 6),
start: String(row.start_time || "").replace(" ", "T"),
end: String(row.end_time || "").replace(" ", "T"),
status: row.status,
kind: row.kind,
orderNumber: row.order_number,
paymentRef: row.payment_ref,
paymentMethod: row.payment_method,
customerName: row.customer_name,
customerEmail: row.customer_email,
phone: row.phone,
peopleCount: row.people_count,
wantInvoice: Number(row.want_invoice || 0) === 1,
invoiceData: parseInvoiceData(row.invoice_data),
paymentStatus: row.payment_status,
createdAt: String(row.created_at || "").replace(" ", "T"),
})),
});
} catch (err) {
next(err);
}
});
router.get("/support-emails", async (_req, res, next) => {
try {
const { rows } = await db.query(`SELECT id, email, active FROM support_emails ORDER BY email ASC`);
res.json({
ok: true,
emails: rows.map((row) => ({ id: row.id, email: row.email, active: Number(row.active || 0) === 1 })),
});
} catch (err) {
next(err);
}
});
router.post("/support-emails", async (req, res, next) => {
try {
const email = String(req.body?.email || "").trim().toLowerCase();
if (!email || !email.includes("@")) {
return res.status(400).json({ ok: false, reason: "INVALID_EMAIL" });
}
await db.query(`INSERT OR IGNORE INTO support_emails (email, active) VALUES (?, 1)`, [email]);
await db.query(`UPDATE support_emails SET active = 1 WHERE email = ?`, [email]);
res.json({ ok: true });
} catch (err) {
next(err);
}
});
router.delete("/support-emails/:id", async (req, res, next) => {
try {
const id = Number(req.params.id || 0);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ ok: false, reason: "INVALID_ID" });
}
await db.query(`DELETE FROM support_emails WHERE id = ?`, [id]);
res.json({ ok: true });
} catch (err) {
next(err);
}
});
router.get("/analytics", async (_req, res, next) => {
try {
const [{ total = 0 } = { total: 0 }] = (await db.query(`SELECT COUNT(*) AS total FROM reservations`)).rows;
const [{ confirmed = 0 } = { confirmed: 0 }] = (await db.query(
`SELECT COUNT(*) AS confirmed FROM reservations WHERE status = 'CONFIRMED'`
)).rows;
const [{ ownerBlocks = 0 } = { ownerBlocks: 0 }] = (await db.query(
`SELECT COUNT(*) AS ownerBlocks FROM reservations WHERE kind = 'OWNER_BLOCK' AND status IN ('HOLD','CONFIRMED')`
)).rows;
const [{ activeHolds = 0 } = { activeHolds: 0 }] = (await db.query(
`SELECT COUNT(*) AS activeHolds FROM reservations WHERE status = 'HOLD'`
)).rows;
res.json({
ok: true,
analytics: {
totalReservations: Number(total),
confirmedReservations: Number(confirmed),
ownerBlocks: Number(ownerBlocks),
activeHolds: Number(activeHolds),
},
});
} catch (err) {
next(err);
}
});
router.post("/blocks", async (req, res, next) => {
try {
const resourceName = String(req.body?.resourceName || "").trim();
const date = String(req.body?.date || "").trim();
const start = String(req.body?.start || "").trim();
const end = String(req.body?.end || "").trim();
if (!resourceName) return res.status(400).json({ ok: false, reason: "RESOURCE_REQUIRED" });
const { startMin, endMin } = validateRangeInput({ date, start, end });
const resourceResult = await db.query(
`SELECT id FROM resources WHERE active = 1 AND name = ? LIMIT 1`,
[resourceName]
);
const resource = resourceResult.rows[0];
if (!resource) return res.status(404).json({ ok: false, reason: "RESOURCE_NOT_FOUND" });
const startTs = toSqlTimestamp(date, startMin);
const endTs = toSqlTimestamp(date, endMin);
const conflict = await db.query(
`SELECT id
FROM reservations
WHERE resource_id = ?
AND status IN ('HOLD','CONFIRMED')
AND ? < end_time
AND ? > start_time
LIMIT 1`,
[resource.id, startTs, endTs]
);
if (conflict.rows.length > 0) {
return res.status(409).json({ ok: false, reason: "BUSY" });
}
const result = await db.query(
`INSERT INTO reservations (
resource_id,
start_time,
end_time,
status,
kind,
payment_status,
customer_name
) VALUES (?, ?, ?, 'CONFIRMED', 'OWNER_BLOCK', 'NOT_STARTED', 'OWNER_BLOCK')`,
[resource.id, startTs, endTs]
);
res.json({ ok: true, reservationId: Number(result.lastID || 0) });
} catch (err) {
next(err);
}
});
router.delete("/reservations/:id", async (req, res, next) => {
try {
const id = Number(req.params.id || 0);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ ok: false, reason: "INVALID_ID" });
}
const result = await db.query(`DELETE FROM reservations WHERE id = ?`, [id]);
if (Number(result.rowCount || 0) === 0) {
return res.status(404).json({ ok: false, reason: "NOT_FOUND" });
}
res.json({ ok: true });
} catch (err) {
next(err);
}
});
return router;
};
server/routes/payments.js (lines 1–231)
- Repository
- wynajem_motorowek
- Path
server/routes/payments.js
const express = require("express");
const { buildCancelToken, buildOrderNumber } = require("../lib/orders");
const { sendReservationConfirmedEmails } = require("../lib/notifications");
function toClientIso(value) {
if (!value) return null;
if (value instanceof Date) return value.toISOString();
return String(value).replace(" ", "T");
}
module.exports = function paymentsRoute({ db }) {
const router = express.Router();
function nowIso() {
return new Date().toISOString();
}
async function getHoldById(holdId) {
const holdResult = await db.query(
`SELECT rv.id,
rv.resource_id,
rv.start_time,
rv.end_time,
rv.status,
rv.expires_at,
rv.payment_status,
rv.payment_method,
rv.payment_ref,
rv.customer_email,
rv.order_number,
rv.cancel_token,
r.name AS resource_name
FROM reservations rv
JOIN resources r ON r.id = rv.resource_id
WHERE rv.id = ?
LIMIT 1`,
[holdId]
);
return holdResult.rows[0] || null;
}
async function finalizeReservation({ holdId, paymentStatus, paymentRef, paymentMethod }) {
await db.query(
`UPDATE reservations
SET status = 'CONFIRMED',
payment_status = ?,
payment_method = ?,
payment_ref = ?
WHERE id = ?`,
[paymentStatus, paymentMethod, paymentRef, holdId]
);
const current = await getHoldById(holdId);
if (!current) return null;
let orderNumber = String(current.order_number || "");
let cancelToken = String(current.cancel_token || "");
if (!orderNumber) {
orderNumber = buildOrderNumber(current.id);
}
if (!cancelToken) {
cancelToken = buildCancelToken(current.id);
}
await db.query(
`UPDATE reservations
SET order_number = ?,
cancel_token = ?
WHERE id = ?`,
[orderNumber, cancelToken, holdId]
);
const updated = await getHoldById(holdId);
if (updated) {
try {
await sendReservationConfirmedEmails({ db, reservation: updated, resourceName: updated.resource_name });
} catch (mailError) {
console.error("[payments] confirmation email failed", String(mailError && mailError.message ? mailError.message : mailError));
}
}
return updated;
}
function testPaymentsEnabled() {
return String(process.env.ENABLE_TEST_PAYMENTS || "false").toLowerCase() === "true";
}
router.post("/test/confirm", async (req, res, next) => {
try {
if (!testPaymentsEnabled()) {
return res.status(404).json({ ok: false, reason: "TEST_PAYMENTS_DISABLED" });
}
await db.query(
`UPDATE reservations
SET status = 'EXPIRED', payment_status = COALESCE(payment_status, 'EXPIRED')
WHERE status = 'HOLD' AND expires_at < ?`,
[nowIso()]
);
const holdId = Number(req.body?.holdId || 0);
if (!Number.isInteger(holdId) || holdId <= 0) {
return res.status(400).json({ ok: false, reason: "INVALID_HOLD_ID" });
}
const hold = await getHoldById(holdId);
if (!hold) {
return res.status(404).json({ ok: false, reason: "HOLD_NOT_FOUND" });
}
if (hold.status !== "HOLD") {
return res.status(409).json({ ok: false, reason: hold.status === "EXPIRED" ? "EXPIRED" : "HOLD_NOT_ACTIVE" });
}
if (hold.expires_at && new Date(toClientIso(hold.expires_at)).getTime() <= Date.now()) {
await db.query(
`UPDATE reservations
SET status = 'EXPIRED', payment_status = 'EXPIRED'
WHERE id = ?`,
[holdId]
);
return res.status(409).json({ ok: false, reason: "EXPIRED" });
}
const updated = await finalizeReservation({
holdId,
paymentStatus: "PAID",
paymentMethod: hold.payment_method || "ONLINE",
paymentRef: `TEST_CONFIRM_${holdId}`,
});
return res.json({ ok: true, status: "CONFIRMED", orderNumber: updated?.order_number || null, paymentRef: updated?.payment_ref || null });
} catch (err) {
next(err);
}
});
router.post("/cash/confirm", async (req, res, next) => {
try {
await db.query(
`UPDATE reservations
SET status = 'EXPIRED', payment_status = COALESCE(payment_status, 'EXPIRED')
WHERE status = 'HOLD' AND expires_at < ?`,
[nowIso()]
);
const holdId = Number(req.body?.holdId || 0);
if (!Number.isInteger(holdId) || holdId <= 0) {
return res.status(400).json({ ok: false, reason: "INVALID_HOLD_ID" });
}
const hold = await getHoldById(holdId);
if (!hold) {
return res.status(404).json({ ok: false, reason: "HOLD_NOT_FOUND" });
}
if (hold.status !== "HOLD") {
return res.status(409).json({ ok: false, reason: hold.status === "EXPIRED" ? "EXPIRED" : "HOLD_NOT_ACTIVE" });
}
if (hold.expires_at && new Date(toClientIso(hold.expires_at)).getTime() <= Date.now()) {
await db.query(
`UPDATE reservations
SET status = 'EXPIRED', payment_status = 'EXPIRED'
WHERE id = ?`,
[holdId]
);
return res.status(409).json({ ok: false, reason: "EXPIRED" });
}
const updated = await finalizeReservation({
holdId,
paymentStatus: "PAID",
paymentMethod: "CASH",
paymentRef: `CASH_${holdId}`,
});
return res.json({ ok: true, status: "CONFIRMED", orderNumber: updated?.order_number || null, paymentRef: updated?.payment_ref || null });
} catch (err) {
next(err);
}
});
router.post("/test/fail", async (req, res, next) => {
try {
if (!testPaymentsEnabled()) {
return res.status(404).json({ ok: false, reason: "TEST_PAYMENTS_DISABLED" });
}
await db.query(
`UPDATE reservations
SET status = 'EXPIRED', payment_status = COALESCE(payment_status, 'EXPIRED')
WHERE status = 'HOLD' AND expires_at < ?`,
[nowIso()]
);
const holdId = Number(req.body?.holdId || 0);
if (!Number.isInteger(holdId) || holdId <= 0) {
return res.status(400).json({ ok: false, reason: "INVALID_HOLD_ID" });
}
const holdResult = await db.query(
`SELECT id, status
FROM reservations
WHERE id = ?
LIMIT 1`,
[holdId]
);
const hold = holdResult.rows[0];
if (!hold) {
return res.status(404).json({ ok: false, reason: "HOLD_NOT_FOUND" });
}
if (hold.status === "CONFIRMED") {
return res.status(409).json({ ok: false, reason: "ALREADY_CONFIRMED" });
}
await db.query(
`UPDATE reservations
SET status = 'EXPIRED',
payment_status = 'FAILED',
payment_ref = 'TEST_FAIL_' || id
WHERE id = ?`,
[holdId]
);
return res.json({ ok: true, status: "EXPIRED" });
} catch (err) {
next(err);
}
});
return router;
};