350 lines
9.5 KiB
TypeScript
Executable File
350 lines
9.5 KiB
TypeScript
Executable File
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Put,
|
|
Delete,
|
|
Param,
|
|
Body,
|
|
Query,
|
|
UseInterceptors,
|
|
Inject,
|
|
NotFoundException,
|
|
BadRequestException,
|
|
} from "@nestjs/common";
|
|
import {
|
|
CacheInterceptor,
|
|
CacheKey,
|
|
CacheTTL,
|
|
CACHE_MANAGER,
|
|
} from "@nestjs/cache-manager";
|
|
import * as cacheManager from "cache-manager";
|
|
import {
|
|
ApiTags,
|
|
ApiBearerAuth,
|
|
ApiOperation,
|
|
ApiResponse as SwaggerResponse,
|
|
} from "@nestjs/swagger";
|
|
import { Roles } from "../../common/decorators";
|
|
import { PrismaService } from "../../database/prisma.service";
|
|
import { PaginationDto } from "../../common/dto/pagination.dto";
|
|
import {
|
|
ApiResponse,
|
|
createSuccessResponse,
|
|
createPaginatedResponse,
|
|
PaginatedData,
|
|
} from "../../common/types/api-response.type";
|
|
import { plainToInstance } from "class-transformer";
|
|
import { UserResponseDto } from "../users/dto/user.dto";
|
|
import { UserRole } from "@prisma/client";
|
|
import { SubscriptionsService } from "../subscriptions/subscriptions.service";
|
|
import { PlanType } from "../subscriptions/dto/subscription.dto";
|
|
|
|
@ApiTags("Admin")
|
|
@ApiBearerAuth()
|
|
@Controller("admin")
|
|
@Roles("superadmin")
|
|
export class AdminController {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
@Inject(CACHE_MANAGER) private cacheManager: cacheManager.Cache,
|
|
private readonly subscriptionsService: SubscriptionsService,
|
|
) {}
|
|
|
|
// ================== Users Management ==================
|
|
|
|
@Get("users")
|
|
@ApiOperation({ summary: "Get all users (admin)" })
|
|
@SwaggerResponse({ status: 200, type: [UserResponseDto] })
|
|
async getAllUsers(
|
|
@Query() pagination: PaginationDto,
|
|
): Promise<ApiResponse<PaginatedData<UserResponseDto>>> {
|
|
const { skip, take, orderBy } = pagination;
|
|
|
|
const [users, total] = await Promise.all([
|
|
this.prisma.user.findMany({
|
|
skip,
|
|
take,
|
|
orderBy,
|
|
}),
|
|
this.prisma.user.count(),
|
|
]);
|
|
|
|
const dtos = plainToInstance(
|
|
UserResponseDto,
|
|
users,
|
|
) as unknown as UserResponseDto[];
|
|
|
|
return createPaginatedResponse(
|
|
dtos,
|
|
total,
|
|
pagination.page || 1,
|
|
pagination.limit || 10,
|
|
);
|
|
}
|
|
|
|
@Get("users/:id")
|
|
@ApiOperation({ summary: "Get user by ID" })
|
|
@SwaggerResponse({ status: 200, type: UserResponseDto })
|
|
async getUserById(
|
|
@Param("id") id: string,
|
|
): Promise<ApiResponse<UserResponseDto>> {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id },
|
|
include: {
|
|
usageLimit: true,
|
|
analyses: {
|
|
take: 5,
|
|
orderBy: { createdAt: "desc" },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException("User not found");
|
|
}
|
|
|
|
return createSuccessResponse(plainToInstance(UserResponseDto, user));
|
|
}
|
|
|
|
@Put("users/:id/toggle-active")
|
|
@ApiOperation({ summary: "Toggle user active status" })
|
|
@SwaggerResponse({ status: 200, type: UserResponseDto })
|
|
async toggleUserActive(
|
|
@Param("id") id: string,
|
|
): Promise<ApiResponse<UserResponseDto>> {
|
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
|
|
if (!user) {
|
|
throw new NotFoundException("User not found");
|
|
}
|
|
|
|
const updated = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { isActive: !user.isActive },
|
|
});
|
|
|
|
return createSuccessResponse(
|
|
plainToInstance(UserResponseDto, updated),
|
|
"common.SUCCESS_USER_STATUS_UPDATED",
|
|
);
|
|
}
|
|
|
|
@Put("users/:id/role")
|
|
@ApiOperation({ summary: "Update user role" })
|
|
@SwaggerResponse({ status: 200, type: UserResponseDto })
|
|
async updateUserRole(
|
|
@Param("id") id: string,
|
|
@Body() data: { role: UserRole },
|
|
): Promise<ApiResponse<UserResponseDto>> {
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { role: data.role },
|
|
});
|
|
|
|
return createSuccessResponse(
|
|
plainToInstance(UserResponseDto, user),
|
|
"common.SUCCESS_USER_ROLE_UPDATED",
|
|
);
|
|
}
|
|
|
|
@Delete("users/:id")
|
|
@ApiOperation({ summary: "Soft delete a user" })
|
|
@SwaggerResponse({ status: 200, description: "User deleted" })
|
|
async deleteUser(@Param("id") id: string): Promise<ApiResponse<null>> {
|
|
await this.prisma.user.update({
|
|
where: { id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
return createSuccessResponse(null, "common.SUCCESS_USER_DELETED");
|
|
}
|
|
|
|
// ================== App Settings ==================
|
|
|
|
@Get("settings")
|
|
@UseInterceptors(CacheInterceptor)
|
|
@CacheKey("app_settings")
|
|
@CacheTTL(60 * 1000)
|
|
@ApiOperation({ summary: "Get all app settings" })
|
|
@SwaggerResponse({
|
|
status: 200,
|
|
schema: { type: "object", additionalProperties: { type: "string" } },
|
|
})
|
|
async getAllSettings(): Promise<ApiResponse<Record<string, string>>> {
|
|
const settings = await this.prisma.appSetting.findMany();
|
|
const settingsMap: Record<string, string> = {};
|
|
for (const s of settings) {
|
|
settingsMap[s.key] = s.value || "";
|
|
}
|
|
return createSuccessResponse(settingsMap);
|
|
}
|
|
|
|
@Put("settings/:key")
|
|
@ApiOperation({ summary: "Update an app setting" })
|
|
@SwaggerResponse({
|
|
status: 200,
|
|
schema: {
|
|
type: "object",
|
|
properties: { key: { type: "string" }, value: { type: "string" } },
|
|
},
|
|
})
|
|
async updateSetting(
|
|
@Param("key") key: string,
|
|
@Body() data: { value: string },
|
|
): Promise<ApiResponse<{ key: string; value: string }>> {
|
|
const setting = await this.prisma.appSetting.upsert({
|
|
where: { key },
|
|
update: { value: data.value },
|
|
create: { key, value: data.value },
|
|
});
|
|
await this.cacheManager.del("app_settings");
|
|
return createSuccessResponse(
|
|
{ key: setting.key, value: setting.value || "" },
|
|
"common.SUCCESS_SETTING_UPDATED",
|
|
);
|
|
}
|
|
|
|
// ================== Usage Limits ==================
|
|
|
|
@Get("usage-limits")
|
|
@ApiOperation({ summary: "Get all usage limits" })
|
|
@SwaggerResponse({
|
|
status: 200,
|
|
schema: { type: "array", items: { type: "object" } },
|
|
})
|
|
async getAllUsageLimits(@Query() pagination: PaginationDto) {
|
|
const { skip, take } = pagination;
|
|
|
|
const [limits, total] = await Promise.all([
|
|
this.prisma.usageLimit.findMany({
|
|
skip,
|
|
take,
|
|
include: {
|
|
user: {
|
|
select: { id: true, email: true, firstName: true, lastName: true },
|
|
},
|
|
},
|
|
orderBy: { lastResetDate: "desc" },
|
|
}),
|
|
this.prisma.usageLimit.count(),
|
|
]);
|
|
|
|
return createPaginatedResponse(
|
|
limits,
|
|
total,
|
|
pagination.page || 1,
|
|
pagination.limit || 10,
|
|
);
|
|
}
|
|
|
|
@Post("usage-limits/reset-all")
|
|
@ApiOperation({ summary: "Reset all usage limits" })
|
|
@SwaggerResponse({
|
|
status: 200,
|
|
schema: { type: "object", properties: { count: { type: "number" } } },
|
|
})
|
|
async resetAllUsageLimits(): Promise<ApiResponse<{ count: number }>> {
|
|
const result = await this.prisma.usageLimit.updateMany({
|
|
data: {
|
|
analysisCount: 0,
|
|
couponCount: 0,
|
|
lastResetDate: new Date(),
|
|
},
|
|
});
|
|
|
|
return createSuccessResponse(
|
|
{ count: result.count },
|
|
"common.SUCCESS_ALL_LIMITS_RESET",
|
|
);
|
|
}
|
|
|
|
@Post("usage-limits/reset/:userId")
|
|
@ApiOperation({ summary: "Reset usage limits for a single user" })
|
|
@SwaggerResponse({ status: 200 })
|
|
async resetUserUsageLimits(
|
|
@Param("userId") userId: string,
|
|
): Promise<ApiResponse<null>> {
|
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
if (!user) throw new NotFoundException("USER_NOT_FOUND");
|
|
|
|
await this.prisma.usageLimit.update({
|
|
where: { userId },
|
|
data: {
|
|
analysisCount: 0,
|
|
couponCount: 0,
|
|
lastResetDate: new Date(),
|
|
},
|
|
});
|
|
|
|
return createSuccessResponse(null, "common.SUCCESS_USER_LIMITS_RESET");
|
|
}
|
|
|
|
@Put("users/:userId/subscription")
|
|
@ApiOperation({ summary: "Update a user's subscription tier" })
|
|
@SwaggerResponse({ status: 200 })
|
|
async updateUserSubscription(
|
|
@Param("userId") userId: string,
|
|
@Body() data: { plan: string },
|
|
): Promise<ApiResponse<null>> {
|
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
if (!user) throw new NotFoundException("USER_NOT_FOUND");
|
|
|
|
const validPlans = [PlanType.FREE, PlanType.PLUS, PlanType.PREMIUM];
|
|
const newPlan = data.plan as PlanType;
|
|
if (!validPlans.includes(newPlan)) {
|
|
throw new BadRequestException("INVALID_PLAN_TYPE");
|
|
}
|
|
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: { subscriptionStatus: newPlan },
|
|
});
|
|
|
|
await this.subscriptionsService.syncLimitsWithPlan(userId, newPlan);
|
|
|
|
return createSuccessResponse(
|
|
null,
|
|
"common.SUCCESS_USER_SUBSCRIPTION_UPDATED",
|
|
);
|
|
}
|
|
|
|
// ================== Analytics ==================
|
|
|
|
@Get("analytics/overview")
|
|
@ApiOperation({ summary: "Get system analytics overview" })
|
|
@SwaggerResponse({ status: 200, schema: { type: "object" } })
|
|
async getAnalyticsOverview() {
|
|
const [
|
|
totalUsers,
|
|
activeUsers,
|
|
premiumUsers,
|
|
totalMatches,
|
|
totalPredictions,
|
|
totalCoupons,
|
|
] = await Promise.all([
|
|
this.prisma.user.count(),
|
|
this.prisma.user.count({ where: { isActive: true } }),
|
|
this.prisma.user.count({
|
|
where: { subscriptionStatus: { in: ["plus", "premium"] } },
|
|
}),
|
|
this.prisma.match.count(),
|
|
this.prisma.prediction.count(),
|
|
this.prisma.userCoupon.count(),
|
|
]);
|
|
|
|
return createSuccessResponse({
|
|
totalUsers,
|
|
activeUsers,
|
|
totalPredictions,
|
|
totalCoupons,
|
|
users: {
|
|
total: totalUsers,
|
|
active: activeUsers,
|
|
premium: premiumUsers,
|
|
},
|
|
matches: totalMatches,
|
|
predictions: totalPredictions,
|
|
});
|
|
}
|
|
}
|