Backend half

This commit is contained in:
2025-07-11 19:56:28 +02:00
parent fa868e7c1d
commit 8600fa7c1d
19426 changed files with 3750448 additions and 8108 deletions
-25
View File
@@ -1,25 +0,0 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { User } from './user.entity';
import { Company } from './company.entity';
import { QBank } from './qbank.entity';
// Get the database type from environment variables or default to MariaDB
const dbType = process.env.DB_TYPE || 'mariadb';
// Create the options object with correct typing
const options: DataSourceOptions = {
type: dbType as any, // Using 'as any' to bypass TypeScript's strict type checking
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT) || 3306,
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE || 'serpent_race',
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV !== 'production',
entities: [User, Company, QBank],
migrations: [],
subscribers: [],
};
// Create the DataSource with properly typed options
export const AppDataSourceOne = new DataSource(options);
-52
View File
@@ -1,52 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany,
ManyToOne,
JoinColumn
} from 'typeorm';
import { Company } from './company.entity';
import { QBank } from './qbank.entity';
// User entity for the SerpentRace API
// This entity represents a user in the system, with fields for personal information,
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
Username!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
FirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
LastName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
Password!: string; // Consider hashing this password before storing
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
Email!: string;
@CreateDateColumn()
RegDate!: Date;
@Column({ nullable: true })
CompanyId: number = 0;
@Column({ type: 'varchar', length: 255, nullable: true })
CompanyToken: string = '';
@ManyToOne(() => Company, (company) => company.users)
@JoinColumn({ name: 'CompanyId' })
company!: Company;
@OneToMany(() => QBank, qbank => qbank.creator)
questionBanks!: QBank[];
}
@@ -0,0 +1,83 @@
export class CompanyBasicDto {
CompanyId: number;
Name: string;
constructor(CompanyId: number, Name: string) {
this.CompanyId = CompanyId;
this.Name = Name;
}
}
export class CompanyUpdateDto {
Name?: string;
ContactFirstName?: string;
ContactLastName?: string;
ContactEmail?: string;
FirstAPI?: string;
TokenAPI?: string;
constructor(data: Partial<CompanyUpdateDto> = {}) {
this.Name = data.Name;
this.ContactFirstName = data.ContactFirstName;
this.ContactLastName = data.ContactLastName;
this.ContactEmail = data.ContactEmail;
this.FirstAPI = data.FirstAPI;
this.TokenAPI = data.TokenAPI;
}
}
export class CompanyCreateDto {
Name: string;
ContactFirstName: string;
ContactLastName: string;
ContactEmail: string;
FirstAPI: string;
TokenAPI: string;
constructor(
Name: string,
ContactFirstName: string,
ContactLastName: string,
ContactEmail: string,
FirstAPI: string,
TokenAPI: string
) {
this.Name = Name;
this.ContactFirstName = ContactFirstName;
this.ContactLastName = ContactLastName;
this.ContactEmail = ContactEmail;
this.FirstAPI = FirstAPI;
this.TokenAPI = TokenAPI;
}
}
export class CompanyResponseDto {
CompanyId: number;
Name: string;
ContactFirstName: string;
ContactLastName: string;
ContactEmail: string;
FirstAPI: string;
TokenAPI: string;
RegDate: Date;
constructor(
CompanyId: number,
Name: string,
ContactFirstName: string,
ContactLastName: string,
ContactEmail: string,
FirstAPI: string,
TokenAPI: string,
RegDate: Date
) {
this.CompanyId = CompanyId;
this.Name = Name;
this.ContactFirstName = ContactFirstName;
this.ContactLastName = ContactLastName;
this.ContactEmail = ContactEmail;
this.FirstAPI = FirstAPI;
this.TokenAPI = TokenAPI;
this.RegDate = RegDate;
}
}
@@ -0,0 +1,59 @@
export class QBankBasicDto {
QBankId: number;
Title: string;
constructor(QBankId: number, Title: string) {
this.QBankId = QBankId;
this.Title = Title;
}
}
export class QBankUpdateDto {
Title?: string;
no_question?: number;
constructor(data: Partial<QBankUpdateDto> = {}) {
this.Title = data.Title;
this.no_question = data.no_question;
}
}
export class QBankCreateDto {
Title: string;
no_question: number;
constructor(Title: string, no_question: number) {
this.Title = Title;
this.no_question = no_question;
}
}
export class QBankResponseDto {
QBankId: number;
Title: string;
Creator: number;
Creation_Date: Date;
no_question: number;
constructor(
QBankId: number,
Title: string,
Creator: number,
Creation_Date: Date,
no_question: number
) {
this.QBankId = QBankId;
this.Title = Title;
this.Creator = Creator;
this.Creation_Date = Creation_Date;
this.no_question = no_question;
}
}
export class QBankListResponseDto {
questionBanks: QBankResponseDto[];
constructor(questionBanks: QBankResponseDto[]) {
this.questionBanks = questionBanks;
}
}
@@ -0,0 +1,96 @@
export class UserBasicDto3 {
id: number;
username: string;
CompanyId?: number;
constructor(id: number, username: string, CompanyId?: number) {
this.id = id;
this.username = username;
this.CompanyId = CompanyId || 0;
}
}
export class UserBasicDto {
id: number;
username: string;
CompanyId?: number;
constructor(id: number, username: string, CompanyId?: number) {
this.id = id;
this.username = username;
this.CompanyId = CompanyId || 0; // Default to 0 if not provided
}
}
export class UserUpdateDto {
username?: string;
FirstName?: string;
LastName?: string;
email?: string;
password?: string;
constructor(data: Partial<UserUpdateDto> = {}) {
this.username = data.username;
this.FirstName = data.FirstName;
this.LastName = data.LastName;
this.email = data.email;
this.password = data.password;
}
}
export class UserCreateDto {
username: string;
FirstName: string;
LastName: string;
email: string;
password: string;
CompanyId?: number; // Add optional CompanyId
constructor(
username: string,
FirstName: string,
LastName: string,
email: string,
password: string,
CompanyId?: number // Add optional parameter
) {
this.username = username;
this.FirstName = FirstName;
this.LastName = LastName;
this.email = email;
this.password = password;
this.CompanyId = CompanyId; // Don't set default value
}
}
export class UserResponseDto {
id: number;
username: string;
FirstName: string;
LastName: string;
email: string;
RegDate: Date;
constructor(
id: number,
username: string,
FirstName: string,
LastName: string,
email: string,
RegDate: Date
) {
this.id = id;
this.username = username;
this.FirstName = FirstName;
this.LastName = LastName;
this.email = email;
this.RegDate = RegDate;
}
}
export class UsersListResponseDto {
users: UserResponseDto[];
constructor(users: UserResponseDto[]) {
this.users = users;
}
}
@@ -7,7 +7,7 @@ import {
} from 'typeorm';
import { User } from './user.entity';
@Entity('companies')
@Entity('Company')
export class Company {
@PrimaryGeneratedColumn()
CompanyId!: number;
@@ -1,7 +1,7 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('q_bank')
@Entity('QBank')
export class QBank {
@PrimaryGeneratedColumn()
QBankId!: number;
@@ -0,0 +1,50 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
ManyToOne,
JoinColumn,
OneToMany
} from "typeorm";
import {Company} from "./company.entity";
import {QBank} from "./qbank.entity";
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 255 , nullable: false, unique: true })
username!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
FirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
LastName!: string;
@Column({ type: 'varchar', length: 255 , nullable: false, unique: true })
email!: string;
@Column({ type: 'varchar', length: 255 , nullable: false })
password!: string;
@CreateDateColumn()
RegDate!: Date;
@Column({ nullable: true })
CompanyId?: number;
@Column({ type: 'varchar', length: 255, nullable: true })
CompanyToken?: string;
@ManyToOne(() => Company, (company) => company.users)
@JoinColumn({ name: 'CompanyId' })
company?: Company;
@OneToMany(() => QBank, qbank => qbank.creator)
questionBanks!: QBank[];
}
@@ -0,0 +1,43 @@
import { Company } from '../entities/company.entity';
import { CompanyBasicDto, CompanyUpdateDto, CompanyCreateDto, CompanyResponseDto } from '../dto/company.dto';
export class CompanyMapper {
static toBasicDto(company: Company): CompanyBasicDto {
return new CompanyBasicDto(company.CompanyId, company.Name);
}
static toResponseDto(company: Company): CompanyResponseDto {
return new CompanyResponseDto(
company.CompanyId,
company.Name,
company.ContactFirstName,
company.ContactLastName,
company.ContactEmail,
company.FirstAPI,
company.TokenAPI,
company.RegDate
);
}
static toEntity(createDto: CompanyCreateDto): Company {
const company = new Company();
company.Name = createDto.Name;
company.ContactFirstName = createDto.ContactFirstName;
company.ContactLastName = createDto.ContactLastName;
company.ContactEmail = createDto.ContactEmail;
company.FirstAPI = createDto.FirstAPI;
company.TokenAPI = createDto.TokenAPI;
return company;
}
static updateEntity(original: Company, updateDto: CompanyUpdateDto): Company {
if (updateDto.Name !== undefined) original.Name = updateDto.Name;
if (updateDto.ContactFirstName !== undefined) original.ContactFirstName = updateDto.ContactFirstName;
if (updateDto.ContactLastName !== undefined) original.ContactLastName = updateDto.ContactLastName;
if (updateDto.ContactEmail !== undefined) original.ContactEmail = updateDto.ContactEmail;
if (updateDto.FirstAPI !== undefined) original.FirstAPI = updateDto.FirstAPI;
if (updateDto.TokenAPI !== undefined) original.TokenAPI = updateDto.TokenAPI;
return original;
}
}
@@ -0,0 +1,38 @@
import { QBank } from '../entities/qbank.entity';
import { QBankBasicDto, QBankUpdateDto, QBankCreateDto, QBankResponseDto } from '../dto/qbank.dto';
export class QBankMapper {
static toBasicDto(qbank: QBank): QBankBasicDto {
return new QBankBasicDto(qbank.QBankId, qbank.Title);
}
static toResponseDto(qbank: QBank): QBankResponseDto {
return new QBankResponseDto(
qbank.QBankId,
qbank.Title,
qbank.Creator,
qbank.Creation_Date,
qbank.no_question
);
}
static toEntity(createDto: QBankCreateDto, creatorId: number): QBank {
const qbank = new QBank();
qbank.Title = createDto.Title;
qbank.Creator = creatorId;
qbank.Creation_Date = new Date();
qbank.no_question = createDto.no_question;
return qbank;
}
static updateEntity(original: QBank, updateDto: QBankUpdateDto): QBank {
if (updateDto.Title !== undefined) original.Title = updateDto.Title;
if (updateDto.no_question !== undefined) original.no_question = updateDto.no_question;
return original;
}
static QBankListResponseDto(qbanks: QBank[]): QBankResponseDto[] {
return qbanks.map(qbank => this.toResponseDto(qbank));
}
}
@@ -0,0 +1,49 @@
import { User } from '../entities/user.entity';
import { UserBasicDto, UserUpdateDto, UserCreateDto, UserResponseDto } from '../dto/user.dto';
export class UserMapper {
static toBasicDto(user: User): UserBasicDto {
return new UserBasicDto(user.id, user.username, user.CompanyId || undefined);
}
static toResponseDto(user: User): UserResponseDto {
return new UserResponseDto(
user.id,
user.username,
user.FirstName,
user.LastName,
user.email,
user.RegDate
);
}
static toEntity(createDto: UserCreateDto): User {
const user = new User();
user.username = createDto.username;
user.FirstName = createDto.FirstName;
user.LastName = createDto.LastName;
user.email = createDto.email;
user.password = createDto.password;
// Only set CompanyId if it's provided and not null/undefined
if (createDto.CompanyId !== undefined && createDto.CompanyId !== null) {
user.CompanyId = createDto.CompanyId;
}
// If not provided, leave it as undefined (which will be NULL in database)
return user;
}
static updateEntity(original: User, updateDto: UserUpdateDto): User {
if (updateDto.username !== undefined) original.username = updateDto.username;
if (updateDto.FirstName !== undefined) original.FirstName = updateDto.FirstName;
if (updateDto.LastName !== undefined) original.LastName = updateDto.LastName;
if (updateDto.email !== undefined) original.email = updateDto.email;
if (updateDto.password !== undefined) original.password = updateDto.password;
return original;
}
static UsersListResponseDto(users: User[]): UserResponseDto[] {
return users.map(user => this.toResponseDto(user));
}
}
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ModifiedUser1751827771881 implements MigrationInterface {
name = 'ModifiedUser1751827771881'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`Company\` (\`CompanyId\` int NOT NULL AUTO_INCREMENT, \`Name\` varchar(255) NOT NULL, \`ContactFirstName\` varchar(255) NOT NULL, \`ContactLastName\` varchar(255) NOT NULL, \`ContactEmail\` varchar(255) NOT NULL, \`FirstAPI\` varchar(255) NOT NULL, \`TokenAPI\` varchar(255) NOT NULL, \`RegDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`CompanyId\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`user\` (\`id\` int NOT NULL AUTO_INCREMENT, \`username\` varchar(255) NOT NULL, \`FirstName\` varchar(255) NOT NULL, \`LastName\` varchar(255) NOT NULL, \`email\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, \`RegDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`CompanyId\` int NULL, \`CompanyToken\` varchar(255) NULL, UNIQUE INDEX \`IDX_78a916df40e02a9deb1c4b75ed\` (\`username\`), UNIQUE INDEX \`IDX_e12875dfb3b1d92d7d7c5377e2\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`QBank\` (\`QBankId\` int NOT NULL AUTO_INCREMENT, \`Title\` varchar(255) NOT NULL, \`Creator\` int NOT NULL, \`Creation_Date\` datetime NOT NULL, \`no_question\` int NOT NULL, PRIMARY KEY (\`QBankId\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`user\` ADD CONSTRAINT \`FK_85994badd6742add52ae81cfb15\` FOREIGN KEY (\`CompanyId\`) REFERENCES \`Company\`(\`CompanyId\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE \`QBank\` ADD CONSTRAINT \`FK_ce24a25a13d18a471bd7c0df014\` FOREIGN KEY (\`Creator\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`QBank\` DROP FOREIGN KEY \`FK_ce24a25a13d18a471bd7c0df014\``);
await queryRunner.query(`ALTER TABLE \`user\` DROP FOREIGN KEY \`FK_85994badd6742add52ae81cfb15\``);
await queryRunner.query(`DROP TABLE \`QBank\``);
await queryRunner.query(`DROP INDEX \`IDX_e12875dfb3b1d92d7d7c5377e2\` ON \`user\``);
await queryRunner.query(`DROP INDEX \`IDX_78a916df40e02a9deb1c4b75ed\` ON \`user\``);
await queryRunner.query(`DROP TABLE \`user\``);
await queryRunner.query(`DROP TABLE \`Company\``);
}
}
@@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ModifiedUser1751827770376 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -0,0 +1,155 @@
import { Repository, DataSource } from 'typeorm';
import { Company } from '../Database/entities/company.entity';
import { CompanyCreateDto, CompanyUpdateDto, CompanyResponseDto, CompanyBasicDto } from '../Database/dto/company.dto';
import { CompanyMapper } from '../Database/mappers/company.mapper';
import { ICompanyRepository } from './interfaces/ICompanyRepository';
export class CompanyRepository implements ICompanyRepository {
private repository: Repository<Company>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(Company);
}
// Basic CRUD operations
async create(companyCreateDto: CompanyCreateDto): Promise<CompanyResponseDto> {
const company = CompanyMapper.toEntity(companyCreateDto);
const savedCompany = await this.repository.save(company);
return CompanyMapper.toResponseDto(savedCompany);
}
async findById(id: number): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({ where: { CompanyId: id } });
return company ? CompanyMapper.toResponseDto(company) : null;
}
async findByName(name: string): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({ where: { Name: name } });
return company ? CompanyMapper.toResponseDto(company) : null;
}
// async findByContactEmail(email: string): Promise<CompanyResponseDto | null> {
// const company = await this.repository.findOne({ where: { ContactEmail: email } });
// return company ? CompanyMapper.toResponseDto(company) : null;
// }
async findAll(): Promise<CompanyResponseDto[]> {
const companies = await this.repository.find({
order: { RegDate: 'DESC' }
});
return companies.map(company => CompanyMapper.toResponseDto(company));
}
async update(id: number, companyUpdateDto: CompanyUpdateDto): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({ where: { CompanyId: id } });
if (!company) return null;
const updatedCompany = CompanyMapper.updateEntity(company, companyUpdateDto);
const savedCompany = await this.repository.save(updatedCompany);
return CompanyMapper.toResponseDto(savedCompany);
}
async deleteById(id: number): Promise<boolean> {
const result = await this.repository.delete({ CompanyId: id });
return result.affected !== 0;
}
// async exists(id: number): Promise<boolean> {
// const count = await this.repository.count({ where: { CompanyId: id } });
// return count > 0;
// }
// async nameExists(name: string): Promise<boolean> {
// const count = await this.repository.count({ where: { Name: name } });
// return count > 0;
// }
// async contactEmailExists(email: string): Promise<boolean> {
// const count = await this.repository.count({ where: { ContactEmail: email } });
// return count > 0;
// }
// Search and filtering
// async findByPartialName(partialName: string): Promise<CompanyResponseDto[]> {
// const companies = await this.repository
// .createQueryBuilder('company')
// .where('company.Name LIKE :name', { name: `%${partialName}%` })
// .orderBy('company.RegDate', 'DESC')
// .getMany();
// return companies.map(company => CompanyMapper.toResponseDto(company));
// }
// async findByPartialContactName(partialName: string): Promise<CompanyResponseDto[]> {
// const companies = await this.repository
// .createQueryBuilder('company')
// .where('company.ContactFirstName LIKE :name', { name: `%${partialName}%` })
// .orWhere('company.ContactLastName LIKE :name', { name: `%${partialName}%` })
// .orderBy('company.RegDate', 'DESC')
// .getMany();
// return companies.map(company => CompanyMapper.toResponseDto(company));
// }
// Pagination
// async findWithPagination(skip: number, take: number): Promise<{ companies: CompanyResponseDto[], total: number }> {
// const [companies, total] = await this.repository.findAndCount({
// skip,
// take,
// order: { RegDate: 'DESC' }
// });
// const companyDtos = companies.map(company => CompanyMapper.toResponseDto(company));
// return {
// companies: companyDtos,
// total
// };
// }
// Basic info
async findBasicById(id: number): Promise<CompanyBasicDto | null> {
const company = await this.repository.findOne({ where: { CompanyId: id } });
return company ? CompanyMapper.toBasicDto(company) : null;
}
async findAllBasic(): Promise<CompanyBasicDto[]> {
const companies = await this.repository.find({
order: { RegDate: 'DESC' }
});
return companies.map(company => CompanyMapper.toBasicDto(company));
}
// Relations
async findWithUsers(id: number): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({
where: { CompanyId: id },
relations: ['users']
});
return company ? CompanyMapper.toResponseDto(company) : null;
}
// Counting
// async count(): Promise<number> {
// return await this.repository.count();
// }
// Date range queries
// async findByDateRange(startDate: Date, endDate: Date): Promise<CompanyResponseDto[]> {
// const companies = await this.repository
// .createQueryBuilder('company')
// .where('company.RegDate >= :startDate', { startDate })
// .andWhere('company.RegDate <= :endDate', { endDate })
// .orderBy('company.RegDate', 'DESC')
// .getMany();
// return companies.map(company => CompanyMapper.toResponseDto(company));
// }
// Bulk operations
// async createMany(companyCreateDtos: CompanyCreateDto[]): Promise<CompanyResponseDto[]> {
// const companies = companyCreateDtos.map(dto => CompanyMapper.toEntity(dto));
// const savedCompanies = await this.repository.save(companies);
// return savedCompanies.map(company => CompanyMapper.toResponseDto(company));
// }
// async deleteByIds(ids: number[]): Promise<boolean> {
// const result = await this.repository.delete(ids);
// return result.affected !== 0;
// }
}
@@ -0,0 +1,203 @@
import { Repository, DataSource } from 'typeorm';
import { QBank } from '../Database/entities/qbank.entity';
import { QBankCreateDto, QBankUpdateDto, QBankResponseDto, QBankBasicDto, QBankListResponseDto } from '../Database/dto/qbank.dto';
import { QBankMapper } from '../Database/mappers/qbank.mapper';
import { IQBankRepository } from './interfaces/IQBankRepository';
export class QBankRepository implements IQBankRepository {
private repository: Repository<QBank>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(QBank);
}
// Basic CRUD operations
async create(qbankCreateDto: QBankCreateDto, creatorId: number): Promise<QBankResponseDto> {
const qbank = QBankMapper.toEntity(qbankCreateDto, creatorId);
const savedQBank = await this.repository.save(qbank);
return QBankMapper.toResponseDto(savedQBank);
}
async findById(id: number): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({ where: { QBankId: id } });
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
async findByTitle(title: string): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({ where: { Title: title } });
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
async findAll(): Promise<QBankListResponseDto> {
const qbanks = await this.repository.find();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async update(id: number, qbankUpdateDto: QBankUpdateDto): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({ where: { QBankId: id } });
if (!qbank) return null;
const updatedQBank = QBankMapper.updateEntity(qbank, qbankUpdateDto);
const savedQBank = await this.repository.save(updatedQBank);
return QBankMapper.toResponseDto(savedQBank);
}
async deleteById(id: number): Promise<boolean> {
const result = await this.repository.delete({ QBankId: id });
return result.affected !== 0;
}
async exists(id: number): Promise<boolean> {
const count = await this.repository.count({ where: { QBankId: id } });
return count > 0;
}
async titleExists(title: string): Promise<boolean> {
const count = await this.repository.count({ where: { Title: title } });
return count > 0;
}
// Creator related queries
async findByCreator(creatorId: number): Promise<QBankListResponseDto> {
const qbanks = await this.repository.find({ where: { Creator: creatorId } });
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async findByCreatorAndTitle(creatorId: number, title: string): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({
where: { Creator: creatorId, Title: title }
});
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
// Search and filtering
async findByPartialTitle(partialTitle: string): Promise<QBankListResponseDto> {
const qbanks = await this.repository
.createQueryBuilder('qbank')
.where('qbank.Title LIKE :title', { title: `%${partialTitle}%` })
.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async findByQuestionCount(minCount: number, maxCount?: number): Promise<QBankListResponseDto> {
let query = this.repository
.createQueryBuilder('qbank')
.where('qbank.no_question >= :minCount', { minCount });
if (maxCount !== undefined) {
query = query.andWhere('qbank.no_question <= :maxCount', { maxCount });
}
const qbanks = await query.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
// Pagination
async findWithPagination(skip: number, take: number): Promise<{ qbanks: QBankListResponseDto, total: number }> {
const [qbanks, total] = await this.repository.findAndCount({
skip,
take,
order: { Creation_Date: 'DESC' }
});
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return {
qbanks: new QBankListResponseDto(qbankDtos),
total
};
}
// Basic info
async findBasicById(id: number): Promise<QBankBasicDto | null> {
const qbank = await this.repository.findOne({ where: { QBankId: id } });
return qbank ? QBankMapper.toBasicDto(qbank) : null;
}
async findAllBasic(): Promise<QBankBasicDto[]> {
const qbanks = await this.repository.find();
return qbanks.map(qbank => QBankMapper.toBasicDto(qbank));
}
async findBasicByCreator(creatorId: number): Promise<QBankBasicDto[]> {
const qbanks = await this.repository.find({ where: { Creator: creatorId } });
return qbanks.map(qbank => QBankMapper.toBasicDto(qbank));
}
// Relations
async findWithCreator(id: number): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({
where: { QBankId: id },
relations: ['creator']
});
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
// Counting
async count(): Promise<number> {
return await this.repository.count();
}
async countByCreator(creatorId: number): Promise<number> {
return await this.repository.count({ where: { Creator: creatorId } });
}
// Date range queries
async findByDateRange(startDate: Date, endDate: Date): Promise<QBankListResponseDto> {
const qbanks = await this.repository
.createQueryBuilder('qbank')
.where('qbank.Creation_Date >= :startDate', { startDate })
.andWhere('qbank.Creation_Date <= :endDate', { endDate })
.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async findByCreatorAndDateRange(creatorId: number, startDate: Date, endDate: Date): Promise<QBankListResponseDto> {
const qbanks = await this.repository
.createQueryBuilder('qbank')
.where('qbank.Creator = :creatorId', { creatorId })
.andWhere('qbank.Creation_Date >= :startDate', { startDate })
.andWhere('qbank.Creation_Date <= :endDate', { endDate })
.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
// Bulk operations
async createMany(qbankCreateDtos: QBankCreateDto[], creatorId: number): Promise<QBankListResponseDto> {
const qbanks = qbankCreateDtos.map(dto => QBankMapper.toEntity(dto, creatorId));
const savedQBanks = await this.repository.save(qbanks);
const qbankDtos = savedQBanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async deleteByIds(ids: number[]): Promise<boolean> {
const result = await this.repository.delete(ids);
return result.affected !== 0;
}
async deleteByCreator(creatorId: number): Promise<boolean> {
const result = await this.repository.delete({ Creator: creatorId });
return result.affected !== 0;
}
// Statistics
async getTotalQuestions(): Promise<number> {
const result = await this.repository
.createQueryBuilder('qbank')
.select('SUM(qbank.no_question)', 'total')
.getRawOne();
return parseInt(result.total) || 0;
}
async getAverageQuestionsPerBank(): Promise<number> {
const result = await this.repository
.createQueryBuilder('qbank')
.select('AVG(qbank.no_question)', 'average')
.getRawOne();
return parseFloat(result.average) || 0;
}
}
@@ -0,0 +1,205 @@
import { Repository, DataSource } from 'typeorm';
import { User } from '../Database/entities/user.entity';
import { UserCreateDto, UserUpdateDto, UserResponseDto, UserBasicDto, UsersListResponseDto } from '../Database/dto/user.dto';
import { UserMapper } from '../Database/mappers/user.mapper';
import { IUserRepository } from './interfaces/IUserRepository';
export class UserRepository implements IUserRepository {
private repository: Repository<User>;
constructor(dataSource: DataSource) {
// console.log('🗃️ UserRepository constructor called with dataSource:', !!dataSource);
this.repository = dataSource.getRepository(User);
// console.log('🗃️ TypeORM Repository initialized:', !!this.repository);
}
// Basic CRUD operations
async create(userCreateDto: UserCreateDto): Promise<UserResponseDto> {
// console.log('Creating user with DTO:', userCreateDto);
const user = UserMapper.toEntity(userCreateDto);
// console.log('Creating user with DTO:', user);
const savedUser = await this.repository.save(user);
// console.log('User created:', savedUser);
return UserMapper.toResponseDto(savedUser);
}
async findById(id: number): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { id } });
return user ? UserMapper.toResponseDto(user) : null;
}
async findByUsername(username: string): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { username } });
return user ? UserMapper.toResponseDto(user) : null;
}
async findByEmail(email: string): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { email } });
return user ? UserMapper.toResponseDto(user) : null;
}
async findAll(): Promise<UsersListResponseDto> {
const users = await this.repository.find();
const userDtos = users.map(user => UserMapper.toResponseDto(user));
return new UsersListResponseDto(userDtos);
}
async update(id: number, userUpdateDto: UserUpdateDto): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { id } });
if (!user) return null;
const updatedUser = UserMapper.updateEntity(user, userUpdateDto);
const savedUser = await this.repository.save(updatedUser);
return UserMapper.toResponseDto(savedUser);
}
async deleteById(id: number): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected !== 0;
}
async exists(id: number): Promise<boolean> {
const count = await this.repository.count({ where: { id } });
return count > 0;
}
async usernameExists(username: string): Promise<boolean> {
const count = await this.repository.count({ where: { username } });
return count > 0;
}
async emailExists(email: string): Promise<boolean> {
const count = await this.repository.count({ where: { email } });
return count > 0;
}
// Company related queries
// async findByCompanyId(companyId: number): Promise<UsersListResponseDto> {
// const users = await this.repository.find({ where: { CompanyId: companyId } });
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// async findByCompanyToken(companyToken: string): Promise<UsersListResponseDto> {
// const users = await this.repository.find({ where: { CompanyToken: companyToken } });
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// Search and filtering
// async findByPartialUsername(partialUsername: string): Promise<UsersListResponseDto> {
// const users = await this.repository
// .createQueryBuilder('user')
// .where('user.username LIKE :username', { username: `%${partialUsername}%` })
// .getMany();
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// async findByPartialName(partialName: string): Promise<UsersListResponseDto> {
// const users = await this.repository
// .createQueryBuilder('user')
// .where('user.FirstName LIKE :name', { name: `%${partialName}%` })
// .orWhere('user.LastName LIKE :name', { name: `%${partialName}%` })
// .getMany();
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// Pagination
// async findWithPagination(skip: number, take: number): Promise<{ users: UsersListResponseDto, total: number }> {
// const [users, total] = await this.repository.findAndCount({
// skip,
// take,
// order: { RegDate: 'DESC' }
// });
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return {
// users: new UsersListResponseDto(userDtos),
// total
// };
// }
// Basic info
// async findBasicById(id: number): Promise<UserBasicDto | null> {
// const user = await this.repository.findOne({ where: { id } });
// return user ? UserMapper.toBasicDto(user) : null;
// }
async findAllBasic(): Promise<UserBasicDto[]> {
const users = await this.repository.find();
return users.map(user => UserMapper.toBasicDto(user));
}
// Relations
async findWithCompany(id: number): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({
where: { id },
relations: ['company']
});
return user ? UserMapper.toResponseDto(user) : null;
}
async findWithQuestionBanks(id: number): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({
where: { id },
relations: ['questionBanks']
});
return user ? UserMapper.toResponseDto(user) : null;
}
// async findWithAllRelations(id: number): Promise<UserResponseDto | null> {
// const user = await this.repository.findOne({
// where: { id },
// relations: ['company', 'questionBanks']
// });
// return user ? UserMapper.toResponseDto(user) : null;
// }
// Counting
// async count(): Promise<number> {
// return await this.repository.count();
// }
// async countByCompany(companyId: number): Promise<number> {
// return await this.repository.count({ where: { CompanyId: companyId } });
// }
// Date range queries
// async findByDateRange(startDate: Date, endDate: Date): Promise<UsersListResponseDto> {
// const users = await this.repository
// .createQueryBuilder('user')
// .where('user.RegDate >= :startDate', { startDate })
// .andWhere('user.RegDate <= :endDate', { endDate })
// .getMany();
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// Bulk operations
// async createMany(userCreateDtos: UserCreateDto[]): Promise<UsersListResponseDto> {
// const users = userCreateDtos.map(dto => UserMapper.toEntity(dto));
// const savedUsers = await this.repository.save(users);
// const userDtos = savedUsers.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// async deleteByIds(ids: number[]): Promise<boolean> {
// const result = await this.repository.delete(ids);
// return result.affected !== 0;
// }
// Authentication support (returns raw entity for password verification)
async findRawByUsername(username: string): Promise<User | null> {
console
return await this.repository.findOne({ where: { username } });
}
// async findRawById(id: number): Promise<User | null> {
// return await this.repository.findOne({ where: { id } });
// }
async findRawByEmail(email: string): Promise<User | null> {
return await this.repository.findOne({ where: { email } });
}
}
@@ -0,0 +1,39 @@
import { CompanyCreateDto, CompanyUpdateDto, CompanyResponseDto, CompanyBasicDto } from '../../Database/dto/company.dto';
export interface ICompanyRepository {
// Basic CRUD operations
create(companyCreateDto: CompanyCreateDto): Promise<CompanyResponseDto>;
findById(id: number): Promise<CompanyResponseDto | null>;
findByName(name: string): Promise<CompanyResponseDto | null>;
// findByContactEmail(email: string): Promise<CompanyResponseDto | null>;
findAll(): Promise<CompanyResponseDto[]>;
update(id: number, companyUpdateDto: CompanyUpdateDto): Promise<CompanyResponseDto | null>;
deleteById(id: number): Promise<boolean>;
// exists(id: number): Promise<boolean>;
// nameExists(name: string): Promise<boolean>;
// contactEmailExists(email: string): Promise<boolean>;
// Search and filtering
// // findByPartialName(partialName: string): Promise<CompanyResponseDto[]>;
// // findByPartialContactName(partialName: string): Promise<CompanyResponseDto[]>;
// Pagination
// findWithPagination(skip: number, take: number): Promise<{ companies: CompanyResponseDto[], total: number }>;
// Basic info
findBasicById(id: number): Promise<CompanyBasicDto | null>;
findAllBasic(): Promise<CompanyBasicDto[]>;
// Relations
// findWithUsers(id: number): Promise<CompanyResponseDto | null>;
// Counting
// count(): Promise<number>;
// Date range queries
// findByDateRange(startDate: Date, endDate: Date): Promise<CompanyResponseDto[]>;
// Bulk operations
// createMany(companyCreateDtos: CompanyCreateDto[]): Promise<CompanyResponseDto[]>;
// deleteByIds(ids: number[]): Promise<boolean>;
}
@@ -0,0 +1,50 @@
import { QBank } from '../../Database/entities/qbank.entity';
import { QBankCreateDto, QBankUpdateDto, QBankResponseDto, QBankBasicDto, QBankListResponseDto } from '../../Database/dto/qbank.dto';
export interface IQBankRepository {
// Basic CRUD operations
create(qbankCreateDto: QBankCreateDto, creatorId: number): Promise<QBankResponseDto>;
findById(id: number): Promise<QBankResponseDto | null>;
findByTitle(title: string): Promise<QBankResponseDto | null>;
findAll(): Promise<QBankListResponseDto>;
update(id: number, qbankUpdateDto: QBankUpdateDto): Promise<QBankResponseDto | null>;
deleteById(id: number): Promise<boolean>;
exists(id: number): Promise<boolean>;
titleExists(title: string): Promise<boolean>;
// Creator related queries
findByCreator(creatorId: number): Promise<QBankListResponseDto>;
findByCreatorAndTitle(creatorId: number, title: string): Promise<QBankResponseDto | null>;
// Search and filtering
findByPartialTitle(partialTitle: string): Promise<QBankListResponseDto>;
findByQuestionCount(minCount: number, maxCount?: number): Promise<QBankListResponseDto>;
// Pagination
findWithPagination(skip: number, take: number): Promise<{ qbanks: QBankListResponseDto, total: number }>;
// Basic info
findBasicById(id: number): Promise<QBankBasicDto | null>;
findAllBasic(): Promise<QBankBasicDto[]>;
findBasicByCreator(creatorId: number): Promise<QBankBasicDto[]>;
// Relations
findWithCreator(id: number): Promise<QBankResponseDto | null>;
// Counting
count(): Promise<number>;
countByCreator(creatorId: number): Promise<number>;
// Date range queries
findByDateRange(startDate: Date, endDate: Date): Promise<QBankListResponseDto>;
findByCreatorAndDateRange(creatorId: number, startDate: Date, endDate: Date): Promise<QBankListResponseDto>;
// Bulk operations
createMany(qbankCreateDtos: QBankCreateDto[], creatorId: number): Promise<QBankListResponseDto>;
deleteByIds(ids: number[]): Promise<boolean>;
deleteByCreator(creatorId: number): Promise<boolean>;
// Statistics
getTotalQuestions(): Promise<number>;
getAverageQuestionsPerBank(): Promise<number>;
}
@@ -0,0 +1,52 @@
import { User } from '../../Database/entities/user.entity';
import { UserCreateDto, UserUpdateDto, UserResponseDto, UserBasicDto, UsersListResponseDto } from '../../Database/dto/user.dto';
export interface IUserRepository {
// Basic CRUD operations
create(userCreateDto: UserCreateDto): Promise<UserResponseDto>;
findById(id: number): Promise<UserResponseDto | null>;
// findByUsername(username: string): Promise<UserResponseDto | null>;
findByEmail(email: string): Promise<UserResponseDto | null>;
findAll(): Promise<UsersListResponseDto>;
update(id: number, userUpdateDto: UserUpdateDto): Promise<UserResponseDto | null>;
deleteById(id: number): Promise<boolean>;
exists(id: number): Promise<boolean>;
usernameExists(username: string): Promise<boolean>;
emailExists(email: string): Promise<boolean>;
// Company related queries
// findByCompanyId(companyId: number): Promise<UsersListResponseDto>;
// findByCompanyToken(companyToken: string): Promise<UsersListResponseDto>;
// Search and filtering
// findByPartialUsername(partialUsername: string): Promise<UsersListResponseDto>;
// findByPartialName(partialName: string): Promise<UsersListResponseDto>;
// Pagination
// findWithPagination(skip: number, take: number): Promise<{ users: UsersListResponseDto, total: number }>;
// Basic info
// findBasicById(id: number): Promise<UserBasicDto | null>;
findAllBasic(): Promise<UserBasicDto[]>;
// Relations
findWithCompany(id: number): Promise<UserResponseDto | null>;
findWithQuestionBanks(id: number): Promise<UserResponseDto | null>;
// findWithAllRelations(id: number): Promise<UserResponseDto | null>;
// Counting
// count(): Promise<number>;
// countByCompany(companyId: number): Promise<number>;
// Date range queries
// findByDateRange(startDate: Date, endDate: Date): Promise<UsersListResponseDto>;
// Bulk operations
// createMany(userCreateDtos: UserCreateDto[]): Promise<UsersListResponseDto>;
// deleteByIds(ids: number[]): Promise<boolean>;
// Authentication support (returns raw entity for password verification)
findRawByUsername(username: string): Promise<User | null>;
// findRawById(id: number): Promise<User | null>;
findRawByEmail(email: string): Promise<User | null>;
}
@@ -0,0 +1,44 @@
import { Request, Response } from 'express';
import { CompanyQueryDispatcher } from '../../functions/Company/Queries/CompanyQueryDispatcher';
import { CompanyCommandDispatcher } from '../../functions/Company/Commands/CompanyCommandDispatcher';
import { CreateCompanyCommand } from '../../functions/Company/Commands/CompanyCommand';
import { CompanyCreateDto } from '../../Database/dto/company.dto';
export const createCompany = async (
req: Request,
res: Response,
queryDispatcher: CompanyQueryDispatcher,
commandDispatcher: CompanyCommandDispatcher
): Promise<void> => {
try {
const companyCreateDto: CompanyCreateDto = req.body;
console.log('Creating company with data:', companyCreateDto);
// Validate required fields
if (!companyCreateDto.Name || !companyCreateDto.FirstNameContact || !companyCreateDto.LastNameContact) {
res.status(400).json({
success: false,
message: 'Name, FirstNameContact, and LastNameContact are required',
received: companyCreateDto
});
return;
}
// Dispatch the command
const result = await commandDispatcher.dispatch(new CreateCompanyCommand(companyCreateDto));
res.status(201).json({
success: true,
data: result,
message: 'Company created successfully'
});
} catch (error: any) {
console.error('Error creating company:', error);
res.status(500).json({
success: false,
message: error.message || 'Internal server error'
});
}
};
@@ -0,0 +1,118 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import {
GetUserByUsernameQuery,
GetUserByEmailQuery,
GetRawUserByUsernameQuery,
GetRawUserByEmailQuery
} from '../../functions/Users/Queries/UserQuery';
import { comparePasswords } from '../../middlewares/security';
import { createTokenFromUserResponse, clearAuthCookie } from '../../middlewares/authentication';
export async function authenticateUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { Email, Username, Password } = req.body;
if (!Password || (!Email && !Username)) {
clearAuthCookie(res);
res.status(400).json({
success: false,
message: 'Password and either Email or Username are required',
autoLogout: true
});
return;
}
// Get the raw user entity for password verification
let rawUser = null;
if (Username) {
const rawQuery = new GetRawUserByUsernameQuery(Username);
rawUser = await queryDispatcher.dispatch(rawQuery);
} else if (Email) {
const rawQuery = new GetRawUserByEmailQuery(Email);
rawUser = await queryDispatcher.dispatch(rawQuery);
}
console.log('Raw user found:', rawUser);
if (!rawUser) {
clearAuthCookie(res);
res.status(401).json({
success: false,
message: 'Invalid credentials',
autoLogout: true
});
return;
}
// Verify password using security middleware
const isPasswordValid = await comparePasswords(Password, rawUser.password);
if (!isPasswordValid) {
clearAuthCookie(res);
res.status(401).json({
success: false,
message: 'Invalid credentials',
autoLogout: true
});
return;
}
// Get user response DTO (without password)
let user = null;
if (Username) {
const query = new GetUserByUsernameQuery(Username);
user = await queryDispatcher.dispatch(query);
} else if (Email) {
const query = new GetUserByEmailQuery(Email);
user = await queryDispatcher.dispatch(query);
}
if (!user) {
clearAuthCookie(res);
res.status(500).json({
success: false,
message: 'Authentication failed',
autoLogout: true
});
return;
}
// Create JWT token with basic user data
const token = createTokenFromUserResponse({
id: user.id,
username: user.username,
CompanyId: rawUser.CompanyId || undefined // Use undefined instead of 0
});
// Set token as HTTP-only cookie
res.cookie("jwt", token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? "none" : "lax",
maxAge: 60 * 60 * 1000, // 1 hour
});
res.status(200).json({
success: true,
message: 'Authentication successful',
data: user
});
} catch (error: any) {
clearAuthCookie(res);
res.status(500).json({
success: false,
message: error.message || 'Authentication failed',
autoLogout: true
});
}
}
@@ -0,0 +1,27 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { EmailExistsQuery } from '../../functions/Users/Queries/UserQuery';
export async function checkEmailExists(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { email } = req.params;
const query = new EmailExistsQuery(email);
const exists = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
exists
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to check email existence'
});
}
}
@@ -0,0 +1,34 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UserExistsQuery } from '../../functions/Users/Queries/UserQuery';
export async function checkUserExists(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const id = req.user.id.toString();
const query = new UserExistsQuery(parseInt(id));
const exists = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
exists
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to check user existence'
});
}
}
@@ -0,0 +1,27 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UsernameExistsQuery } from '../../functions/Users/Queries/UserQuery';
export async function checkUsernameExists(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { username } = req.params;
const query = new UsernameExistsQuery(username);
const exists = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
exists
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to check username existence'
});
}
}
@@ -0,0 +1,49 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UserCreateDto } from '../../Database/dto/user.dto';
import { CreateUserCommand } from '../../functions/Users/Commands/UserCommand';
export async function createUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { Username, FirstName, LastName, Password, Email } = req.body;
// Log the request details
// console.log('📝 Create user request:', {
// method: req.method,
// url: req.originalUrl,
// body: req.body
// });
// Validate required fields
if (!Username || !FirstName || !LastName || !Password || !Email) {
res.status(400).json({
success: false,
message: 'All fields are required: Username, FirstName, LastName, Password, Email'
});
return;
}
const userCreateDto = new UserCreateDto(Username, FirstName, LastName, Email, Password);
// Check if dto is valid
// console.log('UserCreateDto:', userCreateDto);
const command = new CreateUserCommand(userCreateDto);
// console.log('Dispatching CreateUserCommand:', command);
const newUser = await commandDispatcher.dispatch(command);
res.status(201).json({
success: true,
message: 'User created successfully',
data: newUser
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to create user'
});
}
}
@@ -0,0 +1,51 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { DeleteUserCommand } from '../../functions/Users/Commands/UserCommand';
export async function deleteUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const userId = req.user.id.toString();
if (!userId) {
res.status(400).json({
success: false,
message: 'User ID is required'
});
return;
}
const command = new DeleteUserCommand(parseInt(userId));
const deleted = await commandDispatcher.dispatch(command);
if (!deleted) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'User deleted successfully'
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to delete user'
});
}
}
@@ -0,0 +1,26 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetAllUsersQuery } from '../../functions/Users/Queries/UserQuery';
export async function getAllUsers(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const query = new GetAllUsersQuery();
const users = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
data: users
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get users'
});
}
}
@@ -0,0 +1,31 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
export async function getCurrentUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
// Return the authenticated user's basic info from the JWT token
if (!req.user) {
res.status(401).json({
success: false,
message: 'Not authenticated'
});
return;
}
res.status(200).json({
success: true,
data: req.user // Returns UserBasicDto
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get current user'
});
}
}
@@ -0,0 +1,42 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUserBasicByIdQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUserBasic(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const id = req.user.id.toString();
const query = new GetUserBasicByIdQuery(parseInt(id));
const user = await queryDispatcher.dispatch(query);
if (!user) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get user'
});
}
}
@@ -0,0 +1,50 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUserByIdQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUserDetails(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const userId = req.user.id.toString();
if (!userId) {
res.status(400).json({
success: false,
message: 'User ID is required'
});
return;
}
const query = new GetUserByIdQuery(parseInt(userId));
const user = await queryDispatcher.dispatch(query);
if (!user) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get user details'
});
}
}
@@ -0,0 +1,26 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUsersCountQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUsersCount(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const query = new GetUsersCountQuery();
const count = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
count
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get users count'
});
}
}
@@ -0,0 +1,35 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUsersWithPaginationQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUsersPaginated(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const query = new GetUsersWithPaginationQuery(page, limit);
const result = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
data: result.users,
pagination: {
page,
limit,
total: result.total,
totalPages: Math.ceil(result.total / limit)
}
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get paginated users'
});
}
}
@@ -0,0 +1,15 @@
export { createUser } from './createUser';
export { authenticateUser } from './authenticateUser';
export { logoutUser } from './logoutUser';
export { getCurrentUser } from './getCurrentUser';
export { getUserDetails } from './getUserDetails';
export { updateUser } from './updateUser';
export { deleteUser } from './deleteUser';
export { getAllUsers } from './getAllUsers';
export { getUserBasic } from './getUserBasic';
export { checkUserExists } from './checkUserExists';
export { checkUsernameExists } from './checkUsernameExists';
export { checkEmailExists } from './checkEmailExists';
export { getUsersCount } from './getUsersCount';
export { getUsersPaginated } from './getUsersPaginated';
export { resetPassword } from './resetPassword';
@@ -0,0 +1,29 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { clearAuthCookie } from '../../middlewares/authentication';
export async function logoutUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
// Clear the JWT cookie using the centralized function
clearAuthCookie(res);
res.status(200).json({
success: true,
message: 'Logout successful'
});
} catch (error: any) {
// Even if there's an error, still try to clear the cookie
clearAuthCookie(res);
res.status(500).json({
success: false,
message: error.message || 'Logout failed'
});
}
}
@@ -0,0 +1,52 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { ResetUserPasswordCommand } from '../../functions/Users/Commands/UserCommand';
export async function resetPassword(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const id = req.user.id.toString();
const { newPassword } = req.body;
if (!newPassword) {
res.status(400).json({
success: false,
message: 'New password is required'
});
return;
}
const command = new ResetUserPasswordCommand(parseInt(id), newPassword);
const updatedUser = await commandDispatcher.dispatch(command);
if (!updatedUser) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'Password reset successfully'
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to reset password'
});
}
}
@@ -0,0 +1,63 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UserUpdateDto } from '../../Database/dto/user.dto';
import { UpdateUserCommand } from '../../functions/Users/Commands/UserCommand';
export async function updateUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const userId = req.user.id.toString();
const { Username, FirstName, LastName, Password, Email } = req.body;
if (!userId) {
res.status(400).json({
success: false,
message: 'User ID is required'
});
return;
}
const userUpdateDto = new UserUpdateDto({
username: Username,
FirstName,
LastName,
password: Password,
email: Email
});
const command = new UpdateUserCommand(parseInt(userId), userUpdateDto);
const updatedUser = await commandDispatcher.dispatch(command);
if (!updatedUser) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'User updated successfully',
data: updatedUser
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to update user'
});
}
}
@@ -0,0 +1,105 @@
import { Router } from 'express';
import { DataSource } from 'typeorm';
import { UserRepository } from '../Repositories/UserRepository';
import { UserQueryDispatcher } from '../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../functions/Users/Commands/UserCommandDispatcher';
import { auth } from '../middlewares/authentication';
// Import all router functions
import { createUser, authenticateUser,
getUserDetails, updateUser,
deleteUser, getAllUsers,
getUserBasic, checkUserExists,
checkUsernameExists, checkEmailExists,
getUsersCount, getUsersPaginated,
resetPassword, logoutUser, getCurrentUser } from '../RouterFunctions/Users/index';
export class UserRouter {
private router: Router;
private queryDispatcher: UserQueryDispatcher;
private commandDispatcher: UserCommandDispatcher;
constructor(dataSource: DataSource) {
this.router = Router();
const userRepository = new UserRepository(dataSource);
this.queryDispatcher = new UserQueryDispatcher(userRepository);
this.commandDispatcher = new UserCommandDispatcher(userRepository);
this.initializeRoutes();
}
private initializeRoutes(): void {
// PUBLIC ROUTES (No authentication required)
this.router.post('/create', (req, res) => {
createUser(req, res, this.queryDispatcher, this.commandDispatcher);
});
this.router.post('/authenticate', (req, res) =>
authenticateUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
// PROTECTED ROUTES (Authentication required)
this.router.post('/logout', auth, (req, res) =>
logoutUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.get('/current', auth, (req, res) =>
getCurrentUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.get('/details', auth, (req, res) =>
getUserDetails(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.put('/update', auth, (req, res) =>
updateUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.delete('/delete', auth, (req, res) =>
deleteUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
// Additional protected routes
this.router.get('/all', auth, (req, res) =>
getAllUsers(req, res, this.queryDispatcher, this.commandDispatcher)
);
// this.router.get('/basic/:id', auth, (req, res) =>
// getUserBasic(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/exists/:id', auth, (req, res) =>
// checkUserExists(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/username-exists/:username', auth, (req, res) =>
// checkUsernameExists(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/email-exists/:email', auth, (req, res) =>
// checkEmailExists(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/count', auth, (req, res) =>
// getUsersCount(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/paginated', auth, (req, res) =>
// getUsersPaginated(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.put('/reset-password/:id', auth, (req, res) =>
// resetPassword(req, res, this.queryDispatcher, this.commandDispatcher)
// );
}
public getRouter(): Router {
return this.router;
}
}
// Export a function to create the router
export default function createUserRouter(dataSource: DataSource): Router {
const userRouter = new UserRouter(dataSource);
return userRouter.getRouter();
}
-116
View File
@@ -1,116 +0,0 @@
//User class for the all the user related operations in the SerpentRace Backend
import { comparePasswords, hashPassword } from '../middleware/secure';
import { FindOptionsWhere, Equal } from 'typeorm';
import { AppDataSourceOne } from '../DB/Datasource';
import { User } from '../DB/user.entity';
import { Company } from '../DB/company.entity';
import { Request, Response } from 'express';
export class CompanyService {
private companyRepository = AppDataSourceOne.getRepository(Company);
async createCompany(company: Company): Promise<Company> {
return this.companyRepository.save(company);
}
private async findCompanyById(id: number): Promise<Company | null> {
const where: FindOptionsWhere<Company> = { CompanyId: Equal(id) };
return this.companyRepository.findOneBy(where);
}
private async findCompanyByName(name: string): Promise<Company | null> {
const where: FindOptionsWhere<Company> = { Name: Equal(name) };
return this.companyRepository.findOneBy(where);
}
async getCompanyDetails(req: Request, res: Response): Promise<void> {
const companyId = parseInt(req.params.companyid);
if (isNaN(companyId)) {
res.status(400).json({ message: 'Invalid company ID' });
return;
}
const company = await this.findCompanyById(companyId);
if (!company) {
res.status(404).json({ message: 'Company not found' });
return;
}
res.status(200).json(company);
}
async updateCompany(req: Request, res: Response): Promise<void> {
const companyId = parseInt(req.params.companyid);
if (isNaN(companyId)) {
res.status(400).json({ message: 'Invalid company ID' });
return;
}
const company = await this.findCompanyById(companyId);
if (!company) {
res.status(404).json({ message: 'Company not found' });
return;
}
const updatedData = req.body;
Object.assign(company, updatedData);
const updatedCompany = await this.companyRepository.save(company);
res.status(200).json(updatedCompany);
}
async deleteCompany(req: Request, res: Response): Promise<void> {
const companyId = parseInt(req.params.companyid);
if (isNaN(companyId)) {
res.status(400).json({ message: 'Invalid company ID' });
return;
}
const company = await this.findCompanyById(companyId);
if (!company) {
res.status(404).json({ message: 'Company not found' });
return;
}
await this.companyRepository.remove(company);
res.status(204).send();
}
private async addUserToCompany(userId: number, companyId: number): Promise<void> {
const user = await AppDataSourceOne.getRepository(User).findOneBy({ id: userId });
if (!user) {
throw new Error('User not found');
}
const company = await this.findCompanyById(companyId);
if (!company) {
throw new Error('Company not found');
}
user.CompanyId = companyId;
await AppDataSourceOne.getRepository(User).save(user);
// Optionally, you can also add the user to the company's user list if needed
if (!company.users) {
company.users = [];
}
company.users.push(user);
}
private async removeUserFromCompany(userId: number, companyId: number): Promise<void> {
const user = await AppDataSourceOne.getRepository(User).findOneBy({ id: userId });
if (!user) {
throw new Error('User not found');
}
const company = await this.findCompanyById(companyId);
if (!company) {
throw new Error('Company not found');
}
user.CompanyId = 0; // Remove association
user.CompanyToken = ''; // Remove association
await AppDataSourceOne.getRepository(User).save(user);
// Optionally, remove the user from the company's user list
company.users = company.users?.filter(u => u.id !== userId) || [];
await this.companyRepository.save(company);
}
}
-113
View File
@@ -1,113 +0,0 @@
//User class for the all the user related operations in the SerpentRace Backend
import { comparePasswords, hashPassword } from '../middleware/secure';
import { FindOptionsWhere, Equal } from 'typeorm';
import { AppDataSourceOne } from '../DB/Datasource';
import { createToken } from '../middleware/auth';
import { User } from '../DB/user.entity';
import { Request, Response } from 'express';
export class UserService {
private userRepository = AppDataSourceOne.getRepository(User);
async createUser(user : User): Promise<User> {
// Hash the password before saving the user
user.Password = await hashPassword(user.Password);
return this.userRepository.save(user);
}
private async findUserByEmail(email: string): Promise<User | null> {
const where: FindOptionsWhere<User> = { Email: Equal(email) };
return this.userRepository.findOneBy(where);
}
private async findUserByUsername(username: string): Promise<User | null> {
const where: FindOptionsWhere<User> = { Username: Equal(username) };
return this.userRepository.findOneBy(where);
}
private async findUserById(id: number): Promise<User | null> {
const where: FindOptionsWhere<User> = { id: Equal(id) };
return this.userRepository.findOneBy(where);
}
async authenticateUser(req: Request, res: Response): Promise<void> {
const { String_data, Password } = req.body;
if (!String_data || !Password) {
res.status(400).json({ message: 'Email and password are required' });
return;
}
// Check if String_data is an email or username
let user: User | null = null;
if (String_data.includes('@')) {
user = await this.findUserByEmail(String_data.toLowerCase());
}
else {
user = await this.findUserByUsername(String_data);
}
// If user is not found or password does not match, return 401
if (!user || !(await comparePasswords(Password, user.Password))) {
res.status(401).json({ message: 'Invalid credentials' });
return;
}
const token = createToken(user);
// Set the token in the response header
res.cookie("jwt", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 1000
});
res.status(200).json({ message: 'Authentication successful' });
}
async getUserDetails(req: Request, res: Response): Promise<void> {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
res.json(user);
}
async updateUser(req: Request, res: Response): Promise<void> {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await this.findUserById(userId);
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
Object.assign(user, req.body);
if (req.body.Password) {
user.Password = await hashPassword(req.body.Password);
}
await this.userRepository.save(user);
res.json({ message: 'User details updated successfully' });
}
async deleteUser(req: Request, res: Response): Promise<void> {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await this.findUserById(userId);
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
await this.userRepository.remove(user);
res.json({ message: 'User deleted successfully' });
}
}
@@ -1,58 +0,0 @@
//Router for user-related routes
import { Router } from 'express';
import { UserService } from './User';
import { auth } from '../middleware/auth';
import { User } from '../DB/user.entity';
const userRouter = Router();
const userService = new UserService();
// Route to create a new user
userRouter.post('/create', async (req, res) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json({ message: 'User created successfully'});
} catch (error) {
res.status(500).json({ message: 'Error creating user', error });
}
});
// Route to authenticate a user
userRouter.post('/authenticate', async (req, res) => {
try {
await userService.authenticateUser(req, res);
} catch (error) {
res.status(500).json({ message: 'Error authenticating user', error });
}
});
// Route to get user details (protected route)
userRouter.get('/details', auth, async (req, res) => {
try {
await userService.getUserDetails(req, res);
} catch (error) {
res.status(500).json({ message: 'Error fetching user details', error });
}
});
// Route to update user details (protected route)
userRouter.put('/update', auth, async (req, res) =>{
try{
await userService.updateUser(req, res);
}
catch (error) {
res.status(500).json({ message: 'Error updating user', error });
}
});
// Route to delete a user (protected route)
userRouter.delete('/delete', auth, async (req, res) => {
try {
await userService.deleteUser(req, res);
} catch (error) {
res.status(500).json({ message: 'Error deleting user', error });
}
});
//export default userRouter;
export { userRouter };
@@ -1,9 +0,0 @@
import Express from 'express';
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
@@ -0,0 +1,19 @@
import { CompanyCreateDto, CompanyUpdateDto } from '../../../Database/dto/company.dto';
// Create company command
export class CreateCompanyCommand {
constructor(public readonly companyCreateDto: CompanyCreateDto) {}
}
// Update company command
export class UpdateCompanyCommand {
constructor(
public readonly id: number,
public readonly companyUpdateDto: CompanyUpdateDto
) {}
}
// Delete company command
export class DeleteCompanyCommand {
constructor(public readonly id: number) {}
}
@@ -0,0 +1,52 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyCommandHandler } from './CompanyCommandHandler';
import {
CreateCompanyCommand,
UpdateCompanyCommand,
DeleteCompanyCommand
} from './CompanyCommand';
import { appLogger } from '../../../utils/logger';
export class CompanyCommandDispatcher {
private commandHandler: CompanyCommandHandler;
constructor(companyRepository: ICompanyRepository) {
appLogger.startup('Initializing CompanyCommandDispatcher...');
this.commandHandler = new CompanyCommandHandler(companyRepository);
appLogger.startup('CompanyCommandDispatcher initialized');
}
async dispatch(command: any): Promise<any> {
try {
appLogger.info('Dispatching company command', {
commandType: command.constructor.name,
action: 'dispatch_company_command'
});
switch (command.constructor) {
case CreateCompanyCommand:
return await this.commandHandler.handleCreate(command);
case UpdateCompanyCommand:
return await this.commandHandler.handleUpdate(command);
case DeleteCompanyCommand:
return await this.commandHandler.handleDelete(command);
default:
const errorMessage = `Unknown command type: ${command.constructor.name}`;
appLogger.errorEvent('Unknown company command type', 'dispatch_error', {
commandType: command.constructor.name
});
throw new Error(errorMessage);
}
} catch (error: any) {
appLogger.errorEvent('Error dispatching company command', 'dispatch_error', {
error: error.message,
commandType: command.constructor.name
});
throw error;
}
}
}
@@ -0,0 +1,126 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyResponseDto } from '../../../Database/dto/company.dto';
import {
CreateCompanyCommand,
UpdateCompanyCommand,
DeleteCompanyCommand
} from './CompanyCommand';
import { appLogger } from '../../../utils/logger';
export class CompanyCommandHandler {
constructor(private readonly companyRepository: ICompanyRepository) {
appLogger.info('CompanyCommandHandler initialized');
}
// Handle create company
async handleCreate(command: CreateCompanyCommand): Promise<CompanyResponseDto> {
try {
const { companyCreateDto } = command;
appLogger.info('Creating company', {
name: companyCreateDto.Name,
contactEmail: companyCreateDto.ContactEmail,
action: 'create_company'
});
// Validate required fields
if (!companyCreateDto.Name || !companyCreateDto.ContactFirstName ||
!companyCreateDto.ContactLastName || !companyCreateDto.ContactEmail ||
!companyCreateDto.FirstAPI || !companyCreateDto.TokenAPI) {
throw new Error('All company fields are required: Name, ContactFirstName, ContactLastName, ContactEmail, FirstAPI, TokenAPI');
}
const result = await this.companyRepository.create(companyCreateDto);
appLogger.info('Company created successfully', {
companyId: result.CompanyId,
name: result.Name,
contactEmail: result.ContactEmail
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error creating company', 'company_creation', {
error: error.message,
companyData: {
name: command.companyCreateDto?.Name,
contactEmail: command.companyCreateDto?.ContactEmail
}
});
throw new Error(`Failed to create company: ${error.message}`);
}
}
// Handle update company
async handleUpdate(command: UpdateCompanyCommand): Promise<CompanyResponseDto | null> {
try {
const { id, companyUpdateDto } = command;
appLogger.info('Updating company', {
companyId: id,
updateFields: Object.keys(companyUpdateDto),
action: 'update_company'
});
const result = await this.companyRepository.update(id, companyUpdateDto);
if (!result) {
appLogger.warn('Company not found for update', {
companyId: id
});
throw new Error(`Company with ID ${id} not found`);
}
appLogger.info('Company updated successfully', {
companyId: result.CompanyId,
name: result.Name,
updatedFields: Object.keys(companyUpdateDto)
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error updating company', 'company_update', {
error: error.message,
companyId: command.id,
updateData: command.companyUpdateDto
});
throw new Error(`Failed to update company: ${error.message}`);
}
}
// Handle delete company
async handleDelete(command: DeleteCompanyCommand): Promise<boolean> {
try {
const { id } = command;
appLogger.info('Deleting company', {
companyId: id,
action: 'delete_company'
});
const result = await this.companyRepository.deleteById(id);
if (!result) {
appLogger.warn('Company not found for deletion', {
companyId: id
});
throw new Error(`Company with ID ${id} not found`);
}
appLogger.info('Company deleted successfully', {
companyId: id
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error deleting company', 'company_deletion', {
error: error.message,
companyId: command.id
});
throw new Error(`Failed to delete company: ${error.message}`);
}
}
}
@@ -0,0 +1,26 @@
// Company Query Classes
// Get company by ID
export class GetCompanyByIdQuery {
constructor(public readonly id: number) {}
}
// Get company by name
export class GetCompanyByNameQuery {
constructor(public readonly name: string) {}
}
// Get all companies
export class GetAllCompaniesQuery {
constructor() {}
}
// Get basic company by ID
export class GetBasicCompanyByIdQuery {
constructor(public readonly id: number) {}
}
// Get all basic companies
export class GetAllBasicCompaniesQuery {
constructor() {}
}
@@ -0,0 +1,60 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyQueryHandler } from './CompanyQueryHandler';
import {
GetCompanyByIdQuery,
GetCompanyByNameQuery,
GetAllCompaniesQuery,
GetBasicCompanyByIdQuery,
GetAllBasicCompaniesQuery
} from './CompanyQuery';
import { appLogger } from '../../../utils/logger';
export class CompanyQueryDispatcher {
private queryHandler: CompanyQueryHandler;
constructor(companyRepository: ICompanyRepository) {
appLogger.startup('Initializing CompanyQueryDispatcher...');
this.queryHandler = new CompanyQueryHandler(companyRepository);
appLogger.startup('CompanyQueryDispatcher initialized');
}
async dispatch(query: any): Promise<any> {
try {
appLogger.info('Dispatching company query', {
queryType: query.constructor.name,
action: 'dispatch_company_query'
});
switch (query.constructor) {
case GetCompanyByIdQuery:
return await this.queryHandler.handleGetById(query);
case GetCompanyByNameQuery:
return await this.queryHandler.handleGetByName(query);
case GetAllCompaniesQuery:
return await this.queryHandler.handleGetAll(query);
case GetBasicCompanyByIdQuery:
return await this.queryHandler.handleGetBasicById(query);
case GetAllBasicCompaniesQuery:
return await this.queryHandler.handleGetAllBasic(query);
default:
const errorMessage = `Unknown query type: ${query.constructor.name}`;
appLogger.errorEvent('Unknown company query type', 'dispatch_error', {
queryType: query.constructor.name
});
throw new Error(errorMessage);
}
} catch (error: any) {
appLogger.errorEvent('Error dispatching company query', 'dispatch_error', {
error: error.message,
queryType: query.constructor.name
});
throw error;
}
}
}
@@ -0,0 +1,176 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyResponseDto, CompanyBasicDto } from '../../../Database/dto/company.dto';
import {
GetCompanyByIdQuery,
GetCompanyByNameQuery,
GetAllCompaniesQuery,
GetBasicCompanyByIdQuery,
GetAllBasicCompaniesQuery
} from './CompanyQuery';
import { appLogger } from '../../../utils/logger';
export class CompanyQueryHandler {
constructor(private readonly companyRepository: ICompanyRepository) {
appLogger.info('CompanyQueryHandler initialized');
}
// Get company by ID
async handleGetById(query: GetCompanyByIdQuery): Promise<CompanyResponseDto | null> {
try {
appLogger.info('Getting company by ID', {
companyId: query.id,
action: 'get_company_by_id'
});
// Validate ID
if (!query.id || query.id <= 0) {
throw new Error('Valid company ID is required');
}
const result = await this.companyRepository.findById(query.id);
if (!result) {
appLogger.warn('Company not found', {
companyId: query.id
});
return null;
}
appLogger.info('Company found successfully', {
companyId: result.CompanyId,
name: result.Name
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting company by ID', 'company_query', {
error: error.message,
companyId: query.id
});
throw new Error(`Failed to get company by ID: ${error.message}`);
}
}
// Get company by name
async handleGetByName(query: GetCompanyByNameQuery): Promise<CompanyResponseDto | null> {
try {
appLogger.info('Getting company by name', {
companyName: query.name,
action: 'get_company_by_name'
});
// Validate name
if (!query.name || query.name.trim() === '') {
throw new Error('Valid company name is required');
}
const result = await this.companyRepository.findByName(query.name.trim());
if (!result) {
appLogger.warn('Company not found by name', {
companyName: query.name
});
return null;
}
appLogger.info('Company found by name', {
companyId: result.CompanyId,
name: result.Name
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting company by name', 'company_query', {
error: error.message,
companyName: query.name
});
throw new Error(`Failed to get company by name: ${error.message}`);
}
}
// Get all companies
async handleGetAll(query: GetAllCompaniesQuery): Promise<CompanyResponseDto[]> {
try {
appLogger.info('Getting all companies', {
action: 'get_all_companies'
});
const result = await this.companyRepository.findAll();
appLogger.info('Companies retrieved successfully', {
count: result.length
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting all companies', 'company_query', {
error: error.message
});
throw new Error(`Failed to get all companies: ${error.message}`);
}
}
// Get basic company by ID
async handleGetBasicById(query: GetBasicCompanyByIdQuery): Promise<CompanyBasicDto | null> {
try {
appLogger.info('Getting basic company by ID', {
companyId: query.id,
action: 'get_basic_company_by_id'
});
// Validate ID
if (!query.id || query.id <= 0) {
throw new Error('Valid company ID is required');
}
const result = await this.companyRepository.findBasicById(query.id);
if (!result) {
appLogger.warn('Basic company not found', {
companyId: query.id
});
return null;
}
appLogger.info('Basic company found successfully', {
companyId: result.CompanyId,
name: result.Name
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting basic company by ID', 'company_query', {
error: error.message,
companyId: query.id
});
throw new Error(`Failed to get basic company by ID: ${error.message}`);
}
}
// Get all basic companies
async handleGetAllBasic(query: GetAllBasicCompaniesQuery): Promise<CompanyBasicDto[]> {
try {
appLogger.info('Getting all basic companies', {
action: 'get_all_basic_companies'
});
const result = await this.companyRepository.findAllBasic();
appLogger.info('Basic companies retrieved successfully', {
count: result.length
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting all basic companies', 'company_query', {
error: error.message
});
throw new Error(`Failed to get all basic companies: ${error.message}`);
}
}
}
@@ -0,0 +1,46 @@
import { UserCreateDto, UserUpdateDto } from '../../../Database/dto/user.dto';
// Create user command
export class CreateUserCommand {
constructor(public readonly userCreateDto: UserCreateDto) {}
}
// Update user command
export class UpdateUserCommand {
constructor(
public readonly id: number,
public readonly userUpdateDto: UserUpdateDto
) {}
}
// Delete user command
export class DeleteUserCommand {
constructor(public readonly id: number) {}
}
// Delete multiple users command
export class DeleteUsersCommand {
constructor(public readonly ids: number[]) {}
}
// Create multiple users command
export class CreateUsersCommand {
constructor(public readonly userCreateDtos: UserCreateDto[]) {}
}
// Update user password command
export class UpdateUserPasswordCommand {
constructor(
public readonly userId: number,
public readonly currentPassword: string,
public readonly newPassword: string
) {}
}
// Reset user password command (admin only)
export class ResetUserPasswordCommand {
constructor(
public readonly userId: number,
public readonly newPassword: string
) {}
}
@@ -0,0 +1,47 @@
import { UserCommandHandler } from './UserCommandHandler';
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import {
CreateUserCommand,
UpdateUserCommand,
DeleteUserCommand,
DeleteUsersCommand,
CreateUsersCommand,
UpdateUserPasswordCommand,
ResetUserPasswordCommand
} from './UserCommand';
export class UserCommandDispatcher {
private commandHandler: UserCommandHandler;
constructor(userRepository: IUserRepository) {
this.commandHandler = new UserCommandHandler(userRepository);
}
async dispatch(command: any): Promise<any> {
switch (command.constructor) {
case CreateUserCommand:
return await this.commandHandler.handleCreate(command);
case UpdateUserCommand:
return await this.commandHandler.handleUpdate(command);
case DeleteUserCommand:
return await this.commandHandler.handleDelete(command);
case DeleteUsersCommand:
return await this.commandHandler.handleDeleteUsers(command);
case CreateUsersCommand:
return await this.commandHandler.handleCreateUsers(command);
case UpdateUserPasswordCommand:
return await this.commandHandler.handleUpdatePassword(command);
case ResetUserPasswordCommand:
return await this.commandHandler.handleResetPassword(command);
default:
throw new Error(`Unknown command type: ${command.constructor.name}`);
}
}
}
@@ -0,0 +1,205 @@
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import { UserResponseDto, UsersListResponseDto, UserCreateDto, UserUpdateDto } from '../../../Database/dto/user.dto';
import { hashPassword, comparePasswords } from '../../../middlewares/security';
import {
CreateUserCommand,
UpdateUserCommand,
DeleteUserCommand,
DeleteUsersCommand,
CreateUsersCommand,
UpdateUserPasswordCommand,
ResetUserPasswordCommand
} from './UserCommand';
export class UserCommandHandler {
constructor(private readonly userRepository: IUserRepository) {}
// Create user
async handleCreate(command: CreateUserCommand): Promise<UserResponseDto> {
// Check if username already exists
const usernameExists = await this.userRepository.usernameExists(command.userCreateDto.username);
if (usernameExists) {
throw new Error('Username already exists');
}
// Check if email already exists
const emailExists = await this.userRepository.emailExists(command.userCreateDto.email);
if (emailExists) {
throw new Error('Email already exists');
}
console.log('Creating user with DTO:', command.userCreateDto);
// Hash password using security middleware
const hashedPassword = await hashPassword(command.userCreateDto.password);
console.log('Hashed password:', hashedPassword);
const userWithHashedPassword = new UserCreateDto(
command.userCreateDto.username,
command.userCreateDto.FirstName,
command.userCreateDto.LastName,
command.userCreateDto.email,
hashedPassword,
command.userCreateDto.CompanyId // Pass the optional CompanyId
);
console.log('Creating user with hashed password:', userWithHashedPassword);
return await this.userRepository.create(userWithHashedPassword);
}
// Update user
async handleUpdate(command: UpdateUserCommand): Promise<UserResponseDto | null> {
// Check if user exists
const userExists = await this.userRepository.exists(command.id);
if (!userExists) {
throw new Error('User not found');
}
// If updating username, check if it's already taken by another user
if (command.userUpdateDto.username) {
const existingUser = await this.userRepository.findByUsername(command.userUpdateDto.username);
if (existingUser && existingUser.id !== command.id) {
throw new Error('Username already exists');
}
}
// If updating email, check if it's already taken by another user
if (command.userUpdateDto.email) {
const existingUser = await this.userRepository.findByEmail(command.userUpdateDto.email);
if (existingUser && existingUser.id !== command.id) {
throw new Error('Email already exists');
}
}
// Create new DTO with hashed password if password is being updated
let updateDto = command.userUpdateDto;
if (command.userUpdateDto.password) {
const hashedPassword = await hashPassword(command.userUpdateDto.password);
updateDto = new UserUpdateDto({
...command.userUpdateDto,
password: hashedPassword
});
}
return await this.userRepository.update(command.id, updateDto);
}
// Delete user
async handleDelete(command: DeleteUserCommand): Promise<boolean> {
const userExists = await this.userRepository.exists(command.id);
if (!userExists) {
throw new Error('User not found');
}
return await this.userRepository.deleteById(command.id);
}
// Delete multiple users
async handleDeleteUsers(command: DeleteUsersCommand): Promise<boolean> {
if (command.ids.length === 0) {
throw new Error('No user IDs provided');
}
// Check if all users exist
for (const id of command.ids) {
const userExists = await this.userRepository.exists(id);
if (!userExists) {
throw new Error(`User with ID ${id} not found`);
}
}
return await this.userRepository.deleteByIds(command.ids);
}
// Create multiple users
async handleCreateUsers(command: CreateUsersCommand): Promise<UsersListResponseDto> {
if (command.userCreateDtos.length === 0) {
throw new Error('No users provided');
}
// Check for duplicate usernames and emails within the batch
const usernames = command.userCreateDtos.map(dto => dto.username);
const emails = command.userCreateDtos.map(dto => dto.email);
const uniqueUsernames = new Set(usernames);
const uniqueEmails = new Set(emails);
if (uniqueUsernames.size !== usernames.length) {
throw new Error('Duplicate usernames in batch');
}
if (uniqueEmails.size !== emails.length) {
throw new Error('Duplicate emails in batch');
}
// Check if any usernames or emails already exist
for (const dto of command.userCreateDtos) {
const usernameExists = await this.userRepository.usernameExists(dto.username);
if (usernameExists) {
throw new Error(`Username ${dto.username} already exists`);
}
const emailExists = await this.userRepository.emailExists(dto.email);
if (emailExists) {
throw new Error(`Email ${dto.email} already exists`);
}
}
// Hash all passwords using security middleware
const usersWithHashedPasswords = await Promise.all(
command.userCreateDtos.map(async (dto) => {
const hashedPassword = await hashPassword(dto.password);
return new UserCreateDto(
dto.username,
dto.FirstName,
dto.LastName,
dto.email,
hashedPassword
);
})
);
return await this.userRepository.createMany(usersWithHashedPasswords);
}
// Update user password (with current password verification)
async handleUpdatePassword(command: UpdateUserPasswordCommand): Promise<UserResponseDto | null> {
// Get raw user entity for password verification
const user = await this.userRepository.findRawById(command.userId);
if (!user) {
throw new Error('User not found');
}
// Verify current password using security middleware
const isCurrentPasswordValid = await comparePasswords(command.currentPassword, user.password);
if (!isCurrentPasswordValid) {
throw new Error('Current password is incorrect');
}
// Hash new password using security middleware
const hashedNewPassword = await hashPassword(command.newPassword);
const updateDto = new UserUpdateDto({
password: hashedNewPassword
});
return await this.userRepository.update(command.userId, updateDto);
}
// Reset user password (admin only - no current password verification)
async handleResetPassword(command: ResetUserPasswordCommand): Promise<UserResponseDto | null> {
const userExists = await this.userRepository.exists(command.userId);
if (!userExists) {
throw new Error('User not found');
}
// Hash new password using security middleware
const hashedNewPassword = await hashPassword(command.newPassword);
const updateDto = new UserUpdateDto({
password: hashedNewPassword
});
return await this.userRepository.update(command.userId, updateDto);
}
}
@@ -0,0 +1,122 @@
import { UserBasicDto, UserResponseDto, UsersListResponseDto } from '../../../Database/dto/user.dto';
// Get user by ID
export class GetUserByIdQuery {
constructor(public readonly id: number) {}
}
// Get user by username
export class GetUserByUsernameQuery {
constructor(public readonly username: string) {}
}
// Get user by email
export class GetUserByEmailQuery {
constructor(public readonly email: string) {}
}
// Get all users
export class GetAllUsersQuery {
constructor() {}
}
// Get users by company
// export class GetUsersByCompanyQuery {
// constructor(public readonly companyId: number) {}
// }
// Get users by company token
// export class GetUsersByCompanyTokenQuery {
// constructor(public readonly companyToken: string) {}
// }
// Search users by partial username
export class SearchUsersByUsernameQuery {
constructor(public readonly partialUsername: string) {}
}
// Search users by partial name
// export class SearchUsersByNameQuery {
// constructor(public readonly partialName: string) {}
// }
// Get users with pagination
// export class GetUsersWithPaginationQuery {
// constructor(
// public readonly page: number,
// public readonly limit: number
// ) {}
// }
// Get basic user info by ID
export class GetUserBasicByIdQuery {
constructor(public readonly id: number) {}
}
// Get all users basic info
export class GetAllUsersBasicQuery {
constructor() {}
}
// Get user with company relation
export class GetUserWithCompanyQuery {
constructor(public readonly id: number) {}
}
// Get user with question banks relation
export class GetUserWithQuestionBanksQuery {
constructor(public readonly id: number) {}
}
// Get user with all relations
// export class GetUserWithAllRelationsQuery {
// constructor(public readonly id: number) {}
// }
// Get users count
// export class GetUsersCountQuery {
// constructor() {}
// }
// Get users count by company
// export class GetUsersCountByCompanyQuery {
// constructor(public readonly companyId: number) {}
// }
// Get users by date range
// export class GetUsersByDateRangeQuery {
// constructor(
// public readonly startDate: Date,
// public readonly endDate: Date
// ) {}
// }
// Check if user exists
export class UserExistsQuery {
constructor(public readonly id: number) {}
}
// Check if username exists
export class UsernameExistsQuery {
constructor(public readonly username: string) {}
}
// Check if email exists
export class EmailExistsQuery {
constructor(public readonly email: string) {}
}
// Get raw user by username (for authentication)
export class GetRawUserByUsernameQuery {
constructor(public readonly username: string) {}
}
// Get raw user by email (for authentication)
export class GetRawUserByEmailQuery {
constructor(public readonly email: string) {}
}
// Get raw user by ID (for authentication)
// export class GetRawUserByIdQuery {
// constructor(public readonly id: number) {}
// }
@@ -0,0 +1,111 @@
import { UserQueryHandler } from './UserQueryHandler';
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import {
GetUserByIdQuery,
GetUserByUsernameQuery,
GetUserByEmailQuery,
GetAllUsersQuery,
// GetUsersByCompanyQuery,
// GetUsersByCompanyTokenQuery,
SearchUsersByUsernameQuery,
// SearchUsersByNameQuery,
// GetUsersWithPaginationQuery,
GetUserBasicByIdQuery,
GetAllUsersBasicQuery,
GetUserWithCompanyQuery,
GetUserWithQuestionBanksQuery,
// GetUserWithAllRelationsQuery,
// GetUsersCountQuery,
// GetUsersCountByCompanyQuery,
// GetUsersByDateRangeQuery,
UserExistsQuery,
UsernameExistsQuery,
EmailExistsQuery,
GetRawUserByUsernameQuery,
GetRawUserByEmailQuery,
// GetRawUserByIdQuery
} from './UserQuery';
export class UserQueryDispatcher {
private queryHandler: UserQueryHandler;
constructor(userRepository: IUserRepository) {
this.queryHandler = new UserQueryHandler(userRepository);
}
async dispatch(query: any): Promise<any> {
switch (query.constructor) {
case GetUserByIdQuery:
return await this.queryHandler.handle(query);
// case GetUserByUsernameQuery:
// return await this.queryHandler.handleGetByUsername(query);
case GetUserByEmailQuery:
return await this.queryHandler.handleGetByEmail(query);
case GetAllUsersQuery:
return await this.queryHandler.handleGetAll(query);
// case GetUsersByCompanyQuery:
// return await this.queryHandler.handleGetByCompany(query);
// case GetUsersByCompanyTokenQuery:
// return await this.queryHandler.handleGetByCompanyToken(query);
// case SearchUsersByUsernameQuery:
// return await this.queryHandler.handleSearchByUsername(query);
// case SearchUsersByNameQuery:
// return await this.queryHandler.handleSearchByName(query);
// case GetUsersWithPaginationQuery:
// return await this.queryHandler.handleGetWithPagination(query);
// case GetUserBasicByIdQuery:
// return await this.queryHandler.handleGetBasicById(query);
case GetAllUsersBasicQuery:
return await this.queryHandler.handleGetAllBasic(query);
case GetUserWithCompanyQuery:
return await this.queryHandler.handleGetWithCompany(query);
case GetUserWithQuestionBanksQuery:
return await this.queryHandler.handleGetWithQuestionBanks(query);
// case GetUserWithAllRelationsQuery:
// return await this.queryHandler.handleGetWithAllRelations(query);
// case GetUsersCountQuery:
// return await this.queryHandler.handleGetCount(query);
// case GetUsersCountByCompanyQuery:
// return await this.queryHandler.handleGetCountByCompany(query);
// case GetUsersByDateRangeQuery:
// return await this.queryHandler.handleGetByDateRange(query);
// case UserExistsQuery:
// return await this.queryHandler.handleUserExists(query);
case UsernameExistsQuery:
return await this.queryHandler.handleUsernameExists(query);
case EmailExistsQuery:
return await this.queryHandler.handleEmailExists(query);
case GetRawUserByUsernameQuery:
return await this.queryHandler.handleGetRawByUsername(query);
case GetRawUserByEmailQuery:
return await this.queryHandler.handleGetRawByEmail(query);
// case GetRawUserByIdQuery:
// return await this.queryHandler.handleGetRawById(query);
default:
throw new Error(`Unknown query type: ${query.constructor.name}`);
}
}
}
@@ -0,0 +1,148 @@
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import { UserBasicDto, UserResponseDto, UsersListResponseDto } from '../../../Database/dto/user.dto';
import { User } from '../../../Database/entities/user.entity';
import {
GetUserByIdQuery,
GetUserByUsernameQuery,
GetUserByEmailQuery,
GetAllUsersQuery,
// GetUsersByCompanyQuery,
// GetUsersByCompanyTokenQuery,
SearchUsersByUsernameQuery,
// SearchUsersByNameQuery,
// GetUsersWithPaginationQuery,
GetUserBasicByIdQuery,
GetAllUsersBasicQuery,
GetUserWithCompanyQuery,
GetUserWithQuestionBanksQuery,
// GetUserWithAllRelationsQuery,
// GetUsersCountQuery,
// GetUsersCountByCompanyQuery,
// GetUsersByDateRangeQuery,
UserExistsQuery,
UsernameExistsQuery,
EmailExistsQuery,
GetRawUserByUsernameQuery,
GetRawUserByEmailQuery,
// GetRawUserByIdQuery
} from './UserQuery';
export class UserQueryHandler {
constructor(private readonly userRepository: IUserRepository) {}
// Get user by ID
async handle(query: GetUserByIdQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findById(query.id);
}
// Get user by username
// async handleGetByUsername(query: GetUserByUsernameQuery): Promise<UserResponseDto | null> {
// return await this.userRepository.findByUsername(query.username);
// }
// Get user by email
async handleGetByEmail(query: GetUserByEmailQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findByEmail(query.email);
}
// Get all users
async handleGetAll(query: GetAllUsersQuery): Promise<UsersListResponseDto> {
return await this.userRepository.findAll();
}
// Get users by company
// async handleGetByCompany(query: GetUsersByCompanyQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByCompanyId(query.companyId);
// }
// Get users by company token
// async handleGetByCompanyToken(query: GetUsersByCompanyTokenQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByCompanyToken(query.companyToken);
// }
// Search users by partial username
// async handleSearchByUsername(query: SearchUsersByUsernameQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByPartialUsername(query.partialUsername);
// }
// Search users by partial name
// async handleSearchByName(query: SearchUsersByNameQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByPartialName(query.partialName);
// }
// Get users with pagination
// async handleGetWithPagination(query: GetUsersWithPaginationQuery): Promise<{ users: UsersListResponseDto, total: number }> {
// const skip = (query.page - 1) * query.limit;
// return await this.userRepository.findWithPagination(skip, query.limit);
// }
// Get basic user info by ID
// async handleGetBasicById(query: GetUserBasicByIdQuery): Promise<UserBasicDto | null> {
// return await this.userRepository.findBasicById(query.id);
// }
// Get all users basic info
async handleGetAllBasic(query: GetAllUsersBasicQuery): Promise<UserBasicDto[]> {
return await this.userRepository.findAllBasic();
}
// Get user with company relation
async handleGetWithCompany(query: GetUserWithCompanyQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findWithCompany(query.id);
}
// Get user with question banks relation
async handleGetWithQuestionBanks(query: GetUserWithQuestionBanksQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findWithQuestionBanks(query.id);
}
// Get user with all relations
// async handleGetWithAllRelations(query: GetUserWithAllRelationsQuery): Promise<UserResponseDto | null> {
// return await this.userRepository.findWithAllRelations(query.id);
// }
// Get users count
// async handleGetCount(query: GetUsersCountQuery): Promise<number> {
// return await this.userRepository.count();
// }
// Get users count by company
// async handleGetCountByCompany(query: GetUsersCountByCompanyQuery): Promise<number> {
// return await this.userRepository.countByCompany(query.companyId);
// }
// Get users by date range
// async handleGetByDateRange(query: GetUsersByDateRangeQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByDateRange(query.startDate, query.endDate);
// }
// Check if user exists
async handleUserExists(query: UserExistsQuery): Promise<boolean> {
return await this.userRepository.exists(query.id);
}
// Check if username exists
async handleUsernameExists(query: UsernameExistsQuery): Promise<boolean> {
return await this.userRepository.usernameExists(query.username);
}
// Check if email exists
async handleEmailExists(query: EmailExistsQuery): Promise<boolean> {
return await this.userRepository.emailExists(query.email);
}
// Get raw user by username (for password verification)
async handleGetRawByUsername(query: GetRawUserByUsernameQuery): Promise<User | null> {
return await this.userRepository.findRawByUsername(query.username);
}
// Get raw user by email (for password verification)
async handleGetRawByEmail(query: GetRawUserByEmailQuery): Promise<User | null> {
return await this.userRepository.findRawByEmail(query.email);
}
// Get raw user by ID (for password verification)
// async handleGetRawById(query: GetRawUserByIdQuery): Promise<User | null> {
// return await this.userRepository.findRawById(query.id);
// }
}
+201 -18
View File
@@ -1,23 +1,206 @@
console.log(process.env.DATABASE_URL_1 as string);
import express from 'express';
import cors from "cors";
import cookieParser from "cookie-parser";
import { mainRouter } from './router';
import { AppDataSourceOne } from './DB/Datasource';
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import compression from 'compression';
import dotenv from 'dotenv';
import { AppDataSource } from '../ormconfig';
import { authErrorHandler } from './middlewares/authErrorHandler';
import createUserRouter from './Routers/UserRouter';
import { appLogger, initializeMinIOBucket } from './utils/logger';
// Load environment variables
dotenv.config();
const app = express();
// Initialize the database connection
AppDataSourceOne.initialize();
class App {
public readonly app: Application = express();
private readonly port: number = parseInt(process.env.PORT || '3000');
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));
app.use(cookieParser());
constructor() {
// Initialize application asynchronously
this.initializeApplication().catch((error) => {
appLogger.errorEvent('Application startup failed', 'startup', { error });
process.exit(1);
});
}
app.use('/', mainRouter);
private async initializeApplication(): Promise<void> {
appLogger.startup('🚀 Starting SerpentRace Backend');
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
try {
// Initialize core services
await this.initializeMinIO();
await this.initializeDatabase();
// Setup application
this.setupMiddlewares();
this.setupRoutes();
this.setupErrorHandling();
appLogger.startup('✅ Application ready');
} catch (error) {
appLogger.errorEvent('Initialization failed', 'startup', { error });
throw error;
}
}
private async initializeMinIO(): Promise<void> {
try {
await initializeMinIOBucket();
appLogger.startup('MinIO initialized');
} catch (error) {
appLogger.errorEvent('MinIO failed', 'minio', { error });
throw error;
}
}
private async initializeDatabase(): Promise<void> {
try {
await AppDataSource.initialize();
appLogger.startup('Database connected');
} catch (error) {
appLogger.errorEvent('Database failed', 'database', { error });
throw error;
}
}
private setupMiddlewares(): void {
// Security
this.app.use(helmet({
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
// CORS
this.app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Body parsing
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
this.app.use(cookieParser());
this.app.use(compression());
appLogger.startup('Middlewares loaded');
}
private setupRoutes(): void {
// User routes - Mount on both paths for compatibility
const userRouter = createUserRouter(AppDataSource);
this.app.use('/user', userRouter);
// 404 handler
this.app.use('*', (req: Request, res: Response) => {
appLogger.security('Route not found', 'low', {
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
res.status(404).json({
success: false,
message: 'Route not found'
});
});
appLogger.startup('Routes configured');
}
private setupErrorHandling(): void {
// Auth error handler
this.app.use(authErrorHandler);
// General error handler
this.app.use((err: any, req: Request, res: Response, next: NextFunction) => {
appLogger.errorEvent('Application error', 'express', {
message: err.message,
url: req.url,
method: req.method,
ip: req.ip
});
res.status(err.status || 500).json({
success: false,
message: err.message || 'Internal Server Error',
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
});
// Process error handlers (set only once)
if (!process.listenerCount('unhandledRejection')) {
process.on('unhandledRejection', (reason: any) => {
appLogger.errorEvent('Unhandled rejection', 'process', { reason });
this.shutdown('Unhandled Rejection');
});
}
if (!process.listenerCount('uncaughtException')) {
process.on('uncaughtException', (error: Error) => {
appLogger.errorEvent('Uncaught exception', 'process', { error: error.message });
this.shutdown('Uncaught Exception');
});
}
if (!process.listenerCount('SIGTERM')) {
process.on('SIGTERM', () => this.shutdown('SIGTERM'));
}
if (!process.listenerCount('SIGINT')) {
process.on('SIGINT', () => this.shutdown('SIGINT'));
}
appLogger.startup('Error handling configured');
}
private async shutdown(signal: string): Promise<void> {
appLogger.system(`Shutting down (${signal})`);
try {
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
}
appLogger.system('Shutdown complete');
process.exit(0);
} catch (error) {
appLogger.errorEvent('Shutdown error', 'shutdown', { error });
process.exit(1);
}
}
public listen(): void {
this.app.listen(this.port, () => {
appLogger.startup(`🎉 Server running on port ${this.port}`, {
port: this.port,
environment: process.env.NODE_ENV || 'development',
urls: {
users: `http://localhost:${this.port}/api/user`,
}
});
console.log(`
🚀 SerpentRace Backend Server
📡 Port: ${this.port}
🔗 Users: http://localhost:${this.port}/user
`);
});
}
}
// Create and start application
const app = new App();
app.listen();
export default app.app;
@@ -1,47 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export function auth(req: Request, res: Response, next: NextFunction): void {
// Read token from cookie named 'jwt'
const token = req.cookies.jwt;
if (!token) {
res.status(401).json({ message: "No token provided" });
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || "secret");
if (typeof decoded === "string") {
res.status(401).json({ message: "Invalid token payload" });
return;
}
req.user = decoded;
// Check if expiring soon & refresh if needed
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp - now < 60 * 5) {
const { iat, exp, ...payload } = decoded;
const newToken = createToken(payload);
res.cookie("jwt", newToken, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 1000, // 1 hour
});
}
next();
} catch (err) {
res.status(401).json({ message: "Invalid token" });
return;
}
}
export function createToken(user: any): string {
return jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET || 'secret', {
expiresIn: '1h',
});
}
@@ -0,0 +1,20 @@
import { Request, Response, NextFunction } from 'express';
import { clearAuthCookie } from './authentication';
// Middleware to handle authentication errors globally
export function authErrorHandler(err: any, req: Request, res: Response, next: NextFunction): void {
// Check if it's an authentication-related error
if (err.name === 'UnauthorizedError' || err.status === 401) {
clearAuthCookie(res);
res.status(401).json({
success: false,
message: 'Authentication failed',
autoLogout: true
});
return;
}
// Pass other errors to the next error handler
next(err);
}
@@ -0,0 +1,123 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { UserBasicDto } from '../Database/dto/user.dto';
// Extend Request interface to include user
declare global {
namespace Express {
interface Request {
user?: UserBasicDto;
}
}
}
// Helper function to clear JWT cookie (auto-logout)
function clearAuthCookie(res: Response): void {
res.clearCookie("jwt", {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? "none" : "lax",
});
}
export function auth(req: Request, res: Response, next: NextFunction): void {
// Read token from cookie named 'jwt'
const token = req.cookies.jwt;
if (!token) {
clearAuthCookie(res); // Clear any existing invalid cookie
res.status(401).json({
message: "No token provided",
autoLogout: true
});
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || "secret");
if (typeof decoded === "string") {
clearAuthCookie(res); // Clear invalid token cookie
res.status(401).json({
message: "Invalid token payload",
autoLogout: true
});
return;
}
// Create UserBasicDto from token payload
const userDto = new UserBasicDto(
decoded.id,
decoded.username,
decoded.CompanyId
);
req.user = userDto;
// Check if expiring soon & refresh if needed
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp - now < 60 * 5) {
const { iat, exp, ...payload } = decoded;
const newToken = createToken(userDto);
res.cookie("jwt", newToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? "none" : "lax",
maxAge: 60 * 60 * 1000, // 1 hour
});
}
next();
} catch (err) {
// Clear the invalid/expired token cookie
clearAuthCookie(res);
// Determine the specific error type
let message = "Invalid token";
if (err instanceof jwt.TokenExpiredError) {
message = "Token expired";
} else if (err instanceof jwt.JsonWebTokenError) {
message = "Invalid token";
} else if (err instanceof jwt.NotBeforeError) {
message = "Token not active";
}
res.status(401).json({
message,
autoLogout: true
});
return;
}
}
export function createToken(user: UserBasicDto): string {
return jwt.sign(
{
id: user.id,
username: user.username,
CompanyId: user.CompanyId
},
process.env.JWT_SECRET || 'secret',
{
expiresIn: '1h',
}
);
}
// Helper function to create token from user response data
export function createTokenFromUserResponse(userData: {
id: number;
username: string;
CompanyId?: number;
}): string {
const userDto = new UserBasicDto(
userData.id,
userData.username,
userData.CompanyId
);
return createToken(userDto);
}
// Export the clearAuthCookie function for use in other parts of the app
export { clearAuthCookie };
@@ -1,4 +1,3 @@
//hash password
import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
-13
View File
@@ -1,13 +0,0 @@
//collection of all routes
import { Router } from 'express';
import { userRouter } from '../User/UserRouter';
// import { companyRouter } from '../User/CompanyRouter';
const router = Router();
// Use the user router for user-related routes
router.use('/user', userRouter);
// Use the company router for company-related routes
// router.use('/company', companyRouter);
// Add more routers as needed
// Export the main router
export { router as mainRouter };
+313
View File
@@ -0,0 +1,313 @@
import winston from 'winston';
import AWS from 'aws-sdk';
import path from 'path';
import fs from 'fs';
// MinIO Configuration
const minioConfig = {
endpoint: process.env.MINIO_ENDPOINT || 'http://127.0.0.1:9000',
accessKeyId: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretAccessKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
bucket: process.env.MINIO_BUCKET || 'logs',
region: 'us-east-1'
};
// Initialize S3 client for MinIO (single instance)
const s3Client = new AWS.S3({
endpoint: minioConfig.endpoint,
accessKeyId: minioConfig.accessKeyId,
secretAccessKey: minioConfig.secretAccessKey,
s3ForcePathStyle: true,
signatureVersion: 'v4',
region: minioConfig.region
});
// Ensure logs directory exists (single initialization)
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Logging formats (single definition)
const simpleFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, category, ...meta }) => {
let output = `${timestamp} [${level.toUpperCase()}]`;
if (category) output += ` [${category}]`;
output += `: ${message}`;
if (Object.keys(meta).length > 0) {
output += ` ${JSON.stringify(meta)}`;
}
return output;
})
);
const fileFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);
// Create Winston logger (single instance)
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: fileFormat,
defaultMeta: {
service: 'serpentrace-backend',
environment: process.env.NODE_ENV || 'development'
},
transports: [
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: path.join(logsDir, 'app.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// Add console transport for development (single initialization)
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: simpleFormat
}));
}
// S3 upload utility (single definition)
const uploadLogToS3 = async (logData: string, filename: string): Promise<void> => {
try {
const key = `serpentrace-backend/${new Date().toISOString().split('T')[0]}/${filename}`;
await s3Client.upload({
Bucket: minioConfig.bucket,
Key: key,
Body: logData,
ContentType: 'text/plain'
}).promise();
console.log(`✅ Log uploaded to MinIO: ${key}`);
} catch (error) {
console.error('❌ Failed to upload log to MinIO:', error);
}
};
// Initialize MinIO bucket (single function)
const initializeMinIOBucket = async (): Promise<void> => {
try {
await s3Client.headBucket({ Bucket: minioConfig.bucket }).promise();
console.log(`✅ MinIO bucket '${minioConfig.bucket}' is accessible`);
} catch (error: any) {
if (error.statusCode === 404) {
try {
await s3Client.createBucket({ Bucket: minioConfig.bucket }).promise();
console.log(`✅ Created MinIO bucket '${minioConfig.bucket}'`);
} catch (createError) {
console.error(`❌ Failed to create MinIO bucket:`, createError);
}
}
}
};
// Enhanced application logger with event categories
export class AppLogger {
private winston: winston.Logger;
constructor(winstonLogger: winston.Logger) {
this.winston = winstonLogger;
}
// Standard log levels
error(message: string, meta?: any): void {
this.winston.error(message, meta);
}
warn(message: string, meta?: any): void {
this.winston.warn(message, meta);
}
info(message: string, meta?: any): void {
this.winston.info(message, meta);
}
debug(message: string, meta?: any): void {
this.winston.debug(message, meta);
}
// STARTUP EVENTS
startup(message: string, meta?: any): void {
this.winston.info(message, {
category: 'STARTUP',
...meta
});
}
// SYSTEM EVENTS
system(message: string, meta?: any): void {
this.winston.info(message, {
category: 'SYSTEM',
...meta
});
}
// DATABASE EVENTS
database(operation: string, table?: string, duration?: number, meta?: any): void {
this.winston.info(`Database: ${operation}`, {
category: 'DATABASE',
operation,
table,
duration: duration ? `${duration}ms` : undefined,
...meta
});
}
// AUTHENTICATION EVENTS
auth(action: string, success: boolean, meta?: any): void {
this.winston.info(`Auth: ${action}`, {
category: 'AUTH',
action,
success,
...meta
});
}
// API REQUEST EVENTS
request(req: any, res: any, duration?: number): void {
this.winston.info(`Request: ${req.method} ${req.originalUrl || req.url}`, {
category: 'API',
method: req.method,
url: req.originalUrl || req.url,
statusCode: res.statusCode,
ip: req.ip,
duration: duration ? `${duration}ms` : undefined,
userId: req.user?.id
});
}
// SECURITY EVENTS
security(event: string, severity: 'low' | 'medium' | 'high' | 'critical', meta?: any): void {
const logLevel = severity === 'critical' || severity === 'high' ? 'error' : 'warn';
this[logLevel](`Security: ${event}`, {
category: 'SECURITY',
event,
severity,
...meta
});
}
// BUSINESS LOGIC EVENTS
business(action: string, entity: string, meta?: any): void {
this.winston.info(`Business: ${action} ${entity}`, {
category: 'BUSINESS',
action,
entity,
...meta
});
}
// ERROR EVENTS
errorEvent(error: string, context: string, meta?: any): void {
this.winston.error(`Error in ${context}: ${error}`, {
category: 'ERROR',
context,
error,
...meta
});
}
// PERFORMANCE EVENTS
performance(metric: string, value: number, unit: string, meta?: any): void {
this.winston.info(`Performance: ${metric}`, {
category: 'PERFORMANCE',
metric,
value,
unit,
...meta
});
}
// CONFIGURATION EVENTS
config(component: string, status: 'loaded' | 'failed' | 'updated', meta?: any): void {
this.winston.info(`Config: ${component} ${status}`, {
category: 'CONFIG',
component,
status,
...meta
});
}
// MIDDLEWARE EVENTS
middleware(name: string, action: string, meta?: any): void {
this.winston.info(`Middleware: ${name} ${action}`, {
category: 'MIDDLEWARE',
name,
action,
...meta
});
}
// ROUTE EVENTS
route(path: string, method: string, action: string, meta?: any): void {
this.winston.info(`Route: ${method} ${path} ${action}`, {
category: 'ROUTE',
path,
method,
action,
...meta
});
}
// Upload current log file to MinIO
async uploadLogs(): Promise<void> {
try {
const logFile = path.join(logsDir, 'app.log');
if (fs.existsSync(logFile)) {
const logData = fs.readFileSync(logFile, 'utf8');
const filename = `app-${Date.now()}.log`;
await uploadLogToS3(logData, filename);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.error('Failed to upload logs', { error: errorMessage });
}
}
}
// Create and export the app logger instance (single instance)
export const appLogger = new AppLogger(logger);
// Setup periodic log upload (single initialization)
let uploadInterval: NodeJS.Timeout | null = null;
if (process.env.NODE_ENV === 'production' && !uploadInterval) {
uploadInterval = setInterval(() => {
appLogger.uploadLogs();
}, 60 * 60 * 1000); // 1 hour
}
// Graceful shutdown handlers (single initialization)
let shutdownHandlersSet = false;
if (!shutdownHandlersSet) {
shutdownHandlersSet = true;
process.on('SIGTERM', () => {
appLogger.system('Application shutting down via SIGTERM');
appLogger.uploadLogs().finally(() => {
process.exit(0);
});
});
process.on('SIGINT', () => {
appLogger.system('Application interrupted via SIGINT');
appLogger.uploadLogs().finally(() => {
process.exit(0);
});
});
}
// Export utilities
export { logger, initializeMinIOBucket, uploadLogToS3 };
export default appLogger;