chore: dockerize monorepo with multi-stage builds and update frontend
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable pnpm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter @lucky-wheel/api build
|
||||
RUN pnpm deploy --filter @lucky-wheel/api --prod /prod/api
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /prod/api .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/src/main"]
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, HttpCode, HttpStatus, Patch, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
|
||||
import { AdminAuthService } from './admin-auth.service';
|
||||
import { AdminLoginDto } from './dto/admin-login.dto';
|
||||
import { AdminRefreshDto } from './dto/admin-refresh.dto';
|
||||
import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto';
|
||||
|
||||
@ApiTags('Admin Auth (مدیریت)')
|
||||
@Controller('admin/auth')
|
||||
@@ -30,6 +31,15 @@ export class AdminAuthController {
|
||||
return this.adminAuthService.refreshToken(dto);
|
||||
}
|
||||
|
||||
@Patch('profile')
|
||||
@UseGuards(JwtAdminGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiBearerAuth('AdminAuth')
|
||||
@ApiOperation({ summary: 'ویرایش اطلاعات حساب کاربری مدیر جاری' })
|
||||
async updateProfile(@Req() req: any, @Body() dto: UpdateAdminProfileDto) {
|
||||
return this.adminAuthService.updateProfile(req.user.id, dto);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@UseGuards(JwtAdminGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
@@ -11,6 +12,7 @@ import * as bcrypt from 'bcrypt';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { AdminLoginDto } from './dto/admin-login.dto';
|
||||
import { AdminRefreshDto } from './dto/admin-refresh.dto';
|
||||
import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthService implements OnModuleInit {
|
||||
@@ -152,4 +154,38 @@ export class AdminAuthService implements OnModuleInit {
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async updateProfile(adminId: string, dto: UpdateAdminProfileDto) {
|
||||
const dataToUpdate: any = {};
|
||||
if (dto.name) dataToUpdate.name = dto.name.trim();
|
||||
if (dto.mobile) dataToUpdate.mobile = dto.mobile.trim();
|
||||
if (dto.email) {
|
||||
const emailLower = dto.email.toLowerCase().trim();
|
||||
const existing = await this.prisma.admin.findFirst({
|
||||
where: { email: emailLower, id: { not: adminId } },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BadRequestException('این ایمیل قبلاً توسط مدیر دیگری ثبت شده است.');
|
||||
}
|
||||
dataToUpdate.email = emailLower;
|
||||
}
|
||||
if (dto.password) {
|
||||
dataToUpdate.passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
}
|
||||
|
||||
const updated = await this.prisma.admin.update({
|
||||
where: { id: adminId },
|
||||
data: dataToUpdate,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
mobile: true,
|
||||
role: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEmail, IsOptional, IsString, Length, Matches } from 'class-validator';
|
||||
|
||||
export class UpdateAdminProfileDto {
|
||||
@ApiPropertyOptional({ description: 'نام و نام خانوادگی مدیر' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(2, 50, { message: 'نام باید بین ۲ تا ۵۰ کاراکتر باشد.' })
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'شماره موبایل مدیر' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^09\d{9}$/, { message: 'شماره موبایل باید ۱۱ رقم و با ۰۹ شروع شود.' })
|
||||
mobile?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ایمیل مدیر' })
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: 'فرمت ایمیل نامعتبر است.' })
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'کلمه عبور جدید (اختیاری)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(6, 50, { message: 'کلمه عبور باید حداقل ۶ کاراکتر باشد.' })
|
||||
password?: string;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export class ReportService {
|
||||
totalDistributedPrizes,
|
||||
consumedChancesAggr,
|
||||
activePrizesCount,
|
||||
activeCampaignsCount,
|
||||
activeCampaignsList,
|
||||
recentWinners,
|
||||
] = await Promise.all([
|
||||
this.prisma.user.count(),
|
||||
@@ -37,13 +39,35 @@ export class ReportService {
|
||||
_sum: { amount: true },
|
||||
}),
|
||||
this.prisma.prize.count({ where: { status: 'ACTIVE' } }),
|
||||
this.prisma.campaign.count({ where: { status: 'ACTIVE' } }),
|
||||
this.prisma.campaign.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
take: 4,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
status: true,
|
||||
startAt: true,
|
||||
endAt: true,
|
||||
wheelDesign: true,
|
||||
_count: {
|
||||
select: {
|
||||
spins: true,
|
||||
userPrizes: true,
|
||||
prizes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.prisma.userPrize.findMany({
|
||||
take: 10,
|
||||
orderBy: { wonAt: 'desc' },
|
||||
include: {
|
||||
user: { select: { mobile: true, name: true } },
|
||||
user: { select: { id: true, mobile: true, name: true } },
|
||||
prize: { select: { title: true, type: true, image: true } },
|
||||
campaign: { select: { title: true } },
|
||||
campaign: { select: { id: true, title: true } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
@@ -102,6 +126,8 @@ export class ReportService {
|
||||
percentage: totalWon > 0 ? Number(((p._count.id / totalWon) * 100).toFixed(1)) : 0,
|
||||
}));
|
||||
|
||||
const winRate = totalSpins > 0 ? Number(((totalWinners / totalSpins) * 100).toFixed(1)) : 0;
|
||||
|
||||
return {
|
||||
stats: {
|
||||
totalUsers,
|
||||
@@ -112,7 +138,10 @@ export class ReportService {
|
||||
totalDistributedPrizes,
|
||||
consumedChances: Math.abs(consumedChancesAggr._sum.amount || 0),
|
||||
activePrizesCount,
|
||||
activeCampaignsCount,
|
||||
winRate,
|
||||
},
|
||||
activeCampaigns: activeCampaignsList,
|
||||
spinsByDay,
|
||||
prizeDistribution,
|
||||
recentWinners,
|
||||
|
||||
Reference in New Issue
Block a user