apps/recallassess/recallassess-api/src/api/shared/email/services/unsubscribe-token.service.ts
Properties |
|
Methods |
|
constructor(prisma: BNestPrismaService)
|
||||||
|
Parameters :
|
| Async generateToken | ||||||||||||
generateToken(email: string, userType: "participant" | "participant_admin" | "super_admin", userId: number)
|
||||||||||||
|
Generate secure unsubscribe token
Parameters :
Returns :
Promise<string>
|
| Async getOrGenerateToken | ||||||
getOrGenerateToken(participantId: number)
|
||||||
|
Get or generate token for participant Reuses existing valid token if available, otherwise generates new one
Parameters :
Returns :
Promise<string>
|
| Async getOrGenerateTokenForUser | ||||||
getOrGenerateTokenForUser(userId: number)
|
||||||
|
Get or generate token for super admin (User)
Parameters :
Returns :
Promise<string>
|
| Async markTokenAsUsed | ||||||
markTokenAsUsed(token: string)
|
||||||
|
Mark token as used
Parameters :
Returns :
Promise<void>
|
| Async validateToken | ||||||
validateToken(token: string)
|
||||||
|
Validate and get token data
Parameters :
Returns :
Promise<literal type>
|
| Private Readonly logger |
Type : unknown
|
Default value : new Logger(UnsubscribeTokenService.name)
|
import { Injectable, Logger, BadRequestException } from "@nestjs/common";
import { BNestPrismaService } from "@bish-nest/core/services";
import * as crypto from "crypto";
@Injectable()
export class UnsubscribeTokenService {
private readonly logger = new Logger(UnsubscribeTokenService.name);
constructor(private readonly prisma: BNestPrismaService) {}
/**
* Generate secure unsubscribe token
*/
async generateToken(
email: string,
userType: "participant" | "participant_admin" | "super_admin",
userId: number,
): Promise<string> {
// Generate secure token (32 bytes = 64 hex characters)
const token = crypto.randomBytes(32).toString("hex");
// Expires in 90 days
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 90);
await this.prisma.client.emailUnsubscribeToken.create({
data: {
token,
email: email.toLowerCase(),
user_type: userType,
user_id: userId,
expires_at: expiresAt,
},
});
this.logger.debug(`Generated unsubscribe token for ${email} (${userType})`);
return token;
}
/**
* Validate and get token data
*/
async validateToken(token: string): Promise<{
email: string;
user_type: string;
user_id: number;
}> {
if (!token) {
throw new BadRequestException("Token is required");
}
const tokenRecord = await this.prisma.client.emailUnsubscribeToken.findUnique({
where: { token },
});
if (!tokenRecord) {
throw new BadRequestException("Invalid unsubscribe token");
}
if (tokenRecord.used) {
throw new BadRequestException("This unsubscribe link has already been used");
}
if (new Date() > tokenRecord.expires_at) {
throw new BadRequestException("Unsubscribe link has expired");
}
return {
email: tokenRecord.email,
user_type: tokenRecord.user_type,
user_id: tokenRecord.user_id,
};
}
/**
* Mark token as used
*/
async markTokenAsUsed(token: string): Promise<void> {
await this.prisma.client.emailUnsubscribeToken.update({
where: { token },
data: {
used: true,
used_at: new Date(),
},
});
this.logger.debug(`Marked unsubscribe token as used: ${token.substring(0, 8)}...`);
}
/**
* Get or generate token for participant
* Reuses existing valid token if available, otherwise generates new one
*/
async getOrGenerateToken(participantId: number): Promise<string> {
const participant = await this.prisma.client.participant.findUnique({
where: { id: participantId },
select: {
id: true,
email: true,
role: true,
},
});
if (!participant) {
throw new BadRequestException(`Participant not found: ${participantId}`);
}
// Check for existing valid token
const existingToken = await this.prisma.client.emailUnsubscribeToken.findFirst({
where: {
email: participant.email.toLowerCase(),
user_type: participant.role === "PARTICIPANT_ADMIN" ? "participant_admin" : "participant",
user_id: participantId,
used: false,
expires_at: { gt: new Date() },
},
orderBy: { created_at: "desc" },
});
if (existingToken) {
return existingToken.token;
}
// Generate new token
return this.generateToken(
participant.email,
participant.role === "PARTICIPANT_ADMIN" ? "participant_admin" : "participant",
participantId,
);
}
/**
* Get or generate token for super admin (User)
*/
async getOrGenerateTokenForUser(userId: number): Promise<string> {
const user = await this.prisma.client.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
},
});
if (!user) {
throw new BadRequestException(`User not found: ${userId}`);
}
// Check for existing valid token
const existingToken = await this.prisma.client.emailUnsubscribeToken.findFirst({
where: {
email: user.email.toLowerCase(),
user_type: "super_admin",
user_id: userId,
used: false,
expires_at: { gt: new Date() },
},
orderBy: { created_at: "desc" },
});
if (existingToken) {
return existingToken.token;
}
// Generate new token
return this.generateToken(user.email, "super_admin", userId);
}
}