initial: standalone repo from monorepo split
This commit is contained in:
11
src/portal/dto/create-portal-ticket.dto.ts
Normal file
11
src/portal/dto/create-portal-ticket.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IsString, MinLength, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreatePortalTicketDto {
|
||||
@IsString()
|
||||
@MinLength(5)
|
||||
title: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
11
src/portal/dto/portal-login.dto.ts
Normal file
11
src/portal/dto/portal-login.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class PortalLoginDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
accountNumber: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
phone: string;
|
||||
}
|
||||
22
src/portal/portal-jwt.strategy.ts
Normal file
22
src/portal/portal-jwt.strategy.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
@Injectable()
|
||||
export class PortalJwtStrategy extends PassportStrategy(Strategy, 'portal-jwt') {
|
||||
constructor(config: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: config.get<string>('JWT_SECRET') || 'fallback',
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: any) {
|
||||
if (payload.type !== 'portal') {
|
||||
throw new UnauthorizedException('Invalid token type');
|
||||
}
|
||||
return { sub: payload.sub, tenantId: payload.tenantId, type: 'portal' };
|
||||
}
|
||||
}
|
||||
59
src/portal/portal.controller.ts
Normal file
59
src/portal/portal.controller.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Controller, Get, Post, Body, UseGuards, Req, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { PortalService } from './portal.service';
|
||||
import { PortalLoginDto } from './dto/portal-login.dto';
|
||||
import { CreatePortalTicketDto } from './dto/create-portal-ticket.dto';
|
||||
|
||||
@Controller('portal')
|
||||
export class PortalController {
|
||||
constructor(private readonly portalService: PortalService) {}
|
||||
|
||||
@Post('auth/login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async login(@Body() dto: PortalLoginDto) {
|
||||
return this.portalService.login(dto);
|
||||
}
|
||||
|
||||
@Get('profile')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async getProfile(@Req() req: any) {
|
||||
return this.portalService.getProfile(req.user.sub);
|
||||
}
|
||||
|
||||
@Get('dashboard')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async getDashboard(@Req() req: any) {
|
||||
return this.portalService.getDashboard(req.user.sub, req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('subscription')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async getSubscription(@Req() req: any) {
|
||||
return this.portalService.getSubscription(req.user.sub, req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('invoices')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async getInvoices(@Req() req: any) {
|
||||
return this.portalService.getInvoices(req.user.sub, req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('payments')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async getPayments(@Req() req: any) {
|
||||
return this.portalService.getPayments(req.user.sub, req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('tickets')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async getTickets(@Req() req: any) {
|
||||
return this.portalService.getTickets(req.user.sub, req.user.tenantId);
|
||||
}
|
||||
|
||||
@Post('tickets')
|
||||
@UseGuards(AuthGuard('portal-jwt'))
|
||||
async createTicket(@Req() req: any, @Body() dto: CreatePortalTicketDto) {
|
||||
return this.portalService.createTicket(req.user.sub, req.user.tenantId, dto);
|
||||
}
|
||||
}
|
||||
21
src/portal/portal.module.ts
Normal file
21
src/portal/portal.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PortalController } from './portal.controller';
|
||||
import { PortalService } from './portal.service';
|
||||
import { PortalJwtStrategy } from './portal-jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get<string>('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '24h' as any },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [PortalController],
|
||||
providers: [PortalService, PortalJwtStrategy],
|
||||
})
|
||||
export class PortalModule {}
|
||||
132
src/portal/portal.service.ts
Normal file
132
src/portal/portal.service.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Injectable, UnauthorizedException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { PortalLoginDto } from './dto/portal-login.dto';
|
||||
import { CreatePortalTicketDto } from './dto/create-portal-ticket.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PortalService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly jwt: JwtService,
|
||||
) {}
|
||||
|
||||
async login(dto: PortalLoginDto) {
|
||||
const client = await this.prisma.client.findFirst({
|
||||
where: {
|
||||
accountNumber: dto.accountNumber,
|
||||
phone: dto.phone,
|
||||
status: 'active',
|
||||
},
|
||||
include: { area: { select: { name: true } } },
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
throw new UnauthorizedException('Invalid account number or phone number');
|
||||
}
|
||||
|
||||
const token = this.jwt.sign({
|
||||
sub: client.id,
|
||||
tenantId: client.tenantId,
|
||||
type: 'portal',
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: token,
|
||||
client: {
|
||||
id: client.id,
|
||||
accountNumber: client.accountNumber,
|
||||
firstName: client.firstName,
|
||||
lastName: client.lastName,
|
||||
email: client.email,
|
||||
phone: client.phone,
|
||||
address: client.address,
|
||||
area: client.area?.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getProfile(clientId: string) {
|
||||
const client = await this.prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
include: {
|
||||
area: { select: { name: true } },
|
||||
subscriptions: {
|
||||
where: { status: { in: ['active', 'pending'] } },
|
||||
include: { plan: { select: { name: true, price: true, speedDown: true, speedUp: true, billingCycle: true } } },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!client) throw new NotFoundException('Client not found');
|
||||
return client;
|
||||
}
|
||||
|
||||
async getSubscription(clientId: string, tenantId: string) {
|
||||
return this.prisma.subscription.findFirst({
|
||||
where: { clientId, tenantId, status: { in: ['active', 'pending', 'suspended'] } },
|
||||
include: { plan: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getInvoices(clientId: string, tenantId: string) {
|
||||
return this.prisma.invoice.findMany({
|
||||
where: { clientId, tenantId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
}
|
||||
|
||||
async getPayments(clientId: string, tenantId: string) {
|
||||
return this.prisma.payment.findMany({
|
||||
where: { clientId, tenantId },
|
||||
include: { invoice: { select: { number: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
}
|
||||
|
||||
async getTickets(clientId: string, tenantId: string) {
|
||||
return this.prisma.ticket.findMany({
|
||||
where: { clientId, tenantId },
|
||||
include: { assignee: { select: { firstName: true, lastName: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
}
|
||||
|
||||
async createTicket(clientId: string, tenantId: string, dto: CreatePortalTicketDto) {
|
||||
// Find an admin user to attribute as creator
|
||||
const admin = await this.prisma.user.findFirst({
|
||||
where: { tenantId },
|
||||
include: { roles: { where: { role: 'tenant_admin' } } },
|
||||
});
|
||||
|
||||
return this.prisma.ticket.create({
|
||||
data: {
|
||||
tenantId,
|
||||
clientId,
|
||||
createdById: admin?.id || '',
|
||||
type: 'support',
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
priority: 'normal',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getDashboard(clientId: string, tenantId: string) {
|
||||
const [subscription, unpaidInvoices, recentPayment, openTickets] = await Promise.all([
|
||||
this.prisma.subscription.findFirst({
|
||||
where: { clientId, tenantId, status: 'active' },
|
||||
include: { plan: { select: { name: true, speedDown: true, speedUp: true, price: true } } },
|
||||
}),
|
||||
this.prisma.invoice.count({ where: { clientId, tenantId, status: { in: ['sent', 'partial'] } } }),
|
||||
this.prisma.payment.findFirst({ where: { clientId, tenantId }, orderBy: { createdAt: 'desc' } }),
|
||||
this.prisma.ticket.count({ where: { clientId, tenantId, status: { in: ['open', 'in_progress'] } } }),
|
||||
]);
|
||||
|
||||
return { subscription, unpaidInvoices, recentPayment, openTickets };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user