first commit
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
||||
NODE_ENV=
|
||||
PORT=
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret
|
||||
JWT_ACCESS_EXPIRATION_MINUTES=230
|
||||
JWT_REFRESH_EXPIRATION_DAYS=30
|
||||
JWT_RESET_PASSWORD_EXPIRATION_MINUTES=10
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES=10
|
||||
|
||||
|
||||
# DataBase
|
||||
DB_USERNAME=username
|
||||
DB_PASSWORD=password
|
||||
DB_DATABASE_NAME=database_name
|
||||
DB_HOSTNAME=host
|
||||
DB_PORT=port
|
||||
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
bin
|
||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Convert text file line endings to lf
|
||||
* text eol=lf
|
||||
*.js text
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
node_modules/
|
||||
dist/
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
.env
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
yarn lint-staged
|
||||
6
.lintstagedrc.json
Normal file
6
.lintstagedrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"*.js": [
|
||||
"eslint",
|
||||
"prettier --write **/*.js"
|
||||
]
|
||||
}
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Swapnil Bendal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# **TypeScript Backend Template**
|
||||
|
||||
This backend template follows the principles of CLEAN Architecture to ensure:
|
||||
|
||||
- **Separation of Concerns:** Layers are organized to separate business logic, application logic, and infrastructure concerns.
|
||||
- **Scalability:** Modular and well-organized codebase makes it easy to extend functionality.
|
||||
- **Testability:** Well-defined boundaries between layers simplify unit and integration testing.
|
||||
- **Maintainability:** Consistent structure and adherence to SOLID principles reduce technical debt over time.
|
||||
|
||||
### **Structure Overview**
|
||||
- **Core Domain:** Contains the business rules and logic.
|
||||
- **Use Cases:** Implements application-specific rules and orchestrates the flow of data.
|
||||
- **Infrastructure:** Deals with external systems (e.g., database, APIs).
|
||||
- **Presentation:** Manages HTTP communication and routes.
|
||||
|
||||
Refer to the documentation for details on how to structure and organize your code.
|
||||
|
||||
---
|
||||
|
||||
## **Table of Contents**
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Scripts](#scripts)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## **Installation**
|
||||
|
||||
### **Prerequisites**
|
||||
- [Node.js](https://nodejs.org/) (version 14 or higher recommended)
|
||||
- [npm](https://www.npmjs.com/) (bundled with Node.js)
|
||||
|
||||
### **Steps**
|
||||
1. Fork the repository to your GitHub or GitLab account.
|
||||
2. Clone your forked repository:
|
||||
```bash
|
||||
git clone https://<your-git-account>/TypeScript-Backend-Template.git
|
||||
```
|
||||
3. Navigate to the project directory:
|
||||
```bash
|
||||
cd TypeScript-Backend-Template
|
||||
```
|
||||
4. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Usage**
|
||||
|
||||
### **Development Mode**
|
||||
1. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
2. Open your browser and navigate to:
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
### **Production Mode**
|
||||
1. Install [PM2](https://pm2.keymetrics.io/) globally for process management:
|
||||
```bash
|
||||
npm install pm2 -g
|
||||
```
|
||||
2. Start the production server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Environment Variables**
|
||||
|
||||
Create a `.env` file in the root directory based on the structure of [`.env.example`](.env.example).
|
||||
|
||||
---
|
||||
|
||||
## **Scripts**
|
||||
|
||||
| Script | Description |
|
||||
|---------------------|-------------------------------------------------------------|
|
||||
| `npm start` | Starts the app in production mode using PM2. |
|
||||
| `npm run dev` | Starts the app in development mode with `nodemon`. |
|
||||
| `npm run lint` | Runs ESLint to check for code quality issues. |
|
||||
| `npm run lint:fix` | Fixes fixable issues detected by ESLint. |
|
||||
| `npm run prettier` | Checks code formatting using Prettier. |
|
||||
| `npm run prettier:fix` | Formats code files according to Prettier rules. |
|
||||
| `npm run prepare` | Prepares Husky for managing Git hooks. |
|
||||
|
||||
---
|
||||
|
||||
## **License**
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
17
ecosystem.config.json
Normal file
17
ecosystem.config.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "Typescript-Backend",
|
||||
"script": "npm",
|
||||
"args": "run start",
|
||||
"instances": 1,
|
||||
"autorestart": true,
|
||||
"watch": false,
|
||||
"time": true,
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"PORT": 3000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
eslint.config.mjs
Normal file
14
eslint.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
}
|
||||
},
|
||||
);
|
||||
57
package.json
Normal file
57
package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "tanami-backend",
|
||||
"version": "v1.0.0",
|
||||
"main": "src/index.ts",
|
||||
"author": "Swapnil Bendal",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development nodemon src/index.ts",
|
||||
"start": "ts-node src/index.ts",
|
||||
"typeorm": "cross-env NODE_ENV=development typeorm-ts-node-commonjs",
|
||||
"build": "tsc",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"prettier": "prettier --check **/*.js",
|
||||
"prettier:fix": "prettier --write **/*.js",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"inversify": "^6.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pg": "^8.13.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"request-ip": "^3.3.0",
|
||||
"typeorm": "0.3.20",
|
||||
"winston": "^3.17.0",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^16.11.10",
|
||||
"@types/request-ip": "^0.0.41",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"globals": "^15.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.11",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.0"
|
||||
}
|
||||
}
|
||||
53
src/app.ts
Normal file
53
src/app.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import requestId from 'request-ip';
|
||||
import express, { Application, NextFunction, Request, Response } from "express";
|
||||
import config from "./config/config";
|
||||
import morgan from "./config/morgan";
|
||||
import path from 'path';
|
||||
import logger from './config/logger';
|
||||
import error from './middleware/error';
|
||||
import routes from './routes';
|
||||
import ApiError from './utils/helper/ApiError';
|
||||
|
||||
class App {
|
||||
private app: Application;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.initializeMiddlewares();
|
||||
this.initializeRoutes();
|
||||
this.initializeErrorHandling();
|
||||
}
|
||||
|
||||
private initializeMiddlewares(): void {
|
||||
if (config.env !== "test") {
|
||||
this.app.use(morgan.successHandler);
|
||||
this.app.use(morgan.errorHandler);
|
||||
}
|
||||
this.app.use(requestId.mw());
|
||||
this.app.use(express.json());
|
||||
this.app.use(express.urlencoded({ extended: true }));
|
||||
this.app.use("/public", express.static(path.join(__dirname, "../public")));
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
this.app.use('/api', routes);
|
||||
// Define our routes
|
||||
this.app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
next(new ApiError(404, "Not found"));
|
||||
})
|
||||
}
|
||||
|
||||
private initializeErrorHandling(): void {
|
||||
this.app.use(error.errorConverter);
|
||||
this.app.use(error.errorHandler);
|
||||
}
|
||||
|
||||
public listen(port: number): ReturnType<typeof this.app.listen> {
|
||||
return this.app.listen(port, () => {
|
||||
logger.info(`Server listening on port ${config.port}`);
|
||||
logger.info(`Environment :- ${config.env}`);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new App();
|
||||
89
src/config/config.ts
Normal file
89
src/config/config.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import * as yup from 'yup';
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, '../../.env') });
|
||||
|
||||
const envVarsSchema = yup.object().shape({
|
||||
NODE_ENV: yup.string().oneOf(["production", "development", "test"]).required(),
|
||||
PORT: yup.number().default(3000),
|
||||
|
||||
JWT_SECRET: yup.string().required('JWT secret key is required'),
|
||||
JWT_ACCESS_EXPIRATION_MINUTES: yup.number().default(30).required('minutes after which access tokens expire'),
|
||||
JWT_REFRESH_EXPIRATION_DAYS: yup.number().default(30).required('days after which refresh tokens expire'),
|
||||
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: yup.number().default(10).required('minutes after which reset password token expires'),
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: yup.number().default(10).required('minutes after which verify email token expires'),
|
||||
|
||||
// DataBase
|
||||
DB_USERNAME: yup.string().required('DB Username is required'),
|
||||
DB_PASSWORD: yup.string().required('DB Password is required'),
|
||||
DB_DATABASE_NAME: yup.string().required('Database name is required'),
|
||||
DB_HOSTNAME: yup.string().default('127.0.0.1').required('DB Hostname is required'),
|
||||
DB_PORT: yup.number().default(3306).required('DB Port is required'),
|
||||
}).noUnknown(true)
|
||||
|
||||
// Validate and prepare the configuration
|
||||
function getConfig() {
|
||||
try {
|
||||
// Validate the environment variables
|
||||
const envVars = envVarsSchema.validateSync(process.env, {
|
||||
abortEarly: false, // Validate all fields before throwing errors
|
||||
stripUnknown: true, // Remove fields not in the schema
|
||||
});
|
||||
|
||||
// Return the validated configuration
|
||||
return {
|
||||
env: envVars.NODE_ENV,
|
||||
port: envVars.PORT,
|
||||
jwt: {
|
||||
secret: envVars.JWT_SECRET,
|
||||
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
|
||||
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
|
||||
resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
|
||||
verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
|
||||
},
|
||||
database: {
|
||||
development: {
|
||||
host: envVars.DB_HOSTNAME,
|
||||
port: envVars.DB_PORT,
|
||||
username: envVars.DB_USERNAME,
|
||||
password: envVars.DB_PASSWORD,
|
||||
database: envVars.DB_DATABASE_NAME,
|
||||
logging: false,
|
||||
},
|
||||
test: {
|
||||
host: envVars.DB_HOSTNAME,
|
||||
port: envVars.DB_PORT,
|
||||
username: envVars.DB_USERNAME,
|
||||
password: envVars.DB_PASSWORD,
|
||||
database: envVars.DB_DATABASE_NAME,
|
||||
logging: false,
|
||||
socketPath: "/var/run/mysqld/mysqld.sock",
|
||||
},
|
||||
production: {
|
||||
host: envVars.DB_HOSTNAME,
|
||||
port: envVars.DB_PORT,
|
||||
username: envVars.DB_USERNAME,
|
||||
password: envVars.DB_PASSWORD,
|
||||
database: envVars.DB_DATABASE_NAME,
|
||||
logging: false,
|
||||
socketPath: "/var/run/mysqld/mysqld.sock",
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof yup.ValidationError) {
|
||||
console.error("Validation Errors:", error.errors.join(", "));
|
||||
} else {
|
||||
console.error("Unexpected error during configuration validation:", error);
|
||||
}
|
||||
|
||||
console.error("Server shut down due to incomplete environment variable configuration.");
|
||||
process.exit(1); // Exit with error code 1
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and export configuration only if validation succeeds
|
||||
const config = getConfig();
|
||||
export default config;
|
||||
17
src/config/data-source.ts
Normal file
17
src/config/data-source.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import "reflect-metadata"
|
||||
import { DataSource } from "typeorm"
|
||||
import config from "./config"
|
||||
import path from "path"
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: "postgres",
|
||||
host: config.database[config.env].host, // Dynamically set host based on environment
|
||||
port: config.database[config.env].port, // Dynamically set port based on environment
|
||||
username: config.database[config.env].username, // Dynamically set username
|
||||
password: config.database[config.env].password, // Dynamically set password
|
||||
database: config.database[config.env].database, // Dynamically set database
|
||||
synchronize: true,
|
||||
entities: [path.resolve(__dirname, "../entities/**/*.ts")],
|
||||
migrations: [path.resolve(__dirname, "../migration/**/*.ts")],
|
||||
subscribers: [],
|
||||
})
|
||||
44
src/config/logger.ts
Normal file
44
src/config/logger.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import winston, { Logger, format } from "winston";
|
||||
import config from "./config";
|
||||
|
||||
const logger: Logger = winston.createLogger({
|
||||
level: config.env === "development" ? "debug" : "info",
|
||||
format: format.combine(
|
||||
format.splat(), // Supports string interpolation
|
||||
format.timestamp(), // Adds timestamp to logs
|
||||
format.printf((info) => {
|
||||
const { timestamp, level, message } = info as {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message?: unknown;
|
||||
};
|
||||
return `${timestamp ? new Date(timestamp).toLocaleString() : ""} ${level}: ${message}`;
|
||||
})
|
||||
),
|
||||
transports: [
|
||||
// Log to console
|
||||
new winston.transports.Console({
|
||||
level: config.env === "development" ? "debug" : "info",
|
||||
stderrLevels: ["error"], // Log errors to stderr
|
||||
format: format.combine(
|
||||
format.colorize(), // Adds color for console output
|
||||
format.printf((info) => `${info.level}: ${info.message}`)
|
||||
),
|
||||
}),
|
||||
|
||||
// Optional: Log to a file in production
|
||||
...(config.env === "production"
|
||||
? [
|
||||
new winston.transports.File({
|
||||
filename: "logs/error.log",
|
||||
level: "error",
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: "logs/combined.log",
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
export default logger;
|
||||
28
src/config/morgan.ts
Normal file
28
src/config/morgan.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Request, Response } from "express";
|
||||
import requestId from "request-ip";
|
||||
import morgan from "morgan";
|
||||
import config from "./config";
|
||||
import logger from "./logger";
|
||||
|
||||
morgan.token('clientId', (req: Request) => requestId.getClientIp(req) || "");
|
||||
morgan.token('message', (req: Request, res: Response) => res.locals.errorMessage || "");
|
||||
|
||||
// Formats based on environment
|
||||
const ipFormat = config.env === 'production' ? ':clientIp - ' : '';
|
||||
const successFormat = `${ipFormat}:method :url :status - :response-time ms`;
|
||||
const errorFormat = `${ipFormat}:method :url :status - :response-time ms - message: :message`;
|
||||
|
||||
// Handlers
|
||||
const successHandler = morgan(successFormat, {
|
||||
skip: (req: Request, res: Response) => res.statusCode >= 400,
|
||||
stream: { write: (msg) => logger.info(msg.trim()) },
|
||||
});
|
||||
|
||||
const errorHandler = morgan(errorFormat, {
|
||||
skip: (req: Request, res: Response) => res.statusCode < 400,
|
||||
stream: { write: (msg) => logger.error(msg.trim()) },
|
||||
});
|
||||
|
||||
export default {
|
||||
successHandler, errorHandler,
|
||||
}
|
||||
33
src/controllers/auth/login.controller.ts
Normal file
33
src/controllers/auth/login.controller.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AsyncHandler } from "../../utils";
|
||||
import { IIamPrincipalInteractor, ITokenInteractor } from "../../interfaces/interactor";
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import { compareSync } from 'bcrypt';
|
||||
import ApiResponse from '../../utils/helper/ApiResponse';
|
||||
|
||||
export class LoginController {
|
||||
private tokenInteractor: ITokenInteractor;
|
||||
private iamPrincipalInteractor: IIamPrincipalInteractor;
|
||||
|
||||
constructor(tokenInteractor: ITokenInteractor, iamPrincipalInteractor: IIamPrincipalInteractor) {
|
||||
this.tokenInteractor = tokenInteractor;
|
||||
this.iamPrincipalInteractor = iamPrincipalInteractor;
|
||||
}
|
||||
|
||||
@AsyncHandler()
|
||||
async login(req: Request, res: Response) {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const principal = await this.iamPrincipalInteractor.getByEmailPrincipal(email);
|
||||
if (!principal) { throw new ApiError(400, 'Invalid email or password'); }
|
||||
|
||||
const isValidPassword = compareSync(password, principal.password_hash);
|
||||
if (!isValidPassword) { throw new ApiError(400, 'Invalid email or password'); }
|
||||
|
||||
await this.iamPrincipalInteractor.updateLastLogin(principal.id);
|
||||
|
||||
const tokens = await this.tokenInteractor.generateAuthToken(principal.id);
|
||||
|
||||
res.status(200).json(new ApiResponse(200, tokens, 'Login success',));
|
||||
}
|
||||
}
|
||||
20
src/controllers/auth/registration.controller.ts
Normal file
20
src/controllers/auth/registration.controller.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { genSaltSync, hashSync } from 'bcrypt';
|
||||
import { Request, Response } from 'express';
|
||||
import { AsyncHandler } from "../../utils";
|
||||
import { IIamPrincipalInteractor } from "../../interfaces/interactor";
|
||||
import ApiResponse from '../../utils/helper/ApiResponse';
|
||||
|
||||
export class RegistrationController {
|
||||
private readonly interactor: IIamPrincipalInteractor;
|
||||
|
||||
constructor(interactor: IIamPrincipalInteractor) {
|
||||
this.interactor = interactor;
|
||||
}
|
||||
|
||||
@AsyncHandler()
|
||||
async registerEmail(req: Request, res: Response) {
|
||||
const salt = genSaltSync(10);
|
||||
await this.interactor.createPrincipal({ password_hash: hashSync(req.body.password, salt), ...req.body });
|
||||
res.status(200).json(new ApiResponse(200, {}, 'Registration success',));
|
||||
}
|
||||
}
|
||||
3
src/controllers/index.ts
Normal file
3
src/controllers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './product/productController';
|
||||
export * from './auth/login.controller';
|
||||
export * from './auth/registration.controller';
|
||||
53
src/controllers/product/productController.ts
Normal file
53
src/controllers/product/productController.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { IProductInteractor } from '../../interfaces/interactor/product/IProductInteractor';
|
||||
import ApiResponse from '../../utils/helper/ApiResponse';
|
||||
import { AsyncHandler } from '../../utils/handler/async.handler';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { INTERFACE_TYPE } from '../../utils';
|
||||
@injectable()
|
||||
export class ProductController {
|
||||
private interactor: IProductInteractor;
|
||||
|
||||
constructor(
|
||||
@inject(INTERFACE_TYPE.ProductInteractor) interactor: IProductInteractor
|
||||
) {
|
||||
this.interactor = interactor;
|
||||
}
|
||||
|
||||
@AsyncHandler()
|
||||
async onCreateProduct(req: Request, res: Response) {
|
||||
const data = await this.interactor.createProduct(req.body);
|
||||
res.status(201).json(new ApiResponse(201, data, 'Successfully created'));
|
||||
}
|
||||
|
||||
@AsyncHandler()
|
||||
async onGetProducts(req: Request, res: Response) {
|
||||
const offset = Number.isInteger(Number(req.query.offset)) ? parseInt(`${req.query.offset}`, 10) : 0;
|
||||
const limit = Number.isInteger(Number(req.query.limit)) ? parseInt(`(${req.query.limit}`, 10) : 10;
|
||||
|
||||
const data = await this.interactor.getProducts(limit, offset);
|
||||
|
||||
if (data.length === 0) {
|
||||
res.status(200).json(new ApiResponse(200, null, 'No products found'));
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(new ApiResponse(200, data, 'Products retrieved successfully'));
|
||||
}
|
||||
|
||||
@AsyncHandler()
|
||||
async onGetByIDProducts(req: Request, res: Response) {
|
||||
const data = await this.interactor.getByIdProduct(Number(req.params.id));
|
||||
if (!data) {
|
||||
res.status(200).json(new ApiResponse(200, null, 'No products found'));
|
||||
return;
|
||||
}
|
||||
res.status(200).json(new ApiResponse(200, data, 'Products retrieved successfully'));
|
||||
}
|
||||
|
||||
@AsyncHandler()
|
||||
async onUpdateStock(req: Request, res: Response) {
|
||||
const data = await this.interactor.updateStock(parseInt(`${req.params.id}`, 10), parseInt(`${req.body.stock}`, 10));
|
||||
res.status(200).json(new ApiResponse(200, data, 'Successfully updated'));
|
||||
}
|
||||
}
|
||||
86
src/entities/IAM/iamPrincipal.entity.ts
Normal file
86
src/entities/IAM/iamPrincipal.entity.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { IamSource } from './../master/iamSource.entity';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
import { IamType } from "../master/iamType.entity";
|
||||
|
||||
@Entity({ name: "iam_principals", })
|
||||
export class IamPrincipal extends BaseEntity {
|
||||
|
||||
@Column({ nullable: true, length: 100 })
|
||||
username!: string
|
||||
|
||||
@Unique(["emailAddress"])
|
||||
@Column({ length: 100 })
|
||||
emailAddress!: string
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
password_hash!: string
|
||||
|
||||
@Column({ nullable: true, length: 100 })
|
||||
firstName!: string
|
||||
|
||||
@Column({ nullable: true, length: 100 })
|
||||
lastName!: string
|
||||
|
||||
@Unique(["phoneNumber"])
|
||||
@Column({ nullable: true, length: 15 })
|
||||
phoneNumber!: string
|
||||
|
||||
@Column({ nullable: true, length: 255 })
|
||||
profileImage!: string
|
||||
|
||||
@Column({ nullable: true, length: 10 })
|
||||
gender!: string
|
||||
|
||||
@Column({ nullable: true, type: "timestamp" })
|
||||
dateOfBirth!: Date
|
||||
|
||||
@Column({ nullable: true, length: 255 })
|
||||
address!: string
|
||||
|
||||
@Column({ nullable: true, length: 100 })
|
||||
city!: string
|
||||
|
||||
@Column({ nullable: true, length: 100 })
|
||||
state!: string
|
||||
|
||||
@Column({ nullable: true, length: 100 })
|
||||
country!: string
|
||||
|
||||
@Column({ nullable: true, length: 20 })
|
||||
postalCode!: string
|
||||
|
||||
@Column({ default: () => "CURRENT_TIMESTAMP", type: "timestamp" })
|
||||
lastLogin!: Date;
|
||||
|
||||
|
||||
// Flags
|
||||
|
||||
@Column({ nullable: true })
|
||||
isEmailVerified!: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
isPhoneVerified!: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
isProfileComplete!: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
isBlocked!: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
isSuspended!: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
isDeleted!: boolean
|
||||
|
||||
// Foreign Keys
|
||||
@ManyToOne(() => IamType, { nullable: true })
|
||||
@JoinColumn({ name: "principalType_xid" })
|
||||
principalType!: IamType
|
||||
|
||||
@ManyToOne(() => IamSource, { nullable: true })
|
||||
@JoinColumn({ name: "principalSource_xid" })
|
||||
principalSource!: IamSource
|
||||
|
||||
}
|
||||
24
src/entities/abstract/base.entity.ts
Normal file
24
src/entities/abstract/base.entity.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Column, CreateDateColumn, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||
import { IamPrincipal } from "../IAM/iamPrincipal.entity";
|
||||
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryGeneratedColumn({ unsigned: true })
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => IamPrincipal, creator => creator.id, { nullable: true })
|
||||
@JoinColumn({ name: 'created_by' })
|
||||
createdBy!: IamPrincipal;
|
||||
|
||||
@ManyToOne(() => IamPrincipal, modifier => modifier.id, { nullable: true })
|
||||
@JoinColumn({ name: 'modified_by' })
|
||||
modifiedBy!: IamPrincipal;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
9
src/entities/index.ts
Normal file
9
src/entities/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './IAM/iamPrincipal.entity';
|
||||
export * from './master/appAction.entity';
|
||||
export * from './master/appAction.entity';
|
||||
export * from './master/appResource.entity';
|
||||
export * from './master/iamGroup.entity';
|
||||
export * from './master/iamRole.entity';
|
||||
export * from './master/iamSource.entity';
|
||||
export * from './master/iamType.entity';
|
||||
export * from './main/token.entity';
|
||||
20
src/entities/main/Product.ts
Normal file
20
src/entities/main/Product.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class Product {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
name!: string
|
||||
|
||||
@Column()
|
||||
description!: string
|
||||
|
||||
@Column()
|
||||
price!: number
|
||||
|
||||
@Column()
|
||||
stock!: number
|
||||
}
|
||||
18
src/entities/main/User.ts
Normal file
18
src/entities/main/User.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Column()
|
||||
firstName!: string
|
||||
|
||||
@Column()
|
||||
lastName!: string
|
||||
|
||||
@Column()
|
||||
age!: number
|
||||
|
||||
}
|
||||
22
src/entities/main/token.entity.ts
Normal file
22
src/entities/main/token.entity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
import { IamPrincipal } from "../IAM/iamPrincipal.entity";
|
||||
|
||||
@Entity({ name: "tokens" })
|
||||
export class Token extends BaseEntity {
|
||||
@Column()
|
||||
token!: string;
|
||||
|
||||
@Column({ type: "timestamp" })
|
||||
expiresAt!: Date;
|
||||
|
||||
@Column()
|
||||
type!: string;
|
||||
|
||||
@Column({ default: false })
|
||||
isBlacklisted!: boolean;
|
||||
|
||||
@ManyToOne(() => IamPrincipal, principal => principal.id, { nullable: true })
|
||||
@JoinColumn({ name: 'principal_xid' })
|
||||
iamPrincipal!: IamPrincipal;
|
||||
}
|
||||
8
src/entities/master/appAction.entity.ts
Normal file
8
src/entities/master/appAction.entity.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
|
||||
@Entity({ name: "app_actions" })
|
||||
export class appAction extends BaseEntity {
|
||||
@Column({ length: 50 })
|
||||
action!: string
|
||||
}
|
||||
8
src/entities/master/appResource.entity.ts
Normal file
8
src/entities/master/appResource.entity.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
|
||||
@Entity({ name: "app_resources" })
|
||||
export class appResource extends BaseEntity{
|
||||
@Column({ length: 50 })
|
||||
resource!: string
|
||||
}
|
||||
8
src/entities/master/iamGroup.entity.ts
Normal file
8
src/entities/master/iamGroup.entity.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
|
||||
@Entity({ name: "iam_groups" })
|
||||
export class IamGroup extends BaseEntity {
|
||||
@Column({ length: 50 })
|
||||
group!: string
|
||||
}
|
||||
8
src/entities/master/iamRole.entity.ts
Normal file
8
src/entities/master/iamRole.entity.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
|
||||
@Entity({ name: "iam_roles" })
|
||||
export class IamRole extends BaseEntity {
|
||||
@Column({ length: 50 })
|
||||
role!: string
|
||||
}
|
||||
9
src/entities/master/iamSource.entity.ts
Normal file
9
src/entities/master/iamSource.entity.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
|
||||
@Entity({ name: "iam_sources" })
|
||||
export class IamSource extends BaseEntity {
|
||||
@Column({ length: 50 })
|
||||
source!: string
|
||||
|
||||
}
|
||||
9
src/entities/master/iamType.entity.ts
Normal file
9
src/entities/master/iamType.entity.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseEntity } from "../abstract/base.entity";
|
||||
|
||||
@Entity({ name: "iam_types" })
|
||||
export class IamType extends BaseEntity {
|
||||
@Column({ length: 50 })
|
||||
type!: string
|
||||
|
||||
}
|
||||
23
src/external-libraries/mailer.ts
Normal file
23
src/external-libraries/mailer.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { injectable } from "inversify";
|
||||
import logger from "../config/logger";
|
||||
import { IMailer } from "../interfaces/external-libraries/IMailer";
|
||||
|
||||
@injectable()
|
||||
export class Mailer implements IMailer {
|
||||
async sendWelcomeMail(to: string, subject: string, body: unknown): Promise<boolean> {
|
||||
try {
|
||||
logger.debug("Sending welcome mail...", { to, subject, body });
|
||||
// Here you would integrate with your email service provider
|
||||
// For example, using nodemailer or any other email service
|
||||
// await emailService.send({ to, subject, body });
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Failed to send welcome mail", { error });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async SendMail(to: string, subject: string, body: unknown): Promise<boolean> {
|
||||
logger.debug("Sending mail...", { to, subject, body });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
12
src/external-libraries/messageBroker.ts
Normal file
12
src/external-libraries/messageBroker.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { injectable } from "inversify";
|
||||
import logger from "../config/logger";
|
||||
import { IMessageBroker } from "../interfaces/external-libraries/IMessageBroker";
|
||||
|
||||
@injectable()
|
||||
export class MessageBroker implements IMessageBroker {
|
||||
async NotifyToPromotionService(product: unknown): Promise<boolean> {
|
||||
logger.debug("Message send ...", product);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
45
src/index.ts
Normal file
45
src/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import "reflect-metadata"
|
||||
import app from "./app";
|
||||
import config from "./config/config";
|
||||
import { AppDataSource } from "./config/data-source";
|
||||
import logger from "./config/logger";
|
||||
|
||||
let server: ReturnType<typeof app.listen>;
|
||||
|
||||
AppDataSource.initialize()
|
||||
.then(() => {
|
||||
server = app.listen(config.port)
|
||||
logger.info("Data Source has been initialized!")
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.log(err);
|
||||
|
||||
logger.error("Error during Data Source initialization", err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
const exitHandler = () => {
|
||||
if (server) {
|
||||
server.close(() => {
|
||||
logger.info('server closed')
|
||||
process.exit(1)
|
||||
})
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const unexpectedErrorHandler = (error: Error) => {
|
||||
logger.error("Un-Expected Error: ", error)
|
||||
exitHandler()
|
||||
}
|
||||
|
||||
process.on('uncaughtException', unexpectedErrorHandler)
|
||||
process.on('unhandledRejection', unexpectedErrorHandler)
|
||||
|
||||
process.on('SIGALRM', () => {
|
||||
logger.info('SIGALRM signal received')
|
||||
if (server) {
|
||||
server.close()
|
||||
}
|
||||
})
|
||||
51
src/interactors/IAM/iamPrincipalInteractor.ts
Normal file
51
src/interactors/IAM/iamPrincipalInteractor.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { IamPrincipal } from "../../entities";
|
||||
import { IIamPrincipalInteractor } from "../../interfaces/interactor";
|
||||
import { IIamPrincipalRepository } from '../../interfaces/repository';
|
||||
|
||||
export class IamPrincipalInteractor implements IIamPrincipalInteractor {
|
||||
private repository: IIamPrincipalRepository;
|
||||
|
||||
constructor(repository: IIamPrincipalRepository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
async updateLastLogin(id: number): Promise<IamPrincipal> {
|
||||
const principal = await this.repository.findById(id);
|
||||
if (!principal) {
|
||||
throw new Error(`Principal with id ${id} not found`);
|
||||
}
|
||||
principal.lastLogin = new Date();
|
||||
const updatedPrincipal = await this.repository.update(id, principal);
|
||||
return updatedPrincipal;
|
||||
}
|
||||
async checkPrincipalIsExist(email: string): Promise<boolean> {
|
||||
const principal = await this.repository.findByEmail(email);
|
||||
return principal !== null;
|
||||
}
|
||||
|
||||
async createPrincipal(data: Partial<IamPrincipal>): Promise<IamPrincipal> {
|
||||
const newPrincipal = await this.repository.create(data);
|
||||
return newPrincipal;
|
||||
}
|
||||
|
||||
async updatePrincipal(id: number, data: Partial<IamPrincipal>): Promise<IamPrincipal> {
|
||||
const updatedPrincipal = await this.repository.update(id, data);
|
||||
return updatedPrincipal;
|
||||
}
|
||||
|
||||
async getPrincipals(limit: number, offset: number): Promise<IamPrincipal[]> {
|
||||
const principals = await this.repository.findAll(limit, offset);
|
||||
return principals;
|
||||
}
|
||||
|
||||
async getByIdPrincipal(id: number): Promise<IamPrincipal> {
|
||||
const principal = await this.repository.findById(id);
|
||||
return principal;
|
||||
}
|
||||
|
||||
async getByEmailPrincipal(email: string): Promise<IamPrincipal> {
|
||||
const principal = await this.repository.findByEmail(email);
|
||||
return principal;
|
||||
}
|
||||
|
||||
}
|
||||
3
src/interactors/index.ts
Normal file
3
src/interactors/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './product/productInteractor';
|
||||
export * from './IAM/iamPrincipalInteractor';
|
||||
export * from './token/token.interactor';
|
||||
45
src/interactors/product/productInteractor.ts
Normal file
45
src/interactors/product/productInteractor.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IProductRepository } from './../../interfaces/repository';
|
||||
import { IProductInteractor } from './../../interfaces/interactor';
|
||||
import { IMailer } from "../../interfaces/external-libraries/IMailer";
|
||||
import { Product } from "../../entities/main/Product";
|
||||
import { IMessageBroker } from "../../interfaces/external-libraries/IMessageBroker";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { INTERFACE_TYPE } from "../../utils";
|
||||
|
||||
@injectable()
|
||||
export class ProductInteractor implements IProductInteractor {
|
||||
|
||||
private repository: IProductRepository
|
||||
private mailer: IMailer
|
||||
private broker: IMessageBroker
|
||||
|
||||
constructor(
|
||||
@inject(INTERFACE_TYPE.ProductRepository) repository: IProductRepository,
|
||||
@inject(INTERFACE_TYPE.Mailer) mailer: IMailer,
|
||||
@inject(INTERFACE_TYPE.MessageBroker) broker: IMessageBroker
|
||||
) {
|
||||
this.repository = repository
|
||||
this.mailer = mailer
|
||||
this.broker = broker
|
||||
}
|
||||
|
||||
async createProduct(input: never): Promise<Product> {
|
||||
const data = await this.repository.create(input)
|
||||
// Do something notify promotion message
|
||||
await this.broker.NotifyToPromotionService(data)
|
||||
return data
|
||||
}
|
||||
async updateStock(id: number, stock: number): Promise<Product> {
|
||||
const data = await this.repository.update(id, { stock: stock })
|
||||
// Do some update Admin update a stock
|
||||
await this.mailer.SendMail("someone@gmail.com", "Update Stock", data)
|
||||
return data
|
||||
}
|
||||
async getProducts(limit: number, offset: number): Promise<Product[]> {
|
||||
return await this.repository.find(limit, offset)
|
||||
}
|
||||
async getByIdProduct(id: number): Promise<Product> {
|
||||
return await this.repository.findById(id)
|
||||
}
|
||||
|
||||
}
|
||||
58
src/interactors/token/token.interactor.ts
Normal file
58
src/interactors/token/token.interactor.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { ITokenRepository } from "../../interfaces/repository";
|
||||
import { ITokenInteractor } from "../../interfaces/interactor";
|
||||
import { sign } from "jsonwebtoken";
|
||||
import moment from 'moment';
|
||||
import config from "../../config/config";
|
||||
|
||||
@injectable()
|
||||
export class TokenInteractor implements ITokenInteractor {
|
||||
|
||||
private repository: ITokenRepository
|
||||
|
||||
constructor(repository: ITokenRepository) {
|
||||
this.repository = repository
|
||||
}
|
||||
|
||||
generateToken(principal_xid: number, expiresIn: Date, type: string, secret: string): { token: string; expires: Date; } {
|
||||
const token = sign(
|
||||
{ sub: principal_xid, iat: moment().unix(), exp: moment(expiresIn).unix(), type },
|
||||
// { issuer: 'your-issuer', }, if we need to add issuer
|
||||
secret,
|
||||
);
|
||||
return { token, expires: expiresIn };
|
||||
}
|
||||
|
||||
async generateAuthToken(principal_xid: number): Promise<{ access: { token: string; expires: Date; }; refresh: { token: string; expires: Date; }; }> {
|
||||
const accessTokenExpires = new Date();
|
||||
accessTokenExpires.setHours(accessTokenExpires.getMinutes() + config.jwt.accessExpirationMinutes); // Access token expires in 1 hour
|
||||
|
||||
const refreshTokenExpires = new Date();
|
||||
refreshTokenExpires.setDate(refreshTokenExpires.getDate() + 7); // Refresh token expires in 7 days
|
||||
|
||||
const accessToken = this.generateToken(principal_xid, accessTokenExpires, 'access', config.jwt.secret);
|
||||
const refreshToken = this.generateToken(principal_xid, refreshTokenExpires, 'refresh', config.jwt.secret);
|
||||
|
||||
await this.repository.saveToken(refreshToken.token, principal_xid, accessToken.expires, 'refresh', false);
|
||||
|
||||
return {
|
||||
access: accessToken,
|
||||
refresh: refreshToken
|
||||
};
|
||||
}
|
||||
|
||||
async revokeToken(token: string): Promise<boolean> {
|
||||
await this.repository.makeBackListToken(token);
|
||||
return true;
|
||||
}
|
||||
|
||||
async isTokenBlackListed(token: string): Promise<boolean> {
|
||||
return this.repository.findToken(token).then((data) => {
|
||||
if (data) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
4
src/interfaces/external-libraries/IMailer.ts
Normal file
4
src/interfaces/external-libraries/IMailer.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IMailer {
|
||||
SendMail(to: string, subject: string, body: unknown): Promise<boolean>;
|
||||
sendWelcomeMail(to: string, subject: string, body: unknown): Promise<boolean>;
|
||||
}
|
||||
3
src/interfaces/external-libraries/IMessageBroker.ts
Normal file
3
src/interfaces/external-libraries/IMessageBroker.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IMessageBroker {
|
||||
NotifyToPromotionService(product: unknown): Promise<boolean>;
|
||||
}
|
||||
2
src/interfaces/external-libraries/index.ts
Normal file
2
src/interfaces/external-libraries/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './IMailer';
|
||||
export * from './IMessageBroker';
|
||||
11
src/interfaces/interactor/IAM/IIamPrincipalInteractor.ts
Normal file
11
src/interfaces/interactor/IAM/IIamPrincipalInteractor.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IamPrincipal } from "./../../../entities";
|
||||
|
||||
export interface IIamPrincipalInteractor {
|
||||
checkPrincipalIsExist(email: string): Promise<boolean>;
|
||||
createPrincipal(data: Partial<IamPrincipal>): Promise<IamPrincipal>;
|
||||
updatePrincipal(id: number, data: Partial<IamPrincipal>): Promise<IamPrincipal>;
|
||||
getPrincipals(limit: number, offset: number): Promise<IamPrincipal[]>;
|
||||
getByIdPrincipal(id: number): Promise<IamPrincipal>;
|
||||
getByEmailPrincipal(email: string): Promise<IamPrincipal>;
|
||||
updateLastLogin(id: number): Promise<IamPrincipal>;
|
||||
}
|
||||
3
src/interfaces/interactor/index.ts
Normal file
3
src/interfaces/interactor/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './IAM/IIamPrincipalInteractor';
|
||||
export * from './product/IProductInteractor';
|
||||
export * from './token/ITokenInteractor';
|
||||
8
src/interfaces/interactor/product/IProductInteractor.ts
Normal file
8
src/interfaces/interactor/product/IProductInteractor.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Product } from "../../../entities/main/Product";
|
||||
|
||||
export interface IProductInteractor {
|
||||
createProduct(input: Partial<Product>): Promise<Product>;
|
||||
updateStock(id: number, stock: number): Promise<Product>;
|
||||
getProducts(limit: number, offset: number): Promise<Product[]>;
|
||||
getByIdProduct(id: number): Promise<Product>;
|
||||
}
|
||||
6
src/interfaces/interactor/token/ITokenInteractor.ts
Normal file
6
src/interfaces/interactor/token/ITokenInteractor.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ITokenInteractor {
|
||||
generateAuthToken(principal_xid: number): Promise<{ access: { token: string, expires: Date }, refresh: { token: string, expires: Date } }>;
|
||||
revokeToken(token: string): Promise<boolean>;
|
||||
isTokenBlackListed(token: string): Promise<boolean>;
|
||||
generateToken(principal_xid: number, expiresIn: Date, type: string, secret: string): { token: string, expires: Date };
|
||||
}
|
||||
9
src/interfaces/repository/IAM/IIamPrincipalRepository.ts
Normal file
9
src/interfaces/repository/IAM/IIamPrincipalRepository.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IamPrincipal } from './../../../entities/IAM/iamPrincipal.entity';
|
||||
|
||||
export interface IIamPrincipalRepository {
|
||||
create(data: Partial<IamPrincipal>): Promise<IamPrincipal>;
|
||||
update(id: number, data: Partial<IamPrincipal>): Promise<IamPrincipal>;
|
||||
findAll(limit: number, offset: number): Promise<IamPrincipal[]>;
|
||||
findById(id: number): Promise<IamPrincipal>;
|
||||
findByEmail(email: string): Promise<IamPrincipal>;
|
||||
}
|
||||
3
src/interfaces/repository/index.ts
Normal file
3
src/interfaces/repository/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './IAM/IIamPrincipalRepository';
|
||||
export * from './product/IProductRepository';
|
||||
export * from './token/ITokenRepository';
|
||||
8
src/interfaces/repository/product/IProductRepository.ts
Normal file
8
src/interfaces/repository/product/IProductRepository.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Product } from "../entities/Product";
|
||||
|
||||
export interface IProductRepository {
|
||||
create(data: Product): Promise<Product>;
|
||||
update(id: number, data: Partial<Product>): Promise<Product>;
|
||||
find(limit: number, offset: number): Promise<Product[]>;
|
||||
findById(id: number): Promise<Product>;
|
||||
}
|
||||
7
src/interfaces/repository/token/ITokenRepository.ts
Normal file
7
src/interfaces/repository/token/ITokenRepository.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Token } from "../../../entities";
|
||||
|
||||
export interface ITokenRepository {
|
||||
saveToken(token: string, principal_xid: number, expiresAt: Date, type: string, isBlacklisted: boolean): Promise<Token>;
|
||||
findToken(token: string): Promise<Token | null>;
|
||||
makeBackListToken(token: string): Promise<Token | null>;
|
||||
}
|
||||
76
src/middleware/error.ts
Normal file
76
src/middleware/error.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import ApiError from '../utils/helper/ApiError';
|
||||
import config from '../config/config';
|
||||
import logger from '../config/logger';
|
||||
import ApiResponse from '../utils/helper/ApiResponse';
|
||||
|
||||
class error {
|
||||
static errorConverter(
|
||||
err: ApiError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
// Define a broader type for error
|
||||
let error: ApiError | Error & { statusCode?: number; errors?: Error[] } = err;
|
||||
|
||||
// Handle Sequelize validation and unique constraint errors
|
||||
if (error.errors && Array.isArray(error.errors)) {
|
||||
const messages = error.errors.map((e: Error) => e.message);
|
||||
error = new ApiError(
|
||||
400,
|
||||
messages.join(', '),
|
||||
error.errors,
|
||||
true,
|
||||
err.stack
|
||||
);
|
||||
}
|
||||
|
||||
if (!(error instanceof ApiError)) {
|
||||
// Handle other errors
|
||||
const statusCode = error.statusCode || 500;
|
||||
const message = error.message || "Something went wrong";
|
||||
error = new ApiError(statusCode, message, [], false, err.stack);
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
|
||||
static errorHandler(
|
||||
err: ApiError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
_next: NextFunction // Retain this even if unused
|
||||
): void {
|
||||
// Extract error details with fallback defaults
|
||||
let { statusCode = 500, message = "Internal server error" } = err;
|
||||
|
||||
// Production: Ensure only operational errors reveal their messages
|
||||
if (config.env === "production" && !err.isOperational) {
|
||||
statusCode = 500;
|
||||
message = "An unexpected error occurred";
|
||||
}
|
||||
|
||||
// Attach error message to response locals for potential templating
|
||||
res.locals.errorMessage = err.message;
|
||||
|
||||
// Log the error in all environments
|
||||
logger.error({
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
statusCode,
|
||||
errors: err.errors || [],
|
||||
});
|
||||
|
||||
// Send the response
|
||||
res.status(statusCode).json(new ApiResponse(statusCode, null, message, {
|
||||
stack: config.env === "development" && err.stack ? err.stack : undefined,
|
||||
errors: config.env === "development" && err.errors ? err.errors.map((e: Error) => e.message) : [],
|
||||
}));
|
||||
|
||||
// Do not call `next()` here, as this is the final error handler.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default error;
|
||||
77
src/middleware/uploader.ts
Normal file
77
src/middleware/uploader.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import multer, { StorageEngine } from 'multer';
|
||||
import ApiError from '../utils/helper/ApiError';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import path from 'path';
|
||||
const maxFileSize = 10 * 1024 * 1024; // 10 MB (in bytes)
|
||||
const allowedFileTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'video/mp4',
|
||||
'video/x-msvideo',
|
||||
'video/x-matroska',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
];
|
||||
|
||||
const createFolder = (folderPath: string): void => {
|
||||
if (!existsSync(folderPath)) {
|
||||
try {
|
||||
mkdirSync(folderPath, { recursive: true });
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to create folder at ${folderPath}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const storage = (folderName: string = 'asset'): StorageEngine => multer.diskStorage({
|
||||
destination: (req, file, cb: (error: Error | null, destination: string) => void): void => {
|
||||
try {
|
||||
const basePath = path.resolve('public');
|
||||
createFolder(basePath);
|
||||
const targetPath = path.join(basePath, folderName);
|
||||
createFolder(targetPath);
|
||||
cb(null, targetPath);
|
||||
} catch (err) {
|
||||
cb(err as Error, '');
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
|
||||
const ext = path.extname(file.originalname);
|
||||
const baseName = path.basename(file.originalname, ext);
|
||||
cb(null, `${baseName}-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (allowedTypes: string[]) => (req: Express.Request, file: Express.Multer.File, cb: multer.FileFilterCallback): void => {
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
const error = new ApiError(400, `File type ${file.mimetype} is not allowed`);
|
||||
cb(error as unknown as null, false);
|
||||
}
|
||||
};
|
||||
|
||||
interface UploaderOptions {
|
||||
fileSize?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const uploader = (folderName: string = 'uploads', allowedTypes: string[] = allowedFileTypes, options: UploaderOptions = {}): multer.Multer => {
|
||||
const {
|
||||
fileSize = maxFileSize,
|
||||
...otherOptions
|
||||
} = options;
|
||||
|
||||
return multer({
|
||||
storage: storage(folderName),
|
||||
limits: { fileSize },
|
||||
fileFilter: fileFilter(allowedTypes),
|
||||
...otherOptions,
|
||||
});
|
||||
};
|
||||
46
src/middleware/validate.ts
Normal file
46
src/middleware/validate.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ObjectSchema, ValidationError } from 'yup';
|
||||
import ApiError from '../utils/helper/ApiError';
|
||||
import { pick } from '../utils/handler/pick.handler';
|
||||
|
||||
/**
|
||||
* Validation middleware for Express routes.
|
||||
* @param schema - Validation schema object (Yup schema for request parts like params, query, body, etc.).
|
||||
* @returns Middleware function to validate request properties.
|
||||
*/
|
||||
const validate =
|
||||
(schema: Partial<Record<keyof Request, ObjectSchema<never>>>) =>
|
||||
(req: Request, res: Response, next: NextFunction): void => {
|
||||
// Define valid request keys explicitly
|
||||
const validRequestKeys = ['params', 'query', 'body', 'file', 'files'] as (keyof Request)[];
|
||||
|
||||
// Extract the schemas for valid parts of the request
|
||||
const validSchema = pick(schema, validRequestKeys);
|
||||
|
||||
// Extract the corresponding request properties
|
||||
const object = pick(req, Object.keys(validSchema) as (keyof Request)[]);
|
||||
|
||||
// Validate each part of the request
|
||||
const promises = Object.keys(validSchema).map((key) =>
|
||||
validSchema[key as keyof Request]?.validate(object[key as keyof Request], { abortEarly: false })
|
||||
);
|
||||
|
||||
// Process validation results
|
||||
Promise.all(promises)
|
||||
.then((validatedValues) => {
|
||||
// Assign validated values back to the request object
|
||||
validatedValues.forEach((value, index) => {
|
||||
const key = Object.keys(validSchema)[index];
|
||||
// Safely assign the validated value to the request object
|
||||
req[key as keyof Request] = value; // Use `Request` here instead of `Request.ResBody`
|
||||
});
|
||||
next();
|
||||
})
|
||||
.catch((err: ValidationError) => {
|
||||
// Collect and format error messages
|
||||
const errorMessage = err.inner.map((detail) => detail.message).join(', ');
|
||||
next(new ApiError(400, errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export default validate;
|
||||
41
src/repositories/IAM/iamPrincipal.repository.ts
Normal file
41
src/repositories/IAM/iamPrincipal.repository.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Repository } from "typeorm";
|
||||
import { IamPrincipal } from "../../entities";
|
||||
import { IIamPrincipalRepository } from "../../interfaces/repository"
|
||||
import { AppDataSource } from "../../config/data-source";
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
|
||||
@injectable()
|
||||
export class IamPrincipalRepository implements IIamPrincipalRepository {
|
||||
private readonly iamPrincipalRepository: Repository<IamPrincipal>;
|
||||
|
||||
constructor() {
|
||||
this.iamPrincipalRepository = AppDataSource.getRepository(IamPrincipal);
|
||||
}
|
||||
|
||||
async create(data: Partial<IamPrincipal>): Promise<IamPrincipal> {
|
||||
const principal = this.iamPrincipalRepository.create(data);
|
||||
return await this.iamPrincipalRepository.save(principal);
|
||||
}
|
||||
async update(id: number, data: Partial<IamPrincipal>): Promise<IamPrincipal> {
|
||||
const existingPrincipal = await this.iamPrincipalRepository.findOne({ where: { id } });
|
||||
if (!existingPrincipal) {
|
||||
throw new ApiError(404, `Principal with ID ${id} not found`);
|
||||
}
|
||||
await this.iamPrincipalRepository.update(id, data);
|
||||
return this.iamPrincipalRepository.findOneOrFail({ where: { id } });
|
||||
}
|
||||
async findAll(limit: number, offset: number): Promise<IamPrincipal[]> {
|
||||
return await this.iamPrincipalRepository.find({
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { id: "ASC" }
|
||||
});
|
||||
}
|
||||
async findById(id: number): Promise<IamPrincipal> {
|
||||
return await this.iamPrincipalRepository.findOneOrFail({ where: { id } });
|
||||
}
|
||||
async findByEmail(emailAddress: string): Promise<IamPrincipal> {
|
||||
return await this.iamPrincipalRepository.findOneOrFail({ where: { emailAddress }, select: ['id', 'emailAddress', 'password_hash'] });
|
||||
}
|
||||
}
|
||||
3
src/repositories/index.ts
Normal file
3
src/repositories/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './IAM/iamPrincipal.repository';
|
||||
export * from './product/product.repository';
|
||||
export * from './token/token.repository';
|
||||
48
src/repositories/product/product.repository.ts
Normal file
48
src/repositories/product/product.repository.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Repository } from "typeorm";
|
||||
import { AppDataSource } from "../../config/data-source";
|
||||
import { Product } from "../../entities/main/Product";
|
||||
import ApiError from "../../utils/helper/ApiError";
|
||||
import { injectable } from "inversify";
|
||||
import { IProductRepository } from "../../interfaces/repository";
|
||||
|
||||
@injectable()
|
||||
export class ProductRepository implements IProductRepository {
|
||||
private readonly productRepository: Repository<Product>;
|
||||
|
||||
constructor() {
|
||||
this.productRepository = AppDataSource.getRepository(Product);
|
||||
}
|
||||
|
||||
// Create a new product
|
||||
async create(data: Product): Promise<Product> {
|
||||
const product = this.productRepository.create(data); // Prepare new product entity
|
||||
return await this.productRepository.save(product); // Save to the database
|
||||
}
|
||||
|
||||
// Update a product
|
||||
async update(id: number, data: Partial<Product>): Promise<Product> {
|
||||
// Check if the product exists before updating
|
||||
const existingProduct = await this.productRepository.findOne({ where: { id } });
|
||||
if (!existingProduct) {
|
||||
throw new ApiError(404, `Product with ID ${id} not found`);
|
||||
}
|
||||
|
||||
// Update the product and return the updated entity
|
||||
await this.productRepository.update(id, data);
|
||||
return this.productRepository.findOneOrFail({ where: { id } });
|
||||
}
|
||||
|
||||
// Find products with pagination
|
||||
async find(limit: number, offset: number): Promise<Product[]> {
|
||||
return await this.productRepository.find({
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { id: "ASC" }, // Optional: Add sorting for consistent results
|
||||
});
|
||||
}
|
||||
|
||||
// Find by ID Product
|
||||
async findById(id: number): Promise<Product> {
|
||||
return await this.productRepository.findOneOrFail({ where: { id } });
|
||||
}
|
||||
}
|
||||
34
src/repositories/token/token.repository.ts
Normal file
34
src/repositories/token/token.repository.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AppDataSource } from "../../config/data-source";
|
||||
import { Token } from "../../entities";
|
||||
import { ITokenRepository } from "../../interfaces/repository";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
export class TokenRepository implements ITokenRepository {
|
||||
private readonly tokenRepository: Repository<Token>;
|
||||
|
||||
constructor() {
|
||||
this.tokenRepository = AppDataSource.getRepository(Token);
|
||||
}
|
||||
|
||||
async saveToken(token: string, principal_xid: number, expiresAt: Date, type: string, isBlacklisted: boolean): Promise<Token> {
|
||||
const newToken = this.tokenRepository.create({
|
||||
token,
|
||||
iamPrincipal: { id: principal_xid }, // Assuming the `iamPrincipal` relation uses `id` as the primary key
|
||||
expiresAt,
|
||||
type,
|
||||
isBlacklisted
|
||||
});
|
||||
return await this.tokenRepository.save(newToken);
|
||||
}
|
||||
|
||||
async findToken(token: string): Promise<Token | null> {
|
||||
return await this.tokenRepository.findOne({ where: { token } });
|
||||
}
|
||||
|
||||
async makeBackListToken(token: string): Promise<Token | null> {
|
||||
const existingToken = await this.findToken(token);
|
||||
if (!existingToken) return null;
|
||||
existingToken.isBlacklisted = true;
|
||||
return await this.tokenRepository.save(existingToken);
|
||||
}
|
||||
}
|
||||
7
src/routes/auth/index.ts
Normal file
7
src/routes/auth/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import express from 'express';
|
||||
const authRouter = express.Router();
|
||||
import userRoutes from './user.routes';
|
||||
|
||||
authRouter.use('/', userRoutes);
|
||||
|
||||
export default authRouter;
|
||||
24
src/routes/auth/user.routes.ts
Normal file
24
src/routes/auth/user.routes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import { IamPrincipalInteractor, TokenInteractor } from '../../interactors';
|
||||
import { IIamPrincipalInteractor, ITokenInteractor } from '../../interfaces/interactor';
|
||||
import { Router } from 'express';
|
||||
import { IIamPrincipalRepository, ITokenRepository } from '../../interfaces/repository';
|
||||
import { IamPrincipalRepository, TokenRepository } from '../../repositories';
|
||||
import { LoginController, RegistrationController } from '../../controllers';
|
||||
|
||||
const userRoutes = Router();
|
||||
|
||||
const tokenRepository: ITokenRepository = new TokenRepository();
|
||||
const principalRepository: IIamPrincipalRepository = new IamPrincipalRepository();
|
||||
const tokenInteractor: ITokenInteractor = new TokenInteractor(tokenRepository);
|
||||
|
||||
const iamPrincipalInteractor: IIamPrincipalInteractor = new IamPrincipalInteractor(principalRepository);
|
||||
|
||||
|
||||
const controller = new LoginController(tokenInteractor, iamPrincipalInteractor);
|
||||
const registrationController = new RegistrationController(iamPrincipalInteractor);
|
||||
|
||||
userRoutes.route('/login-email').post(controller.login.bind(controller));
|
||||
userRoutes.route('/register').post(registrationController.registerEmail.bind(registrationController));
|
||||
|
||||
export default userRoutes;
|
||||
83
src/routes/index.ts
Normal file
83
src/routes/index.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import express, { Router } from 'express';
|
||||
import config from '../config/config';
|
||||
|
||||
// Define a reusable type for route definitions
|
||||
interface RouteDefinition {
|
||||
path: string;
|
||||
route: () => Promise<Router>; // Function returning a Promise of Router
|
||||
}
|
||||
|
||||
class Routes {
|
||||
private router: Router;
|
||||
private defaultRoutes: RouteDefinition[];
|
||||
private devRoutes: RouteDefinition[];
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.defaultRoutes = this.initializeDefaultRoutes();
|
||||
this.devRoutes = this.initializeDevRoutes();
|
||||
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
// Initialize default routes with dynamic imports
|
||||
private initializeDefaultRoutes(): RouteDefinition[] {
|
||||
return [
|
||||
{
|
||||
path: '/products',
|
||||
route: () => import('./productRoutes').then((module) => module.default)
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
route: () => import('./auth').then((module) => module.default)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Initialize development-specific routes with dynamic imports
|
||||
private initializeDevRoutes(): RouteDefinition[] {
|
||||
return [
|
||||
// {
|
||||
// path: '/docs',
|
||||
// route: () => import('./docs/docs.route').then((module) => module.default)
|
||||
// }
|
||||
];
|
||||
}
|
||||
|
||||
// Register all routes
|
||||
private setupRoutes(): void {
|
||||
this.defaultRoutes.forEach(({ path, route }) => {
|
||||
this.registerRoute(path, route);
|
||||
});
|
||||
|
||||
this.setupEnvironmentSpecificRoutes();
|
||||
}
|
||||
|
||||
// Register environment-specific routes
|
||||
private setupEnvironmentSpecificRoutes(): void {
|
||||
if (config.env === 'development') {
|
||||
this.devRoutes.forEach(({ path, route }) => {
|
||||
this.registerRoute(path, route);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register a single route safely with async handling
|
||||
private async registerRoute(path: string, route: () => Promise<Router>): Promise<void> {
|
||||
try {
|
||||
const loadedRoute = await route();
|
||||
this.router.use(path, loadedRoute);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load route at path: ${path}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the router instance
|
||||
public getRouter(): Router {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the initialized router
|
||||
const routes = new Routes();
|
||||
export default routes.getRouter();
|
||||
41
src/routes/productRoutes.ts
Normal file
41
src/routes/productRoutes.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import express from 'express';
|
||||
import { ProductController } from '../controllers/product/productController';
|
||||
import { ProductRepository } from '../repositories/product/product.repository';
|
||||
import { ProductInteractor } from '../interactors/product/productInteractor';
|
||||
import { IProductInteractor } from '../interfaces/interactor/product/IProductInteractor';
|
||||
import { Mailer } from '../external-libraries/mailer';
|
||||
import { MessageBroker } from '../external-libraries/messageBroker';
|
||||
import { Container } from 'inversify';
|
||||
import { INTERFACE_TYPE } from '../utils';
|
||||
import { IMailer } from '../interfaces/external-libraries/IMailer';
|
||||
import { IMessageBroker } from '../interfaces/external-libraries/IMessageBroker';
|
||||
import { IProductRepository } from './../interfaces/repository';
|
||||
|
||||
/**
|
||||
const repository = new ProductRepository()
|
||||
const mailer = new Mailer();
|
||||
const broker = new MessageBroker()
|
||||
const interactor: IProductInteractor = new ProductInteractor(repository, mailer, broker)
|
||||
const productController = new ProductController(interactor);
|
||||
|
||||
*/
|
||||
|
||||
const container = new Container();
|
||||
|
||||
container.bind<IProductRepository>(INTERFACE_TYPE.ProductRepository).to(ProductRepository);
|
||||
container.bind<IProductInteractor>(INTERFACE_TYPE.ProductInteractor).to(ProductInteractor);
|
||||
container.bind<IMailer>(INTERFACE_TYPE.Mailer).to(Mailer);
|
||||
container.bind<IMessageBroker>(INTERFACE_TYPE.MessageBroker).to(MessageBroker);
|
||||
container.bind(INTERFACE_TYPE.ProductController).to(ProductController);
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const productController = container.get<ProductController>(INTERFACE_TYPE.ProductController);
|
||||
|
||||
router.post('/', productController.onCreateProduct.bind(productController));
|
||||
router.get('/', productController.onGetProducts.bind(productController));
|
||||
router.patch('/:id', productController.onUpdateStock.bind(productController));
|
||||
router.get('/:id', productController.onGetByIDProducts.bind(productController));
|
||||
|
||||
export default router;
|
||||
17
src/utils/constant/appConstant.ts
Normal file
17
src/utils/constant/appConstant.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const INTERFACE_TYPE = {
|
||||
ProductRepository: Symbol.for("ProductRepository"),
|
||||
ProductInteractor: Symbol.for("ProductInteractor"),
|
||||
ProductController: Symbol.for("ProductController"),
|
||||
Mailer: Symbol.for("Mailer"),
|
||||
MessageBroker: Symbol.for("MessageBroker"),
|
||||
|
||||
// Add more interface type here
|
||||
IamPrincipalRepository: Symbol.for("IamPrincipalRepository"),
|
||||
IamPrincipalInteractor: Symbol.for("IamPrincipalInteractor"),
|
||||
|
||||
TokenRepository: Symbol.for("TokenRepository"),
|
||||
TokenInteractor: Symbol.for("TokenInteractor"),
|
||||
|
||||
LoginController: Symbol.for("LoginController"),
|
||||
RegistrationController: Symbol.for("RegistrationController"),
|
||||
}
|
||||
15
src/utils/handler/async.handler.ts
Normal file
15
src/utils/handler/async.handler.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export function AsyncHandler() {
|
||||
return function (
|
||||
target: object,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
): void {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function (req: Request, res: Response, next: NextFunction) {
|
||||
Promise.resolve(originalMethod.call(this, req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
}
|
||||
17
src/utils/handler/pick.handler.ts
Normal file
17
src/utils/handler/pick.handler.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Create an object composed of the picked object properties.
|
||||
* @function pick
|
||||
*
|
||||
* @param object - The source object to pick properties from.
|
||||
* @param keys - The array of property names to pick from the source object.
|
||||
* @returns New object with only the picked properties.
|
||||
*/
|
||||
export const pick = <T extends object, K extends keyof T>(object: T, keys: K[]): Pick<T, K> => {
|
||||
return keys.reduce((result, key) => {
|
||||
// Check if the object has the specified property
|
||||
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
||||
result[key] = object[key]; // Assign the property to the result object
|
||||
}
|
||||
return result;
|
||||
}, {} as Pick<T, K>); // Type the accumulator as Pick<T, K>
|
||||
};
|
||||
33
src/utils/helper/ApiError.ts
Normal file
33
src/utils/helper/ApiError.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
class ApiError<T = unknown> extends Error {
|
||||
statusCode: number;
|
||||
data: T | null;
|
||||
message: string;
|
||||
success: boolean;
|
||||
errors: Array<Error>;
|
||||
isOperational: boolean;
|
||||
stack?: string;
|
||||
|
||||
constructor(
|
||||
statusCode: number,
|
||||
message: string = 'Something went wrong',
|
||||
errors: Array<Error> = [],
|
||||
isOperational: boolean = true,
|
||||
stack?: string
|
||||
) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.data = null;
|
||||
this.message = message;
|
||||
this.success = false;
|
||||
this.errors = errors;
|
||||
this.isOperational = isOperational;
|
||||
|
||||
if (stack) {
|
||||
this.stack = stack;
|
||||
} else {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiError;
|
||||
28
src/utils/helper/ApiResponse.ts
Normal file
28
src/utils/helper/ApiResponse.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
class ApiResponse<T> {
|
||||
statusCode: number;
|
||||
data: T | null;
|
||||
message: string;
|
||||
success: boolean;
|
||||
stack?: string; // Made optional
|
||||
errors?: string [] | Error[]; // Made optional
|
||||
|
||||
constructor(
|
||||
statusCode: number,
|
||||
data: T | null,
|
||||
message: string = 'Success',
|
||||
options: { stack?: string; errors?: string [] | Error[] } = {}
|
||||
) {
|
||||
this.statusCode = statusCode;
|
||||
this.data = data;
|
||||
this.message = message;
|
||||
this.success = statusCode < 400;
|
||||
|
||||
// Include `stack` and `errors` only if success is false.
|
||||
if (!this.success) {
|
||||
this.stack = options.stack || undefined;
|
||||
this.errors = options.errors || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiResponse;
|
||||
5
src/utils/index.ts
Normal file
5
src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./handler/async.handler";
|
||||
export * from "./handler/pick.handler";
|
||||
export * from "./helper/ApiError";
|
||||
export * from "./helper/ApiResponse"
|
||||
export * from "./constant/appConstant";
|
||||
48
tsconfig.json
Normal file
48
tsconfig.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript */
|
||||
"experimentalDecorators": true, /* Enable experimental support for legacy decorators */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify the module system to use */
|
||||
"esModuleInterop": true, /* Enable interop with CommonJS modules */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that file names are case-sensitive */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files */
|
||||
|
||||
/* Emit */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript */
|
||||
"outDir": "./dist", /* Specify an output folder for emitted files */
|
||||
"removeComments": true, /* Disable emitting comments in output */
|
||||
|
||||
/* Interop Constraints */
|
||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules without default exports */
|
||||
"resolveJsonModule": true, /* Allow importing JSON files */
|
||||
|
||||
/* Optional Features */
|
||||
"lib": ["es2016", "dom"], /* Specify the set of library files to include */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy (Node.js style) */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true, /* Allow JavaScript files to be part of the project */
|
||||
|
||||
/* Paths and Base URL */
|
||||
"baseUrl": "./", /* Set the base directory to resolve non-relative module names */
|
||||
"paths": { /* Provide paths mapping for module resolution */
|
||||
"*": ["node_modules/*", "src/types/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts" /* Specify the source directory */
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules", /* Exclude node_modules from being compiled */
|
||||
"dist" /* Exclude dist folder to avoid compilation of output files */
|
||||
]
|
||||
}
|
||||
|
||||
3935
yarn-error.log
Normal file
3935
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user