diff --git a/src/app.module.ts b/src/app.module.ts index 8a4f118..f8aaf4c 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -28,6 +28,7 @@ import { DanakServicesModule } from "./modules/danak-services/danak-services.mod import { DashboardModule } from "./modules/dashboards/dashboard.module"; import { DiscountsModule } from "./modules/discounts/discounts.module"; import { InvoicesModule } from "./modules/invoices/invoices.module"; +import { IconsModule } from "./modules/icons/icons.module"; import { LandingModule } from "./modules/landing/landing.module"; import { LearningModule } from "./modules/learnings/learning.module"; import { LoggerModule } from "./modules/logger/logger.module"; @@ -85,6 +86,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; DiscountsModule, SupportPlansModule, AccessLogsModule, + IconsModule, ], controllers: [], }) diff --git a/src/configs/icons.config.ts b/src/configs/icons.config.ts new file mode 100644 index 0000000..1ac1150 --- /dev/null +++ b/src/configs/icons.config.ts @@ -0,0 +1,20 @@ +import { ConfigService } from "@nestjs/config"; + +export function iconsConfig() { + return { + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + return { + baseUrl: configService.getOrThrow("DMENU_BACKEND_URL"), + username: configService.getOrThrow("DMENU_USERNAME"), + password: configService.getOrThrow("DMENU_PASSWORD"), + }; + }, + }; +} + +export interface IIconsConfig { + baseUrl: string; + username: string; + password: string; +} diff --git a/src/modules/icons/DTO/create-group.dto.ts b/src/modules/icons/DTO/create-group.dto.ts new file mode 100644 index 0000000..a3df71c --- /dev/null +++ b/src/modules/icons/DTO/create-group.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class CreateGroupDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ description: "Group name", example: "Navigation Icons" }) + name: string; + + @IsOptional() + @IsString() + @ApiProperty({ description: "Group description", example: "Icons used for navigation", required: false }) + description?: string; +} + diff --git a/src/modules/icons/DTO/create-icon.dto.ts b/src/modules/icons/DTO/create-icon.dto.ts new file mode 100644 index 0000000..7cb32c7 --- /dev/null +++ b/src/modules/icons/DTO/create-icon.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator"; + +export class CreateIconDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ description: "Icon name", example: "home-icon" }) + name: string; + + @IsOptional() + @IsString() + @ApiProperty({ description: "Icon description", example: "Home icon", required: false }) + description?: string; + + @IsNotEmpty() + @IsUrl({ protocols: ["http", "https"], require_protocol: true }) + @ApiProperty({ description: "Icon URL", example: "https://example.com/icons/home.svg" }) + url: string; + + @IsOptional() + @IsString() + @ApiProperty({ description: "Icon group ID", example: "group-123", required: false }) + groupId?: string; +} + diff --git a/src/modules/icons/DTO/update-group.dto.ts b/src/modules/icons/DTO/update-group.dto.ts new file mode 100644 index 0000000..1c3fdd6 --- /dev/null +++ b/src/modules/icons/DTO/update-group.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateGroupDto } from "./create-group.dto"; + +export class UpdateGroupDto extends PartialType(CreateGroupDto) {} + diff --git a/src/modules/icons/DTO/update-icon.dto.ts b/src/modules/icons/DTO/update-icon.dto.ts new file mode 100644 index 0000000..940a9eb --- /dev/null +++ b/src/modules/icons/DTO/update-icon.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateIconDto } from "./create-icon.dto"; + +export class UpdateIconDto extends PartialType(CreateIconDto) {} + diff --git a/src/modules/icons/constants/index.ts b/src/modules/icons/constants/index.ts new file mode 100644 index 0000000..26e9f90 --- /dev/null +++ b/src/modules/icons/constants/index.ts @@ -0,0 +1,2 @@ +export const ICONS_CONFIG = "ICONS_CONFIG"; + diff --git a/src/modules/icons/icons.controller.ts b/src/modules/icons/icons.controller.ts new file mode 100644 index 0000000..32dec84 --- /dev/null +++ b/src/modules/icons/icons.controller.ts @@ -0,0 +1,92 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common"; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; + +import { CreateGroupDto } from "./DTO/create-group.dto"; +import { CreateIconDto } from "./DTO/create-icon.dto"; +import { UpdateGroupDto } from "./DTO/update-group.dto"; +import { UpdateIconDto } from "./DTO/update-icon.dto"; +import { IconsService } from "./providers/icons.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { PermissionsDec } from "../../common/decorators/permission.decorator"; +import { PermissionEnum } from "../users/enums/permission.enum"; + +@ApiTags("icons") +@Controller("admin/icons") +@AuthGuards() +@PermissionsDec(PermissionEnum.ICONS) +@ApiBearerAuth() +export class IconsController { + constructor(private readonly iconsService: IconsService) {} + + // Icon endpoints + @Post() + @ApiOperation({ summary: "Create icon" }) + @ApiBody({ type: CreateIconDto }) + createIcon(@Body() dto: CreateIconDto) { + return this.iconsService.createIcon(dto); + } + + @Get() + @ApiOperation({ summary: "Get all icons" }) + findAllIcons() { + return this.iconsService.findAllIcons(); + } + + @Get(":id") + @ApiOperation({ summary: "Get icon by id" }) + @ApiParam({ name: "id", required: true, type: String }) + findOneIcon(@Param("id") id: string) { + return this.iconsService.findOneIcon(id); + } + + @Patch(":id") + @ApiOperation({ summary: "Update icon" }) + @ApiParam({ name: "id" }) + @ApiBody({ type: UpdateIconDto }) + updateIcon(@Param("id") id: string, @Body() dto: UpdateIconDto) { + return this.iconsService.updateIcon(id, dto); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete icon" }) + @ApiParam({ name: "id" }) + removeIcon(@Param("id") id: string) { + return this.iconsService.removeIcon(id); + } + + // Group endpoints (must come before :id routes to avoid route conflicts) + @Post("groups") + @ApiOperation({ summary: "Create icon group" }) + @ApiBody({ type: CreateGroupDto }) + createGroup(@Body() dto: CreateGroupDto) { + return this.iconsService.createGroup(dto); + } + + @Get("groups") + @ApiOperation({ summary: "Get all icon groups" }) + findAllGroups() { + return this.iconsService.findAllGroups(); + } + + @Get("groups/:id") + @ApiOperation({ summary: "Get icon group by id" }) + @ApiParam({ name: "id", required: true, type: String }) + findOneGroup(@Param("id") id: string) { + return this.iconsService.findOneGroup(id); + } + + @Patch("groups/:id") + @ApiOperation({ summary: "Update icon group" }) + @ApiParam({ name: "id" }) + @ApiBody({ type: UpdateGroupDto }) + updateGroup(@Param("id") id: string, @Body() dto: UpdateGroupDto) { + return this.iconsService.updateGroup(id, dto); + } + + @Delete("groups/:id") + @ApiOperation({ summary: "Delete icon group" }) + @ApiParam({ name: "id" }) + removeGroup(@Param("id") id: string) { + return this.iconsService.removeGroup(id); + } +} diff --git a/src/modules/icons/icons.module.ts b/src/modules/icons/icons.module.ts new file mode 100644 index 0000000..813a222 --- /dev/null +++ b/src/modules/icons/icons.module.ts @@ -0,0 +1,22 @@ +import { Module } from "@nestjs/common"; + +import { iconsConfig, IIconsConfig } from "../../configs/icons.config"; +import { IconsController } from "./icons.controller"; +import { ICONS_CONFIG } from "./constants"; +import { IconsService } from "./providers/icons.service"; + +@Module({ + imports: [], + controllers: [IconsController], + providers: [ + IconsService, + { + provide: ICONS_CONFIG, + useFactory: iconsConfig().useFactory, + inject: iconsConfig().inject, + }, + ], + exports: [IconsService], +}) +export class IconsModule {} + diff --git a/src/modules/icons/providers/icons.service.ts b/src/modules/icons/providers/icons.service.ts new file mode 100644 index 0000000..690dc1c --- /dev/null +++ b/src/modules/icons/providers/icons.service.ts @@ -0,0 +1,201 @@ +import { HttpService } from "@nestjs/axios"; +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { catchError, firstValueFrom, throwError } from "rxjs"; + +import { IIconsConfig } from "../../../configs/icons.config"; +import { ICONS_CONFIG } from "../constants"; +import { CreateGroupDto } from "../DTO/create-group.dto"; +import { CreateIconDto } from "../DTO/create-icon.dto"; +import { UpdateGroupDto } from "../DTO/update-group.dto"; +import { UpdateIconDto } from "../DTO/update-icon.dto"; + +@Injectable() +export class IconsService { + private readonly logger = new Logger(IconsService.name); + + constructor( + @Inject(ICONS_CONFIG) private readonly config: IIconsConfig, + private readonly httpService: HttpService, + ) {} + + private getHeaders(): Record { + const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64"); + return { + "Content-Type": "application/json", + Authorization: `Basic ${credentials}`, + }; + } + + // Icon methods + async createIcon(dto: CreateIconDto) { + try { + const { data } = await firstValueFrom( + this.httpService.post(`${this.config.baseUrl}/admin/icons`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to create icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error creating icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findAllIcons() { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/admin/icons`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch icons: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching icons: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findOneIcon(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/admin/icons/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async updateIcon(id: string, dto: UpdateIconDto) { + try { + const { data } = await firstValueFrom( + this.httpService.patch(`${this.config.baseUrl}/admin/icons/${id}`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to update icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error updating icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async removeIcon(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService.delete(`${this.config.baseUrl}/admin/icons/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to delete icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error deleting icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + // Group methods + async createGroup(dto: CreateGroupDto) { + try { + const { data } = await firstValueFrom( + this.httpService.post(`${this.config.baseUrl}/admin/icons/groups`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to create group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error creating group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findAllGroups() { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/admin/icons/groups`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch groups: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching groups: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findOneGroup(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/admin/icons/groups/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async updateGroup(id: string, dto: UpdateGroupDto) { + try { + const { data } = await firstValueFrom( + this.httpService.patch(`${this.config.baseUrl}/admin/icons/groups/${id}`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to update group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error updating group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async removeGroup(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService.delete(`${this.config.baseUrl}/admin/icons/groups/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to delete group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error deleting group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } +} \ No newline at end of file diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index a2e4323..aa4657d 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -20,4 +20,5 @@ export enum PermissionEnum { PAYMENTS = "payments", MANAGE_SSO_CLIENTS = "manage_sso_clients", SUPPORT_PLAN = "support_plan", + ICONS = "icons", }