project-startup 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const path = require("path");
4
- const fs = require("fs-extra");
5
- const prompts = require("prompts");
3
+ const path = require("path")
4
+ const prompts = require('prompts');
5
+ const fs = require('fs-extra');
6
6
 
7
7
  async function main() {
8
8
  const response = await prompts({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-startup",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Minimal session-based auth starter using Express, MySQL, React, Vite, and React Router.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -25,4 +25,4 @@
25
25
  "url": "https://github.com/D3lt-a/project-starup/issues"
26
26
  },
27
27
  "homepage": "https://github.com/D3lt-a/project-starup#readme"
28
- }
28
+ }
@@ -0,0 +1,176 @@
1
+ const db = require("../config/db");
2
+
3
+ // GET /api/bookings
4
+ // Manager → all bookings with customer + bus details
5
+ // Customer → only their own bookings
6
+ exports.getBookings = async (req, res) => {
7
+ if (req.user.role === "manager") {
8
+ const [bookings] = await db.query(
9
+ `SELECT
10
+ bk.id,
11
+ bk.seats,
12
+ bk.total_rwf,
13
+ bk.booked_at,
14
+ u.name AS customer_name,
15
+ u.email AS customer_email,
16
+ b.plate_number,
17
+ b.destination,
18
+ b.departure_time,
19
+ b.price_rwf
20
+ FROM bookings bk
21
+ JOIN users u ON u.id = bk.customer_id
22
+ JOIN buses b ON b.id = bk.bus_id
23
+ ORDER BY bk.booked_at DESC`
24
+ );
25
+ return res.json(bookings);
26
+ }
27
+
28
+ // Customer: only their rows
29
+ const [bookings] = await db.query(
30
+ `SELECT
31
+ bk.id,
32
+ bk.seats,
33
+ bk.total_rwf,
34
+ bk.booked_at,
35
+ b.plate_number,
36
+ b.destination,
37
+ b.departure_time,
38
+ b.price_rwf
39
+ FROM bookings bk
40
+ JOIN buses b ON b.id = bk.bus_id
41
+ WHERE bk.customer_id = ?
42
+ ORDER BY bk.booked_at DESC`,
43
+ [req.user.id]
44
+ );
45
+
46
+ res.json(bookings);
47
+ };
48
+
49
+ // POST /api/bookings (customer only)
50
+ exports.createBooking = async (req, res) => {
51
+ const { busId, seats } = req.body;
52
+
53
+ if (!busId || !seats || seats < 1) {
54
+ return res.status(400).json({ error: "busId and seats (≥ 1) are required." });
55
+ }
56
+
57
+ // ── Critical section: read and update in one atomic step ───────────────
58
+ // We use a transaction so two customers can't both grab the last seat.
59
+ const conn = await db.getConnection();
60
+
61
+ try {
62
+ await conn.beginTransaction();
63
+
64
+ // Lock this bus row for the duration of the transaction (FOR UPDATE)
65
+ // This prevents another request from reading stale available_seats
66
+ const [rows] = await conn.query(
67
+ "SELECT * FROM buses WHERE id = ? FOR UPDATE",
68
+ [busId]
69
+ );
70
+
71
+ if (rows.length === 0) {
72
+ await conn.rollback();
73
+ return res.status(404).json({ error: "Bus not found." });
74
+ }
75
+
76
+ const bus = rows[0];
77
+
78
+ if (new Date(bus.departure_time) <= new Date()) {
79
+ await conn.rollback();
80
+ return res.status(400).json({ error: "This bus has already departed." });
81
+ }
82
+
83
+ if (seats > bus.available_seats) {
84
+ await conn.rollback();
85
+ return res.status(400).json({
86
+ error: `Only ${bus.available_seats} seat(s) available.`,
87
+ });
88
+ }
89
+
90
+ // Decrement available_seats
91
+ await conn.query(
92
+ "UPDATE buses SET available_seats = available_seats - ? WHERE id = ?",
93
+ [seats, busId]
94
+ );
95
+
96
+ // Insert the booking record
97
+ const [result] = await conn.query(
98
+ `INSERT INTO bookings (bus_id, customer_id, seats, total_rwf)
99
+ VALUES (?, ?, ?, ?)`,
100
+ [busId, req.user.id, seats, seats * bus.price_rwf]
101
+ );
102
+
103
+ await conn.commit();
104
+
105
+ // Return the new booking with bus details attached
106
+ const [booking] = await conn.query(
107
+ `SELECT
108
+ bk.id,
109
+ bk.seats,
110
+ bk.total_rwf,
111
+ bk.booked_at,
112
+ b.plate_number,
113
+ b.destination,
114
+ b.departure_time
115
+ FROM bookings bk
116
+ JOIN buses b ON b.id = bk.bus_id
117
+ WHERE bk.id = ?`,
118
+ [result.insertId]
119
+ );
120
+
121
+ res.status(201).json(booking[0]);
122
+
123
+ } catch (err) {
124
+ await conn.rollback();
125
+ throw err;
126
+ } finally {
127
+ conn.release(); // always return connection to pool
128
+ }
129
+ };
130
+
131
+ // DELETE /api/bookings/:id (customer only — own bookings)
132
+ exports.cancelBooking = async (req, res) => {
133
+ const conn = await db.getConnection();
134
+
135
+ try {
136
+ await conn.beginTransaction();
137
+
138
+ // Find the booking AND confirm it belongs to this customer
139
+ const [rows] = await conn.query(
140
+ "SELECT * FROM bookings WHERE id = ? AND customer_id = ?",
141
+ [req.params.id, req.user.id]
142
+ );
143
+
144
+ if (rows.length === 0) {
145
+ await conn.rollback();
146
+ return res.status(404).json({ error: "Booking not found." });
147
+ }
148
+
149
+ const booking = rows[0];
150
+
151
+ // Restore seats only if bus hasn't departed yet
152
+ const [busRows] = await conn.query(
153
+ "SELECT departure_time FROM buses WHERE id = ?",
154
+ [booking.bus_id]
155
+ );
156
+
157
+ if (busRows.length > 0 && new Date(busRows[0].departure_time) > new Date()) {
158
+ await conn.query(
159
+ "UPDATE buses SET available_seats = available_seats + ? WHERE id = ?",
160
+ [booking.seats, booking.bus_id]
161
+ );
162
+ }
163
+
164
+ await conn.query("DELETE FROM bookings WHERE id = ?", [booking.id]);
165
+
166
+ await conn.commit();
167
+
168
+ res.json({ message: "Booking cancelled. Seats have been restored." });
169
+
170
+ } catch (err) {
171
+ await conn.rollback();
172
+ throw err;
173
+ } finally {
174
+ conn.release();
175
+ }
176
+ };
@@ -0,0 +1,143 @@
1
+ const db = require("../config/db");
2
+
3
+ // GET /api/buses
4
+ exports.getAllBuses = async (req, res) => {
5
+ const [buses] = await db.query(
6
+ "SELECT * FROM buses ORDER BY departure_time ASC"
7
+ );
8
+ res.json(buses);
9
+ };
10
+
11
+ // GET /api/buses/:id
12
+ exports.getBusById = async (req, res) => {
13
+ const [rows] = await db.query(
14
+ "SELECT * FROM buses WHERE id = ?",
15
+ [req.params.id]
16
+ );
17
+
18
+ if (rows.length === 0) {
19
+ return res.status(404).json({ error: "Bus not found." });
20
+ }
21
+
22
+ res.json(rows[0]);
23
+ };
24
+
25
+ // POST /api/buses (manager only)
26
+ exports.createBus = async (req, res) => {
27
+ const { plateNumber, destination, maxSeats, departureTime, priceRwf } = req.body;
28
+
29
+ if (!plateNumber || !destination || !maxSeats || !departureTime || !priceRwf) {
30
+ return res.status(400).json({ error: "All fields are required." });
31
+ }
32
+
33
+ if (new Date(departureTime) <= new Date()) {
34
+ return res.status(400).json({ error: "Departure time must be in the future." });
35
+ }
36
+
37
+ // Check for duplicate plate number
38
+ const [existing] = await db.query(
39
+ "SELECT id FROM buses WHERE plate_number = ?",
40
+ [plateNumber]
41
+ );
42
+
43
+ if (existing.length > 0) {
44
+ return res.status(400).json({ error: "Plate number already exists." });
45
+ }
46
+
47
+ const [result] = await db.query(
48
+ `INSERT INTO buses
49
+ (plate_number, destination, max_seats, available_seats, departure_time, price_rwf)
50
+ VALUES (?, ?, ?, ?, ?, ?)`,
51
+ [plateNumber, destination, maxSeats, maxSeats, departureTime, priceRwf]
52
+ // ^ available_seats starts equal to max_seats
53
+ );
54
+
55
+ // Fetch and return the newly created row
56
+ const [rows] = await db.query(
57
+ "SELECT * FROM buses WHERE id = ?",
58
+ [result.insertId]
59
+ );
60
+
61
+ res.status(201).json(rows[0]);
62
+ };
63
+
64
+ // PUT /api/buses/:id (manager only)
65
+ exports.updateBus = async (req, res) => {
66
+ const { plateNumber, destination, maxSeats, departureTime, priceRwf } = req.body;
67
+
68
+ // Fetch current state first so we can calculate available_seats correctly
69
+ const [rows] = await db.query(
70
+ "SELECT * FROM buses WHERE id = ?",
71
+ [req.params.id]
72
+ );
73
+
74
+ if (rows.length === 0) {
75
+ return res.status(404).json({ error: "Bus not found." });
76
+ }
77
+
78
+ const bus = rows[0];
79
+
80
+ // If maxSeats is being changed, recalculate available_seats:
81
+ // available = newMax - (oldMax - oldAvailable) ← keeps already-booked seats
82
+ let newAvailable = bus.available_seats;
83
+
84
+ if (maxSeats) {
85
+ const booked = bus.max_seats - bus.available_seats;
86
+ if (parseInt(maxSeats) < booked) {
87
+ return res.status(400).json({
88
+ error: `Cannot reduce max seats below already booked count (${booked}).`,
89
+ });
90
+ }
91
+ newAvailable = parseInt(maxSeats) - booked;
92
+ }
93
+
94
+ await db.query(
95
+ `UPDATE buses SET
96
+ plate_number = COALESCE(?, plate_number),
97
+ destination = COALESCE(?, destination),
98
+ max_seats = COALESCE(?, max_seats),
99
+ available_seats = ?,
100
+ departure_time = COALESCE(?, departure_time),
101
+ price_rwf = COALESCE(?, price_rwf)
102
+ WHERE id = ?`,
103
+ [
104
+ plateNumber || null,
105
+ destination || null,
106
+ maxSeats || null,
107
+ newAvailable,
108
+ departureTime|| null,
109
+ priceRwf || null,
110
+ req.params.id,
111
+ ]
112
+ );
113
+
114
+ // Return updated row
115
+ const [updated] = await db.query("SELECT * FROM buses WHERE id = ?", [req.params.id]);
116
+ res.json(updated[0]);
117
+ };
118
+
119
+ // DELETE /api/buses/:id (manager only)
120
+ exports.deleteBus = async (req, res) => {
121
+ // Check if any bookings exist for this bus
122
+ const [bookings] = await db.query(
123
+ "SELECT id FROM bookings WHERE bus_id = ?",
124
+ [req.params.id]
125
+ );
126
+
127
+ if (bookings.length > 0) {
128
+ return res.status(400).json({
129
+ error: "Cannot delete a bus that has existing bookings.",
130
+ });
131
+ }
132
+
133
+ const [result] = await db.query(
134
+ "DELETE FROM buses WHERE id = ?",
135
+ [req.params.id]
136
+ );
137
+
138
+ if (result.affectedRows === 0) {
139
+ return res.status(404).json({ error: "Bus not found." });
140
+ }
141
+
142
+ res.json({ message: "Bus deleted." });
143
+ };
@@ -0,0 +1,47 @@
1
+ -- Run this file once to set up your database:
2
+ -- mysql -u root -p < schema.sql
3
+
4
+ CREATE DATABASE IF NOT EXISTS fleet_db;
5
+ USE fleet_db;
6
+
7
+ -- ─── Users ────────────────────────────────────────────────────────────────────
8
+ CREATE TABLE IF NOT EXISTS users (
9
+ id INT PRIMARY KEY AUTO_INCREMENT,
10
+ name VARCHAR(100) NOT NULL,
11
+ email VARCHAR(150) UNIQUE NOT NULL,
12
+ password VARCHAR(255) NOT NULL,
13
+ role ENUM('manager', 'customer') NOT NULL,
14
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
15
+ );
16
+
17
+ -- ─── Buses ────────────────────────────────────────────────────────────────────
18
+ CREATE TABLE IF NOT EXISTS buses (
19
+ id INT PRIMARY KEY AUTO_INCREMENT,
20
+ plate_number VARCHAR(20) UNIQUE NOT NULL,
21
+ destination VARCHAR(100) NOT NULL,
22
+ max_seats INT NOT NULL,
23
+ available_seats INT NOT NULL,
24
+ departure_time DATETIME NOT NULL,
25
+ price_rwf INT NOT NULL,
26
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
27
+ );
28
+
29
+ -- ─── Bookings ─────────────────────────────────────────────────────────────────
30
+ CREATE TABLE IF NOT EXISTS bookings (
31
+ id INT PRIMARY KEY AUTO_INCREMENT,
32
+ bus_id INT NOT NULL,
33
+ customer_id INT NOT NULL,
34
+ seats INT NOT NULL,
35
+ total_rwf INT NOT NULL,
36
+ booked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
37
+ FOREIGN KEY (bus_id) REFERENCES buses(id) ON DELETE CASCADE,
38
+ FOREIGN KEY (customer_id) REFERENCES users(id) ON DELETE CASCADE
39
+ );
40
+
41
+ -- ─── Sessions ─────────────────────────────────────────────────────────────────
42
+ CREATE TABLE IF NOT EXISTS sessions (
43
+ token VARCHAR(255) PRIMARY KEY,
44
+ user_id INT NOT NULL,
45
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
46
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
47
+ );