chore: first commit

This commit is contained in:
mahyargdz
2025-06-24 11:26:06 +03:30
commit ccbf743ecb
186 changed files with 22258 additions and 0 deletions
@@ -0,0 +1,73 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, Matches, MinLength } from "class-validator";
export class CreateEmailUserDto {
@ApiProperty({
description: "Username part of the email address (before @domain.com)",
example: "john.doe",
})
@IsString()
@IsNotEmpty()
@MinLength(2)
@Matches(/^[a-zA-Z0-9._-]+$/, {
message: "Username can only contain letters, numbers, dots, underscores, and hyphens",
})
username: string;
@ApiProperty({
description: "User password for email account",
example: "SecurePassword123!",
minLength: 8,
})
@IsString()
@IsNotEmpty()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character",
})
password: string;
@ApiProperty({
description: "Domain ID for the email address",
example: "12345678-1234-1234-1234-123456789012",
})
@IsString()
@IsNotEmpty()
domainId: string;
@ApiPropertyOptional({
description: "Display name for the user",
example: "John Doe",
})
@IsString()
@IsOptional()
@Transform(({ value }) => value?.trim())
displayName?: string;
@ApiPropertyOptional({
description: "Email quota in bytes (default: 1GB)",
example: 1073741824,
})
@IsNumber()
@IsOptional()
quota?: number;
@ApiPropertyOptional({
description: "Additional email aliases for this user",
example: ["johndoe@example.com", "j.doe@example.com"],
})
@IsArray()
@IsString({ each: true })
@IsOptional()
aliases?: string[];
@ApiPropertyOptional({
description: "User role/title",
example: "Marketing Manager",
})
@IsString()
@IsOptional()
@Transform(({ value }) => value?.trim())
title?: string;
}
@@ -0,0 +1,19 @@
import { Entity, ManyToOne, Opt, Property } from "@mikro-orm/core";
import { User } from "./user.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class RefreshToken extends BaseEntity {
@Property({ type: "varchar", length: 255 })
token!: string;
@ManyToOne(() => User)
user!: User;
@Property({ type: "timestamptz" })
expiresAt!: Date;
@Property({ default: false })
isRevoked: boolean & Opt;
}
+14
View File
@@ -0,0 +1,14 @@
import { Collection, Entity, Enum, OneToMany } from "@mikro-orm/core";
import { User } from "./user.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { RoleEnum } from "../enums/role.enum";
@Entity()
export class Role extends BaseEntity {
@Enum({ items: () => RoleEnum, nativeEnumName: "role_enum", default: RoleEnum.USER })
name!: RoleEnum;
@OneToMany(() => User, (user) => user.role)
users = new Collection<User>(this);
}
+55
View File
@@ -0,0 +1,55 @@
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
import { RefreshToken } from "./refresh-token.entity";
import { Role } from "./role.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Business } from "../../businesses/entities/business.entity";
import { Domain } from "../../domains/entities/domain.entity";
import { UserRepository } from "../repositories/user.repository";
@Entity({ repository: () => UserRepository })
export class User extends BaseEntity {
@Property({ type: "varchar", length: 150, nullable: false })
title!: string;
@Property({ type: "varchar", length: 50, nullable: true })
userName!: string;
@Property({ type: "varchar", length: 150, nullable: false })
password!: string;
@Property({ type: "varchar", length: 255, nullable: true })
emailAddress?: string; // Full email address
@Property({ type: "boolean", default: false })
emailEnabled: boolean = false; // Whether email is enabled for this user
@Property({ type: "bigint", nullable: true, default: 1073741824 }) // 1GB default
emailQuota?: number; // Email quota in bytes
// Email account integration fields
@Property({ type: "varchar", length: 255, nullable: true })
emailAccountId?: string; // WildDuck user ID
@Property({ type: "varchar", length: 255, nullable: true })
displayName?: string; // Display name for email
@Property({ default: true, nullable: false })
isActive: boolean = true;
//=========================
@OneToMany(() => RefreshToken, (token) => token.user)
refreshTokens = new Collection<RefreshToken>(this);
@ManyToOne(() => Role, { deleteRule: "restrict" })
role!: Role;
@ManyToOne(() => Business, { deleteRule: "restrict" })
business!: Business;
@ManyToOne(() => Domain, { nullable: true })
domain?: Domain; // The domain this email user belongs to
[EntityRepositoryType]?: UserRepository;
}
+4
View File
@@ -0,0 +1,4 @@
export enum RoleEnum {
ADMIN = "ADMIN",
USER = "USER",
}
@@ -0,0 +1,5 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { User } from "../entities/user.entity";
export class UserRepository extends EntityRepository<User> {}
+255
View File
@@ -0,0 +1,255 @@
import { EntityManager, FilterQuery } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import {
DANAK_IMAPS_ENCRYPTION,
DANAK_IMAPS_PORT,
DANAK_IMAPS_SERVER,
DANAK_SMTPS_ENCRYPTION,
DANAK_SMTPS_PORT,
DANAK_SMTPS_SERVER,
} from "../../../common/constants";
import { BusinessMessage, DomainMessage, MailServerMessage, UserMessage } from "../../../common/enums/message.enum";
import { Business } from "../../businesses/entities/business.entity";
import { DomainStatus } from "../../domains/enums/domain-status.enum";
import { DomainsService } from "../../domains/services/domains.service";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { PasswordService } from "../../utils/services/password.service";
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
import { Role } from "../entities/role.entity";
import { User } from "../entities/user.entity";
import { RoleEnum } from "../enums/role.enum";
import { UserRepository } from "../repositories/user.repository";
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
constructor(
private readonly userRepository: UserRepository,
private readonly em: EntityManager,
private readonly passwordService: PasswordService,
private readonly mailServerService: MailServerService,
private readonly domainsService: DomainsService,
) {}
async findOneWithEmail(email: string, businessId: string) {
const user = await this.userRepository.findOne({ emailAddress: email, business: { id: businessId } }, { populate: ["role"] });
return user;
}
async findOneById(id: string) {
const user = await this.userRepository.findOne({ id }, { populate: ["role"] });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return { user };
}
/*******************************/
async getMe(userId: string) {
const user = await this.userRepository.findOne({ id: userId }, { populate: ["role"] });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return { user };
}
/*******************************/
async findOneByIdWithEntityManager(id: string, em: EntityManager) {
const user = await em.findOne(User, { id }, { populate: ["role"] });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return user;
}
/*******************************/
async createEmailUser(createEmailUserDto: CreateEmailUserDto, businessId: string) {
const { username, password, domainId, displayName, quota, aliases, title } = createEmailUserDto;
const business = await this.em.findOne(Business, { id: businessId });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
const domain = await this.domainsService.getDomainById(domainId);
if (domain.business.id !== businessId) {
throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
}
if (domain.status !== DomainStatus.VERIFIED) {
throw new BadRequestException(DomainMessage.NOT_VERIFIED);
}
const emailAddress = `${username}@${domain.name}`;
// Check if email address already exists
const existingUser = await this.userRepository.findOne({ emailAddress });
if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS);
const hashedPassword = await this.passwordService.hashPassword(password);
let userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
if (!userRole) {
userRole = this.em.create(Role, { name: RoleEnum.USER });
await this.em.persistAndFlush(userRole);
}
try {
const mailServerUser = await firstValueFrom(
this.mailServerService.users.createUser({
username,
password,
address: emailAddress,
name: displayName || username,
quota: quota || 1073741824, // 1GB default
tags: ["dmail-user"],
}),
);
if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
const user = this.userRepository.create({
title: title || displayName || username,
userName: username,
password: hashedPassword,
emailAddress,
emailEnabled: true,
emailQuota: quota || 1073741824,
emailAccountId: mailServerUser.id,
displayName: displayName || username,
isActive: true,
business,
domain,
role: userRole,
});
await this.em.persistAndFlush(user);
// Create aliases if provided
if (aliases && aliases.length > 0) {
for (const alias of aliases) {
try {
await firstValueFrom(
this.mailServerService.addresses.createUserAddress(mailServerUser.id, {
address: alias,
main: false,
}),
);
} catch (error) {
this.logger.warn(`Failed to create alias ${alias} for user ${emailAddress}:`, error);
}
}
}
this.logger.log(`Email user created: ${emailAddress} for business ${businessId}`);
return {
user: {
id: user.id,
emailAddress: user.emailAddress!,
displayName: user.displayName!,
emailAccountId: user.emailAccountId!,
emailQuota: user.emailQuota!,
emailEnabled: user.emailEnabled,
domain: domain.name,
createdAt: user.createdAt,
isActive: user.isActive,
},
setupInfo: {
emailAddress: user.emailAddress!,
imapSettings: {
server: DANAK_IMAPS_SERVER,
port: DANAK_IMAPS_PORT,
encryption: DANAK_IMAPS_ENCRYPTION,
},
smtpSettings: {
server: DANAK_SMTPS_SERVER,
port: DANAK_SMTPS_PORT,
encryption: DANAK_SMTPS_ENCRYPTION,
},
},
message: "Email user created successfully",
};
} catch (error) {
this.logger.error(`Failed to create email user ${emailAddress}:`, error);
throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
}
}
/*******************************/
async getEmailUsers(businessId: string, domainId?: string) {
const whereClause: FilterQuery<User> = {
business: { id: businessId },
emailEnabled: true,
deletedAt: null,
};
if (domainId) whereClause.domain = { id: domainId };
const users = await this.userRepository.find(whereClause, {
populate: ["domain", "business"],
orderBy: { createdAt: "DESC" },
});
return { users };
}
/*******************************/
async deleteEmailUser(userId: string, businessId: string) {
const user = await this.userRepository.findOne({
id: userId,
business: { id: businessId },
emailEnabled: true,
});
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
try {
if (user.emailAccountId) {
await firstValueFrom(this.mailServerService.users.deleteUser(user.emailAccountId));
}
user.deletedAt = new Date();
user.isActive = false;
user.emailEnabled = false;
await this.em.persistAndFlush(user);
this.logger.log(`Email user deleted: ${user.emailAddress} for business ${businessId}`);
return { message: UserMessage.EMAIL_USER_DELETED_SUCCESSFULLY };
} catch (error) {
this.logger.error(`Failed to delete email user ${user.emailAddress}:`, error);
throw new BadRequestException(UserMessage.FAILED_TO_DELETE_EMAIL_USER);
}
}
/**
* Update email user quota
*/
async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) {
const user = await this.userRepository.findOne({
id: userId,
business: { id: businessId },
emailEnabled: true,
});
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
try {
if (user.emailAccountId) {
await firstValueFrom(
this.mailServerService.users.updateUser(user.emailAccountId, {
quota: newQuota,
}),
);
}
user.emailQuota = newQuota;
await this.em.persistAndFlush(user);
this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`);
return { message: UserMessage.EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY, user };
} catch (error) {
this.logger.error(`Failed to update quota for email user ${user.emailAddress}:`, error);
throw new BadRequestException(UserMessage.FAILED_TO_UPDATE_EMAIL_USER_QUOTA);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
import { UsersService } from "./services/users.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { BusinessDec } from "../../common/decorators/business.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
@ApiTags("email-users")
@Controller("email-users")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@AuthGuards()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@ApiOperation({ summary: "Create a new email user" })
@ApiResponse({ status: 201, description: "Email user created successfully" })
createEmailUser(@Body() createEmailUserDto: CreateEmailUserDto, @BusinessDec("id") businessId: string) {
return this.usersService.createEmailUser(createEmailUserDto, businessId);
}
@Get()
@ApiOperation({ summary: "Get email users for business" })
@ApiResponse({ status: 200, description: "Email users retrieved successfully" })
getEmailUsers(@BusinessDec("id") businessId: string, @Query("domainId") domainId?: string) {
return this.usersService.getEmailUsers(businessId, domainId);
}
@Delete(":id")
@ApiOperation({ summary: "Delete email user" })
@ApiResponse({ status: 200, description: "Email user deleted successfully" })
deleteEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.usersService.deleteEmailUser(paramDto.id, businessId);
}
@Patch(":id/quota")
@ApiOperation({ summary: "Update email user quota" })
@ApiResponse({ status: 200, description: "Email user quota updated successfully" })
updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) {
return this.usersService.updateEmailUserQuota(paramDto.id, businessId, body.quota);
}
}
+19
View File
@@ -0,0 +1,19 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { RefreshToken } from "./entities/refresh-token.entity";
import { Role } from "./entities/role.entity";
import { User } from "./entities/user.entity";
import { UsersService } from "./services/users.service";
import { UsersController } from "./users.controller";
import { DomainsModule } from "../domains/domains.module";
import { MailServerModule } from "../mail-server/mail-server.module";
import { UtilsModule } from "../utils/utils.module";
@Module({
imports: [MikroOrmModule.forFeature([User, RefreshToken, Role]), UtilsModule, MailServerModule, DomainsModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}