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
-
8api/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
-
5web/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