first commit
This commit is contained in:
45
src/app/app.module.ts
Normal file
45
src/app/app.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
// Common imports
|
||||
import { PrismaModule } from '../common/database/prisma.module';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
|
||||
// Feature modules
|
||||
// import { AuthModule } from '../modules/auth/auth.module';
|
||||
// import { HostModule } from '../modules/host/host.module'; // Add more modules as you create them
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Global configuration (env variables)
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
|
||||
// JWT for authentication
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
secret: process.env.JWT_SECRET || 'default_secret',
|
||||
signOptions: { expiresIn: '1d' },
|
||||
}),
|
||||
|
||||
// Database
|
||||
PrismaModule,
|
||||
|
||||
// App modules
|
||||
// AuthModule,
|
||||
// HostModule,
|
||||
],
|
||||
|
||||
providers: [
|
||||
// Global guards
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: RolesGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
10
src/common/database/prisma.module.ts
Normal file
10
src/common/database/prisma.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
|
||||
31
src/common/database/prisma.service.ts
Normal file
31
src/common/database/prisma.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy, INestApplication } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
constructor() {
|
||||
super({
|
||||
datasources: {
|
||||
db: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
},
|
||||
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
|
||||
async enableShutdownHooks(app: INestApplication) {
|
||||
process.on('beforeExit', async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
12
src/common/decorators/current-user.decorator.ts
Normal file
12
src/common/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: keyof User | undefined, ctx: ExecutionContext): User | any => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
|
||||
5
src/common/decorators/roles.decorator.ts
Normal file
5
src/common/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
|
||||
55
src/common/dto/pagination.dto.ts
Normal file
55
src/common/dto/pagination.dto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { IsOptional, IsPositive, Min, Max } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class PaginationDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page number',
|
||||
minimum: 1,
|
||||
default: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsPositive()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Number of items per page',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsPositive()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number = 10;
|
||||
}
|
||||
|
||||
export class PaginationMetaDto {
|
||||
@ApiPropertyOptional()
|
||||
page: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
limit: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
total: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
totalPages: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
hasNext: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
export class PaginatedResponseDto<T> {
|
||||
data: T[];
|
||||
meta: PaginationMetaDto;
|
||||
}
|
||||
|
||||
25
src/common/guards/roles.guard.ts
Normal file
25
src/common/guards/roles.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
// For now, allow all requests since we don't have role-based auth implemented
|
||||
// This should be updated when you implement proper role checking
|
||||
return true;
|
||||
}
|
||||
}
|
||||
19
src/common/interfaces/auth.interface.ts
Normal file
19
src/common/interfaces/auth.interface.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { User } from '@prisma/client';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
user: Omit<User, 'password'>;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
103
src/common/middlewares/jwt/authForHost.ts
Normal file
103
src/common/middlewares/jwt/authForHost.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface DecodedToken {
|
||||
id: number;
|
||||
role?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
interface UserPayload {
|
||||
id: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
user?: UserPayload;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies JWT and validates Host user (role_xid = 3)
|
||||
*/
|
||||
const verifyCallback = async (
|
||||
req: Request,
|
||||
resolve: (value?: unknown) => void,
|
||||
reject: (reason?: Error) => void
|
||||
) => {
|
||||
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||
|
||||
if (!token) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
|
||||
|
||||
if (!decoded?.id) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
|
||||
}
|
||||
|
||||
// ✅ Fetch user from Prisma (Host user only)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.id },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
|
||||
}
|
||||
|
||||
// ✅ Check if user is active
|
||||
if (!user.isActive) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Check Host role (role_xid = 3)
|
||||
if (user.roleXid !== 3) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
|
||||
);
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
req.user = { id: user.id.toString(), role: user.role?.roleName };
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
|
||||
);
|
||||
}
|
||||
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Express middleware — use as `auth()` in routes
|
||||
*/
|
||||
const auth =
|
||||
() =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
verifyCallback(req, resolve, reject);
|
||||
})
|
||||
.then(() => next())
|
||||
.catch((err) => next(err));
|
||||
};
|
||||
|
||||
export default auth;
|
||||
103
src/common/middlewares/jwt/authForMinglarAdmin.ts
Normal file
103
src/common/middlewares/jwt/authForMinglarAdmin.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface DecodedToken {
|
||||
id: number;
|
||||
role?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
interface UserPayload {
|
||||
id: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
user?: UserPayload;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies JWT and validates Host user (role_xid = 3)
|
||||
*/
|
||||
const verifyCallback = async (
|
||||
req: Request,
|
||||
resolve: (value?: unknown) => void,
|
||||
reject: (reason?: Error) => void
|
||||
) => {
|
||||
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||
|
||||
if (!token) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
|
||||
|
||||
if (!decoded?.id) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
|
||||
}
|
||||
|
||||
// ✅ Fetch user from Prisma (Host user only)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.id },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
|
||||
}
|
||||
|
||||
// ✅ Check if user is active
|
||||
if (!user.isActive) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Check Admin role (role_xid = 2)
|
||||
if (user.roleXid !== 2) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
|
||||
);
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
req.user = { id: user.id.toString(), role: user.role?.roleName };
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
|
||||
);
|
||||
}
|
||||
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Express middleware — use as `auth()` in routes
|
||||
*/
|
||||
const auth =
|
||||
() =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
verifyCallback(req, resolve, reject);
|
||||
})
|
||||
.then(() => next())
|
||||
.catch((err) => next(err));
|
||||
};
|
||||
|
||||
export default auth;
|
||||
103
src/common/middlewares/jwt/authForUser.ts
Normal file
103
src/common/middlewares/jwt/authForUser.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface DecodedToken {
|
||||
id: number;
|
||||
role?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
interface UserPayload {
|
||||
id: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
user?: UserPayload;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies JWT and validates Host user (role_xid = 3)
|
||||
*/
|
||||
const verifyCallback = async (
|
||||
req: Request,
|
||||
resolve: (value?: unknown) => void,
|
||||
reject: (reason?: Error) => void
|
||||
) => {
|
||||
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||
|
||||
if (!token) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
|
||||
|
||||
if (!decoded?.id) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
|
||||
}
|
||||
|
||||
// ✅ Fetch user from Prisma (Host user only)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.id },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
|
||||
}
|
||||
|
||||
// ✅ Check if user is active
|
||||
if (!user.isActive) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Check User role (role_xid = 1)
|
||||
if (user.roleXid !== 1) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
|
||||
);
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
req.user = { id: user.id.toString(), role: user.role?.roleName };
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return reject(
|
||||
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
|
||||
);
|
||||
}
|
||||
|
||||
return reject(
|
||||
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Express middleware — use as `auth()` in routes
|
||||
*/
|
||||
const auth =
|
||||
() =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
verifyCallback(req, resolve, reject);
|
||||
})
|
||||
.then(() => next())
|
||||
.catch((err) => next(err));
|
||||
};
|
||||
|
||||
export default auth;
|
||||
33
src/common/utils/helper/ApiError.ts
Normal file
33
src/common/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;
|
||||
32
src/common/utils/pagination.util.ts
Normal file
32
src/common/utils/pagination.util.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { PaginationDto, PaginationMetaDto } from '../dto/pagination.dto';
|
||||
|
||||
export function createPaginationMeta(
|
||||
paginationDto: PaginationDto,
|
||||
total: number,
|
||||
): PaginationMetaDto {
|
||||
const { page = 1, limit = 10 } = paginationDto;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function paginate<T>(
|
||||
data: T[],
|
||||
paginationDto: PaginationDto,
|
||||
total: number,
|
||||
) {
|
||||
const meta = createPaginationMeta(paginationDto, total);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
185
src/config/config.ts
Normal file
185
src/config/config.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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),
|
||||
BASEURL: yup.string().required('Base URL is required'),
|
||||
// FRONTEND_URL: yup.string().required('Frontend URL is required'),
|
||||
//JWT
|
||||
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'),
|
||||
//SMTP and BREVO
|
||||
// BREVO_SMTP_HOST: yup
|
||||
// .string()
|
||||
// .nullable()
|
||||
// .required('server that will send the emails'),
|
||||
// BREVO_SMTP_PORT: yup
|
||||
// .number()
|
||||
// .nullable()
|
||||
// .required('port to connect to the email server'),
|
||||
// BREVO_SMTP_USER: yup
|
||||
// .string()
|
||||
// .nullable()
|
||||
// .required('username for email server'),
|
||||
// BREVO_SMTP_PASS: yup
|
||||
// .string()
|
||||
// .nullable()
|
||||
// .required('password for email server'),
|
||||
// BREVO_FROM_EMAIL: yup
|
||||
// .string()
|
||||
// .nullable()
|
||||
// .required('the from field in the emails sent by the app'),
|
||||
// BREVO_EMAIL_API_KEY: yup
|
||||
// .string()
|
||||
// .nullable()
|
||||
// .required('the from field in the emails sent by the app api key'),
|
||||
// BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
|
||||
// //one signal
|
||||
// ONESIGNAL_APPID: yup.string().required('One signal app id is required'),
|
||||
// ONESIGNAL_REST_APIKEY: yup
|
||||
// .string()
|
||||
// .required('One signal api key is required'),
|
||||
//branch IO
|
||||
// BRANCH_IO_KEY: yup.string().required('Branch IO key is required'),
|
||||
|
||||
// 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'),
|
||||
//OTP Bypass
|
||||
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP 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',
|
||||
},
|
||||
},
|
||||
byPassOTP: envVars.BYPASS_OTP,
|
||||
// BaseURL: envVars.BASEURL,
|
||||
// FRONTEND_URL: envVars.FRONTEND_URL,
|
||||
// email: {
|
||||
// smtp: {
|
||||
// host: envVars?.BREVO_SMTP_HOST,
|
||||
// port: envVars?.BREVO_SMTP_PORT,
|
||||
// secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
|
||||
// auth: {
|
||||
// user: envVars?.BREVO_SMTP_USER,
|
||||
// pass: envVars?.BREVO_SMTP_PASS,
|
||||
// },
|
||||
// },
|
||||
// from: envVars?.BREVO_FROM_EMAIL,
|
||||
// api_key: envVars?.BREVO_EMAIL_API_KEY,
|
||||
// BrevobaseURL: envVars?.BREVO_API_BASEURL,
|
||||
// },
|
||||
// oneSignal: {
|
||||
// appID: envVars.ONESIGNAL_APPID,
|
||||
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,
|
||||
// },
|
||||
// branchIO: {
|
||||
// branchIOKey: envVars.BRANCH_IO_KEY,
|
||||
// },
|
||||
};
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Created By : Angad Chauhan
|
||||
* Created at : 31/1/25
|
||||
* Use : For google login .env file global variable
|
||||
*/
|
||||
// export const googleConfig = {
|
||||
// clientID: process.env.GOOGLE_CLIENT_ID!,
|
||||
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
// callbackURL: process.env.GOOGLE_CALLBACK_URL!,
|
||||
// };
|
||||
|
||||
// Validate and export configuration only if validation succeeds
|
||||
const config = getConfig();
|
||||
export default config;
|
||||
82
src/main.ts
Normal file
82
src/main.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import helmet from 'helmet';
|
||||
import { AppModule } from './app/app.module';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
// ✅ Security Middlewares
|
||||
app.use(helmet());
|
||||
app.enableCors({
|
||||
origin: [
|
||||
'http://localhost:3000', // Local Swagger UI
|
||||
'http://localhost:3006', // Local Frontend
|
||||
'https://editor.swagger.io', // Swagger Editor
|
||||
'https://klc-admin.wdiprojects.com',
|
||||
'https://admin-uat.klc.betadelivery.com',
|
||||
],
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// ✅ Global prefix
|
||||
const globalPrefix = configService.get('API_PREFIX') || 'api/v1';
|
||||
app.setGlobalPrefix(globalPrefix);
|
||||
|
||||
// ✅ Logging middleware
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`📝 ${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// ✅ Global validation
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ Swagger setup (only for local/dev)
|
||||
if (configService.get('NODE_ENV') !== 'production') {
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Minglar API')
|
||||
.setDescription('NestJS Backend for Minglar with Lambda-ready endpoints')
|
||||
.setVersion(configService.get('API_VERSION') || '1.0.0')
|
||||
.addBearerAuth()
|
||||
.addServer(`${process.env.SERVER_URL || 'http://localhost:3000'}/`, 'Local Server')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
|
||||
// Optionally write swagger.json for offline usage
|
||||
const outputPath = join(process.cwd(), 'swagger.json');
|
||||
writeFileSync(outputPath, JSON.stringify(document, null, 2), 'utf8');
|
||||
console.log(`✅ Swagger JSON file generated at: ${outputPath}`);
|
||||
}
|
||||
|
||||
// ✅ Start app (only for local or non-Lambda)
|
||||
const port = configService.get('PORT') || 3000;
|
||||
await app.listen(port);
|
||||
|
||||
console.log(`🚀 Server running at http://localhost:${port}`);
|
||||
if (configService.get('NODE_ENV') !== 'production') {
|
||||
console.log(`📚 Swagger docs: http://localhost:${port}/api/docs`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only run bootstrap if not in AWS Lambda
|
||||
if (!process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
||||
bootstrap();
|
||||
}
|
||||
0
src/modules/auth/auth.module.ts
Normal file
0
src/modules/auth/auth.module.ts
Normal file
51
src/modules/host/dto/host.dto.ts
Normal file
51
src/modules/host/dto/host.dto.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// src/modules/host/dto/host.dto.ts
|
||||
import { IsInt, IsOptional, IsString, IsBoolean, IsEmail } from 'class-validator';
|
||||
|
||||
export class CreateHostDto {
|
||||
@IsString()
|
||||
firstName: string;
|
||||
|
||||
@IsString()
|
||||
lastName: string;
|
||||
|
||||
@IsEmail()
|
||||
emailAddress: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
isdCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobileNumber?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userPasscode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
roleXid?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateHostDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
emailAddress?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
68
src/modules/host/handlers/host.handler.ts
Normal file
68
src/modules/host/handlers/host.handler.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// src/modules/host/handler/host.handler.ts
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from 'src/app.module';
|
||||
import { HostService } from '../services/host.service';
|
||||
import { APIGatewayProxyHandler } from 'aws-lambda';
|
||||
import { CreateHostDto, UpdateHostDto } from '../dto/host.dto';
|
||||
|
||||
let app;
|
||||
let hostService: HostService;
|
||||
|
||||
async function bootstrap() {
|
||||
if (!app) {
|
||||
const nestApp = await NestFactory.createApplicationContext(AppModule);
|
||||
hostService = nestApp.get(HostService);
|
||||
app = nestApp;
|
||||
}
|
||||
}
|
||||
|
||||
export const handler: APIGatewayProxyHandler = async (event) => {
|
||||
await bootstrap();
|
||||
|
||||
const method = event.httpMethod;
|
||||
const path = event.path;
|
||||
const body = event.body ? JSON.parse(event.body) : {};
|
||||
|
||||
try {
|
||||
if (method === 'POST' && path.endsWith('/host')) {
|
||||
const host = await hostService.createHost(body as CreateHostDto);
|
||||
return response(201, host);
|
||||
}
|
||||
|
||||
if (method === 'GET' && path.endsWith('/host')) {
|
||||
const hosts = await hostService.getAllHosts();
|
||||
return response(200, hosts);
|
||||
}
|
||||
|
||||
if (method === 'GET' && path.match(/\/host\/\d+$/)) {
|
||||
const id = Number(path.split('/').pop());
|
||||
const host = await hostService.getHostById(id);
|
||||
return response(200, host);
|
||||
}
|
||||
|
||||
if (method === 'PUT' && path.match(/\/host\/\d+$/)) {
|
||||
const id = Number(path.split('/').pop());
|
||||
const host = await hostService.updateHost(id, body as UpdateHostDto);
|
||||
return response(200, host);
|
||||
}
|
||||
|
||||
if (method === 'DELETE' && path.match(/\/host\/\d+$/)) {
|
||||
const id = Number(path.split('/').pop());
|
||||
const host = await hostService.deleteHost(id);
|
||||
return response(200, { message: 'Host deleted successfully', host });
|
||||
}
|
||||
|
||||
return response(404, { message: 'Not Found' });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return response(500, { message: error.message || 'Internal Server Error' });
|
||||
}
|
||||
};
|
||||
|
||||
function response(statusCode: number, body: any) {
|
||||
return {
|
||||
statusCode,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
}
|
||||
34
src/modules/host/services/host.service.ts
Normal file
34
src/modules/host/services/host.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// src/modules/host/services/host.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../common/database/prisma.service';
|
||||
import { CreateHostDto, UpdateHostDto } from '../dto/host.dto';
|
||||
|
||||
@Injectable()
|
||||
export class HostService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async createHost(data: CreateHostDto) {
|
||||
return this.prisma.user.create({ data });
|
||||
}
|
||||
|
||||
async getAllHosts() {
|
||||
return this.prisma.user.findMany({ where: { roleXid: 3 } });
|
||||
}
|
||||
|
||||
async getHostById(id: number) {
|
||||
const host = await this.prisma.user.findUnique({ where: { id } });
|
||||
if (!host || host.roleXid !== 3) throw new Error('Host not found');
|
||||
return host;
|
||||
}
|
||||
|
||||
async updateHost(id: number, data: UpdateHostDto) {
|
||||
return this.prisma.user.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteHost(id: number) {
|
||||
return this.prisma.user.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user