diff --git a/.env.example b/.env.example index b86c3bbf..09be4751 100644 --- a/.env.example +++ b/.env.example @@ -22,11 +22,6 @@ CLIENT_PORT=5173 # ----------------------------------------------------------------------------- # API Configuration # ----------------------------------------------------------------------------- -# Base URL for API calls (used by frontend) -# In development: http://localhost:3000 -# In production: your domain or leave empty for relative URLs -API_BASE_URL=http://localhost:3000 - # SvelteKit public environment variables (PUBLIC_ prefix required) PUBLIC_API_BASE_URL=http://localhost:3000 @@ -34,14 +29,11 @@ PUBLIC_API_BASE_URL=http://localhost:3000 # Database Configuration # ----------------------------------------------------------------------------- # Database file path (SQLite) -DATABASE_PATH=./vehicles.db +DATABASE_PATH=./tracktor.db # ----------------------------------------------------------------------------- # Application Features # ----------------------------------------------------------------------------- -# Enable demo mode (disables certain features for public demos) -DEMO_MODE=false - # SvelteKit public demo mode (PUBLIC_ prefix required) PUBLIC_DEMO_MODE=false diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 34ae5165..ec865ae5 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -58,7 +58,7 @@ jobs: type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern=latest - name: Extract docs metadata id: docs-meta @@ -70,7 +70,7 @@ jobs: type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern=latest - name: Build and push app uses: docker/build-push-action@v5 diff --git a/app/backend/.gitignore b/app/backend/.gitignore index bef14ba0..ef286b02 100644 --- a/app/backend/.gitignore +++ b/app/backend/.gitignore @@ -1,4 +1,4 @@ node_modules .env -vehicles.db +tracktor.db dist/ diff --git a/app/backend/eslint.config.js b/app/backend/eslint.config.js new file mode 100644 index 00000000..330344ba --- /dev/null +++ b/app/backend/eslint.config.js @@ -0,0 +1,39 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import globals from "globals"; +import unusedImports from "eslint-plugin-unused-imports"; + +export default [ + js.configs.recommended, + { + files: ["**/*.{js,ts}"], + languageOptions: { + parser: tsParser, + parserOptions: { + sourceType: "module", + ecmaVersion: 2020, + }, + globals: { + ...globals.node, + ...globals.es2017, + }, + }, + plugins: { + "@typescript-eslint": ts, + "unused-imports": unusedImports, + }, + rules: { + ...ts.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-object-type": "off", + }, + }, + { + ignores: ["node_modules/", "dist/", "*.config.js", "*.config.ts"], + }, +]; diff --git a/app/backend/index.ts b/app/backend/index.ts index 95663794..2b4a4973 100644 --- a/app/backend/index.ts +++ b/app/backend/index.ts @@ -1,9 +1,3 @@ -import { config } from "dotenv"; -import { resolve } from "path"; - -// Load environment variables from root directory -config({ path: resolve(process.cwd(), "../../.env") }); - import express from "express"; import cors from "cors"; import pinRoutes from "@routes/pinRoutes.js"; @@ -18,24 +12,14 @@ validateEnvironment(); const app = express(); -// Configure CORS - simplified for development -const corsOptions = env.isDevelopment() - ? { - // In development, allow all origins for easier debugging - origin: true, - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization", "X-User-PIN"], - optionsSuccessStatus: 200, - } - : { - // In production, use strict origin checking - origin: env.CORS_ORIGINS, - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization", "X-User-PIN"], - optionsSuccessStatus: 200, - }; +// Configure CORS - use explicit origins in all environments +const corsOptions = { + origin: env.CORS_ORIGINS, + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "X-User-PIN"], + optionsSuccessStatus: 200, +}; app.use(cors(corsOptions)); @@ -49,14 +33,12 @@ app.use("/api/vehicles", vehicleRoutes); app.use("/api/config", configRoutes); if (env.isProduction()) { - // @ts-ignore - const { handler } = await import("@frontend/build/handler.js"); + // @ts-expect-error -- Ignore import error for dynamic import + const { handler } = await import("../frontend/build/handler.js"); app.use(handler); } else { - // In dev, redirect to SvelteKit dev server - app.use("/", (req, res) => { - const clientPort = process.env.CLIENT_PORT || 5173; - res.redirect(`http://localhost:${clientPort}${req.originalUrl}`); + app.get("/", (req, res) => { + res.redirect("http://localhost:5173"); }); } @@ -67,17 +49,13 @@ initializeDatabase() app.listen(env.SERVER_PORT, env.SERVER_HOST, () => { console.log("─".repeat(75)); console.log( - `🚀 Server running at http://${env.SERVER_HOST}:${env.SERVER_PORT}`, - ); - console.log(`📊 Environment: ${env.NODE_ENV}`); - console.log(`🗄️ Database: ${env.DATABASE_PATH}`); - console.log(`🎭 Demo Mode: ${env.DEMO_MODE ? "Enabled" : "Disabled"}`); - console.log( - `🌐 CORS: ${env.isDevelopment() ? "Permissive (Development)" : "Strict (Production)"}`, + `🚀 Server running at http://${env.SERVER_HOST}:${env.SERVER_PORT}` ); - if (!env.isDevelopment()) { - console.log(`📋 Allowed origins: ${env.CORS_ORIGINS.join(", ")}`); - } + console.log(`Environment: ${env.NODE_ENV}`); + console.log(`Database: ${env.DATABASE_PATH}`); + console.log(`Demo Mode: ${env.DEMO_MODE ? "Enabled" : "Disabled"}`); + console.log(`CORS: Explicit origins only`); + console.log(`Allowed origins: [ ${env.CORS_ORIGINS.join(", ")} ]`); console.log("─".repeat(75)); }); }) diff --git a/app/backend/package.json b/app/backend/package.json index d8363b11..f1de08a4 100644 --- a/app/backend/package.json +++ b/app/backend/package.json @@ -1,19 +1,21 @@ { - "name": "server", + "name": "backend", "version": "0.0.1", "description": "Tractor backend", "main": "dist/index.js", "type": "module", "scripts": { - "build": "tsc", + "build": "tsc && tsc-alias", "dev": "tsx --watch index.ts", "start": "node dist/index.js", "preview": "npm run build && npm run start", "db:migrate": "tsx src/db/migrate.ts migrate", "db:seed": "tsx src/db/migrate.ts seed", "db:status": "tsx src/db/status.ts", - "format": "prettier --write .", - "clean": "rm -rf dist" + "format": "(eslint --fix . || true) && prettier --write .", + "lint": "eslint . && prettier --check .", + "clean": "rm -rf dist", + "reset": "npm run clean && rm -rf tracktor.db node_modules && npm install" }, "keywords": [], "author": "", @@ -29,18 +31,27 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "umzug": "^3.8.2" + "umzug": "^3.8.2", + "express-rate-limit": "^8.0.1" }, "devDependencies": { + "@eslint/js": "^9.34.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.19", "@types/express": "^4.17.21", "@types/node": "^24.0.13", "@types/sequelize": "^4.28.20", "@types/sqlite3": "^3.1.11", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", + "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-unused-imports": "^4.2.0", + "globals": "^14.0.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", "ts-node": "^10.9.2", + "tsc-alias": "^1.8.16", "tsx": "^4.20.3", "typescript": "^5.8.3" } diff --git a/app/backend/src/config/env.ts b/app/backend/src/config/env.ts index e1406db5..42f6e17a 100644 --- a/app/backend/src/config/env.ts +++ b/app/backend/src/config/env.ts @@ -1,7 +1,29 @@ -/** - * Environment Configuration - * Centralized environment variable management for the backend - */ +import { config } from "dotenv"; +import { resolve } from "path"; + +// Load environment variables from root directory +config({ + path: resolve(process.cwd(), "../../.env"), + override: true, + quiet: true, +}); + +const getOrigins = (): string[] => { + const origins = process.env.CORS_ORIGINS; + if (!origins) { + return [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://localhost:3000", + "http://127.0.0.1:3000", + ]; + } + + return origins + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); +}; export const env = { // Application Environment @@ -12,27 +34,21 @@ export const env = { SERVER_PORT: Number(process.env.SERVER_PORT) || 3000, // Database Configuration - DATABASE_PATH: process.env.DATABASE_PATH || "./vehicles.db", + DATABASE_PATH: process.env.DATABASE_PATH || "./tracktor.db", // Application Features - DEMO_MODE: process.env.DEMO_MODE === "true", + DEMO_MODE: process.env.PUBLIC_DEMO_MODE === "true", // CORS Configuration - CORS_ORIGINS: process.env.CORS_ORIGINS?.split(",").map((origin) => - origin.trim(), - ) || [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ], + CORS_ORIGINS: getOrigins(), // Logging Configuration LOG_LEVEL: process.env.LOG_LEVEL || "info", LOG_REQUESTS: process.env.LOG_REQUESTS === "true", // Helper methods - isDevelopment: () => process.env.NODE_ENV === "development", + isDevelopment: () => + !process.env.NODE_ENV || process.env.NODE_ENV === "development", isProduction: () => process.env.NODE_ENV === "production", isTest: () => process.env.NODE_ENV === "test", } as const; @@ -43,13 +59,13 @@ export default env; * Validate required environment variables */ export function validateEnvironment(): void { - const required = ["NODE_ENV"]; + const required: string[] = []; const missing = required.filter((key) => !process.env[key]); if (missing.length > 0) { console.error( "❌ Missing required environment variables:", - missing.join(", "), + missing.join(", ") ); process.exit(1); } diff --git a/app/backend/src/controllers/MaintenanceLogController.ts b/app/backend/src/controllers/MaintenanceLogController.ts index 0bb907e3..18445867 100644 --- a/app/backend/src/controllers/MaintenanceLogController.ts +++ b/app/backend/src/controllers/MaintenanceLogController.ts @@ -54,7 +54,7 @@ export const getMaintenanceLogById = async (req: Request, res: Response) => { export const updateMaintenanceLog = async (req: Request, res: Response) => { const { id } = req.params; - const { date, odometer, service, cost, notes } = req.body; + const { date, odometer, service, cost } = req.body; if (!id) { throw new MaintenanceLogError( diff --git a/app/backend/src/controllers/PUCCController.ts b/app/backend/src/controllers/PUCCController.ts index aa6338b5..34777342 100644 --- a/app/backend/src/controllers/PUCCController.ts +++ b/app/backend/src/controllers/PUCCController.ts @@ -45,8 +45,7 @@ export const updatePollutionCertificate = async ( res: Response, ) => { const { vehicleId, id } = req.params; - const { certificateNumber, issueDate, expiryDate, testingCenter, notes } = - req.body; + const { certificateNumber, issueDate, expiryDate, testingCenter } = req.body; if (!vehicleId || !id) { throw new PollutionCertificateError( diff --git a/app/backend/src/db/README.md b/app/backend/src/db/README.md index 412ae3cf..21a0b8b1 100644 --- a/app/backend/src/db/README.md +++ b/app/backend/src/db/README.md @@ -46,7 +46,7 @@ The application automatically runs migrations and seeds data on startup based on ## Environment Variables -- `DB_PATH` - SQLite database file path (default: ./vehicles.db) +- `DB_PATH` - SQLite database file path (default: ./tracktor.db) - `DEMO_MODE` - Enable demo data seeding (default: false) - `AUTH_PIN` - Set authentication PIN - `SHOW_SQL` - Show SQL queries in console (default: false) diff --git a/app/backend/src/db/db.ts b/app/backend/src/db/db.ts index 2da8a6c3..c6470678 100644 --- a/app/backend/src/db/db.ts +++ b/app/backend/src/db/db.ts @@ -7,7 +7,7 @@ const showSql = process.env.SHOW_SQL === "true" || false; const db = new Sequelize({ dialect: "sqlite", - storage: `${process.env.DB_PATH || "./vehicles.db"}`, // Use environment variable for database path + storage: `${process.env.DB_PATH || "./tracktor.db"}`, // Use environment variable for database path logging: showSql, }); diff --git a/app/backend/src/db/index.ts b/app/backend/src/db/index.ts index ab2e0cc2..e13f7cea 100644 --- a/app/backend/src/db/index.ts +++ b/app/backend/src/db/index.ts @@ -6,9 +6,13 @@ import { db } from "./db.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// Determine if we're running in development (TypeScript) or production (compiled JavaScript) +const isProduction = __filename.includes("/dist/"); +const migrationExtension = isProduction ? "js" : "ts"; + const umzug = new Umzug({ migrations: { - glob: path.join(__dirname, "migrations/*.ts"), + glob: path.join(__dirname, `migrations/*.${migrationExtension}`), resolve: ({ name, path: migrationPath }) => { return { name, @@ -36,7 +40,7 @@ type Migration = typeof umzug._types.migration; const performDbMigrations = async () => { try { console.log("Running database migrations..."); - await umzug.up(); + await umzug.up({}); console.log("Migrations completed successfully"); } catch (error) { console.error("Migration failed:", error); diff --git a/app/backend/src/db/seeders/index.ts b/app/backend/src/db/seeders/index.ts index 8319e284..d0696fe9 100644 --- a/app/backend/src/db/seeders/index.ts +++ b/app/backend/src/db/seeders/index.ts @@ -122,7 +122,7 @@ export const seedDemoData = async () => { }, ]); - const vehicle1FuelLogs: any = [ + const vehicle1FuelLogs = [ { vehicleId: vehicle1.id, date: "2024-01-15", @@ -140,7 +140,7 @@ export const seedDemoData = async () => { }, ]; - const vehicle2FuelLogs: any = [ + const vehicle2FuelLogs = [ { vehicleId: vehicle2.id, date: "2024-01-20", @@ -167,7 +167,7 @@ export const seedDemoData = async () => { const cost = Math.random() * 16 + 50; vehicle1FuelLogs.push({ vehicleId: vehicle1.id, - date: currentDate1.toISOString().split("T")[0], + date: currentDate1.toISOString().split("T")[0] || "", odometer: currentOdometer1, fuelAmount: parseFloat(fuelAmount.toFixed(2)), cost: parseFloat(cost.toFixed(2)), @@ -183,7 +183,7 @@ export const seedDemoData = async () => { const cost = Math.random() * 16 + 50; vehicle2FuelLogs.push({ vehicleId: vehicle2.id, - date: currentDate2.toISOString().split("T")[0], + date: currentDate2.toISOString().split("T")[0] || "", odometer: currentOdometer2, fuelAmount: parseFloat(fuelAmount.toFixed(2)), cost: parseFloat(cost.toFixed(2)), diff --git a/app/backend/src/exceptions/ServiceError.ts b/app/backend/src/exceptions/ServiceError.ts index bc6d68fa..716c01bc 100644 --- a/app/backend/src/exceptions/ServiceError.ts +++ b/app/backend/src/exceptions/ServiceError.ts @@ -17,7 +17,7 @@ export class ServiceError extends Error { } } -export const statusFromError = (error: any) => { +export const statusFromError = (error: Error) => { if (error instanceof ServiceError) return error.status; if (error instanceof ValidationError) return Status.BAD_REQUEST; if (error instanceof UniqueConstraintError) return Status.CONFLICT; diff --git a/app/backend/src/middleware/async-handler.ts b/app/backend/src/middleware/async-handler.ts new file mode 100644 index 00000000..fa3893c7 --- /dev/null +++ b/app/backend/src/middleware/async-handler.ts @@ -0,0 +1,10 @@ +import { Request, Response, NextFunction } from "express"; + +// Wrapper to catch async errors and pass them to Express error handler +export const asyncHandler = ( + fn: (req: Request, res: Response, next: NextFunction) => void, +) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; diff --git a/app/backend/src/middleware/error-handler.ts b/app/backend/src/middleware/error-handler.ts index e8345c64..764ea79d 100644 --- a/app/backend/src/middleware/error-handler.ts +++ b/app/backend/src/middleware/error-handler.ts @@ -1,34 +1,46 @@ -import { Request, Response } from "express"; -import { ValidationError } from "sequelize"; +import { Request, Response, NextFunction } from "express"; export const errorHandler = ( err: any, req: Request, res: Response, - next: any, + _: NextFunction, ) => { + // Log the error for debugging + console.error("Error in %s %s:", req.method, req.path, err); + res.setHeader("Content-Type", "application/json"); + + let statusCode = 500; let body = {}; + switch (err.name) { case "SequelizeValidationError": + statusCode = 400; body = { type: "ValidationError", - errors: err.errors.map((e: any) => { - return { - message: e.message, - path: e.path, - }; - }), + errors: err.errors.map((e: any) => ({ + message: e.message, + path: e.path, + })), }; break; - default: + case "AuthError": + // Use the status from the error if available, otherwise default to 500 + statusCode = err.status || 500; body = { - type: err.name, + type: "AuthError", errors: [{ message: err.message }], }; + break; + default: + // Check if error has a status property + statusCode = err.status || err.statusCode || 500; + body = { + type: err.name || "Error", + errors: [{ message: err.message || "Internal server error" }], + }; } - console.error(err.name); - res.status(500); - res.send(JSON.stringify(body)); + res.status(statusCode).json(body); }; diff --git a/app/backend/src/models/Insurance.ts b/app/backend/src/models/Insurance.ts index 59623c0f..58ecbbb1 100644 --- a/app/backend/src/models/Insurance.ts +++ b/app/backend/src/models/Insurance.ts @@ -1,6 +1,5 @@ import { DataTypes, Model, Optional } from "sequelize"; import { db } from "@db/index.js"; -import Vehicle from "./Vehicle.js"; import { InsuranceError } from "@exceptions/InsuranceError.js"; import { Status } from "@exceptions/ServiceError.js"; @@ -68,7 +67,7 @@ Insurance.init( msg: "Policy Number must be between length 3 to 50.", }, is: { - args: "^[0-9A-Za-z\s\-]*$", + args: "^[0-9A-Za-z- ]*$", msg: "Only number and characters with space and hyphen are allowed in policy number.", }, }, diff --git a/app/backend/src/models/MaintenanceLog.ts b/app/backend/src/models/MaintenanceLog.ts index 8a1fac96..b8b24604 100644 --- a/app/backend/src/models/MaintenanceLog.ts +++ b/app/backend/src/models/MaintenanceLog.ts @@ -1,6 +1,5 @@ import { DataTypes, Model, Optional } from "sequelize"; import { db } from "@db/index.js"; -import Vehicle from "./Vehicle.js"; interface MaintenanceLogAttributes { id: string; diff --git a/app/backend/src/models/PUCC.ts b/app/backend/src/models/PUCC.ts index 77b33189..a7064951 100644 --- a/app/backend/src/models/PUCC.ts +++ b/app/backend/src/models/PUCC.ts @@ -58,7 +58,7 @@ PollutionCertificate.init( msg: "Certificate Number must be between length 3 to 50.", }, is: { - args: "^[0-9A-Za-z\s\-]*$", + args: "^[0-9A-Za-z- ]*$", msg: "Only number and characters with space and hyphen are allowed in certificate number.", }, }, diff --git a/app/backend/src/models/Vehicle.ts b/app/backend/src/models/Vehicle.ts index 22f3e8f4..070b6f58 100644 --- a/app/backend/src/models/Vehicle.ts +++ b/app/backend/src/models/Vehicle.ts @@ -80,7 +80,7 @@ Vehicle.init( unique: true, validate: { is: { - args: "^[A-Z0-9\- ]{2,10}$", + args: "^[A-Z0-9- ]{2,25}$", msg: "Licence Plate format is incorrect.", }, }, @@ -93,7 +93,7 @@ Vehicle.init( msg: "VIN number can't be an empty string.", }, is: { - args: "^[A-HJ-NPR-Z0-9]{17}$", + args: "^[A-HJ-NPR-Z0-9]{3,}$", msg: "VIN number format is incorrect.", }, async isUnique(value: string) { diff --git a/app/backend/src/routes/configRoutes.ts b/app/backend/src/routes/configRoutes.ts index e97e2119..d887a85a 100644 --- a/app/backend/src/routes/configRoutes.ts +++ b/app/backend/src/routes/configRoutes.ts @@ -4,11 +4,12 @@ import { getConfigByKey, updateConfig, } from "@controllers/ConfigController.js"; +import { asyncHandler } from "@middleware/async-handler.js"; const router = Router(); -router.get("/", getConfig); -router.get("/:key", getConfigByKey); // Alias for getConfig -router.put("/", updateConfig); +router.get("/", asyncHandler(getConfig)); +router.get("/:key", asyncHandler(getConfigByKey)); // Alias for getConfig +router.put("/", asyncHandler(updateConfig)); export default router; diff --git a/app/backend/src/routes/fuelLogRoutes.ts b/app/backend/src/routes/fuelLogRoutes.ts index 0bc22d0d..1190875c 100644 --- a/app/backend/src/routes/fuelLogRoutes.ts +++ b/app/backend/src/routes/fuelLogRoutes.ts @@ -7,13 +7,14 @@ import { deleteFuelLog, } from "@controllers/FuelLogController.js"; import { authenticatePin } from "@middleware/auth.js"; +import { asyncHandler } from "@middleware/async-handler.js"; const router = Router({ mergeParams: true }); -router.post("/", authenticatePin, addFuelLog); -router.get("/", authenticatePin, getFuelLogs); -router.get("/:id", authenticatePin, getFuelLogById); -router.put("/:id", authenticatePin, updateFuelLog); -router.delete("/:id", authenticatePin, deleteFuelLog); +router.post("/", authenticatePin, asyncHandler(addFuelLog)); +router.get("/", authenticatePin, asyncHandler(getFuelLogs)); +router.get("/:id", authenticatePin, asyncHandler(getFuelLogById)); +router.put("/:id", authenticatePin, asyncHandler(updateFuelLog)); +router.delete("/:id", authenticatePin, asyncHandler(deleteFuelLog)); export default router; diff --git a/app/backend/src/routes/insuranceRoutes.ts b/app/backend/src/routes/insuranceRoutes.ts index 1920429b..a3bc3c40 100644 --- a/app/backend/src/routes/insuranceRoutes.ts +++ b/app/backend/src/routes/insuranceRoutes.ts @@ -6,12 +6,13 @@ import { deleteInsurance, } from "@controllers/InsuranceController.js"; import { authenticatePin } from "@middleware/auth.js"; +import { asyncHandler } from "@middleware/async-handler.js"; const router = Router({ mergeParams: true }); -router.post("/", authenticatePin, addInsurance); -router.get("/", authenticatePin, getInsurances); -router.put("/:id", authenticatePin, updateInsurance); -router.delete("/:id", authenticatePin, deleteInsurance); +router.post("/", authenticatePin, asyncHandler(addInsurance)); +router.get("/", authenticatePin, asyncHandler(getInsurances)); +router.put("/:id", authenticatePin, asyncHandler(updateInsurance)); +router.delete("/:id", authenticatePin, asyncHandler(deleteInsurance)); export default router; diff --git a/app/backend/src/routes/maintenanceLogRoutes.ts b/app/backend/src/routes/maintenanceLogRoutes.ts index c9961524..e328fabd 100644 --- a/app/backend/src/routes/maintenanceLogRoutes.ts +++ b/app/backend/src/routes/maintenanceLogRoutes.ts @@ -7,13 +7,14 @@ import { deleteMaintenanceLog, } from "@controllers/MaintenanceLogController.js"; import { authenticatePin } from "@middleware/auth.js"; +import { asyncHandler } from "@middleware/async-handler.js"; const router = Router({ mergeParams: true }); -router.post("/", authenticatePin, addMaintenanceLog); -router.get("/", authenticatePin, getMaintenanceLogs); -router.get("/:id", authenticatePin, getMaintenanceLogById); -router.put("/:id", authenticatePin, updateMaintenanceLog); -router.delete("/:id", authenticatePin, deleteMaintenanceLog); +router.post("/", authenticatePin, asyncHandler(addMaintenanceLog)); +router.get("/", authenticatePin, asyncHandler(getMaintenanceLogs)); +router.get("/:id", authenticatePin, asyncHandler(getMaintenanceLogById)); +router.put("/:id", authenticatePin, asyncHandler(updateMaintenanceLog)); +router.delete("/:id", authenticatePin, asyncHandler(deleteMaintenanceLog)); export default router; diff --git a/app/backend/src/routes/pinRoutes.ts b/app/backend/src/routes/pinRoutes.ts index c980e836..41b73355 100644 --- a/app/backend/src/routes/pinRoutes.ts +++ b/app/backend/src/routes/pinRoutes.ts @@ -1,10 +1,19 @@ import { Router } from "express"; import { setPin, verifyPin, getPinStatus } from "@controllers/PinController.js"; +import { asyncHandler } from "@middleware/async-handler.js"; +import rateLimit from "express-rate-limit"; const router = Router(); -router.post("/pin", setPin); -router.post("/pin/verify", verifyPin); -router.get("/pin/status", getPinStatus); +// Rate limiter for /pin/verify to prevent brute-force/DoS attacks +const pinVerifyLimiter = rateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + max: 5, // limit each IP to 5 requests per windowMs + message: "Too many PIN verification attempts, please try again later." +}); + +router.post("/pin", asyncHandler(setPin)); +router.post("/pin/verify", pinVerifyLimiter, asyncHandler(verifyPin)); +router.get("/pin/status", asyncHandler(getPinStatus)); export default router; diff --git a/app/backend/src/routes/puccRoutes.ts b/app/backend/src/routes/puccRoutes.ts index 8b8d9eb3..e5d65d69 100644 --- a/app/backend/src/routes/puccRoutes.ts +++ b/app/backend/src/routes/puccRoutes.ts @@ -6,12 +6,17 @@ import { deletePollutionCertificate, } from "@controllers/PUCCController.js"; import { authenticatePin } from "@middleware/auth.js"; +import { asyncHandler } from "@middleware/async-handler.js"; const router = Router({ mergeParams: true }); -router.post("/", authenticatePin, addPollutionCertificate); -router.get("/", authenticatePin, getPollutionCertificates); -router.put("/:id", authenticatePin, updatePollutionCertificate); -router.delete("/:id", authenticatePin, deletePollutionCertificate); +router.post("/", authenticatePin, asyncHandler(addPollutionCertificate)); +router.get("/", authenticatePin, asyncHandler(getPollutionCertificates)); +router.put("/:id", authenticatePin, asyncHandler(updatePollutionCertificate)); +router.delete( + "/:id", + authenticatePin, + asyncHandler(deletePollutionCertificate), +); export default router; diff --git a/app/backend/src/routes/vehicleRoutes.ts b/app/backend/src/routes/vehicleRoutes.ts index 0408efc7..57daf9c1 100644 --- a/app/backend/src/routes/vehicleRoutes.ts +++ b/app/backend/src/routes/vehicleRoutes.ts @@ -7,6 +7,7 @@ import { deleteVehicle, } from "@controllers/VehicleController.js"; import { authenticatePin } from "@middleware/auth.js"; +import { asyncHandler } from "@middleware/async-handler.js"; import fuelLogRoutes from "./fuelLogRoutes.js"; import insuranceRoutes from "./insuranceRoutes.js"; import maintenanceLogRoutes from "./maintenanceLogRoutes.js"; @@ -14,11 +15,11 @@ import puccRoutes from "./puccRoutes.js"; const router = Router(); -router.post("/", authenticatePin, addVehicle); -router.get("/", authenticatePin, getAllVehicles); -router.get("/:id", authenticatePin, getVehicleById); -router.put("/:id", authenticatePin, updateVehicle); -router.delete("/:id", authenticatePin, deleteVehicle); +router.post("/", authenticatePin, asyncHandler(addVehicle)); +router.get("/", authenticatePin, asyncHandler(getAllVehicles)); +router.get("/:id", authenticatePin, asyncHandler(getVehicleById)); +router.put("/:id", authenticatePin, asyncHandler(updateVehicle)); +router.delete("/:id", authenticatePin, asyncHandler(deleteVehicle)); router.use("/:vehicleId/fuel-logs", fuelLogRoutes); router.use("/:vehicleId/insurance", insuranceRoutes); diff --git a/app/backend/src/services/configService.ts b/app/backend/src/services/configService.ts index 5a89799e..ba4f4ae7 100644 --- a/app/backend/src/services/configService.ts +++ b/app/backend/src/services/configService.ts @@ -1,5 +1,5 @@ import { ConfigError } from "@exceptions/ConfigError.js"; -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; import Config from "@models/Config.js"; export const getAppConfig = async () => { diff --git a/app/backend/src/services/fuelLogService.ts b/app/backend/src/services/fuelLogService.ts index ddc307cd..4b8970d1 100644 --- a/app/backend/src/services/fuelLogService.ts +++ b/app/backend/src/services/fuelLogService.ts @@ -1,5 +1,5 @@ import { FuelLogError } from "@exceptions/FuelLogError.js"; -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; import { VehicleError } from "@exceptions/VehicleError.js"; import { Vehicle, FuelLog } from "@models/index.js"; diff --git a/app/backend/src/services/insuranceService.ts b/app/backend/src/services/insuranceService.ts index 7be26fbb..a6d952ca 100644 --- a/app/backend/src/services/insuranceService.ts +++ b/app/backend/src/services/insuranceService.ts @@ -1,7 +1,6 @@ import { InsuranceError } from "@exceptions/InsuranceError.js"; -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; import { Insurance, Vehicle } from "@models/index.js"; -import { UniqueConstraintError } from "sequelize"; export const addInsurance = async (vehicleId: string, insuranceData: any) => { const vehicle = await Vehicle.findByPk(vehicleId); diff --git a/app/backend/src/services/maintenanceLogService.ts b/app/backend/src/services/maintenanceLogService.ts index 9f6b54de..b5f574c6 100644 --- a/app/backend/src/services/maintenanceLogService.ts +++ b/app/backend/src/services/maintenanceLogService.ts @@ -1,6 +1,6 @@ import { MaintenanceLog, Vehicle } from "@models/index.js"; import { MaintenanceLogError } from "@exceptions/MaintenanceLogError.js"; -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; export const addMaintenanceLog = async ( vehicleId: string, diff --git a/app/backend/src/services/pinService.ts b/app/backend/src/services/pinService.ts index bde13fc5..1d496d5b 100644 --- a/app/backend/src/services/pinService.ts +++ b/app/backend/src/services/pinService.ts @@ -1,7 +1,7 @@ import bcrypt from "bcrypt"; import { Auth } from "@models/index.js"; import { AuthError } from "@exceptions/AuthError.js"; -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; export const setPin = async (pin: string) => { const hash = await bcrypt.hash(pin, 10); diff --git a/app/backend/src/services/pollutionCertificateService.ts b/app/backend/src/services/pollutionCertificateService.ts index d42ce863..7e98371b 100644 --- a/app/backend/src/services/pollutionCertificateService.ts +++ b/app/backend/src/services/pollutionCertificateService.ts @@ -1,7 +1,6 @@ import { Vehicle, PollutionCertificate } from "@models/index.js"; import { PollutionCertificateError } from "@exceptions/PollutionCertificateError.js"; -import { UniqueConstraintError } from "sequelize"; -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; export const addPollutionCertificate = async ( vehicleId: string, diff --git a/app/backend/src/services/vehicleService.ts b/app/backend/src/services/vehicleService.ts index 7fb7eabf..85b61007 100644 --- a/app/backend/src/services/vehicleService.ts +++ b/app/backend/src/services/vehicleService.ts @@ -1,4 +1,4 @@ -import { Status, statusFromError } from "@exceptions/ServiceError.js"; +import { Status } from "@exceptions/ServiceError.js"; import { VehicleError } from "@exceptions/VehicleError.js"; import { Insurance, PollutionCertificate, Vehicle } from "@models/index.js"; diff --git a/app/backend/tsconfig.json b/app/backend/tsconfig.json index 49c25787..614a6724 100644 --- a/app/backend/tsconfig.json +++ b/app/backend/tsconfig.json @@ -32,7 +32,9 @@ "@services": ["./src/services"], "@services/*": ["./src/services/*"], "@middleware": ["./src/middleware"], - "@middleware/*": ["./src/middleware/*"] + "@middleware/*": ["./src/middleware/*"], + "@frontend": ["./frontend"], + "@frontend/*": ["./frontend/*"] } }, "include": ["**/*.ts"], diff --git a/app/frontend/eslint.config.js b/app/frontend/eslint.config.js new file mode 100644 index 00000000..8966ab4b --- /dev/null +++ b/app/frontend/eslint.config.js @@ -0,0 +1,71 @@ +import js from '@eslint/js'; +import ts from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import svelte from 'eslint-plugin-svelte'; +import svelteParser from 'svelte-eslint-parser'; +import globals from 'globals'; +import unusedImports from 'eslint-plugin-unused-imports'; + +export default [ + js.configs.recommended, + { + files: ['**/*.{js,ts}'], + languageOptions: { + parser: tsParser, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020 + }, + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2017 + } + }, + plugins: { + '@typescript-eslint': ts, + 'unused-imports': unusedImports + }, + rules: { + ...ts.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + 'no-unused-vars': 'off', + 'no-prototype-builtins': 'off' + } + }, + { + files: ['**/*.svelte'], + languageOptions: { + parser: svelteParser, + parserOptions: { + parser: tsParser + }, + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2017, + $state: 'readonly' + } + }, + plugins: { + svelte, + '@typescript-eslint': ts, + 'unused-imports': unusedImports + }, + rules: { + ...svelte.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + 'no-unused-vars': 'off', + 'no-prototype-builtins': 'off' + } + }, + { + ignores: ['node_modules/', '.svelte-kit/', 'build/', 'dist/', '*.config.js', '*.config.ts'] + } +]; diff --git a/app/frontend/package.json b/app/frontend/package.json index 70219272..b8702cbd 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "client", + "name": "frontend", "private": true, "version": "0.0.1", "type": "module", @@ -9,24 +9,34 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "format": "prettier --write .", - "lint": "prettier --check .", - "clean": "rm -rf build .svelte-kit" + "format": "(eslint --fix . || true) && prettier --write .", + "lint": "eslint . && prettier --check .", + "clean": "rm -rf build .svelte-kit", + "reset": "npm run clean && rm -rf node_modules && npm install" }, "devDependencies": { + "@eslint/js": "^9.34.0", "@sveltejs/adapter-node": "^5.2.13", "@sveltejs/kit": "^2.22.0", "@sveltejs/vite-plugin-svelte": "^6.0.0", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", "chart.js": "^4.5.0", "date-fns": "^4.1.0", + "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.11.0", + "eslint-plugin-unused-imports": "^4.2.0", + "globals": "^14.0.0", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.11", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "svelte-eslint-parser": "^1.3.1", "tailwindcss": "^4.0.0", "typescript": "^5.0.0", "vite": "^7.0.4" diff --git a/app/frontend/src/components/chart/DashboardCharts.svelte b/app/frontend/src/components/chart/DashboardCharts.svelte index 03664f35..3f3c05bd 100644 --- a/app/frontend/src/components/chart/DashboardCharts.svelte +++ b/app/frontend/src/components/chart/DashboardCharts.svelte @@ -1,20 +1,9 @@
diff --git a/app/frontend/src/components/common/StatusBlock.svelte b/app/frontend/src/components/common/StatusBlock.svelte index ce3858d2..83f687eb 100644 --- a/app/frontend/src/components/common/StatusBlock.svelte +++ b/app/frontend/src/components/common/StatusBlock.svelte @@ -1,6 +1,4 @@ {#if message} -

+

{#each message.split('\n') as error} {error}
{/each} diff --git a/app/frontend/src/components/forms/ConfigForm.svelte b/app/frontend/src/components/forms/ConfigForm.svelte index 565966d2..80ec3e4b 100644 --- a/app/frontend/src/components/forms/ConfigForm.svelte +++ b/app/frontend/src/components/forms/ConfigForm.svelte @@ -105,7 +105,7 @@ {/if}

{/each} -