Browse Source
Merge pull request #67 from vernu/improve-customer-support
Merge pull request #67 from vernu/improve-customer-support
improve customer support and account deletion request flowspull/68/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 568 additions and 197 deletions
-
2api/src/app.module.ts
-
6api/src/auth/auth.module.ts
-
57api/src/auth/guards/optional-auth.guard.ts
-
12api/src/billing/billing.service.ts
-
80api/src/mail/templates/account-deletion-request.hbs
-
61api/src/mail/templates/customer-support-confirmation.hbs
-
58api/src/support/support.controller.ts
-
26api/src/support/support.module.ts
-
141api/src/support/support.service.ts
-
6api/src/users/schemas/user.schema.ts
-
55web/app/(app)/dashboard/(components)/account-deletion-alert.tsx
-
7web/app/(app)/dashboard/(components)/account-settings.tsx
-
4web/app/(app)/dashboard/(components)/dashboard-layout.tsx
-
59web/app/api/customer-support/route.ts
-
103web/app/api/request-account-deletion/route.ts
-
84web/components/shared/customer-support.tsx
-
4web/config/api.ts
@ -0,0 +1,57 @@ |
|||
import { |
|||
CanActivate, |
|||
ExecutionContext, |
|||
Injectable, |
|||
} from '@nestjs/common' |
|||
import { JwtService } from '@nestjs/jwt' |
|||
import { UsersService } from '../../users/users.service' |
|||
import { AuthService } from '../auth.service' |
|||
import * as bcrypt from 'bcryptjs' |
|||
|
|||
@Injectable() |
|||
// Guard for optionally authenticating users by either jwt token or api key
|
|||
export class OptionalAuthGuard implements CanActivate { |
|||
constructor( |
|||
private jwtService: JwtService, |
|||
private usersService: UsersService, |
|||
private authService: AuthService, |
|||
) {} |
|||
|
|||
async canActivate(context: ExecutionContext): Promise<boolean> { |
|||
const request = context.switchToHttp().getRequest() |
|||
let userId |
|||
const apiKeyString = request.headers['x-api-key'] || request.query.apiKey |
|||
if (request.headers.authorization?.startsWith('Bearer ')) { |
|||
const bearerToken = request.headers.authorization.split(' ')[1] |
|||
try { |
|||
const payload = this.jwtService.verify(bearerToken) |
|||
userId = payload.sub |
|||
} catch (e) { |
|||
// Ignore token verification errors
|
|||
return true |
|||
} |
|||
} else if (apiKeyString) { |
|||
const regex = new RegExp(`^${apiKeyString.substr(0, 17)}`, 'g') |
|||
const apiKey = await this.authService.findApiKey({ |
|||
apiKey: { $regex: regex }, |
|||
$or: [{ revokedAt: null }, { revokedAt: { $exists: false } }], |
|||
}) |
|||
|
|||
if (apiKey && bcrypt.compareSync(apiKeyString, apiKey.hashedApiKey)) { |
|||
userId = apiKey.user |
|||
request.apiKey = apiKey |
|||
} |
|||
} |
|||
|
|||
if (userId) { |
|||
const user = await this.usersService.findOne({ _id: userId }) |
|||
if (user) { |
|||
request.user = user |
|||
this.authService.trackAccessLog({ request }) |
|||
} |
|||
} |
|||
|
|||
// Always return true as authentication is optional
|
|||
return true |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
<html lang='en'> |
|||
<head> |
|||
<meta charset='UTF-8' /> |
|||
<meta name='viewport' content='width=device-width, initial-scale=1.0' /> |
|||
<title>Account Deletion Request</title> |
|||
<style> |
|||
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; |
|||
color: #333; max-width: 600px; margin: 0 auto; padding: 20px; } .header { |
|||
text-align: center; margin-bottom: 20px; } .logo { max-width: 150px; |
|||
margin-bottom: 10px; } h1 { color: #dc2626; margin-bottom: 20px; } |
|||
.content { background-color: #f9fafb; border-radius: 8px; padding: 20px; |
|||
margin-bottom: 20px; } .message-details { background-color: #ffffff; |
|||
border-left: 4px solid #dc2626; padding: 15px; margin: 15px 0; |
|||
border-radius: 4px; } .contact-info { margin-top: 20px; padding-top: 15px; |
|||
border-top: 1px solid #e5e7eb; } .field-label { font-weight: bold; color: |
|||
#4b5563; margin-bottom: 5px; } .footer { text-align: center; font-size: |
|||
14px; color: #6b7280; margin-top: 30px; padding-top: 20px; border-top: 1px |
|||
solid #e5e7eb; } .important-notice { background-color: #fee2e2; |
|||
border-left: 4px solid #dc2626; padding: 15px; margin: 15px 0; |
|||
border-radius: 4px; } .cancel-notice { background-color: #e0f2fe; border: |
|||
2px solid #0284c7; padding: 15px; margin: 20px 0; border-radius: 4px; |
|||
text-align: center; } .cancel-notice h3 { color: #0284c7; margin-top: 0; } |
|||
.cancel-action { font-weight: bold; font-size: 16px; } .cancel-button { |
|||
display: inline-block; margin-top: 10px; padding: 8px 16px; |
|||
background-color: #0284c7; color: white; border-radius: 4px; |
|||
text-decoration: none; font-weight: bold; } |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class='header'> |
|||
{{!-- <img src="{{appLogoUrl}}" alt="TextBee Logo" class="logo"> --}} |
|||
<h1>Account Deletion Request</h1> |
|||
</div> |
|||
|
|||
<div class='content'> |
|||
<p>Hello {{name}},</p> |
|||
|
|||
<p>We have received your request to delete your TextBee account. We're |
|||
sorry to see you go.</p> |
|||
|
|||
<div class='important-notice'> |
|||
<p><strong>Important:</strong> |
|||
Your account has been marked for deletion and will be processed within |
|||
7 days. During this period:</p> |
|||
<ul> |
|||
<li>You can still log in and access your account until the deletion is |
|||
completed</li> |
|||
<li>After the deletion is complete, all your data will be permanently |
|||
removed</li> |
|||
<li>This action cannot be undone once processed</li> |
|||
</ul> |
|||
</div> |
|||
|
|||
<div class='message-details'> |
|||
<div class='field-label'>Reason for deletion:</div> |
|||
<p>{{#if message}}{{message}}{{else}}No reason provided{{/if}}</p> |
|||
</div> |
|||
|
|||
<div class='contact-info'> |
|||
<div class='field-label'>Account Information:</div> |
|||
<p>Name: {{name}}</p> |
|||
<p>Email: {{email}}</p> |
|||
</div> |
|||
|
|||
<div class='cancel-notice'> |
|||
<h3>Changed Your Mind?</h3> |
|||
<p class='cancel-action'>If you didn't request this deletion or want to |
|||
keep your account, you can easily cancel this request!</p> |
|||
<p>Simply reply to this email as soon as possible and we'll immediately |
|||
stop the deletion process.</p> |
|||
<p>Your account and all your data will remain intact. No further action |
|||
will be needed.</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class='footer'> |
|||
<p>© {{currentYear}} textBee.dev.</p> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,61 @@ |
|||
<html lang='en'> |
|||
<head> |
|||
<meta charset='UTF-8' /> |
|||
<meta name='viewport' content='width=device-width, initial-scale=1.0' /> |
|||
<title>Support Request Confirmation</title> |
|||
<style> |
|||
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; |
|||
color: #333; max-width: 600px; margin: 0 auto; padding: 20px; } .header { |
|||
text-align: center; margin-bottom: 20px; } .logo { max-width: 150px; |
|||
margin-bottom: 10px; } h1 { color: #2563eb; margin-bottom: 20px; } |
|||
.content { background-color: #f9fafb; border-radius: 8px; padding: 20px; |
|||
margin-bottom: 20px; } .message-details { background-color: #ffffff; |
|||
border-left: 4px solid #2563eb; padding: 15px; margin: 15px 0; |
|||
border-radius: 4px; } .contact-info { margin-top: 20px; padding-top: 15px; |
|||
border-top: 1px solid #e5e7eb; } .field-label { font-weight: bold; color: |
|||
#4b5563; margin-bottom: 5px; } .footer { text-align: center; font-size: |
|||
14px; color: #6b7280; margin-top: 30px; padding-top: 20px; border-top: 1px |
|||
solid #e5e7eb; } |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class='header'> |
|||
{{!-- <img src="{{appLogoUrl}}" alt="TextBee Logo" class="logo"> --}} |
|||
<h1>Support Request Submitted</h1> |
|||
</div> |
|||
|
|||
<div class='content'> |
|||
<p>Hello {{name}},</p> |
|||
|
|||
<p>Thank you for contacting our support team. We have received your |
|||
message and will get back to you as soon as possible.</p> |
|||
|
|||
<div class='message-details'> |
|||
<div class='field-label'>Category:</div> |
|||
<p>{{category}}</p> |
|||
|
|||
<div class='field-label'>Your Message:</div> |
|||
<p>{{message}}</p> |
|||
</div> |
|||
|
|||
<div class='contact-info'> |
|||
<div class='field-label'>Your Contact Information:</div> |
|||
<p>Name: {{name}}</p> |
|||
<p>Email: {{email}}</p> |
|||
{{#if phone}} |
|||
<p>Phone: {{phone}}</p> |
|||
{{else}} |
|||
<p>Phone: Not provided</p> |
|||
{{/if}} |
|||
</div> |
|||
|
|||
<p>we will review your request and respond to the email address you |
|||
provided. If you have any additional information to share, please reply |
|||
to this email.</p> |
|||
</div> |
|||
|
|||
<div class='footer'> |
|||
<p>© {{currentYear}} textBee.dev.</p> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,58 @@ |
|||
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common' |
|||
import { |
|||
CreateSupportMessageDto, |
|||
SupportCategory, |
|||
} from './dto/create-support-message.dto' |
|||
import { SupportService } from './support.service' |
|||
import { JwtAuthGuard } from '../auth/jwt-auth.guard' |
|||
import { Request } from 'express' |
|||
import { OptionalAuthGuard } from '../auth/guards/optional-auth.guard' |
|||
|
|||
@Controller('support') |
|||
export class SupportController { |
|||
constructor(private readonly supportService: SupportService) {} |
|||
|
|||
@UseGuards(OptionalAuthGuard) |
|||
@Post('customer-support') |
|||
async createSupportMessage( |
|||
@Body() createSupportMessageDto: CreateSupportMessageDto, |
|||
@Req() req: Request, |
|||
) { |
|||
const ip = req.ip || (req.headers['x-forwarded-for'] as string) |
|||
const userAgent = req.headers['user-agent'] as string |
|||
|
|||
// Add request metadata
|
|||
createSupportMessageDto.ip = ip |
|||
createSupportMessageDto.userAgent = userAgent |
|||
|
|||
// If user is authenticated, associate the support request with the user
|
|||
if (req.user) { |
|||
createSupportMessageDto.user = req.user['_id'] |
|||
} |
|||
|
|||
return this.supportService.createSupportMessage(createSupportMessageDto) |
|||
} |
|||
|
|||
@UseGuards(JwtAuthGuard) |
|||
@Post('request-account-deletion') |
|||
async requestAccountDeletion( |
|||
@Body() body: { message: string }, |
|||
@Req() req: Request, |
|||
) { |
|||
const ip = req.ip || (req.headers['x-forwarded-for'] as string) |
|||
const userAgent = req.headers['user-agent'] as string |
|||
const user = req.user |
|||
|
|||
const createSupportMessageDto: CreateSupportMessageDto = { |
|||
user: user['_id'], |
|||
name: user['name'], |
|||
email: user['email'], |
|||
category: SupportCategory.ACCOUNT_DELETION, |
|||
message: body.message, |
|||
ip, |
|||
userAgent, |
|||
} |
|||
|
|||
return this.supportService.requestAccountDeletion(createSupportMessageDto) |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
import { Module } from '@nestjs/common' |
|||
import { MongooseModule } from '@nestjs/mongoose' |
|||
import { |
|||
SupportMessage, |
|||
SupportMessageSchema, |
|||
} from './schemas/support-message.schema' |
|||
import { SupportService } from './support.service' |
|||
import { SupportController } from './support.controller' |
|||
import { UsersModule } from 'src/users/users.module' |
|||
import { BillingModule } from 'src/billing/billing.module' |
|||
import { MailModule } from 'src/mail/mail.module' |
|||
import { AuthModule } from 'src/auth/auth.module' |
|||
@Module({ |
|||
imports: [ |
|||
MongooseModule.forFeature([ |
|||
{ name: SupportMessage.name, schema: SupportMessageSchema }, |
|||
]), |
|||
UsersModule, |
|||
BillingModule, |
|||
MailModule, |
|||
AuthModule, |
|||
], |
|||
controllers: [SupportController], |
|||
providers: [SupportService], |
|||
}) |
|||
export class SupportModule {} |
|||
@ -0,0 +1,141 @@ |
|||
import { |
|||
Injectable, |
|||
ConflictException, |
|||
NotFoundException, |
|||
} from '@nestjs/common' |
|||
import { InjectModel } from '@nestjs/mongoose' |
|||
import { isValidObjectId, Model, Types } from 'mongoose' |
|||
import { |
|||
SupportMessage, |
|||
SupportMessageDocument, |
|||
} from './schemas/support-message.schema' |
|||
import { User, UserDocument } from '../users/schemas/user.schema' |
|||
import { |
|||
CreateSupportMessageDto, |
|||
SupportCategory, |
|||
} from './dto/create-support-message.dto' |
|||
import { MailService } from '../mail/mail.service' |
|||
|
|||
@Injectable() |
|||
export class SupportService { |
|||
constructor( |
|||
@InjectModel(SupportMessage.name) |
|||
private supportMessageModel: Model<SupportMessageDocument>, |
|||
@InjectModel(User.name) private userModel: Model<UserDocument>, |
|||
private readonly mailService: MailService, |
|||
) {} |
|||
|
|||
async createSupportMessage( |
|||
createSupportMessageDto: CreateSupportMessageDto, |
|||
): Promise<{ message: string }> { |
|||
try { |
|||
// Create and save the support message
|
|||
const createdMessage = new this.supportMessageModel( |
|||
createSupportMessageDto, |
|||
) |
|||
const savedMessage = await createdMessage.save() |
|||
|
|||
// Determine if the user is registered
|
|||
let user = null |
|||
if ( |
|||
createSupportMessageDto.user && |
|||
isValidObjectId(createSupportMessageDto.user) |
|||
) { |
|||
user = await this.userModel.findById(createSupportMessageDto.user) |
|||
} |
|||
|
|||
// Send confirmation email to user
|
|||
await this.mailService.sendEmailFromTemplate({ |
|||
to: createSupportMessageDto.email, |
|||
cc: process.env.ADMIN_EMAIL, |
|||
subject: `Support Request Submitted: ${createSupportMessageDto.category}-${savedMessage._id}`, |
|||
template: 'customer-support-confirmation', |
|||
context: { |
|||
name: createSupportMessageDto.name, |
|||
email: createSupportMessageDto.email, |
|||
phone: createSupportMessageDto.phone || 'Not provided', |
|||
category: createSupportMessageDto.category, |
|||
message: createSupportMessageDto.message, |
|||
appLogoUrl: |
|||
process.env.APP_LOGO_URL || 'https://textbee.dev/logo.png', |
|||
currentYear: new Date().getFullYear(), |
|||
}, |
|||
}) |
|||
|
|||
return { message: 'Support request submitted successfully' } |
|||
} catch (error) { |
|||
console.error('Error creating support message:', error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
// Method for account deletion requests
|
|||
async requestAccountDeletion( |
|||
createSupportMessageDto: CreateSupportMessageDto, |
|||
): Promise<{ message: string }> { |
|||
try { |
|||
// Check if user exists
|
|||
if ( |
|||
!createSupportMessageDto.user || |
|||
!isValidObjectId(createSupportMessageDto.user) |
|||
) { |
|||
throw new NotFoundException('User not found') |
|||
} |
|||
|
|||
const userId = new Types.ObjectId(createSupportMessageDto.user.toString()) |
|||
const user = await this.userModel.findById(userId) |
|||
|
|||
if (!user) { |
|||
throw new NotFoundException('User not found') |
|||
} |
|||
|
|||
// Check if user has already requested deletion
|
|||
const existingRequest = await this.supportMessageModel.findOne({ |
|||
user: userId, |
|||
category: SupportCategory.ACCOUNT_DELETION, |
|||
}) |
|||
|
|||
if (existingRequest) { |
|||
throw new ConflictException( |
|||
'Account deletion has already been requested', |
|||
) |
|||
} |
|||
|
|||
// Create and save the support message
|
|||
const createdMessage = new this.supportMessageModel( |
|||
createSupportMessageDto, |
|||
) |
|||
const savedMessage = await createdMessage.save() |
|||
|
|||
// Update user's account deletion requested timestamp
|
|||
await this.userModel.updateOne( |
|||
{ _id: userId }, |
|||
{ |
|||
accountDeletionRequestedAt: new Date(), |
|||
accountDeletionReason: createSupportMessageDto.message || null, |
|||
}, |
|||
) |
|||
|
|||
// Send confirmation email
|
|||
await this.mailService.sendEmailFromTemplate({ |
|||
to: user.email, |
|||
cc: process.env.ADMIN_EMAIL, |
|||
subject: `Account Deletion Request: ${savedMessage._id}`, |
|||
template: 'account-deletion-request', |
|||
context: { |
|||
name: user.name, |
|||
email: user.email, |
|||
message: createSupportMessageDto.message || 'No reason provided', |
|||
appLogoUrl: |
|||
process.env.APP_LOGO_URL || 'https://textbee.dev/logo.png', |
|||
currentYear: new Date().getFullYear(), |
|||
}, |
|||
}) |
|||
|
|||
return { message: 'Account deletion request submitted successfully' } |
|||
} catch (error) { |
|||
console.error('Error requesting account deletion:', error) |
|||
throw error |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
import { Alert, AlertDescription } from '@/components/ui/alert' |
|||
import { ApiEndpoints } from '@/config/api' |
|||
import httpBrowserClient from '@/lib/httpBrowserClient' |
|||
import { useQuery } from '@tanstack/react-query' |
|||
import { AlertTriangle } from 'lucide-react' |
|||
|
|||
export default function AccountDeletionAlert() { |
|||
const { |
|||
data: userData, |
|||
isLoading: isLoadingUserData, |
|||
error: userDataError, |
|||
} = useQuery({ |
|||
queryKey: ['whoAmI'], |
|||
queryFn: () => |
|||
httpBrowserClient |
|||
.get(ApiEndpoints.auth.whoAmI()) |
|||
.then((res) => res.data.data), |
|||
}) |
|||
|
|||
if (isLoadingUserData || !userData || userDataError) { |
|||
return null |
|||
} |
|||
|
|||
// Only show the alert if the user has requested account deletion
|
|||
if (!userData.accountDeletionRequestedAt) { |
|||
return null |
|||
} |
|||
|
|||
// Calculate days remaining until deletion (assuming 7-day window)
|
|||
const deletionDate = new Date(userData.accountDeletionRequestedAt) |
|||
deletionDate.setDate(deletionDate.getDate() + 7) |
|||
const daysRemaining = Math.max( |
|||
0, |
|||
Math.ceil( |
|||
(deletionDate.getTime() - new Date().getTime()) / (1000 * 3600 * 24) |
|||
) |
|||
) |
|||
|
|||
return ( |
|||
<Alert className='bg-gradient-to-r from-amber-600 to-red-600 text-white'> |
|||
<AlertDescription className='flex items-center gap-2'> |
|||
<AlertTriangle className='h-5 w-5 flex-shrink-0' /> |
|||
<div className='text-sm md:text-base'> |
|||
<span className='font-medium'>Your account is pending deletion.</span>{' '} |
|||
Your data will be permanently deleted{' '} |
|||
{daysRemaining > 0 |
|||
? `in ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''}.` |
|||
: 'very soon.'}{' '} |
|||
If you would like to cancel this request, please email{' '} |
|||
<span className='font-medium'>support@textbee.dev</span>. |
|||
</div> |
|||
</AlertDescription> |
|||
</Alert> |
|||
) |
|||
} |
|||
@ -1,59 +0,0 @@ |
|||
import { |
|||
NextRequest, |
|||
NextResponse, |
|||
userAgent, |
|||
userAgentFromString, |
|||
} from 'next/server' |
|||
|
|||
import prismaClient from '@/lib/prismaClient' |
|||
import { sendMail } from '@/lib/mail' |
|||
|
|||
export async function POST(req: NextRequest) { |
|||
const ip = req.ip || req.headers.get('x-forwarded-for') |
|||
const { browser, device, os, isBot, ua } = userAgent(req) |
|||
// const userAgentString = userAgentFromString(ua)
|
|||
|
|||
const body = await req.json() |
|||
|
|||
try { |
|||
const result = await prismaClient.supportMessage.create({ |
|||
data: { |
|||
...body, |
|||
ip, |
|||
userAgent: ua, |
|||
}, |
|||
}) |
|||
|
|||
// send email to user
|
|||
await sendMail({ |
|||
to: body.email, |
|||
cc: process.env.ADMIN_EMAIL, |
|||
subject: `Support request submitted: ${body.category}-${result.id}`, |
|||
html: `<pre>
|
|||
<h1>Support request submitted</h1> |
|||
<p>Thank you for contacting us. We will get back to you soon.</p> |
|||
<p>Here is a copy of your message:</p> |
|||
<hr/> |
|||
<h2>Category</h2><br/>${body.category} |
|||
<h2>Message</h2><br/>${body.message} |
|||
|
|||
<h2>Contact Information</h2> |
|||
<p>Name: ${body.name}</p> |
|||
<p>Email: ${body.email}</p> |
|||
<p>Phone: ${body.phone || 'N/A'}</p> |
|||
</pre>`,
|
|||
}) |
|||
|
|||
return NextResponse.json({ |
|||
message: 'Support request submitted', |
|||
}) |
|||
} catch (error) { |
|||
console.error(error) |
|||
return NextResponse.json( |
|||
{ |
|||
message: `Support request failed to submit : ${error.message}`, |
|||
}, |
|||
{ status: 400 } |
|||
) |
|||
} |
|||
} |
|||
@ -1,103 +0,0 @@ |
|||
import { |
|||
NextRequest, |
|||
NextResponse, |
|||
userAgent, |
|||
userAgentFromString, |
|||
} from 'next/server' |
|||
|
|||
import prismaClient from '@/lib/prismaClient' |
|||
import { sendMail } from '@/lib/mail' |
|||
import { getServerSession, User } from 'next-auth' |
|||
import { authOptions } from '@/lib/auth' |
|||
|
|||
export async function POST(req: NextRequest) { |
|||
const ip = req.ip || req.headers.get('x-forwarded-for') |
|||
const { browser, device, os, isBot, ua } = userAgent(req) |
|||
// const userAgentString = userAgentFromString(ua)
|
|||
|
|||
const body = await req.json() |
|||
|
|||
const session = await getServerSession(authOptions as any) |
|||
if (!session) { |
|||
return NextResponse.json( |
|||
{ |
|||
message: 'You must be logged in to request account deletion', |
|||
}, |
|||
{ status: 401 } |
|||
) |
|||
} |
|||
// @ts-ignore
|
|||
const currentUser = session?.user as User |
|||
|
|||
if (!currentUser) { |
|||
return NextResponse.json( |
|||
{ |
|||
message: 'You must be logged in to request account deletion', |
|||
}, |
|||
{ status: 401 } |
|||
) |
|||
} |
|||
|
|||
const category = 'account-deletion' |
|||
const message = body.message ?? 'No message provided' |
|||
|
|||
try { |
|||
// check if the user has already requested account deletion
|
|||
const existingRequest = await prismaClient.supportMessage.findFirst({ |
|||
where: { |
|||
user: currentUser.id, |
|||
category: 'account-deletion', |
|||
}, |
|||
}) |
|||
|
|||
if (existingRequest) { |
|||
return NextResponse.json( |
|||
{ |
|||
message: 'You have already requested account deletion', |
|||
}, |
|||
{ status: 400 } |
|||
) |
|||
} |
|||
|
|||
const result = await prismaClient.supportMessage.create({ |
|||
data: { |
|||
user: currentUser.id, |
|||
category, |
|||
message, |
|||
ip, |
|||
userAgent: ua, |
|||
}, |
|||
}) |
|||
|
|||
// send email to user
|
|||
await sendMail({ |
|||
to: currentUser.email, |
|||
cc: process.env.ADMIN_EMAIL, |
|||
subject: `Account deletion request submitted: ${category}-${result.id}`, |
|||
html: `<pre>
|
|||
<h1>Account deletion request submitted</h1> |
|||
<p>Thank you for contacting us. We will get back to you soon.</p> |
|||
<p>Here is a copy of your message:</p> |
|||
<hr/> |
|||
<h2>Category</h2><br/>${category} |
|||
<h2>Message</h2><br/>${message} |
|||
|
|||
<h2>Contact Information</h2> |
|||
<p>Name: ${currentUser.name}</p> |
|||
<p>Email: ${currentUser.email}</p> |
|||
</pre>`,
|
|||
}) |
|||
|
|||
return NextResponse.json({ |
|||
message: 'Support request submitted', |
|||
}) |
|||
} catch (error) { |
|||
console.error(error) |
|||
return NextResponse.json( |
|||
{ |
|||
message: `Support request failed to submit : ${error.message}`, |
|||
}, |
|||
{ status: 400 } |
|||
) |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue