diff --git a/land-registry-backend/.env.example b/land-registry-backend/.env.example new file mode 100644 index 00000000..33d863ef --- /dev/null +++ b/land-registry-backend/.env.example @@ -0,0 +1,4 @@ +PORT=3000 +DATABASE_URL= +NODE_ENV=development +CORS_ORIGINS= \ No newline at end of file diff --git a/land-registry-backend/package.json b/land-registry-backend/package.json new file mode 100644 index 00000000..a424732b --- /dev/null +++ b/land-registry-backend/package.json @@ -0,0 +1,32 @@ +{ + "name": "land-registry-backend", + "version": "1.0.0", + "description": "Backend service for Land Registry dApp", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node-dev src/index.ts", + "lint": "eslint . --ext .ts" + }, + "dependencies": { + "express": "^4.18.2", + "pg": "^8.11.0", + "dotenv": "^16.0.3", + "cors": "^2.8.5", + "helmet": "^7.0.0", + "express-validator": "^7.0.1", + "winston": "^3.9.0" + }, + "devDependencies": { + "@types/express": "^4.17.17", + "@types/pg": "^8.10.1", + "@types/cors": "^2.8.13", + "@types/node": "^20.2.5", + "typescript": "^5.0.4", + "ts-node-dev": "^2.0.0", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", + "eslint": "^8.41.0" + } +} \ No newline at end of file diff --git a/land-registry-backend/src/config.ts b/land-registry-backend/src/config.ts new file mode 100644 index 00000000..c6d4372a --- /dev/null +++ b/land-registry-backend/src/config.ts @@ -0,0 +1,9 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +export const config = { + port: process.env.PORT || 3000, + databaseUrl: process.env.DATABASE_URL || '', + environment: process.env.NODE_ENV || 'development', + corsOrigins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'], +}; \ No newline at end of file diff --git a/land-registry-backend/src/controllers/inspectorController.ts b/land-registry-backend/src/controllers/inspectorController.ts new file mode 100644 index 00000000..9170673c --- /dev/null +++ b/land-registry-backend/src/controllers/inspectorController.ts @@ -0,0 +1,27 @@ +/** + * Controller functions for handling inspector data + * + * These functions manage the retrieval and processing of inspector data, + * including all inspectors and their assignments. + */ + +import { Request, Response, NextFunction } from 'express'; +import * as inspectorService from '../services/inspectorService'; + +export async function getAllInspectors(req: Request, res: Response, next: NextFunction) { + try { + const inspectors = await inspectorService.getAllInspectors(); + res.json(inspectors); + } catch (error) { + next(error); + } +} + +export async function getInspectorAssignments(req: Request, res: Response, next: NextFunction) { + try { + const assignments = await inspectorService.getInspectorAssignments(req.params.address); + res.json(assignments); + } catch (error) { + next(error); + } +} \ No newline at end of file diff --git a/land-registry-backend/src/controllers/landController.ts b/land-registry-backend/src/controllers/landController.ts new file mode 100644 index 00000000..6b124e6d --- /dev/null +++ b/land-registry-backend/src/controllers/landController.ts @@ -0,0 +1,58 @@ +/** + * Controller functions for handling land registry data + * + * These functions manage the retrieval and processing of land registry data, + * including all lands, individual land details, and related transfers and verifications. + */ + +import { Request, Response, NextFunction } from 'express'; +import * as landService from '../services/landService'; +import { AppError } from '../middleware/errorHandler'; + +export async function getAllLands(req: Request, res: Response, next: NextFunction) { + try { + const lands = await landService.getAllLands(); + res.json(lands); + } catch (error) { + next(error); + } +} + +export async function getLandById(req: Request, res: Response, next: NextFunction) { + try { + const land = await landService.getLandById(req.params.landId); + if (!land) { + throw new AppError(404, 'Land not found', 'LAND_NOT_FOUND'); + } + res.json(land); + } catch (error) { + next(error); + } +} + +export async function getLandTransfers(req: Request, res: Response, next: NextFunction) { + try { + const transfers = await landService.getLandTransfers(req.params.landId); + res.json(transfers); + } catch (error) { + next(error); + } +} + +export async function getLandVerifications(req: Request, res: Response, next: NextFunction) { + try { + const verifications = await landService.getLandVerifications(req.params.landId); + res.json(verifications); + } catch (error) { + next(error); + } +} + +export async function getLandsByOwner(req: Request, res: Response, next: NextFunction) { + try { + const lands = await landService.getLandsByOwner(req.params.address); + res.json(lands); + } catch (error) { + next(error); + } +} \ No newline at end of file diff --git a/land-registry-backend/src/controllers/listingController.ts b/land-registry-backend/src/controllers/listingController.ts new file mode 100644 index 00000000..92aeac9b --- /dev/null +++ b/land-registry-backend/src/controllers/listingController.ts @@ -0,0 +1,49 @@ +/** + * Controller functions for handling marketplace listings + * + * These functions manage the retrieval and processing of marketplace listings, + * including active listings and individual listing details. + */ + +import { Request, Response, NextFunction } from 'express'; +import * as listingService from '../services/listingService'; +import { AppError } from '../middleware/errorHandler'; + +export async function getActiveListings(req: Request, res: Response, next: NextFunction) { + try { + const listings = await listingService.getActiveListings(); + res.json(listings); + } catch (error) { + next(error); + } +} + +export async function getListingById(req: Request, res: Response, next: NextFunction) { + try { + const listing = await listingService.getListingById(req.params.listingId); + if (!listing) { + throw new AppError(404, 'Listing not found', 'LISTING_NOT_FOUND'); + } + res.json(listing); + } catch (error) { + next(error); + } +} + +export async function getListingPriceHistory(req: Request, res: Response, next: NextFunction) { + try { + const priceHistory = await listingService.getListingPriceHistory(req.params.listingId); + res.json(priceHistory); + } catch (error) { + next(error); + } +} + +export async function getLandSales(req: Request, res: Response, next: NextFunction) { + try { + const sales = await listingService.getLandSales(); + res.json(sales); + } catch (error) { + next(error); + } +} \ No newline at end of file diff --git a/land-registry-backend/src/index.ts b/land-registry-backend/src/index.ts new file mode 100644 index 00000000..cc368165 --- /dev/null +++ b/land-registry-backend/src/index.ts @@ -0,0 +1,28 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import { landRoutes } from './routes/landRoutes'; +import { inspectorRoutes } from './routes/inspectorRoutes'; +import { listingRoutes } from './routes/listingRoutes'; +import { errorHandler } from './middleware/errorHandler'; +import { config } from './config'; +import { logger } from './utils/logger'; + +const app = express(); + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(express.json()); + +// // Routes +// app.use('/api/lands', landRoutes); +// app.use('/api/inspectors', inspectorRoutes); +// app.use('/api/listings', listingRoutes); + +// Error handling +app.use(errorHandler); + +app.listen(config.port, () => { + logger.info(`Server running on port ${config.port}`); +}); \ No newline at end of file diff --git a/land-registry-backend/src/middleware/errorHandler.ts b/land-registry-backend/src/middleware/errorHandler.ts new file mode 100644 index 00000000..57123041 --- /dev/null +++ b/land-registry-backend/src/middleware/errorHandler.ts @@ -0,0 +1,39 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '../utils/logger'; + +export class AppError extends Error { + constructor( + public statusCode: number, + public message: string, + public code?: string + ) { + super(message); + } +} + +export const errorHandler = ( + err: Error, + req: Request, + res: Response, + next: NextFunction +) => { + logger.error('Error:', { + message: err.message, + stack: err.stack, + path: req.path, + method: req.method, + }); + + if (err instanceof AppError) { + return res.status(err.statusCode).json({ + status: 'error', + code: err.code, + message: err.message, + }); + } + + return res.status(500).json({ + status: 'error', + message: 'Internal server error', + }); +}; \ No newline at end of file diff --git a/land-registry-backend/src/middleware/validate.ts b/land-registry-backend/src/middleware/validate.ts new file mode 100644 index 00000000..c8f9c056 --- /dev/null +++ b/land-registry-backend/src/middleware/validate.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from 'express'; +import { validationResult } from 'express-validator'; +import { AppError } from './errorHandler'; + +export const validate = (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + throw new AppError(400, 'Validation error', 'VALIDATION_ERROR'); + } + next(); +}; \ No newline at end of file diff --git a/land-registry-backend/src/routes/inspectorRoutes.ts b/land-registry-backend/src/routes/inspectorRoutes.ts new file mode 100644 index 00000000..9bd12070 --- /dev/null +++ b/land-registry-backend/src/routes/inspectorRoutes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import { param } from 'express-validator'; +import { validate } from '../middleware/validate'; +import * as inspectorController from '../controllers/inspectorController'; + +export const inspectorRoutes = Router(); + +inspectorRoutes.get('/', inspectorController.getAllInspectors); + +inspectorRoutes.get( + '/:address/assignments', + [param('address').isString()], + validate, + inspectorController.getInspectorAssignments +); \ No newline at end of file diff --git a/land-registry-backend/src/routes/landRoutes.ts b/land-registry-backend/src/routes/landRoutes.ts new file mode 100644 index 00000000..f93c54ab --- /dev/null +++ b/land-registry-backend/src/routes/landRoutes.ts @@ -0,0 +1,36 @@ +import { Router } from 'express'; +import { param, query } from 'express-validator'; +import { validate } from '../middleware/validate'; +import * as landController from '../controllers/landController'; + +export const landRoutes = Router(); + +landRoutes.get('/', landController.getAllLands); + +landRoutes.get( + '/:landId', + [param('landId').isString()], + validate, + landController.getLandById +); + +landRoutes.get( + '/:landId/transfers', + [param('landId').isString()], + validate, + landController.getLandTransfers +); + +landRoutes.get( + '/:landId/verifications', + [param('landId').isString()], + validate, + landController.getLandVerifications +); + +landRoutes.get( + '/owner/:address', + [param('address').isString()], + validate, + landController.getLandsByOwner +); \ No newline at end of file diff --git a/land-registry-backend/src/routes/listingRoutes.ts b/land-registry-backend/src/routes/listingRoutes.ts new file mode 100644 index 00000000..68098123 --- /dev/null +++ b/land-registry-backend/src/routes/listingRoutes.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import { param } from 'express-validator'; +import { validate } from '../middleware/validate'; +import * as listingController from '../controllers/listingController'; + +export const listingRoutes = Router(); + +listingRoutes.get('/active', listingController.getActiveListings); + +listingRoutes.get( + '/:listingId', + [param('listingId').isString()], + validate, + listingController.getListingById +); + +listingRoutes.get( + '/:listingId/price-history', + [param('listingId').isString()], + validate, + listingController.getListingPriceHistory +); + +listingRoutes.get('/sales', listingController.getLandSales); \ No newline at end of file diff --git a/land-registry-backend/src/services/db.ts b/land-registry-backend/src/services/db.ts new file mode 100644 index 00000000..a9daf3ee --- /dev/null +++ b/land-registry-backend/src/services/db.ts @@ -0,0 +1,14 @@ +import { Pool } from 'pg'; +import { config } from '../config'; + +export const pool = new Pool({ + connectionString: config.databaseUrl, +}); + +export async function query(text: string, params?: any[]) { + const start = Date.now(); + const res = await pool.query(text, params); + const duration = Date.now() - start; + console.log('Executed query', { text, duration, rows: res.rowCount }); + return res; +} \ No newline at end of file diff --git a/land-registry-backend/src/services/inspectorService.ts b/land-registry-backend/src/services/inspectorService.ts new file mode 100644 index 00000000..059c2b58 --- /dev/null +++ b/land-registry-backend/src/services/inspectorService.ts @@ -0,0 +1,21 @@ +import { query } from './db'; +import { Inspector, InspectorAssignment } from '../types'; + +export async function getAllInspectors(): Promise { + const result = await query(` + SELECT * FROM inspectors + WHERE is_active = true + `); + return result.rows; +} + +export async function getInspectorAssignments(inspectorAddress: string): Promise { + const result = await query(` + SELECT ia.*, l.* + FROM inspector_assignments ia + JOIN lands l ON ia.land_id = l.land_id + WHERE ia.inspector_address = $1 + ORDER BY ia.timestamp DESC + `, [inspectorAddress]); + return result.rows; +} \ No newline at end of file diff --git a/land-registry-backend/src/services/landService.ts b/land-registry-backend/src/services/landService.ts new file mode 100644 index 00000000..4c4a13fb --- /dev/null +++ b/land-registry-backend/src/services/landService.ts @@ -0,0 +1,45 @@ +import { query } from './db'; +import { Land, LandTransfer, LandVerification } from '../types'; + +export async function getAllLands(): Promise { + const result = await query(` + SELECT * FROM lands + ORDER BY created_at DESC + `); + return result.rows; +} + +export async function getLandById(landId: string): Promise { + const result = await query(` + SELECT * FROM lands + WHERE land_id = $1 + `, [landId]); + return result.rows[0] || null; +} + +export async function getLandTransfers(landId: string): Promise { + const result = await query(` + SELECT * FROM land_transfers + WHERE land_id = $1 + ORDER BY timestamp DESC + `, [landId]); + return result.rows; +} + +export async function getLandVerifications(landId: string): Promise { + const result = await query(` + SELECT * FROM land_verifications + WHERE land_id = $1 + ORDER BY timestamp DESC + `, [landId]); + return result.rows; +} + +export async function getLandsByOwner(ownerAddress: string): Promise { + const result = await query(` + SELECT * FROM lands + WHERE owner_address = $1 + ORDER BY created_at DESC + `, [ownerAddress]); + return result.rows; +} \ No newline at end of file diff --git a/land-registry-backend/src/services/listingService.ts b/land-registry-backend/src/services/listingService.ts new file mode 100644 index 00000000..e3382c6d --- /dev/null +++ b/land-registry-backend/src/services/listingService.ts @@ -0,0 +1,43 @@ +import { query } from './db'; +import { Listing, ListingPriceUpdate, LandSale } from '../types'; + +export async function getActiveListings(): Promise { + const result = await query(` + SELECT l.*, lands.* + FROM listings l + JOIN lands ON l.land_id = lands.land_id + WHERE l.status = 'ACTIVE' + ORDER BY l.created_at DESC + `); + return result.rows; +} + +export async function getListingById(listingId: string): Promise { + const result = await query(` + SELECT l.*, lands.* + FROM listings l + JOIN lands ON l.land_id = lands.land_id + WHERE l.id = $1 + `, [listingId]); + return result.rows[0] || null; +} + +export async function getListingPriceHistory(listingId: string): Promise { + const result = await query(` + SELECT * FROM listing_price_updates + WHERE listing_id = $1 + ORDER BY timestamp DESC + `, [listingId]); + return result.rows; +} + +export async function getLandSales(): Promise { + const result = await query(` + SELECT ls.*, l.*, lands.* + FROM land_sales ls + JOIN listings l ON ls.listing_id = l.id + JOIN lands ON ls.land_id = lands.land_id + ORDER BY ls.timestamp DESC + `); + return result.rows; +} \ No newline at end of file diff --git a/land-registry-backend/src/types/index.ts b/land-registry-backend/src/types/index.ts new file mode 100644 index 00000000..8a97dbc1 --- /dev/null +++ b/land-registry-backend/src/types/index.ts @@ -0,0 +1,81 @@ +export interface Land { + land_id: string; + owner_address: string; + location_latitude: number; + location_longitude: number; + area: string; + land_use: string; + status: string; + inspector_address: string | null; + created_at: Date; + updated_at: Date; +} + +export interface LandTransfer { + id: number; + land_id: string; + from_address: string; + to_address: string; + transaction_hash: string; + block_number: string; + timestamp: Date; +} + +export interface LandVerification { + id: number; + land_id: string; + inspector_address: string; + transaction_hash: string; + block_number: string; + timestamp: Date; +} + +export interface Inspector { + address: string; + is_active: boolean; + added_at: Date; + removed_at: Date | null; +} + +export interface InspectorAssignment { + id: number; + land_id: string; + inspector_address: string; + transaction_hash: string; + block_number: string; + timestamp: Date; +} + +export interface Listing { + id: number; + land_id: string; + seller_address: string; + price: string; + status: string; + created_at: Date; + updated_at: Date; + transaction_hash: string; + block_number: string; +} + +export interface ListingPriceUpdate { + id: number; + listing_id: number; + old_price: string; + new_price: string; + transaction_hash: string; + block_number: string; + timestamp: Date; +} + +export interface LandSale { + id: number; + listing_id: number; + land_id: string; + seller_address: string; + buyer_address: string; + price: string; + transaction_hash: string; + block_number: string; + timestamp: Date; +} \ No newline at end of file diff --git a/land-registry-backend/src/utils/logger.ts b/land-registry-backend/src/utils/logger.ts new file mode 100644 index 00000000..ba8a3eca --- /dev/null +++ b/land-registry-backend/src/utils/logger.ts @@ -0,0 +1,18 @@ +import winston from 'winston'; +import { config } from '../config'; + +export const logger = winston.createLogger({ + level: config.environment === 'development' ? 'debug' : 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ), + }), + ], +}); \ No newline at end of file diff --git a/land-registry-backend/tsconfig.json b/land-registry-backend/tsconfig.json new file mode 100644 index 00000000..5862a3b8 --- /dev/null +++ b/land-registry-backend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file