initial commit

This commit is contained in:
Kar k1
2025-08-30 18:18:57 +05:30
commit 7219108342
270 changed files with 70221 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAdminAuth } from '@/lib/admin-middleware'
import { connectDB } from '@/lib/mongodb'
import { Billing } from '@/models/billing'
import { Transaction } from '@/models/transaction'
import { z } from 'zod'
const BillingUpdateSchema = z.object({
payment_status: z.enum(['pending', 'completed', 'failed', 'refunded']).optional(),
service_status: z.enum(['active', 'inactive', 'suspended', 'cancelled']).optional(),
amount: z.number().min(0).optional(),
notes: z.string().optional(),
})
export async function GET(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const search = searchParams.get('search') || ''
const serviceType = searchParams.get('serviceType') || ''
const paymentStatus = searchParams.get('paymentStatus') || ''
const dateFrom = searchParams.get('dateFrom')
const dateTo = searchParams.get('dateTo')
const skip = (page - 1) * limit
// Build filter query
const filter: any = {}
if (search) {
filter.$or = [
{ user_email: { $regex: search, $options: 'i' } },
{ silicon_id: { $regex: search, $options: 'i' } },
{ service_name: { $regex: search, $options: 'i' } },
]
}
if (serviceType && serviceType !== 'all') {
filter.service_type = serviceType
}
if (paymentStatus && paymentStatus !== 'all') {
filter.payment_status = paymentStatus
}
if (dateFrom || dateTo) {
filter.created_at = {}
if (dateFrom) filter.created_at.$gte = new Date(dateFrom)
if (dateTo) filter.created_at.$lte = new Date(dateTo)
}
const [billings, totalBillings] = await Promise.all([
Billing.find(filter).sort({ created_at: -1 }).skip(skip).limit(limit),
Billing.countDocuments(filter),
])
const totalPages = Math.ceil(totalBillings / limit)
// Calculate summary statistics for current filter
const summaryStats = await Billing.aggregate([
{ $match: filter },
{
$group: {
_id: null,
totalAmount: { $sum: '$amount' },
totalTax: { $sum: '$tax_amount' },
totalDiscount: { $sum: '$discount_applied' },
completedCount: {
$sum: { $cond: [{ $eq: ['$payment_status', 'completed'] }, 1, 0] },
},
pendingCount: {
$sum: { $cond: [{ $eq: ['$payment_status', 'pending'] }, 1, 0] },
},
failedCount: {
$sum: { $cond: [{ $eq: ['$payment_status', 'failed'] }, 1, 0] },
},
},
},
])
return NextResponse.json({
billings,
pagination: {
currentPage: page,
totalPages,
totalBillings,
hasNext: page < totalPages,
hasPrev: page > 1,
},
summary: summaryStats[0] || {
totalAmount: 0,
totalTax: 0,
totalDiscount: 0,
completedCount: 0,
pendingCount: 0,
failedCount: 0,
},
})
} catch (error) {
console.error('Admin billing fetch error:', error)
return NextResponse.json({ error: 'Failed to fetch billing data' }, { status: 500 })
}
})
}
export async function POST(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const body = await request.json()
const { action, billingId, data } = body
if (action === 'update') {
const validatedData = BillingUpdateSchema.parse(data)
const billing = await Billing.findByIdAndUpdate(
billingId,
{
...validatedData,
updated_at: new Date(),
},
{ new: true, runValidators: true }
)
if (!billing) {
return NextResponse.json({ error: 'Billing record not found' }, { status: 404 })
}
return NextResponse.json({ billing })
}
if (action === 'refund') {
const billing = await Billing.findById(billingId)
if (!billing) {
return NextResponse.json({ error: 'Billing record not found' }, { status: 404 })
}
if (billing.payment_status !== 'paid') {
return NextResponse.json({ error: 'Can only refund paid payments' }, { status: 400 })
}
// Update billing status
billing.payment_status = 'refunded'
billing.status = 'cancelled'
await billing.save()
// Create refund transaction
const refundTransaction = new Transaction({
userId: billing.user_id,
type: 'credit',
amount: billing.amount,
description: `Refund for ${billing.service_name}`,
status: 'completed',
reference: `refund_${billing._id}`,
metadata: {
originalBillingId: billing._id,
refundedBy: admin.id,
refundReason: data.refundReason || 'Admin refund',
},
})
await refundTransaction.save()
return NextResponse.json({
billing,
transaction: refundTransaction,
message: 'Refund processed successfully',
})
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
console.error('Admin billing action error:', error)
return NextResponse.json({ error: 'Failed to perform billing action' }, { status: 500 })
}
})
}

View File

@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAdminAuth } from '@/lib/admin-middleware'
import { connectDB } from '@/lib/mongodb'
import { User } from '@/models/user'
import { Transaction } from '@/models/transaction'
import { Billing } from '@/models/billing'
import { DeveloperRequest } from '@/models/developer-request'
export async function GET(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
// Get current date for time-based queries
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()))
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())
// User statistics
const totalUsers = await User.countDocuments()
const activeUsers = await User.countDocuments({ lastLogin: { $gte: startOfMonth } })
const newUsersThisMonth = await User.countDocuments({ createdAt: { $gte: startOfMonth } })
const verifiedUsers = await User.countDocuments({ isVerified: true })
// Financial statistics
const totalRevenue = await Billing.aggregate([
{ $group: { _id: null, total: { $sum: '$amount' } } },
])
const monthlyRevenue = await Billing.aggregate([
{ $match: { created_at: { $gte: startOfMonth } } },
{ $group: { _id: null, total: { $sum: '$amount' } } },
])
const weeklyRevenue = await Billing.aggregate([
{ $match: { created_at: { $gte: startOfWeek } } },
{ $group: { _id: null, total: { $sum: '$amount' } } },
])
// Transaction statistics
const totalTransactions = await Transaction.countDocuments()
const monthlyTransactions = await Transaction.countDocuments({
createdAt: { $gte: startOfMonth },
})
// Service statistics
const totalServices = await Billing.countDocuments()
const activeServices = await Billing.countDocuments({
payment_status: 'paid',
service_status: 1, // 1 = active
})
// Developer requests
const totalDeveloperRequests = await DeveloperRequest.countDocuments()
const pendingDeveloperRequests = await DeveloperRequest.countDocuments({
status: 'pending',
})
// Recent activity (last 7 days)
const recentUsers = await User.find({ createdAt: { $gte: startOfWeek } })
.select('name email siliconId createdAt')
.sort({ createdAt: -1 })
.limit(10)
const recentTransactions = await Transaction.find({ createdAt: { $gte: startOfWeek } })
.populate('userId', 'name email siliconId')
.sort({ createdAt: -1 })
.limit(10)
// Service breakdown
const serviceBreakdown = await Billing.aggregate([
{
$group: {
_id: '$service_type',
count: { $sum: 1 },
revenue: { $sum: '$amount' },
},
},
{ $sort: { count: -1 } },
])
// Monthly growth data (last 6 months)
const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
const monthlyGrowth = await User.aggregate([
{ $match: { createdAt: { $gte: sixMonthsAgo } } },
{
$group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' },
},
count: { $sum: 1 },
},
},
{ $sort: { '_id.year': 1, '_id.month': 1 } },
])
const revenueGrowth = await Billing.aggregate([
{ $match: { created_at: { $gte: sixMonthsAgo } } },
{
$group: {
_id: {
year: { $year: '$created_at' },
month: { $month: '$created_at' },
},
revenue: { $sum: '$amount' },
},
},
{ $sort: { '_id.year': 1, '_id.month': 1 } },
])
return NextResponse.json({
users: {
total: totalUsers,
active: activeUsers,
newThisMonth: newUsersThisMonth,
verified: verifiedUsers,
verificationRate: totalUsers > 0 ? ((verifiedUsers / totalUsers) * 100).toFixed(1) : 0,
},
revenue: {
total: totalRevenue[0]?.total || 0,
monthly: monthlyRevenue[0]?.total || 0,
weekly: weeklyRevenue[0]?.total || 0,
},
transactions: {
total: totalTransactions,
monthly: monthlyTransactions,
},
services: {
total: totalServices,
active: activeServices,
breakdown: serviceBreakdown,
},
developerRequests: {
total: totalDeveloperRequests,
pending: pendingDeveloperRequests,
},
recentActivity: {
users: recentUsers,
transactions: recentTransactions,
},
growth: {
users: monthlyGrowth,
revenue: revenueGrowth,
},
})
} catch (error) {
console.error('Admin dashboard error:', error)
return NextResponse.json({ error: 'Failed to fetch dashboard data' }, { status: 500 })
}
})
}

View File

@@ -0,0 +1,187 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAdminAuth } from '@/lib/admin-middleware'
import { connectDB } from '@/lib/mongodb'
import { User } from '@/models/user'
import { Transaction } from '@/models/transaction'
import { Billing } from '@/models/billing'
import { DeveloperRequest } from '@/models/developer-request'
export async function GET(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const { searchParams } = new URL(request.url)
const reportType = searchParams.get('type')
const format = searchParams.get('format') || 'json'
const dateFrom = searchParams.get('dateFrom')
const dateTo = searchParams.get('dateTo')
// Build date filter
const dateFilter: any = {}
if (dateFrom || dateTo) {
dateFilter.createdAt = {}
if (dateFrom) dateFilter.createdAt.$gte = new Date(dateFrom)
if (dateTo) dateFilter.createdAt.$lte = new Date(dateTo)
}
let data: any = {}
switch (reportType) {
case 'users':
data = await User.find(dateFilter)
.select('-password -refreshToken')
.sort({ createdAt: -1 })
break
case 'transactions':
data = await Transaction.find(dateFilter)
.populate('userId', 'name email siliconId')
.sort({ createdAt: -1 })
break
case 'billing':
data = await Billing.find(
dateFilter.createdAt ? { created_at: dateFilter.createdAt } : {}
).sort({ created_at: -1 })
break
case 'developer-requests':
const devFilter = dateFilter.createdAt ? { createdAt: dateFilter.createdAt } : {}
data = await (DeveloperRequest as any)
.find(devFilter)
.populate('userId', 'name email siliconId')
.sort({ createdAt: -1 })
break
case 'summary':
// Generate comprehensive summary report
const [users, transactions, billings, developerRequests] = await Promise.all([
User.find(dateFilter).select('-password -refreshToken'),
Transaction.find(dateFilter).populate('userId', 'name email siliconId'),
Billing.find(dateFilter.createdAt ? { created_at: dateFilter.createdAt } : {}),
(DeveloperRequest as any)
.find(dateFilter.createdAt ? { createdAt: dateFilter.createdAt } : {})
.populate('userId', 'name email siliconId'),
])
// Calculate summary statistics
const userStats = {
total: users.length,
verified: users.filter((u) => u.isVerified).length,
admins: users.filter((u) => u.role === 'admin').length,
totalBalance: users.reduce((sum, u) => sum + (u.balance || 0), 0),
}
const transactionStats = {
total: transactions.length,
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
credits: transactions.filter((t) => t.type === 'credit').length,
debits: transactions.filter((t) => t.type === 'debit').length,
}
const billingStats = {
total: billings.length,
totalRevenue: billings.reduce((sum, b) => sum + b.amount, 0),
completed: billings.filter((b) => b.payment_status === 'paid').length,
pending: billings.filter((b) => b.payment_status === 'pending').length,
}
const developerStats = {
total: developerRequests.length,
pending: developerRequests.filter((d) => d.status === 'pending').length,
inProgress: developerRequests.filter((d) => d.status === 'in_progress').length,
completed: developerRequests.filter((d) => d.status === 'completed').length,
}
data = {
summary: {
users: userStats,
transactions: transactionStats,
billing: billingStats,
developerRequests: developerStats,
},
users: users.slice(0, 100), // Limit for performance
transactions: transactions.slice(0, 100),
billings: billings.slice(0, 100),
developerRequests: developerRequests.slice(0, 100),
}
break
default:
return NextResponse.json({ error: 'Invalid report type' }, { status: 400 })
}
if (format === 'csv') {
// Convert to CSV format
const csv = convertToCSV(data, reportType)
return new NextResponse(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="${reportType}_report_${new Date().toISOString().split('T')[0]}.csv"`,
},
})
}
return NextResponse.json({
reportType,
generatedAt: new Date().toISOString(),
generatedBy: admin.name,
dateRange: { from: dateFrom, to: dateTo },
data,
})
} catch (error) {
console.error('Admin reports error:', error)
return NextResponse.json({ error: 'Failed to generate report' }, { status: 500 })
}
})
}
function convertToCSV(data: any, reportType: string): string {
if (!Array.isArray(data)) {
if (reportType === 'summary' && data.summary) {
// For summary reports, create a simple CSV with key metrics
const lines = [
'Metric,Value',
`Total Users,${data.summary.users.total}`,
`Verified Users,${data.summary.users.verified}`,
`Admin Users,${data.summary.users.admins}`,
`Total Balance,${data.summary.users.totalBalance}`,
`Total Transactions,${data.summary.transactions.total}`,
`Transaction Amount,${data.summary.transactions.totalAmount}`,
`Total Billing Records,${data.summary.billing.total}`,
`Total Revenue,${data.summary.billing.totalRevenue}`,
`Developer Requests,${data.summary.developerRequests.total}`,
]
return lines.join('\n')
}
return 'No data available'
}
if (data.length === 0) {
return 'No data available'
}
// Get headers from first object
const headers = Object.keys(data[0]).filter(
(key) => typeof data[0][key] !== 'object' || data[0][key] === null
)
// Create CSV content
const csvHeaders = headers.join(',')
const csvRows = data.map((row) =>
headers
.map((header) => {
const value = row[header]
if (value === null || value === undefined) return ''
if (typeof value === 'string' && value.includes(',')) {
return `"${value.replace(/"/g, '""')}"`
}
return value
})
.join(',')
)
return [csvHeaders, ...csvRows].join('\n')
}

View File

@@ -0,0 +1,200 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAdminAuth } from '@/lib/admin-middleware'
import { connectDB } from '@/lib/mongodb'
import { Billing } from '@/models/billing'
import { DeveloperRequest } from '@/models/developer-request'
export async function GET(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const serviceType = searchParams.get('serviceType') || ''
const status = searchParams.get('status') || ''
const dateFrom = searchParams.get('dateFrom')
const dateTo = searchParams.get('dateTo')
const skip = (page - 1) * limit
// Build filter query for billing services
const billingFilter: any = {}
if (serviceType && serviceType !== 'all') {
billingFilter.service_type = serviceType
}
if (status && status !== 'all') {
billingFilter.service_status = status
}
if (dateFrom || dateTo) {
billingFilter.created_at = {}
if (dateFrom) billingFilter.created_at.$gte = new Date(dateFrom)
if (dateTo) billingFilter.created_at.$lte = new Date(dateTo)
}
// Fetch billing services
const [billingServices, totalBillingServices] = await Promise.all([
Billing.find(billingFilter).sort({ created_at: -1 }).skip(skip).limit(limit),
Billing.countDocuments(billingFilter),
])
// Build filter for developer requests
const devRequestFilter: any = {}
if (status && status !== 'all') {
devRequestFilter.status = status
}
if (dateFrom || dateTo) {
devRequestFilter.createdAt = {}
if (dateFrom) devRequestFilter.createdAt.$gte = new Date(dateFrom)
if (dateTo) devRequestFilter.createdAt.$lte = new Date(dateTo)
}
// Fetch developer requests
const [developerRequests, totalDeveloperRequests] = await Promise.all([
(DeveloperRequest as any)
.find(devRequestFilter)
.populate('userId', 'name email siliconId')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
(DeveloperRequest as any).countDocuments(devRequestFilter),
])
// Service statistics
const serviceStats = await Billing.aggregate([
{
$group: {
_id: '$service_type',
count: { $sum: 1 },
revenue: { $sum: '$amount' },
activeServices: {
$sum: { $cond: [{ $eq: ['$service_status', 'active'] }, 1, 0] },
},
},
},
{ $sort: { count: -1 } },
])
// Status breakdown
const statusBreakdown = await Billing.aggregate([
{
$group: {
_id: '$service_status',
count: { $sum: 1 },
},
},
])
const totalPages = Math.ceil(Math.max(totalBillingServices, totalDeveloperRequests) / limit)
return NextResponse.json({
billingServices,
developerRequests,
serviceStats,
statusBreakdown,
pagination: {
currentPage: page,
totalPages,
totalBillingServices,
totalDeveloperRequests,
hasNext: page < totalPages,
hasPrev: page > 1,
},
})
} catch (error) {
console.error('Admin services fetch error:', error)
return NextResponse.json({ error: 'Failed to fetch services data' }, { status: 500 })
}
})
}
export async function POST(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const body = await request.json()
const { action, serviceId, serviceType, data } = body
if (action === 'updateBilling') {
const billing = await Billing.findByIdAndUpdate(
serviceId,
{
service_status: data.service_status,
updated_at: new Date(),
},
{ new: true }
)
if (!billing) {
return NextResponse.json({ error: 'Service not found' }, { status: 404 })
}
return NextResponse.json({ service: billing })
}
if (action === 'updateDeveloperRequest') {
const developerRequest = await (DeveloperRequest as any)
.findByIdAndUpdate(
serviceId,
{
status: data.status,
assignedDeveloper: data.assignedDeveloper,
estimatedCompletionDate: data.estimatedCompletionDate,
notes: data.notes,
updatedAt: new Date(),
},
{ new: true }
)
.populate('userId', 'name email siliconId')
if (!developerRequest) {
return NextResponse.json({ error: 'Developer request not found' }, { status: 404 })
}
return NextResponse.json({ service: developerRequest })
}
if (action === 'cancelService') {
if (serviceType === 'billing') {
const billing = await Billing.findByIdAndUpdate(
serviceId,
{
service_status: 'cancelled',
updated_at: new Date(),
},
{ new: true }
)
return NextResponse.json({ service: billing })
}
if (serviceType === 'developer') {
const updatedDeveloperRequest = await (DeveloperRequest as any)
.findByIdAndUpdate(
serviceId,
{
status: 'cancelled',
updatedAt: new Date(),
},
{ new: true }
)
.populate('userId', 'name email siliconId')
return NextResponse.json({ service: updatedDeveloperRequest })
}
}
return NextResponse.json({ error: 'Invalid action or service type' }, { status: 400 })
} catch (error) {
console.error('Admin service action error:', error)
return NextResponse.json({ error: 'Failed to perform service action' }, { status: 500 })
}
})
}

View File

@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAdminAuth } from '@/lib/admin-middleware'
import { connectDB } from '@/lib/mongodb'
import { User } from '@/models/user'
import { z } from 'zod'
import { getSystemSettings, updateSystemSettings } from '@/lib/system-settings'
const SystemSettingsSchema = z.object({
maintenanceMode: z.boolean().optional(),
registrationEnabled: z.boolean().optional(),
emailVerificationRequired: z.boolean().optional(),
maxUserBalance: z.number().min(0).optional(),
defaultUserRole: z.enum(['user', 'admin']).optional(),
systemMessage: z.string().optional(),
paymentGatewayEnabled: z.boolean().optional(),
developerHireEnabled: z.boolean().optional(),
vpsDeploymentEnabled: z.boolean().optional(),
kubernetesDeploymentEnabled: z.boolean().optional(),
vpnServiceEnabled: z.boolean().optional(),
})
// System settings are now managed by the system-settings service
export async function GET(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
// Get system statistics
const totalUsers = await User.countDocuments()
const adminUsers = await User.countDocuments({ role: 'admin' })
const verifiedUsers = await User.countDocuments({ isVerified: true })
const unverifiedUsers = await User.countDocuments({ isVerified: false })
// Get recent admin activities (mock data for now)
const recentActivities = [
{
id: '1',
action: 'User role updated',
details: 'Changed user role from user to admin',
timestamp: new Date().toISOString(),
adminName: admin.name,
},
]
return NextResponse.json({
settings: await getSystemSettings(),
statistics: {
totalUsers,
adminUsers,
verifiedUsers,
unverifiedUsers,
},
recentActivities,
})
} catch (error) {
console.error('Admin settings fetch error:', error)
return NextResponse.json({ error: 'Failed to fetch system settings' }, { status: 500 })
}
})
}
export async function POST(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
const body = await request.json()
const { action, settings } = body
if (action === 'updateSettings') {
const validatedSettings = SystemSettingsSchema.parse(settings)
// Update system settings
const updatedSettings = {
...validatedSettings,
lastUpdated: new Date().toISOString(),
updatedBy: admin.name,
}
await updateSystemSettings(updatedSettings)
return NextResponse.json({
settings: await getSystemSettings(),
message: 'Settings updated successfully',
})
}
if (action === 'clearCache') {
// Mock cache clearing - in a real app, this would clear Redis/memory cache
return NextResponse.json({
message: 'System cache cleared successfully',
})
}
if (action === 'backupDatabase') {
// Mock database backup - in a real app, this would trigger a backup process
return NextResponse.json({
message: 'Database backup initiated successfully',
backupId: `backup_${Date.now()}`,
})
}
if (action === 'sendSystemNotification') {
const { message, targetUsers } = body
// Mock system notification - in a real app, this would send notifications
return NextResponse.json({
message: `System notification sent to ${targetUsers} users`,
notificationId: `notif_${Date.now()}`,
})
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
console.error('Admin settings update error:', error)
return NextResponse.json({ error: 'Failed to update system settings' }, { status: 500 })
}
})
}

View File

@@ -0,0 +1,163 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAdminAuth } from '@/lib/admin-middleware'
import { connectDB } from '@/lib/mongodb'
import { User } from '@/models/user'
import { z } from 'zod'
const UserUpdateSchema = z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
role: z.enum(['user', 'admin']).optional(),
isVerified: z.boolean().optional(),
balance: z.number().min(0).optional(),
})
const UserCreateSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
password: z.string().min(6),
role: z.enum(['user', 'admin']).default('user'),
isVerified: z.boolean().default(false),
balance: z.number().min(0).default(0),
})
export async function GET(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const search = searchParams.get('search') || ''
const role = searchParams.get('role') || ''
const verified = searchParams.get('verified') || ''
const skip = (page - 1) * limit
// Build filter query
const filter: any = {}
if (search) {
filter.$or = [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } },
{ siliconId: { $regex: search, $options: 'i' } },
]
}
if (role && role !== 'all') {
filter.role = role
}
if (verified && verified !== 'all') {
filter.isVerified = verified === 'true'
}
const [users, totalUsers] = await Promise.all([
User.find(filter)
.select('-password -refreshToken')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
User.countDocuments(filter),
])
const totalPages = Math.ceil(totalUsers / limit)
return NextResponse.json({
users,
pagination: {
currentPage: page,
totalPages,
totalUsers,
hasNext: page < totalPages,
hasPrev: page > 1,
},
})
} catch (error) {
console.error('Admin users fetch error:', error)
return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 })
}
})
}
export async function POST(request: NextRequest) {
return withAdminAuth(request, async (req, admin) => {
try {
await connectDB()
const body = await request.json()
const { action, userId, data } = body
if (action === 'update') {
const validatedData = UserUpdateSchema.parse(data)
const user = await User.findByIdAndUpdate(userId, validatedData, {
new: true,
runValidators: true,
}).select('-password -refreshToken')
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json({ user })
}
if (action === 'create') {
const validatedData = UserCreateSchema.parse(data)
// Check if user already exists
const existingUser = await User.findOne({ email: validatedData.email })
if (existingUser) {
return NextResponse.json(
{ error: 'User with this email already exists' },
{ status: 400 }
)
}
// Generate unique Silicon ID
const generateSiliconId = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let result = 'SP'
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
let siliconId = generateSiliconId()
while (await User.findOne({ siliconId })) {
siliconId = generateSiliconId()
}
const user = new User({
...validatedData,
siliconId,
provider: 'local',
})
await user.save()
const userResponse = await User.findById(user._id).select('-password -refreshToken')
return NextResponse.json({ user: userResponse })
}
if (action === 'delete') {
const user = await User.findByIdAndDelete(userId)
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json({ message: 'User deleted successfully' })
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
console.error('Admin user action error:', error)
return NextResponse.json({ error: 'Failed to perform user action' }, { status: 500 })
}
})
}

View File

@@ -0,0 +1,120 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const ForgotPasswordSchema = z.object({
email: z.string().email('Please enter a valid email address'),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate the request body
const validatedData = ForgotPasswordSchema.parse(body)
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 1000))
// Dummy API - Always return an error for demonstration
// You can change this behavior for testing different scenarios
const email = validatedData.email
// Simulate different error scenarios based on email
if (email === 'test@example.com') {
return NextResponse.json(
{
success: false,
error: {
message: 'Email address not found in our system',
code: 'EMAIL_NOT_FOUND'
}
},
{ status: 404 }
)
}
if (email.includes('blocked')) {
return NextResponse.json(
{
success: false,
error: {
message: 'This email address has been temporarily blocked',
code: 'EMAIL_BLOCKED'
}
},
{ status: 429 }
)
}
if (email.includes('invalid')) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid email format',
code: 'INVALID_EMAIL'
}
},
{ status: 400 }
)
}
// Default error response (500 Internal Server Error)
return NextResponse.json(
{
success: false,
error: {
message: 'Unable to process password reset request at this time. Please try again later.',
code: 'SERVER_ERROR'
}
},
{ status: 500 }
)
// Uncomment below for success response (when you want to test success state)
/*
return NextResponse.json(
{
success: true,
message: 'Password reset email sent successfully',
data: {
email: validatedData.email,
resetTokenExpiry: Date.now() + 3600000 // 1 hour from now
}
},
{ status: 200 }
)
*/
} catch (error) {
console.error('Forgot password API error:', error)
// Handle validation errors
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid request data',
code: 'VALIDATION_ERROR',
details: error.issues
}
},
{ status: 400 }
)
}
// Handle other errors
return NextResponse.json(
{
success: false,
error: {
message: 'An unexpected error occurred',
code: 'INTERNAL_ERROR'
}
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server'
import { getGoogleUser } from '@/lib/google-oauth'
import { generateTokens } from '@/lib/jwt'
import connectDB from '@/lib/mongodb'
import { User } from '@/models/user'
import { appConfig } from '@/lib/env'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const state = searchParams.get('state')
const error = searchParams.get('error')
// Handle OAuth errors
if (error) {
return NextResponse.redirect(
new URL(`/auth?error=${encodeURIComponent(error)}`, appConfig.appUrl)
)
}
// Validate required parameters
if (!code || state !== 'google_oauth') {
return NextResponse.redirect(new URL('/auth?error=invalid_oauth_callback', appConfig.appUrl))
}
// Get user info from Google
const googleUser = await getGoogleUser(code)
// Connect to database
await connectDB()
// Check if user exists
let user = await User.findOne({
$or: [
{ email: googleUser.email.toLowerCase() },
{ providerId: googleUser.id, provider: 'google' },
],
})
if (!user) {
// Create new user
user = new User({
email: googleUser.email.toLowerCase(),
name: googleUser.name,
provider: 'google',
providerId: googleUser.id,
avatar: googleUser.picture || undefined,
isVerified: googleUser.verified_email,
lastLogin: new Date(),
})
} else {
// Update existing user with Google info (preserve original provider)
// Only link Google if user doesn't already have a local account
if (user.provider !== 'local') {
user.provider = 'google'
user.providerId = googleUser.id
} else {
// For local users, just add Google info without changing provider
if (!user.providerId) {
user.providerId = googleUser.id
}
}
// Update other info safely
user.name = googleUser.name
user.isVerified = googleUser.verified_email || user.isVerified
user.lastLogin = new Date()
// Update avatar only if user doesn't have one
if (googleUser.picture && !user.avatar) {
user.avatar = googleUser.picture
}
}
// Generate tokens
const { accessToken, refreshToken } = generateTokens({
userId: user._id.toString(),
email: user.email,
role: user.role,
})
// Update user's refresh token
user.refreshToken = refreshToken
await user.save()
// Create redirect response using the public app URL
const redirectURL = new URL('/dashboard', appConfig.appUrl)
const response = NextResponse.redirect(redirectURL)
// Set HTTP-only cookies
response.cookies.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 15 * 60, // 15 minutes
path: '/',
})
response.cookies.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60, // 7 days
path: '/',
})
return response
} catch (error) {
console.error('Google OAuth callback error:', error)
return NextResponse.redirect(new URL('/auth?error=oauth_callback_failed', appConfig.appUrl))
}
}

View File

@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server'
import { getGoogleAuthURL } from '@/lib/google-oauth'
export async function GET(request: NextRequest) {
try {
const authURL = getGoogleAuthURL()
return NextResponse.json({
success: true,
data: { authURL },
})
} catch (error) {
console.error('Google OAuth URL generation error:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to generate Google auth URL', code: 'OAUTH_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import connectDB from '@/lib/mongodb'
import { User } from '@/models/user'
import { generateTokens } from '@/lib/jwt'
const LoginSchema = z.object({
emailOrId: z.string().min(1, 'Email or Silicon ID is required'),
password: z.string().min(1, 'Password is required'),
rememberMe: z.boolean().optional(),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate input
const validatedData = LoginSchema.parse(body)
// Connect to database
await connectDB()
// Find user by email or Silicon ID
const emailOrId = validatedData.emailOrId
const user = await User.findOne({
$or: [{ email: emailOrId.toLowerCase() }, { siliconId: emailOrId }],
})
if (!user) {
return NextResponse.json(
{ success: false, error: { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' } },
{ status: 401 }
)
}
// Check password
const isPasswordValid = await user.comparePassword(validatedData.password)
if (!isPasswordValid) {
return NextResponse.json(
{ success: false, error: { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' } },
{ status: 401 }
)
}
// Generate tokens
const { accessToken, refreshToken } = generateTokens({
userId: user._id.toString(),
email: user.email,
role: user.role,
})
// Update user's refresh token and last login
user.refreshToken = refreshToken
user.lastLogin = new Date()
await user.save()
// Create response with tokens
const response = NextResponse.json({
success: true,
data: {
user: user.toJSON(),
accessToken,
},
})
// Set HTTP-only cookies
response.cookies.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 15 * 60, // 15 minutes
path: '/',
})
response.cookies.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60, // 7 days
path: '/',
})
return response
} catch (error) {
console.error('Login error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: { message: error.issues[0].message, code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
return NextResponse.json(
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import { User } from '@/models/user'
import { verifyRefreshToken } from '@/lib/jwt'
export async function POST(request: NextRequest) {
try {
// Get refresh token from cookie
const refreshToken = request.cookies.get('refreshToken')?.value
if (refreshToken) {
// Verify and decode the refresh token to get user ID
const payload = verifyRefreshToken(refreshToken)
if (payload) {
// Connect to database and remove refresh token
await connectDB()
await User.findByIdAndUpdate(payload.userId, {
$unset: { refreshToken: 1 },
})
}
}
// Create response
const response = NextResponse.json({
success: true,
data: { message: 'Logged out successfully' },
})
// Clear cookies
response.cookies.set('accessToken', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0,
path: '/',
})
response.cookies.set('refreshToken', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0,
path: '/',
})
return response
} catch (error) {
console.error('Logout error:', error)
// Even if there's an error, we should still clear the cookies
const response = NextResponse.json({
success: true,
data: { message: 'Logged out successfully' },
})
response.cookies.set('accessToken', '', { maxAge: 0, path: '/' })
response.cookies.set('refreshToken', '', { maxAge: 0, path: '/' })
return response
}
}

33
app/api/auth/me/route.ts Normal file
View File

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User } from '@/models/user'
export const GET = withAuth(async (request: NextRequest & { user?: any }) => {
try {
// Connect to database
await connectDB()
// Get user details
const user = await User.findById(request.user.userId).select('-password -refreshToken')
if (!user) {
return NextResponse.json(
{ success: false, error: { message: 'User not found', code: 'USER_NOT_FOUND' } },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: { user: user.toJSON() },
})
} catch (error) {
console.error('Get user info error:', error)
return NextResponse.json(
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
{ status: 500 }
)
}
})

View File

@@ -0,0 +1,99 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import { User } from '@/models/user'
import { verifyRefreshToken, generateTokens } from '@/lib/jwt'
export async function POST(request: NextRequest) {
try {
// Get refresh token from cookie
const refreshToken = request.cookies.get('refreshToken')?.value
if (!refreshToken) {
return NextResponse.json(
{
success: false,
error: { message: 'No refresh token provided', code: 'NO_REFRESH_TOKEN' },
},
{ status: 401 }
)
}
// Verify refresh token
const payload = verifyRefreshToken(refreshToken)
if (!payload) {
return NextResponse.json(
{
success: false,
error: { message: 'Invalid refresh token', code: 'INVALID_REFRESH_TOKEN' },
},
{ status: 401 }
)
}
// Connect to database and find user
await connectDB()
const user = await User.findById(payload.userId)
// Check if user exists and refresh token matches
if (!user) {
return NextResponse.json(
{ success: false, error: { message: 'User not found', code: 'USER_NOT_FOUND' } },
{ status: 401 }
)
}
// Verify the stored refresh token matches (both are JWT tokens, so direct comparison is valid)
if (user.refreshToken !== refreshToken) {
return NextResponse.json(
{ success: false, error: { message: 'Refresh token mismatch', code: 'TOKEN_MISMATCH' } },
{ status: 401 }
)
}
// Generate new tokens
const { accessToken, refreshToken: newRefreshToken } = generateTokens({
userId: user._id.toString(),
email: user.email,
role: user.role,
})
// Update user's refresh token
user.refreshToken = newRefreshToken
await user.save()
// Create response with new tokens
const response = NextResponse.json({
success: true,
data: {
accessToken,
user: user.toJSON(),
},
})
// Set new cookies
response.cookies.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 15 * 60, // 15 minutes
path: '/',
})
response.cookies.set('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60, // 7 days
path: '/',
})
return response
} catch (error) {
console.error('Token refresh error:', error)
return NextResponse.json(
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import connectDB from '@/lib/mongodb'
import { User, UserSchema } from '@/models/user'
import { generateTokens } from '@/lib/jwt'
import { generateSiliconId } from '@/lib/siliconId'
const RegisterSchema = UserSchema.pick({
email: true,
name: true,
password: true,
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate input
const validatedData = RegisterSchema.parse(body)
// Connect to database
await connectDB()
// Check if user already exists
const existingUser = await User.findOne({ email: validatedData.email.toLowerCase() })
if (existingUser) {
return NextResponse.json(
{ success: false, error: { message: 'User already exists', code: 'USER_EXISTS' } },
{ status: 409 }
)
}
// Generate tokens first
const tempUser = {
email: validatedData.email.toLowerCase(),
name: validatedData.name,
role: 'user' as const,
}
// Generate unique Silicon ID
const siliconId = generateSiliconId()
console.log('Generated siliconId:', siliconId)
// Create new user
const user = new User({
email: tempUser.email,
name: tempUser.name,
password: validatedData.password,
siliconId: siliconId,
provider: 'local',
})
console.log('User before save:', JSON.stringify(user.toObject(), null, 2))
await user.save()
console.log('user after save:', user)
// Generate tokens with actual user ID
const { accessToken, refreshToken } = generateTokens({
userId: user._id.toString(),
email: user.email,
role: user.role,
})
// Update user with refresh token (single save)
user.refreshToken = refreshToken
await user.save()
// Create response with tokens
const response = NextResponse.json({
success: true,
data: {
user: user.toJSON(),
accessToken,
},
})
// Set HTTP-only cookies
response.cookies.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 15 * 60, // 15 minutes
path: '/',
})
response.cookies.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60, // 7 days
path: '/',
})
return response
} catch (error) {
console.error('Registration error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: { message: error.issues[0].message, code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
return NextResponse.json(
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,237 @@
import { NextRequest, NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
interface AddBalanceRequest {
amount: number
currency: 'INR' | 'USD'
}
interface UserTokenPayload {
siliconId: string
email: string
type: string
}
/**
* Add Balance API
* Initiates "Add Balance" transaction for user accounts
*/
export async function POST(request: NextRequest) {
try {
// Verify user authentication
const token = request.cookies.get('accessToken')?.value
if (!token) {
return NextResponse.json(
{ success: false, message: 'Authentication required' },
{ status: 401 }
)
}
const secret = process.env.JWT_SECRET || 'your-secret-key'
const user = jwt.verify(token, secret) as UserTokenPayload
// Parse request body
const body: AddBalanceRequest = await request.json()
const { amount, currency } = body
// Validate input
if (!amount || amount <= 0 || !['INR', 'USD'].includes(currency)) {
return NextResponse.json(
{ success: false, message: 'Invalid amount or currency' },
{ status: 400 }
)
}
// Validate amount limits
const minAmount = currency === 'INR' ? 100 : 2 // Minimum ₹100 or $2
const maxAmount = currency === 'INR' ? 100000 : 1500 // Maximum ₹1,00,000 or $1500
if (amount < minAmount || amount > maxAmount) {
return NextResponse.json(
{
success: false,
message: `Amount must be between ${currency === 'INR' ? '₹' : '$'}${minAmount} and ${currency === 'INR' ? '₹' : '$'}${maxAmount}`,
},
{ status: 400 }
)
}
try {
// Generate unique transaction ID
const transactionId = generateTransactionId()
// TODO: Save transaction to database
// In real implementation:
// await saveAddBalanceTransaction({
// siliconId: user.siliconId,
// amount,
// currency,
// transaction_id: transactionId,
// status: 'pending'
// })
// Mock database save for demo
await mockSaveAddBalanceTransaction(user.siliconId, amount, currency, transactionId)
// PayU configuration
const merchantKey = process.env.PAYU_MERCHANT_KEY || 'test-key'
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const payuUrl = process.env.PAYU_URL || 'https://test.payu.in/_payment'
// Prepare payment data
const productinfo = 'add_balance'
const firstname = 'Customer'
const email = user.email
const phone = '9876543210' // Default phone or fetch from user profile
// Success and failure URLs for balance transactions
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:4023'
const surl = `${baseUrl}/api/balance/success`
const furl = `${baseUrl}/api/balance/failure`
// Generate PayU hash
const hashString = `${merchantKey}|${transactionId}|${amount}|${productinfo}|${firstname}|${email}|||||||||||${merchantSalt}`
const hash = crypto.createHash('sha512').update(hashString).digest('hex')
// Return payment form data for frontend submission
const paymentData = {
success: true,
transaction_id: transactionId,
currency,
payment_url: payuUrl,
form_data: {
key: merchantKey,
txnid: transactionId,
amount: amount.toFixed(2),
productinfo,
firstname,
email,
phone,
surl,
furl,
hash,
service_provider: 'payu_paisa',
},
}
return NextResponse.json(paymentData)
} catch (dbError) {
console.error('Database error during add balance:', dbError)
return NextResponse.json(
{ success: false, message: 'Failed to initiate balance transaction' },
{ status: 500 }
)
}
} catch (error) {
console.error('Add balance error:', error)
return NextResponse.json(
{ success: false, message: 'Add balance initiation failed' },
{ status: 500 }
)
}
}
/**
* Get Add Balance History
*/
export async function GET(request: NextRequest) {
try {
// Verify user authentication
const token = request.cookies.get('accessToken')?.value
if (!token) {
return NextResponse.json(
{ success: false, message: 'Authentication required' },
{ status: 401 }
)
}
const secret = process.env.JWT_SECRET || 'your-secret-key'
const user = jwt.verify(token, secret) as UserTokenPayload
// TODO: Fetch balance history from database
// In real implementation:
// const history = await getBalanceHistory(user.siliconId)
// Mock balance history for demo
const mockHistory = await getMockBalanceHistory(user.siliconId)
return NextResponse.json({
success: true,
balance_history: mockHistory,
})
} catch (error) {
console.error('Get balance history error:', error)
return NextResponse.json(
{ success: false, message: 'Failed to fetch balance history' },
{ status: 500 }
)
}
}
// Utility functions
function generateTransactionId(): string {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substr(2, 5)
return `bal_${timestamp}${random}`.toUpperCase()
}
// Mock functions for demonstration
async function mockSaveAddBalanceTransaction(
siliconId: string,
amount: number,
currency: string,
transactionId: string
) {
console.log(`Mock DB Save: Add Balance Transaction`)
console.log(`SiliconID: ${siliconId}, Amount: ${amount} ${currency}, TxnID: ${transactionId}`)
// Mock database record
const transaction = {
id: Date.now(),
silicon_id: siliconId,
transaction_id: transactionId,
amount,
currency,
status: 'pending',
created_at: new Date(),
updated_at: new Date(),
}
// Mock delay
await new Promise((resolve) => setTimeout(resolve, 100))
return transaction
}
async function getMockBalanceHistory(siliconId: string) {
// Mock balance transaction history
return [
{
id: 1,
transaction_id: 'BAL_123ABC',
amount: 1000,
currency: 'INR',
status: 'success',
created_at: '2025-01-15T10:30:00Z',
updated_at: '2025-01-15T10:31:00Z',
},
{
id: 2,
transaction_id: 'BAL_456DEF',
amount: 500,
currency: 'INR',
status: 'failed',
created_at: '2025-01-10T14:20:00Z',
updated_at: '2025-01-10T14:21:00Z',
},
{
id: 3,
transaction_id: 'BAL_789GHI',
amount: 2000,
currency: 'INR',
status: 'success',
created_at: '2025-01-05T09:15:00Z',
updated_at: '2025-01-05T09:16:00Z',
},
]
}

View File

@@ -0,0 +1,169 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import { Transaction } from '@/models/transaction'
// Schema for balance deduction request
const DeductBalanceSchema = z.object({
amount: z.number().positive('Amount must be positive'),
service: z.string().min(1, 'Service name is required'),
serviceId: z.string().optional(),
description: z.string().optional(),
transactionId: z.string().optional(),
})
// Schema for balance deduction response
const DeductBalanceResponseSchema = z.object({
success: z.boolean(),
data: z
.object({
transactionId: z.string(),
previousBalance: z.number(),
newBalance: z.number(),
amountDeducted: z.number(),
service: z.string(),
timestamp: z.string(),
})
.optional(),
error: z
.object({
message: z.string(),
code: z.string(),
})
.optional(),
})
export async function POST(request: NextRequest) {
try {
// Authenticate user
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
// Parse and validate request body
const body = await request.json()
const validatedData = DeductBalanceSchema.parse(body)
// Get user's current balance from database
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
const currentBalance = userData.balance || 0
// Check if user has sufficient balance
if (currentBalance < validatedData.amount) {
return NextResponse.json(
{
success: false,
error: {
message: `Insufficient balance. Required: ₹${validatedData.amount}, Available: ₹${currentBalance}`,
code: 'INSUFFICIENT_BALANCE',
},
},
{ status: 400 }
)
}
// Calculate new balance
const newBalance = currentBalance - validatedData.amount
// Update user balance in database
await UserModel.updateOne(
{ email: user.email },
{
$set: { balance: newBalance },
$inc: { __v: 1 }, // Increment version for optimistic locking
}
)
// Generate transaction ID if not provided
const transactionId =
validatedData.transactionId || `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// Save transaction record to database
try {
const newTransaction = new Transaction({
transactionId,
userId: user.id,
email: user.email,
type: 'debit',
amount: validatedData.amount,
service: validatedData.service,
serviceId: validatedData.serviceId,
description: validatedData.description || `Payment for ${validatedData.service}`,
status: 'completed',
previousBalance: currentBalance,
newBalance,
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
},
})
await newTransaction.save()
console.log('Transaction record saved:', transactionId)
} catch (transactionError) {
console.error('Failed to save transaction record:', transactionError)
// Continue even if transaction record fails - balance was already deducted
}
const responseData = {
success: true,
data: {
transactionId,
previousBalance: currentBalance,
newBalance,
amountDeducted: validatedData.amount,
service: validatedData.service,
timestamp: new Date().toISOString(),
},
}
// Validate response format
const validatedResponse = DeductBalanceResponseSchema.parse(responseData)
return NextResponse.json(validatedResponse, { status: 200 })
} catch (error) {
console.error('Balance deduction error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid request data',
code: 'VALIDATION_ERROR',
details: error.issues,
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to deduct balance', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,147 @@
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
interface PayUBalanceFailureResponse {
status: string
txnid: string
amount: string
productinfo: string
firstname: string
email: string
hash: string
key: string
error?: string
error_Message?: string
[key: string]: string | undefined
}
/**
* Balance Addition Failure Handler
* Processes failed "Add Balance" payments from PayU gateway
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const payuResponse: Partial<PayUBalanceFailureResponse> = {}
// Extract all PayU response parameters
for (const [key, value] of formData.entries()) {
payuResponse[key] = value.toString()
}
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey, error, error_Message } = payuResponse
// Log failure details for debugging
console.log(`Balance addition failed - Transaction: ${txnid}, Status: ${status}, Error: ${error || error_Message || 'Unknown'}`)
// Validate required parameters
if (!txnid) {
console.error('Missing transaction ID in balance failure response')
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response&type=balance', request.url))
}
// Verify PayU hash if provided (for security)
if (hash && merchantKey && status && amount) {
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
if (hash.toLowerCase() !== expectedHash) {
console.error(`Hash mismatch in balance failure response for transaction: ${txnid}`)
}
}
try {
// TODO: Update database with failed balance transaction
// In real implementation:
// await updateAddBalanceStatus(txnid, 'failed', error || error_Message)
// Mock database update for demo
await mockUpdateBalanceFailure(txnid as string, error || error_Message || 'Payment failed')
// Determine failure reason for user-friendly message
let failureReason = 'unknown'
let userMessage = 'Your balance addition failed. Please try again.'
if (error_Message?.toLowerCase().includes('insufficient')) {
failureReason = 'insufficient-funds'
userMessage = 'Insufficient funds in your payment method.'
} else if (error_Message?.toLowerCase().includes('declined')) {
failureReason = 'card-declined'
userMessage = 'Your card was declined. Please try a different payment method.'
} else if (error_Message?.toLowerCase().includes('timeout')) {
failureReason = 'timeout'
userMessage = 'Payment timed out. Please try again.'
} else if (error_Message?.toLowerCase().includes('cancelled')) {
failureReason = 'user-cancelled'
userMessage = 'Payment was cancelled.'
} else if (error_Message?.toLowerCase().includes('invalid')) {
failureReason = 'invalid-details'
userMessage = 'Invalid payment details. Please check and try again.'
}
// Redirect to profile page with failure message
const failureUrl = new URL('/profile', request.url)
failureUrl.searchParams.set('payment', 'failed')
failureUrl.searchParams.set('type', 'balance')
failureUrl.searchParams.set('reason', failureReason)
failureUrl.searchParams.set('txn', txnid as string)
failureUrl.searchParams.set('message', encodeURIComponent(userMessage))
if (amount) {
failureUrl.searchParams.set('amount', amount as string)
}
return NextResponse.redirect(failureUrl)
} catch (dbError) {
console.error('Database update error during balance failure handling:', dbError)
// Continue to failure page even if DB update fails
const failureUrl = new URL('/profile', request.url)
failureUrl.searchParams.set('payment', 'failed')
failureUrl.searchParams.set('type', 'balance')
failureUrl.searchParams.set('error', 'db-error')
failureUrl.searchParams.set('txn', txnid as string)
return NextResponse.redirect(failureUrl)
}
} catch (error) {
console.error('Balance failure handler error:', error)
return NextResponse.redirect(new URL('/profile?payment=failed&type=balance&error=processing-error', request.url))
}
}
// Mock function for demonstration
async function mockUpdateBalanceFailure(txnid: string, errorMessage: string) {
console.log(`Mock DB Update: Balance Transaction ${txnid} failed`)
console.log(`Failure reason: ${errorMessage}`)
// Mock: Update add_balance_history status
const historyUpdate = {
transaction_id: txnid,
status: 'failed',
error_message: errorMessage,
updated_at: new Date(),
retry_count: 1, // Could track retry attempts
payment_gateway_response: {
status: 'failed',
error: errorMessage,
failed_at: new Date()
}
}
// Mock: Could also track failed attempt statistics
const analytics = {
failed_balance_additions: 1,
common_failure_reasons: {
[errorMessage]: 1
}
}
console.log('Mock: Balance failure recorded for analysis')
// Mock delay
await new Promise(resolve => setTimeout(resolve, 100))
return { historyUpdate, analytics }
}

View File

@@ -0,0 +1,132 @@
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
interface PayUBalanceResponse {
status: string
txnid: string
amount: string
productinfo: string
firstname: string
email: string
hash: string
key: string
[key: string]: string
}
/**
* Balance Addition Success Handler
* Processes successful "Add Balance" payments from PayU gateway
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const payuResponse: Partial<PayUBalanceResponse> = {}
// Extract all PayU response parameters
for (const [key, value] of formData.entries()) {
payuResponse[key] = value.toString()
}
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey } = payuResponse
// Validate required parameters
if (!status || !txnid || !amount || !hash) {
console.error('Missing required PayU parameters in balance success')
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
}
// Verify payment status
if (status !== 'success') {
console.log(`Balance payment failed for transaction: ${txnid}, status: ${status}`)
return NextResponse.redirect(new URL('/payment/failed?txn=' + txnid + '&type=balance', request.url))
}
// Verify PayU hash for security
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
if (hash?.toLowerCase() !== expectedHash) {
console.error(`Hash mismatch for balance transaction: ${txnid}`)
return NextResponse.redirect(new URL('/payment/failed?error=hash-mismatch&type=balance', request.url))
}
try {
// TODO: Update database with successful balance addition
// In real implementation:
// const siliconId = await getSiliconIdFromTransaction(txnid)
// await updateAddBalanceStatus(txnid, 'success')
// await incrementUserBalance(siliconId, parseFloat(amount))
console.log(`Balance addition successful for transaction: ${txnid}, amount: ₹${amount}`)
// Mock database update for demo
const updateResult = await mockUpdateBalanceSuccess(txnid as string, parseFloat(amount as string))
// Redirect to profile page with success message
const successUrl = new URL('/profile', request.url)
successUrl.searchParams.set('payment', 'success')
successUrl.searchParams.set('type', 'balance')
successUrl.searchParams.set('amount', amount as string)
successUrl.searchParams.set('txn', txnid as string)
return NextResponse.redirect(successUrl)
} catch (dbError) {
console.error('Database update error during balance success:', dbError)
// Log for manual reconciliation - payment was successful at gateway
console.error(`CRITICAL: Balance payment ${txnid} succeeded at PayU but DB update failed`)
// Still redirect to success but with warning
const successUrl = new URL('/profile', request.url)
successUrl.searchParams.set('payment', 'success')
successUrl.searchParams.set('type', 'balance')
successUrl.searchParams.set('amount', amount as string)
successUrl.searchParams.set('txn', txnid as string)
successUrl.searchParams.set('warning', 'db-update-failed')
return NextResponse.redirect(successUrl)
}
} catch (error) {
console.error('Balance success handler error:', error)
return NextResponse.redirect(new URL('/payment/failed?error=processing-error&type=balance', request.url))
}
}
// Mock function for demonstration
async function mockUpdateBalanceSuccess(txnid: string, amount: number) {
console.log(`Mock DB Update: Balance Transaction ${txnid} successful`)
// Mock: Get user Silicon ID from transaction
const mockSiliconId = 'USR_12345' // In real implementation, fetch from add_balance_history table
// Mock: Update add_balance_history status
const historyUpdate = {
transaction_id: txnid,
status: 'success',
updated_at: new Date(),
payment_gateway_response: {
amount: amount,
currency: 'INR',
payment_date: new Date()
}
}
// Mock: Update user balance
const balanceUpdate = {
silicon_id: mockSiliconId,
balance_increment: amount,
previous_balance: 5000, // Mock previous balance
new_balance: 5000 + amount,
updated_at: new Date()
}
console.log(`Mock: User ${mockSiliconId} balance increased by ₹${amount}`)
console.log(`Mock: New balance: ₹${balanceUpdate.new_balance}`)
// Mock delay
await new Promise(resolve => setTimeout(resolve, 150))
return { historyUpdate, balanceUpdate }
}

102
app/api/billing/route.ts Normal file
View File

@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import BillingService from '@/lib/billing-service'
// Schema for billing query parameters
const BillingQuerySchema = z.object({
serviceType: z.string().optional(),
status: z.string().optional(),
limit: z.coerce.number().min(1).max(100).default(20),
offset: z.coerce.number().min(0).default(0),
})
// GET endpoint to fetch user's billing records
export async function GET(request: NextRequest) {
try {
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
const { searchParams } = new URL(request.url)
const queryParams = {
serviceType: searchParams.get('serviceType') || undefined,
status: searchParams.get('status') || undefined,
limit: searchParams.get('limit') || '20',
offset: searchParams.get('offset') || '0',
}
const validatedParams = BillingQuerySchema.parse(queryParams)
// Get user data
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
// Fetch billing records
const billings = await BillingService.getUserBillings(user.email, user.id, {
serviceType: validatedParams.serviceType,
status: validatedParams.status,
limit: validatedParams.limit,
offset: validatedParams.offset,
})
// Get billing statistics
const stats = await BillingService.getBillingStats(user.email, user.id)
return NextResponse.json({
success: true,
data: {
billings,
stats,
pagination: {
limit: validatedParams.limit,
offset: validatedParams.offset,
total: billings.length,
},
},
})
} catch (error) {
console.error('Failed to fetch billing records:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid query parameters',
code: 'VALIDATION_ERROR',
details: error.issues,
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch billing records', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import BillingService from '@/lib/billing-service'
// GET endpoint to fetch user's billing statistics
export async function GET(request: NextRequest) {
try {
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
// Get user data
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
// Get comprehensive billing statistics
const stats = await BillingService.getBillingStats(user.email, user.id)
const activeServices = await BillingService.getActiveServices(user.email, user.id)
return NextResponse.json({
success: true,
data: {
...stats,
activeServicesList: activeServices,
currentBalance: userData.balance || 0,
},
})
} catch (error) {
console.error('Failed to fetch billing statistics:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch billing statistics', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

80
app/api/contact/route.ts Normal file
View File

@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
// Validation schema matching sp_25 structure
const contactSchema = z.object({
name: z.string().min(1, 'Name is required').trim(),
email: z.string().email('Invalid email format').trim(),
company: z.string().optional(),
service_intrest: z.string().min(1, 'Service interest is required'),
message: z.string().min(1, 'Message is required').trim(),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate the request body
const validatedData = contactSchema.parse(body)
// Get client IP address
const ip_address = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown'
// TODO: Database integration
// This would integrate with MongoDB/database in production
// For now, just simulate successful submission
// TODO: Email notification
// This would send email notification in production
// const emailData = {
// to: 'contact@siliconpin.com',
// subject: 'New Contact Form Submission',
// body: `
// Name: ${validatedData.name}
// Email: ${validatedData.email}
// Company: ${validatedData.company || 'Not provided'}
// Service Interest: ${validatedData.service_intrest}
// Message: ${validatedData.message}
// IP Address: ${ip_address}
// `
// }
// Log the submission (for development)
console.log('Contact form submission:', {
...validatedData,
ip_address,
timestamp: new Date().toISOString()
})
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 500))
return NextResponse.json({
success: true,
message: 'Thank you for your message! We\'ll get back to you soon.'
}, { status: 200 })
} catch (error) {
console.error('Contact form error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
message: 'Validation failed',
errors: error.issues.reduce((acc, err) => {
if (err.path.length > 0) {
acc[err.path[0] as string] = err.message
}
return acc
}, {} as Record<string, string>)
}, { status: 400 })
}
return NextResponse.json({
success: false,
message: 'Internal server error. Please try again later.'
}, { status: 500 })
}
}

138
app/api/dashboard/route.ts Normal file
View File

@@ -0,0 +1,138 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import connectDB from '@/lib/mongodb'
import TopicModel from '@/models/topic'
import { authMiddleware } from '@/lib/auth-middleware'
// Response schema for type safety
const DashboardStatsSchema = z.object({
success: z.boolean(),
data: z.object({
stats: z.object({
totalTopics: z.number(),
publishedTopics: z.number(),
draftTopics: z.number(),
totalViews: z.number(),
}),
userTopics: z.array(
z.object({
id: z.string(),
title: z.string(),
slug: z.string(),
publishedAt: z.number(),
isDraft: z.boolean(),
views: z.number().optional(),
excerpt: z.string(),
tags: z.array(
z.object({
name: z.string(),
})
),
})
),
}),
})
export type DashboardResponse = z.infer<typeof DashboardStatsSchema>
export async function GET(request: NextRequest) {
try {
// Authenticate user
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
// Connect to database
await connectDB()
// MongoDB Aggregation Pipeline for user topic statistics
const userTopics = await TopicModel.aggregate([
{
// Match topics by user ownership
$match: {
authorId: user.id,
},
},
{
// Sort by publication date (newest first)
$sort: {
publishedAt: -1,
},
},
{
// Project only the fields we need for the dashboard
$project: {
id: '$id',
title: '$title',
slug: '$slug',
publishedAt: '$publishedAt',
isDraft: '$isDraft',
views: { $ifNull: ['$views', 0] }, // Default to 0 if views field doesn't exist
excerpt: '$excerpt',
tags: {
$map: {
input: '$tags',
as: 'tag',
in: {
name: '$$tag.name',
},
},
},
},
},
])
// Calculate statistics from the aggregated data
const totalTopics = userTopics.length
const publishedTopics = userTopics.filter((topic) => !topic.isDraft).length
const draftTopics = userTopics.filter((topic) => topic.isDraft).length
const totalViews = userTopics.reduce((sum, topic) => sum + (topic.views || 0), 0)
// Prepare response data
const responseData = {
success: true,
data: {
stats: {
totalTopics,
publishedTopics,
draftTopics,
totalViews,
},
userTopics,
},
}
// Validate response format matches frontend expectations
const validatedResponse = DashboardStatsSchema.parse(responseData)
return NextResponse.json(validatedResponse, { status: 200 })
} catch (error) {
console.error('Dashboard API error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: { message: 'Invalid response format', code: 'VALIDATION_ERROR' },
},
{ status: 500 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch dashboard data', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import TopicModel from '@/models/topic'
// Debug endpoint to see what topics exist in the database
export async function GET(request: NextRequest) {
try {
await connectDB()
// Get all topics with basic info
const topics = await TopicModel.find({}, {
id: 1,
slug: 1,
title: 1,
isDraft: 1,
views: 1,
authorId: 1
}).sort({ publishedAt: -1 })
return NextResponse.json({
success: true,
count: topics.length,
topics: topics.map(topic => ({
id: topic.id,
slug: topic.slug,
title: topic.title,
isDraft: topic.isDraft,
views: topic.views || 0,
authorId: topic.authorId
}))
})
} catch (error) {
console.error('Debug topics error:', error)
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}

95
app/api/feedback/route.ts Normal file
View File

@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
// Validation schema matching sp_25 structure
const feedbackSchema = z.object({
type: z.enum(['suggestion', 'report']),
name: z.string().min(1, 'Please enter your name.').trim(),
email: z.string().email('Please enter a valid email address.').trim(),
title: z.string().min(1, 'Title is required').trim(),
details: z.string().min(1, 'Details are required').trim(),
category: z.string().default('general'),
urgency: z.string().default('low'),
siliconId: z.string().nullable().optional()
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate the request body
const validatedData = feedbackSchema.parse(body)
// Get client IP address and referrer
const ip_address = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown'
const referrer = request.headers.get('referer') || 'Direct Access'
// TODO: Database integration
// This would integrate with MongoDB/database in production
// INSERT INTO sp_feedback (type, name, email, title, details, urgency, category, referrer, siliconId, ip_address, created_at)
// TODO: Email notification
const emailData = {
to: 'contact@siliconpin.com',
subject: `New ${validatedData.type.charAt(0).toUpperCase() + validatedData.type.slice(1)} Submission`,
body: `
Type: ${validatedData.type.charAt(0).toUpperCase() + validatedData.type.slice(1)}
Name: ${validatedData.name}
Email: ${validatedData.email}
Title: ${validatedData.title}
Category: ${validatedData.category}
${validatedData.type === 'report' ? `Urgency: ${validatedData.urgency}\n` : ''}
Details:
${validatedData.details}
Referrer: ${referrer}
IP Address: ${ip_address}
${validatedData.siliconId ? `SiliconID: ${validatedData.siliconId}\n` : ''}
`.trim()
}
// Log the submission (for development)
console.log('Feedback submission:', {
...validatedData,
referrer,
ip_address,
timestamp: new Date().toISOString(),
emailData
})
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 500))
const successMessage = validatedData.type === 'suggestion'
? 'Thank you for your suggestion! We appreciate your feedback.'
: 'Your report has been submitted. We will look into it as soon as possible.'
return NextResponse.json({
success: true,
message: successMessage
}, { status: 200 })
} catch (error) {
console.error('Feedback submission error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
message: 'Validation failed',
errors: error.issues.reduce((acc, err) => {
if (err.path.length > 0) {
acc[err.path[0] as string] = err.message
}
return acc
}, {} as Record<string, string>)
}, { status: 400 })
}
return NextResponse.json({
success: false,
message: 'Internal server error. Please try again later.'
}, { status: 500 })
}
}

63
app/api/health/route.ts Normal file
View File

@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server'
import { connectDB } from '@/lib/mongodb'
import { createClient } from 'redis'
export async function GET(request: NextRequest) {
const startTime = Date.now()
const healthCheck = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
version: process.env.npm_package_version || '1.0.0',
checks: {
database: { status: 'unknown' as 'healthy' | 'unhealthy' | 'unknown' },
redis: { status: 'unknown' as 'healthy' | 'unhealthy' | 'unknown' },
},
responseTime: 0,
}
// Check database connection
try {
await connectDB()
healthCheck.checks.database.status = 'healthy'
} catch (error) {
healthCheck.checks.database.status = 'unhealthy'
healthCheck.status = 'unhealthy'
}
// Check Redis connection
try {
const redis = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
})
await redis.connect()
await redis.ping()
await redis.disconnect()
healthCheck.checks.redis.status = 'healthy'
} catch (error) {
healthCheck.checks.redis.status = 'unhealthy'
healthCheck.status = 'unhealthy'
}
// Calculate response time
healthCheck.responseTime = Date.now() - startTime
// Return appropriate status code
const statusCode = healthCheck.status === 'healthy' ? 200 : 503
return NextResponse.json(healthCheck, { status: statusCode })
}
// Readiness check - simpler check for container orchestration
export async function HEAD(request: NextRequest) {
try {
// Quick check if the application is ready to serve requests
return new NextResponse(null, { status: 200 })
} catch (error) {
return new NextResponse(null, { status: 503 })
}
}

View File

@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
interface PayUFailureResponse {
status: string
txnid: string
amount: string
productinfo: string
firstname: string
email: string
hash: string
key: string
error?: string
error_Message?: string
[key: string]: string | undefined
}
/**
* PayU Payment Failure Handler
* Processes failed payment responses from PayU gateway
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const payuResponse: Partial<PayUFailureResponse> = {}
// Extract all PayU response parameters
for (const [key, value] of formData.entries()) {
payuResponse[key] = value.toString()
}
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey, error, error_Message } = payuResponse
// Log failure details for debugging
console.log(`Payment failed - Transaction: ${txnid}, Status: ${status}, Error: ${error || error_Message || 'Unknown'}`)
// Validate required parameters
if (!txnid) {
console.error('Missing transaction ID in failure response')
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
}
// Verify PayU hash if provided (for security)
if (hash && merchantKey && status && amount) {
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
if (hash.toLowerCase() !== expectedHash) {
console.error(`Hash mismatch in failure response for transaction: ${txnid}`)
}
}
try {
// TODO: Update database with failed payment status
// In a real implementation:
// await updateBillingStatus(txnid, 'failed', error || error_Message)
// Mock database update for demo
await mockUpdatePaymentStatus(txnid as string, 'failed', error || error_Message || 'Payment failed')
// Determine failure reason for user-friendly message
let failureReason = 'unknown'
if (error_Message?.toLowerCase().includes('insufficient')) {
failureReason = 'insufficient-funds'
} else if (error_Message?.toLowerCase().includes('declined')) {
failureReason = 'card-declined'
} else if (error_Message?.toLowerCase().includes('timeout')) {
failureReason = 'timeout'
} else if (error_Message?.toLowerCase().includes('cancelled')) {
failureReason = 'user-cancelled'
}
// Redirect to failure page with transaction details
const failureUrl = new URL('/payment/failed', request.url)
failureUrl.searchParams.set('txn', txnid as string)
failureUrl.searchParams.set('reason', failureReason)
if (amount) {
failureUrl.searchParams.set('amount', amount as string)
}
return NextResponse.redirect(failureUrl)
} catch (dbError) {
console.error('Database update error during failure handling:', dbError)
// Continue to failure page even if DB update fails
return NextResponse.redirect(new URL('/payment/failed?error=db-error&txn=' + txnid, request.url))
}
} catch (error) {
console.error('Payment failure handler error:', error)
return NextResponse.redirect(new URL('/payment/failed?error=processing-error', request.url))
}
}
// Mock function for demonstration
async function mockUpdatePaymentStatus(txnid: string, status: string, errorMessage: string) {
// In real implementation, this would update MongoDB/database
console.log(`Mock DB Update: Transaction ${txnid} marked as ${status}`)
console.log(`Failure reason: ${errorMessage}`)
// Simulate database operations
const billingUpdate = {
billing_id: txnid,
payment_status: status,
payment_date: new Date(),
error_message: errorMessage,
retry_count: 1 // Could track retry attempts
}
// Mock delay
await new Promise(resolve => setTimeout(resolve, 100))
return billingUpdate
}

View File

@@ -0,0 +1,112 @@
import { NextRequest, NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
interface PaymentInitiateRequest {
billing_id: string
amount: number
service: string
}
interface UserTokenPayload {
siliconId: string
email: string
type: string
}
/**
* PayU Payment Gateway Integration
* Initiates payment for billing records
*/
export async function POST(request: NextRequest) {
try {
// Verify user authentication
const token = request.cookies.get('accessToken')?.value
if (!token) {
return NextResponse.json(
{ success: false, message: 'Authentication required' },
{ status: 401 }
)
}
const secret = process.env.JWT_SECRET || 'your-secret-key'
const user = jwt.verify(token, secret) as UserTokenPayload
// Parse request body
const body: PaymentInitiateRequest = await request.json()
const { billing_id, amount, service } = body
// Validate input
if (!billing_id || !amount || amount <= 0 || !service) {
return NextResponse.json(
{ success: false, message: 'Invalid payment parameters' },
{ status: 400 }
)
}
// TODO: Verify billing record exists and belongs to user
// In a real implementation, you would check database:
// const billing = await verifyBillingRecord(billing_id, user.siliconId)
// PayU configuration (from environment variables)
const merchantKey = process.env.PAYU_MERCHANT_KEY || 'test-key'
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const payuUrl = process.env.PAYU_URL || 'https://test.payu.in/_payment'
// Prepare payment data
const txnid = billing_id
const productinfo = service.substring(0, 100)
const firstname = 'Customer'
const email = user.email
const phone = '9876543210' // Default phone or fetch from user profile
// Success and failure URLs
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:4023'
const surl = `${baseUrl}/api/payments/success`
const furl = `${baseUrl}/api/payments/failure`
// Generate PayU hash
const hashString = `${merchantKey}|${txnid}|${amount}|${productinfo}|${firstname}|${email}|||||||||||${merchantSalt}`
const hash = crypto.createHash('sha512').update(hashString).digest('hex')
// Return payment form data for frontend submission
const paymentData = {
success: true,
payment_url: payuUrl,
form_data: {
key: merchantKey,
txnid,
amount: amount.toFixed(2),
productinfo,
firstname,
email,
phone,
surl,
furl,
hash,
service_provider: 'payu_paisa',
},
}
return NextResponse.json(paymentData)
} catch (error) {
console.error('Payment initiation error:', error)
return NextResponse.json(
{ success: false, message: 'Payment initiation failed' },
{ status: 500 }
)
}
}
// Mock function - in real implementation, verify against database
async function verifyBillingRecord(billingId: string, siliconId: string) {
// TODO: Implement database verification
// Check if billing record exists and belongs to the user
return {
billing_id: billingId,
amount: 1000,
service: 'Cloud Instance',
user_silicon_id: siliconId,
status: 'pending',
}
}

View File

@@ -0,0 +1,110 @@
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
interface PayUResponse {
status: string
txnid: string
amount: string
productinfo: string
firstname: string
email: string
hash: string
key: string
[key: string]: string
}
/**
* PayU Payment Success Handler
* Processes successful payment responses from PayU gateway
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const payuResponse: Partial<PayUResponse> = {}
// Extract all PayU response parameters
for (const [key, value] of formData.entries()) {
payuResponse[key] = value.toString()
}
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey } = payuResponse
// Validate required parameters
if (!status || !txnid || !amount || !hash) {
console.error('Missing required PayU parameters')
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
}
// Verify payment status
if (status !== 'success') {
console.log(`Payment failed for transaction: ${txnid}, status: ${status}`)
return NextResponse.redirect(new URL('/payment/failed?txn=' + txnid, request.url))
}
// Verify PayU hash for security
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
if (hash?.toLowerCase() !== expectedHash) {
console.error(`Hash mismatch for transaction: ${txnid}`)
// Log potential fraud attempt
console.error('Expected hash:', expectedHash)
console.error('Received hash:', hash?.toLowerCase())
return NextResponse.redirect(new URL('/payment/failed?error=hash-mismatch', request.url))
}
try {
// TODO: Update database with successful payment
// In a real implementation:
// await updateBillingStatus(txnid, 'success')
// await updateUserBalance(userSiliconId, parseFloat(amount))
console.log(`Payment successful for transaction: ${txnid}, amount: ₹${amount}`)
// Mock database update for demo
await mockUpdatePaymentStatus(txnid as string, 'success', parseFloat(amount as string))
// Redirect to success page with transaction details
const successUrl = new URL('/payment/success', request.url)
successUrl.searchParams.set('txn', txnid as string)
successUrl.searchParams.set('amount', amount as string)
return NextResponse.redirect(successUrl)
} catch (dbError) {
console.error('Database update error:', dbError)
// Even if DB update fails, payment was successful at gateway
// Log for manual reconciliation
return NextResponse.redirect(new URL('/payment/success?warning=db-update-failed&txn=' + txnid, request.url))
}
} catch (error) {
console.error('Payment success handler error:', error)
return NextResponse.redirect(new URL('/payment/failed?error=processing-error', request.url))
}
}
// Mock function for demonstration
async function mockUpdatePaymentStatus(txnid: string, status: string, amount: number) {
// In real implementation, this would update MongoDB/database
console.log(`Mock DB Update: Transaction ${txnid} marked as ${status}, amount: ₹${amount}`)
// Simulate database operations
const billingUpdate = {
billing_id: txnid,
payment_status: status,
payment_date: new Date(),
amount: amount
}
const userBalanceUpdate = {
// This would update user's account balance for "add_balance" transactions
increment: status === 'success' && txnid.includes('balance') ? amount : 0
}
// Mock delay
await new Promise(resolve => setTimeout(resolve, 100))
return { billingUpdate, userBalanceUpdate }
}

View File

@@ -0,0 +1,198 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import BillingService from '@/lib/billing-service'
import { checkServiceAvailability } from '@/lib/system-settings'
// VPC subnet mapping based on datacenter
const VPC_SUBNET_MAP: { [key: string]: string } = {
inmumbaizone2: '3889f7ca-ca19-4851-abc7-5c6b4798b3fe', // Mumbai public subnet
inbangalore: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976', // Bangalore public subnet
innoida: '95c60b59-4925-4b7e-bd05-afbf940d8000', // Delhi/Noida public subnet
defra1: '72ea201d-e1e7-4d30-a186-bc709efafad8', // Frankfurt public subnet
uslosangeles: '0e253cd1-8ecc-4b65-ae0f-6acbc53b0fb6', // Los Angeles public subnet
inbangalore3: 'f687a58b-04f0-4ebe-b583-65788a1d18bf', // Bangalore DC3 public subnet
}
export async function POST(request: NextRequest) {
try {
// Check if VPS deployment service is enabled
if (!(await checkServiceAvailability('vps'))) {
return NextResponse.json(
{
status: 'error',
message: 'VPS deployment service is currently disabled by administrator',
},
{ status: 503 }
)
}
// Check authentication using your auth middleware
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{ status: 'error', message: 'Unauthorized: Please login to continue' },
{ status: 401 }
)
}
// Get input data from request
const input = await request.json()
if (!input) {
return NextResponse.json({ status: 'error', message: 'Invalid JSON' }, { status: 400 })
}
// Get API key from environment
const UTHO_API_KEY = process.env.UTHO_API_KEY
if (!UTHO_API_KEY) {
return NextResponse.json(
{ status: 'error', message: 'API configuration missing' },
{ status: 500 }
)
}
// Connect to MongoDB
await connectDB()
// Get user data for billing
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json({ status: 'error', message: 'User not found' }, { status: 404 })
}
const requiredAmount = input.amount || 0
// Check balance only if amount > 0 (some services might be free)
if (requiredAmount > 0) {
const currentBalance = userData.balance || 0
if (currentBalance < requiredAmount) {
return NextResponse.json(
{
status: 'error',
message: `Insufficient balance. Required: ₹${requiredAmount}, Available: ₹${currentBalance}`,
code: 'INSUFFICIENT_BALANCE',
},
{ status: 400 }
)
}
}
// Get VPC subnet based on datacenter
const vpcSubnet = VPC_SUBNET_MAP[input.dclocation] || VPC_SUBNET_MAP['inbangalore']
// Prepare Utho payload - use the exact format from your PHP code
const uthoPayload = {
dcslug: input.dclocation,
planid: input.planid,
billingcycle: 'hourly',
auth: 'option2',
enable_publicip: input.publicip !== false,
subnetRequired: false,
firewall: '23434645',
cpumodel: 'intel',
enablebackup: input.backup || false,
root_password: input.password,
support: 'unmanaged',
vpc: vpcSubnet,
cloud: [
{
hostname: input.hostname,
},
],
image: input.image,
sshkeys: '',
}
console.log('Sending to Utho API:', uthoPayload)
// Make API request to Utho
const UTHO_API_URL = 'https://api.utho.com/v2/cloud/deploy'
const response = await fetch(UTHO_API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${UTHO_API_KEY}`,
'Content-Type': 'application/json',
Accept: '*/*',
},
body: JSON.stringify(uthoPayload),
})
const httpCode = response.status
const deploymentSuccess = httpCode >= 200 && httpCode < 300
const responseData = await response.json()
// Add status field like PHP does
responseData.status = deploymentSuccess ? 'success' : 'error'
console.log('Utho API response:', { httpCode, responseData })
// Process billing using the new comprehensive billing service
try {
const billingResult = await BillingService.processServiceDeployment({
user: {
id: user.id,
email: user.email,
siliconId: userData.siliconId,
},
service: {
name: 'VPS Server',
type: 'vps',
id: responseData.server_id || responseData.id,
instanceId: responseData.server_id || responseData.id,
config: {
hostname: input.hostname,
planid: input.planid,
dclocation: input.dclocation,
image: input.image,
backup: input.backup,
publicip: input.publicip,
},
},
amount: requiredAmount,
currency: 'INR',
cycle: (input.cycle as any) || 'onetime',
deploymentSuccess,
deploymentResponse: responseData,
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
uthoPayload,
httpCode,
},
})
console.log('Billing processed:', {
billingId: billingResult.billing.billing_id,
transactionId: billingResult.transaction?.transactionId,
balanceUpdated: billingResult.balanceUpdated,
})
} catch (billingError) {
console.error('Billing processing failed:', billingError)
// Continue even if billing fails, but return error if it's balance-related
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
return NextResponse.json(
{
status: 'error',
message: billingError.message,
code: 'INSUFFICIENT_BALANCE',
},
{ status: 400 }
)
}
}
// Return the exact same response format as PHP
return NextResponse.json(responseData, { status: httpCode })
} catch (error) {
console.error('Cloud deployment error:', error)
return NextResponse.json(
{
status: 'error',
message: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,247 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import BillingService from '@/lib/billing-service'
// Hardcoded VPC as requested
const K8S_VPC = '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4'
export async function POST(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{ status: 'error', message: 'Unauthorized: Please login to continue' },
{ status: 401 }
)
}
// Get input data from request
const input = await request.json()
if (!input) {
return NextResponse.json({ status: 'error', message: 'Invalid JSON' }, { status: 400 })
}
// Validate required fields
if (!input.cluster_label || !input.nodepools) {
return NextResponse.json(
{ status: 'error', message: 'Cluster label and nodepools are required' },
{ status: 400 }
)
}
// Get API key from environment
const UTHO_API_KEY = process.env.UTHO_API_KEY
if (!UTHO_API_KEY) {
return NextResponse.json(
{ status: 'error', message: 'API configuration missing' },
{ status: 500 }
)
}
// Connect to MongoDB
await connectDB()
// Get user data for billing
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json({ status: 'error', message: 'User not found' }, { status: 404 })
}
const requiredAmount = input.amount || 0
// Check balance only if amount > 0
if (requiredAmount > 0) {
const currentBalance = userData.balance || 0
if (currentBalance < requiredAmount) {
return NextResponse.json(
{
status: 'error',
message: `Insufficient balance. Required: ₹${requiredAmount}, Available: ₹${currentBalance}`,
code: 'INSUFFICIENT_BALANCE',
},
{ status: 400 }
)
}
}
// Prepare Utho payload for Kubernetes
const uthoPayload = {
dcslug: 'inmumbaizone2', // Hardcoded as requested
cluster_label: input.cluster_label,
cluster_version: input.cluster_version || '1.30.0-utho',
nodepools: input.nodepools,
vpc: K8S_VPC, // Hardcoded VPC
network_type: 'publicprivate',
cpumodel: 'amd',
}
console.log('Sending to Utho Kubernetes API:', uthoPayload)
// Make API request to Utho Kubernetes
const UTHO_API_URL = 'https://api.utho.com/v2/kubernetes/deploy'
const response = await fetch(UTHO_API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${UTHO_API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(uthoPayload),
})
const httpCode = response.status
const deploymentSuccess = httpCode >= 200 && httpCode < 300
const responseData = await response.json()
// Add status field and clusterId for consistency
responseData.status = deploymentSuccess ? 'success' : 'error'
if (deploymentSuccess && responseData.id) {
responseData.clusterId = responseData.id
}
console.log('Utho Kubernetes API response:', { httpCode, responseData })
// Process billing using the new comprehensive billing service
try {
const billingResult = await BillingService.processServiceDeployment({
user: {
id: user.id,
email: user.email,
siliconId: userData.siliconId,
},
service: {
name: `Kubernetes Cluster - ${input.cluster_label}`,
type: 'kubernetes',
id: responseData.clusterId || responseData.id,
clusterId: responseData.clusterId || responseData.id,
config: {
cluster_label: input.cluster_label,
cluster_version: input.cluster_version,
nodepools: input.nodepools,
dcslug: 'inmumbaizone2',
vpc: K8S_VPC,
network_type: 'publicprivate',
cpumodel: 'amd',
},
},
amount: requiredAmount,
currency: 'INR',
cycle: (input.cycle as any) || 'onetime',
deploymentSuccess,
deploymentResponse: responseData,
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
uthoPayload,
httpCode,
},
})
console.log('Billing processed:', {
billingId: billingResult.billing.billing_id,
transactionId: billingResult.transaction?.transactionId,
balanceUpdated: billingResult.balanceUpdated,
})
} catch (billingError) {
console.error('Billing processing failed:', billingError)
// Continue even if billing fails, but return error if it's balance-related
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
return NextResponse.json(
{
status: 'error',
message: billingError.message,
code: 'INSUFFICIENT_BALANCE',
},
{ status: 400 }
)
}
}
return NextResponse.json(responseData, { status: httpCode })
} catch (error) {
console.error('Kubernetes deployment error:', error)
return NextResponse.json(
{
status: 'error',
message: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// GET endpoint for downloading kubeconfig
export async function GET(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{ status: 'error', message: 'Unauthorized: Please login to continue' },
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const clusterId = searchParams.get('clusterId')
if (!clusterId) {
return NextResponse.json(
{ status: 'error', message: 'Cluster ID is required' },
{ status: 400 }
)
}
// Get API key from environment
const UTHO_API_KEY = process.env.UTHO_API_KEY
if (!UTHO_API_KEY) {
return NextResponse.json(
{ status: 'error', message: 'API configuration missing' },
{ status: 500 }
)
}
// Download kubeconfig
const KUBECONFIG_URL = `https://api.utho.com/v2/kubernetes/${clusterId}/download`
const response = await fetch(KUBECONFIG_URL, {
headers: {
Authorization: `Bearer ${UTHO_API_KEY}`,
Accept: 'application/yaml',
},
})
if (response.ok) {
const kubeconfig = await response.text()
// Return as downloadable file
return new NextResponse(kubeconfig, {
status: 200,
headers: {
'Content-Type': 'application/yaml',
'Content-Disposition': `attachment; filename="kubeconfig-${clusterId}.yaml"`,
},
})
} else {
const errorText = await response.text()
console.error('Kubeconfig download failed:', response.status, errorText)
return NextResponse.json(
{ status: 'error', message: 'Failed to download kubeconfig' },
{ status: response.status }
)
}
} catch (error) {
console.error('Kubeconfig download error:', error)
return NextResponse.json(
{
status: 'error',
message: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,162 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import BillingService from '@/lib/billing-service'
// Define your VPN endpoints
const VPN_ENDPOINTS = {
america: 'https://wireguard-vpn.3027622.siliconpin.com/vpn',
europe: 'https://wireguard.vps20.siliconpin.com/vpn',
}
export async function POST(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get data from request body
const { orderId, location, plan, amount } = await request.json()
if (!orderId) {
return NextResponse.json({ error: 'Order ID is required' }, { status: 400 })
}
if (!location || !VPN_ENDPOINTS[location as keyof typeof VPN_ENDPOINTS]) {
return NextResponse.json({ error: 'Valid VPN location is required' }, { status: 400 })
}
// Connect to MongoDB
await connectDB()
// Get user data for billing
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
const requiredAmount = amount || 0
// Check balance only if amount > 0
if (requiredAmount > 0) {
const currentBalance = userData.balance || 0
if (currentBalance < requiredAmount) {
return NextResponse.json(
{
error: `Insufficient balance. Required: ₹${requiredAmount}, Available: ₹${currentBalance}`,
code: 'INSUFFICIENT_BALANCE',
},
{ status: 400 }
)
}
}
// Get environment variables
const VPN_API_KEY = process.env.VPN_API_KEY
if (!VPN_API_KEY) {
return NextResponse.json({ error: 'VPN API configuration missing' }, { status: 500 })
}
// Get the endpoint for the selected location
const endpoint = VPN_ENDPOINTS[location as keyof typeof VPN_ENDPOINTS]
// Make API request to the selected VPN endpoint
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'X-API-Key': VPN_API_KEY,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
new: orderId.toString(),
userId: user.id,
plan,
location,
}),
})
console.log('VPN_API_KEY', VPN_API_KEY)
// Handle non-200 responses
if (!response.ok) {
const errorData = await response.text()
throw new Error(`VPN API returned HTTP ${response.status}: ${errorData}`)
}
// Parse the response
const vpnData = await response.json()
const deploymentSuccess = response.ok && vpnData?.config
if (!deploymentSuccess) {
throw new Error('Invalid response from VPN API: Missing config')
}
// Process billing using the new comprehensive billing service
try {
const billingResult = await BillingService.processServiceDeployment({
user: {
id: user.id,
email: user.email,
siliconId: userData.siliconId,
},
service: {
name: `VPN Service - ${location}`,
type: 'vpn',
id: orderId.toString(),
config: {
orderId,
location,
plan,
endpoint: endpoint,
},
},
amount: requiredAmount,
currency: 'INR',
cycle: 'monthly',
deploymentSuccess,
deploymentResponse: vpnData,
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
vpnLocation: location,
vpnPlan: plan,
},
})
console.log('VPN billing processed:', {
billingId: billingResult.billing.billing_id,
transactionId: billingResult.transaction?.transactionId,
balanceUpdated: billingResult.balanceUpdated,
})
} catch (billingError) {
console.error('Billing processing failed:', billingError)
// Continue even if billing fails, but return error if it's balance-related
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
return NextResponse.json(
{
error: billingError.message,
code: 'INSUFFICIENT_BALANCE',
},
{ status: 400 }
)
}
}
// Return the VPN configuration
return NextResponse.json({
success: true,
data: vpnData,
})
} catch (error) {
console.error('VPN deployment error:', error)
return NextResponse.json(
{
error: 'VPN deployment failed',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import BillingService from '@/lib/billing-service'
export async function GET(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get billing ID from query parameters
const { searchParams } = new URL(request.url)
const billingId = searchParams.get('billing_id')
const amount = parseFloat(searchParams.get('amount') || '0')
if (!billingId) {
return NextResponse.json({ error: 'Billing ID is required' }, { status: 400 })
}
// Connect to MongoDB
await connectDB()
// Get user data for billing
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Mock hosting configuration (in production, this would come from database)
const hostingConfig = {
billing_id: billingId,
siliconId: user.id,
domain: `demo-${billingId}.siliconpin.com`,
cp_url: `https://cp-${billingId}.siliconpin.com:2083`,
panel: 'cPanel',
user_id: `user_${billingId}`,
password: `secure_password_${Date.now()}`,
server_ip: '192.168.1.100',
nameservers: ['ns1.siliconpin.com', 'ns2.siliconpin.com'],
ftp_settings: {
host: `ftp.demo-${billingId}.siliconpin.com`,
username: `user_${billingId}`,
port: 21,
},
database_settings: {
host: 'localhost',
prefix: `db_${billingId}_`,
},
ssl_certificate: {
enabled: true,
type: "Let's Encrypt",
auto_renew: true,
},
}
const configJson = JSON.stringify(hostingConfig, null, 2)
// Process billing for hosting configuration download
try {
await BillingService.processServiceDeployment({
user: {
id: user.id,
email: user.email,
siliconId: userData.siliconId,
},
service: {
name: `Hosting Configuration - ${hostingConfig.domain}`,
type: 'hosting',
id: billingId,
config: hostingConfig,
},
amount: amount,
currency: 'INR',
cycle: 'onetime',
deploymentSuccess: true,
deploymentResponse: { configDownloaded: true },
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
downloadType: 'hosting_config',
domain: hostingConfig.domain,
},
})
console.log('Hosting config billing processed for:', billingId)
} catch (billingError) {
console.error('Billing processing failed:', billingError)
// Continue with download even if billing fails
}
// Return the config as a downloadable JSON file
return new NextResponse(configJson, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="sp_credential_${billingId}.json"`,
'Cache-Control': 'no-cache',
},
})
} catch (error) {
console.error('Download error:', error)
return NextResponse.json({ error: 'Download failed' }, { status: 500 })
}
}

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
export async function GET(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get cluster ID from query parameters
const { searchParams } = new URL(request.url)
const clusterId = searchParams.get('cluster_id')
if (!clusterId) {
return NextResponse.json({ error: 'Cluster ID is required' }, { status: 400 })
}
// Mock Kubernetes configuration
const kubeConfig = `apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJRWRtTFUzZUNCUXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TkRBeE1EUXhOekF6TXpCYUZ3MHpOREF4TURFeE56QXpNekJhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLLQEKQVFJREFRQUI=
server: https://k8s-api.siliconpin.com:6443
name: ${clusterId}
contexts:
- context:
cluster: ${clusterId}
user: ${clusterId}-admin
name: ${clusterId}
current-context: ${clusterId}
kind: Config
preferences: {}
users:
- name: ${clusterId}-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ==`
// Return the config as a downloadable file
return new NextResponse(kubeConfig, {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="kubeconfig-${clusterId}.yaml"`,
'Cache-Control': 'no-cache',
},
})
} catch (error) {
console.error('Download error:', error)
return NextResponse.json({ error: 'Download failed' }, { status: 500 })
}
}

View File

@@ -0,0 +1,285 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import { DeveloperRequest, IDeveloperRequest } from '@/models/developer-request'
import { Transaction } from '@/models/transaction'
import BillingService from '@/lib/billing-service'
import { checkServiceAvailability } from '@/lib/system-settings'
// Schema for developer hire request
const HireDeveloperSchema = z.object({
planId: z.enum(['hourly', 'daily', 'monthly']),
planName: z.string().min(1),
planPrice: z.number().positive(),
requirements: z.string().min(10, 'Requirements must be at least 10 characters').max(5000),
contactInfo: z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Valid email is required'),
phone: z.string().optional(),
}),
})
// Schema for response
const HireDeveloperResponseSchema = z.object({
success: z.boolean(),
data: z
.object({
requestId: z.string(),
transactionId: z.string(),
status: z.string(),
message: z.string(),
estimatedResponse: z.string(),
})
.optional(),
error: z
.object({
message: z.string(),
code: z.string(),
})
.optional(),
})
export async function POST(request: NextRequest) {
try {
// Check if developer hire service is enabled
if (!(await checkServiceAvailability('developer'))) {
return NextResponse.json(
{
success: false,
error: {
message: 'Developer hire service is currently disabled by administrator',
code: 'SERVICE_DISABLED',
},
},
{ status: 503 }
)
}
// Authenticate user
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
// Parse and validate request body
const body = await request.json()
const validatedData = HireDeveloperSchema.parse(body)
// Get user's current balance from database
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
const currentBalance = userData.balance || 0
// Check if user has sufficient balance (minimum deposit required)
const minimumDeposit = Math.min(validatedData.planPrice * 0.5, 10000) // 50% or max ₹10,000
if (currentBalance < minimumDeposit) {
return NextResponse.json(
{
success: false,
error: {
message: `Insufficient balance. Minimum deposit required: ₹${minimumDeposit}, Available: ₹${currentBalance}`,
code: 'INSUFFICIENT_BALANCE',
},
},
{ status: 400 }
)
}
// Create developer request first
const developerRequest = new DeveloperRequest({
userId: userData._id,
planId: validatedData.planId,
planName: validatedData.planName,
planPrice: validatedData.planPrice,
requirements: validatedData.requirements,
contactInfo: validatedData.contactInfo,
status: 'pending',
paymentStatus: 'pending', // Will be updated after billing
transactionId: null, // Will be set after transaction is created
})
// Save developer request first
const savedRequest = await developerRequest.save()
// Process billing using the new comprehensive billing service
try {
const billingResult = await BillingService.processServiceDeployment({
user: {
id: user.id,
email: user.email,
siliconId: userData.siliconId,
},
service: {
name: `Developer Hire - ${validatedData.planName}`,
type: 'developer_hire',
id: savedRequest._id.toString(),
config: {
planId: validatedData.planId,
planName: validatedData.planName,
planPrice: validatedData.planPrice,
requirements: validatedData.requirements,
contactInfo: validatedData.contactInfo,
},
},
amount: minimumDeposit,
currency: 'INR',
cycle: 'onetime',
deploymentSuccess: true, // Developer hire request is always successful
deploymentResponse: { requestId: savedRequest._id.toString() },
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
depositAmount: minimumDeposit,
fullPlanPrice: validatedData.planPrice,
},
})
// Update developer request with billing info
await (DeveloperRequest as any).updateOne(
{ _id: savedRequest._id },
{
$set: {
paymentStatus: 'paid',
transactionId: billingResult.transaction?._id,
},
}
)
console.log('Developer hire billing processed:', {
requestId: savedRequest._id,
billingId: billingResult.billing.billing_id,
transactionId: billingResult.transaction?.transactionId,
})
} catch (billingError) {
console.error('Billing processing failed:', billingError)
// Rollback developer request if billing fails
await (DeveloperRequest as any).deleteOne({ _id: savedRequest._id })
if (billingError instanceof Error && billingError.message.includes('Insufficient balance')) {
return NextResponse.json(
{
success: false,
error: {
message: billingError.message,
code: 'INSUFFICIENT_BALANCE',
},
},
{ status: 400 }
)
}
throw new Error('Failed to process payment')
}
const responseData = {
success: true,
data: {
requestId: savedRequest._id.toString(),
transactionId: 'processed_via_billing',
status: 'pending',
message:
'Your developer hire request has been submitted successfully! Our team will review your requirements and contact you within 24 hours.',
estimatedResponse: '24 hours',
},
}
const validatedResponse = HireDeveloperResponseSchema.parse(responseData)
return NextResponse.json(validatedResponse, { status: 200 })
} catch (error) {
console.error('Developer hire error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid request data',
code: 'VALIDATION_ERROR',
details: error.issues,
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to process developer hire request', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}
// GET endpoint to fetch user's developer requests
export async function GET(request: NextRequest) {
try {
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
// Get user's developer requests
const requests = await (DeveloperRequest as any)
.find({ userId: userData._id })
.sort({ createdAt: -1 })
.populate('transactionId')
.lean()
return NextResponse.json({
success: true,
data: {
requests,
total: requests.length,
},
})
} catch (error) {
console.error('Failed to fetch developer requests:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch requests', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,52 @@
import { NextResponse } from 'next/server'
import { testMongoConnection } from '@/lib/mongodb'
export async function GET() {
try {
console.log('🚀 Running startup connection tests...')
const mongoStatus = await testMongoConnection()
// Test Redis connection (basic check)
let redisStatus = false
try {
// Import redis client
const { redisClient } = await import('@/lib/redis')
await redisClient.ping()
console.log('✅ Redis connection test successful')
redisStatus = true
} catch (error) {
console.error('❌ Redis connection test failed:', error)
console.error('Redis is optional but recommended for session storage')
}
const overallStatus = mongoStatus // Redis is optional, so we only require MongoDB
if (overallStatus) {
console.log('🎉 All critical services are connected and ready!')
} else {
console.log('⚠️ Some services failed connection tests')
}
return NextResponse.json({
success: overallStatus,
services: {
mongodb: mongoStatus,
redis: redisStatus,
},
message: overallStatus
? 'All critical services connected successfully'
: 'Some services failed connection tests - check logs',
})
} catch (error) {
console.error('❌ Startup test failed:', error)
return NextResponse.json(
{
success: false,
error: 'Startup test failed',
message: 'Check server logs for details',
},
{ status: 500 }
)
}
}

76
app/api/tags/route.ts Normal file
View File

@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server'
import TopicModel from '@/models/topic'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const includeStats = searchParams.get('stats') === 'true'
// Get all published topics and extract tags
const topics = await TopicModel.find({ isDraft: false })
.select('tags')
.lean()
if (!topics || topics.length === 0) {
return NextResponse.json(
{
success: true,
data: [],
},
{ status: 200 }
)
}
// Create a map to track tag usage
const tagMap = new Map<string, { name: string; count: number }>()
topics.forEach((topic) => {
topic.tags?.forEach((tag: { id?: string; name: string }) => {
const tagName = tag.name
const existing = tagMap.get(tagName.toLowerCase())
if (existing) {
existing.count++
} else {
tagMap.set(tagName.toLowerCase(), {
name: tagName,
count: 1,
})
}
})
})
// Convert to array and sort by usage count (descending)
const tagsArray = Array.from(tagMap.values())
.sort((a, b) => b.count - a.count)
// If stats are requested, include the count
const result = tagsArray.map((tag, index) => ({
id: `tag-${index + 1}`,
name: tag.name,
...(includeStats && { count: tag.count }),
}))
return NextResponse.json(
{
success: true,
data: result,
meta: {
totalTags: result.length,
totalTopics: topics.length,
},
},
{ status: 200 }
)
} catch (error) {
console.error('Error fetching tags:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch tags', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
export async function POST(request: NextRequest) {
try {
// No authentication required for web-speech tool
const { message, systemPrompt } = await request.json()
if (!message || typeof message !== 'string') {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
}
// Check if OpenAI API key is configured
if (!process.env.OPENAI_API_KEY) {
return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 500 })
}
// Prepare messages for OpenAI
const messages = [
{
role: 'system',
content:
systemPrompt ||
'You are a helpful AI assistant. Provide clear, concise, and helpful responses to user queries.',
},
{
role: 'user',
content: message,
},
]
// Call OpenAI Chat Completions API
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: messages,
max_tokens: 1000,
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
}),
})
if (!response.ok) {
const errorData = await response.text()
console.error('OpenAI Chat API error:', errorData)
return NextResponse.json({ error: 'AI processing failed' }, { status: 500 })
}
const chatResult = await response.json()
if (!chatResult.choices || chatResult.choices.length === 0) {
return NextResponse.json({ error: 'No response from AI' }, { status: 500 })
}
return NextResponse.json({
response: chatResult.choices[0].message.content,
usage: chatResult.usage,
})
} catch (error) {
console.error('OpenAI chat error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,166 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
// POST /api/topic-content-image - Upload images for topic content (rich text editor)
export async function POST(request: NextRequest) {
try {
// Authentication required for image uploads
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const formData = await request.formData()
const image = formData.get('file') as File
if (!image) {
return NextResponse.json(
{
success: false,
error: { message: 'Image file is required', code: 'MISSING_IMAGE' },
},
{ status: 400 }
)
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
if (!allowedTypes.includes(image.type)) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed',
code: 'INVALID_FILE_TYPE',
},
},
{ status: 400 }
)
}
// Validate file size (5MB limit)
const maxSize = 5 * 1024 * 1024 // 5MB
if (image.size > maxSize) {
return NextResponse.json(
{
success: false,
error: {
message: 'File size too large. Maximum size is 5MB',
code: 'FILE_TOO_LARGE',
},
},
{ status: 400 }
)
}
console.log('Processing topic content image upload for user:', user.id)
try {
// Convert file to buffer (exactly like dev-portfolio)
const bytes = await image.arrayBuffer()
const buffer = Buffer.from(bytes)
// Upload directly to external API (no temp storage needed)
const uploadResult = await uploadFile(buffer, image.name, image.type, user.id)
const imageUrl = uploadResult.url
const uniqueFilename = uploadResult.filename
const permanentPath = uploadResult.url
console.log('Topic content image uploaded successfully:', {
originalName: image.name,
permanentPath,
uploadedBy: user.id,
})
// Return URL format expected by BlockNote editor
return NextResponse.json(
{
success: true,
data: {
url: imageUrl, // BlockNote expects URL here
fileName: uniqueFilename,
path: permanentPath,
size: image.size,
type: image.type,
uploadedBy: user.id,
uploadedAt: new Date().toISOString(),
},
},
{ status: 200 }
)
} catch (error) {
console.error('Failed to upload topic content image:', error)
throw error
}
} catch (error) {
console.error('Error uploading topic content image:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to upload image', code: 'UPLOAD_ERROR' },
},
{ status: 500 }
)
}
}
// GET /api/topic-content-image - Get image metadata or URL by path (like dev-portfolio)
export async function GET(request: NextRequest) {
try {
// Authentication required to access image metadata
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const imagePath = searchParams.get('path')
if (!imagePath) {
return NextResponse.json(
{
success: false,
error: { message: 'Image path parameter is required', code: 'MISSING_PATH' },
},
{ status: 400 }
)
}
// Get accessible URL for the image (like dev-portfolio)
const imageUrl = await getFileUrl(imagePath)
return NextResponse.json(
{
success: true,
data: {
path: imagePath,
url: imageUrl,
},
},
{ status: 200 }
)
} catch (error) {
console.error('Error serving topic content image:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to retrieve image', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}

261
app/api/topic/[id]/route.ts Normal file
View File

@@ -0,0 +1,261 @@
import { NextRequest, NextResponse } from 'next/server'
import TopicModel, { transformToTopic } from '@/models/topic'
import { authMiddleware } from '@/lib/auth-middleware'
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
import { z } from 'zod'
// Validation schema for topic updates
const topicUpdateSchema = z.object({
title: z.string().min(1).max(100).optional(),
author: z.string().min(1).optional(),
excerpt: z.string().min(1).max(200).optional(),
content: z.string().min(1).optional(),
contentRTE: z.unknown().optional(),
contentImages: z.array(z.string()).optional(),
tags: z
.array(
z.object({
id: z.string().optional(),
name: z.string(),
})
)
.min(1)
.optional(),
featured: z.boolean().optional(),
isDraft: z.boolean().optional(),
coverImage: z.string().url().optional(),
coverImageKey: z.string().optional(),
})
// GET /api/topic/[id] - Get topic by ID
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const topic = await TopicModel.findOne({ id }).lean()
if (!topic) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found', code: 'NOT_FOUND' },
},
{ status: 404 }
)
}
// Check if topic is draft and user is not the owner
if (topic.isDraft) {
const user = await authMiddleware(request)
if (!user || user.id !== topic.authorId) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found', code: 'NOT_FOUND' },
},
{ status: 404 }
)
}
}
const transformedTopic = transformToTopic(topic)
return NextResponse.json(
{
success: true,
data: transformedTopic,
},
{ status: 200 }
)
} catch (error) {
console.error('Error fetching topic:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch topic', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}
// PUT /api/topic/[id] - Update topic by ID
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// Authentication required for updating topics
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const { id } = await params
// Find the topic first
const existingTopic = await TopicModel.findOne({ id })
if (!existingTopic) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found', code: 'NOT_FOUND' },
},
{ status: 404 }
)
}
// Check ownership - users can only update their own topics
if (existingTopic.authorId !== user.id) {
return NextResponse.json(
{
success: false,
error: { message: 'You can only edit your own topics', code: 'FORBIDDEN' },
},
{ status: 403 }
)
}
const body = await request.json()
// Validate request body
const validatedData = topicUpdateSchema.parse(body)
console.log('Updating topic for user:', user.id, 'Topic ID:', id)
try {
// Update the topic with new data
Object.assign(existingTopic, validatedData)
existingTopic.publishedAt = Date.now() // Update timestamp
const updatedTopic = await existingTopic.save()
console.log('Topic updated successfully:', updatedTopic.id)
return NextResponse.json(
{
success: true,
data: updatedTopic,
},
{ status: 200 }
)
} catch (error) {
console.error('Failed to update topic:', error)
if (error.code === 11000) {
return NextResponse.json(
{
success: false,
error: { message: 'A topic with this slug already exists', code: 'DUPLICATE_SLUG' },
},
{ status: 409 }
)
}
throw error
}
} catch (error) {
console.error('Error updating topic:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Validation failed',
code: 'VALIDATION_ERROR',
details: error.issues,
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to update topic', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}
// DELETE /api/topic/[id] - Delete topic by ID
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Authentication required for deleting topics
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const { id } = await params
// Find the topic first
const existingTopic = await TopicModel.findOne({ id })
if (!existingTopic) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found', code: 'NOT_FOUND' },
},
{ status: 404 }
)
}
// Check ownership - users can only delete their own topics
if (existingTopic.authorId !== user.id) {
return NextResponse.json(
{
success: false,
error: { message: 'You can only delete your own topics', code: 'FORBIDDEN' },
},
{ status: 403 }
)
}
console.log('Deleting topic for user:', user.id, 'Topic ID:', id)
try {
await TopicModel.deleteOne({ id })
console.log('Topic deleted successfully:', id)
return NextResponse.json(
{
success: true,
message: 'Topic deleted successfully',
},
{ status: 200 }
)
} catch (error) {
console.error('Failed to delete topic:', error)
throw error
}
} catch (error) {
console.error('Error deleting topic:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to delete topic', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}

427
app/api/topic/route.ts Normal file
View File

@@ -0,0 +1,427 @@
import { NextRequest, NextResponse } from 'next/server'
import { randomUUID } from 'crypto'
import TopicModel from '@/models/topic'
import { authMiddleware } from '@/lib/auth-middleware'
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
export async function POST(request: NextRequest) {
try {
console.log('Starting topic post processing')
// Debug authentication data
const authHeader = request.headers.get('authorization')
const accessTokenCookie = request.cookies.get('accessToken')?.value
console.log('🔍 Auth debug:', {
hasAuthHeader: !!authHeader,
authHeaderPrefix: authHeader?.substring(0, 20),
hasAccessTokenCookie: !!accessTokenCookie,
cookiePrefix: accessTokenCookie?.substring(0, 20),
allCookies: Object.fromEntries(
request.cookies.getAll().map((c) => [c.name, c.value?.substring(0, 20)])
),
})
// Authentication required
const user = await authMiddleware(request)
console.log('🔍 Auth result:', { user: user ? { id: user.id, email: user.email } : null })
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const formData = await request.formData()
const coverImage = formData.get('coverImage') as File
const topicId = request.nextUrl.searchParams.get('topicId')
console.log('Form data received', { topicId, hasCoverImage: !!coverImage })
// Get other form data with proper validation
const title = formData.get('title') as string
const author = formData.get('author') as string
const excerpt = formData.get('excerpt') as string
const content = formData.get('content') as string
const contentRTE = formData.get('contentRTE') as string
const featured = formData.get('featured') === 'true'
// Get the LAST isDraft value (in case of duplicates)
const allIsDraftValues = formData.getAll('isDraft')
console.log('🐛 All isDraft values in FormData:', allIsDraftValues)
const isDraft = allIsDraftValues[allIsDraftValues.length - 1] === 'true'
console.log('🐛 Final isDraft value:', isDraft)
// Parse JSON fields safely (like dev-portfolio)
let contentImages: string[] = []
let tags: any[] = []
try {
const contentImagesStr = formData.get('contentImages') as string
contentImages = contentImagesStr ? JSON.parse(contentImagesStr) : []
} catch (error) {
console.warn('Failed to parse contentImages, using empty array:', error)
contentImages = []
}
try {
const tagsStr = formData.get('tags') as string
tags = tagsStr ? JSON.parse(tagsStr) : []
} catch (error) {
console.warn('Failed to parse tags, using empty array:', error)
tags = []
}
// Validate required fields (like dev-portfolio)
if (!title?.trim()) {
return NextResponse.json(
{ success: false, error: { message: 'Title is required', code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
if (!excerpt?.trim()) {
return NextResponse.json(
{ success: false, error: { message: 'Excerpt is required', code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
if (!content?.trim()) {
return NextResponse.json(
{ success: false, error: { message: 'Content is required', code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
if (!Array.isArray(tags) || tags.length === 0) {
return NextResponse.json(
{
success: false,
error: { message: 'At least one tag is required', code: 'VALIDATION_ERROR' },
},
{ status: 400 }
)
}
const targetTopicId = topicId ? topicId : randomUUID()
// For existing topic, fetch the current data
let existingTopic = null
if (topicId) {
existingTopic = await TopicModel.findOne({ id: topicId })
if (!existingTopic) {
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
}
}
// Handle cover image upload exactly like dev-portfolio
let tempImageResult = null
if (coverImage && coverImage instanceof File) {
try {
console.log('🖼️ Processing cover image upload:', {
name: coverImage.name,
size: coverImage.size,
type: coverImage.type,
})
// Upload using external API
try {
// Convert file to buffer
const bytes = await coverImage.arrayBuffer()
const buffer = Buffer.from(bytes)
console.log('📤 Buffer created, starting upload...')
// Upload directly to external API (no temp storage needed)
const uploadResult = await uploadFile(buffer, coverImage.name, coverImage.type, user.id)
const coverImageUrl = uploadResult.url
const permanentPath = uploadResult.url
tempImageResult = {
coverImage: coverImageUrl,
coverImageKey: permanentPath,
}
console.log('✅ Cover image uploaded successfully:', { permanentPath })
} catch (uploadError) {
console.error('❌ Cover image upload failed:', uploadError)
throw new Error(`Failed to upload cover image: ${uploadError.message}`)
}
} catch (error) {
console.error('Error processing cover image:', error)
}
}
const dataToSave = {
id: targetTopicId,
publishedAt: Date.now(),
title,
author,
authorId: user.id, // Add user ownership
excerpt,
content,
contentRTE: contentRTE ? JSON.parse(contentRTE) : [],
contentImages,
featured,
tags,
isDraft,
// Use uploaded image or existing image for updates, require image for new topics like dev-portfolio
coverImage:
tempImageResult?.coverImage ||
existingTopic?.coverImage ||
'https://via.placeholder.com/800x400',
coverImageKey: tempImageResult?.coverImageKey || existingTopic?.coverImageKey || '',
}
console.log('Preparing to save topic data', { targetTopicId })
let savedArticle
try {
if (existingTopic) {
// Update existing topic
console.log('Updating existing topic')
Object.assign(existingTopic, dataToSave)
savedArticle = await existingTopic.save()
} else {
// Create new topic
console.log('Creating new topic')
const newArticle = new TopicModel(dataToSave)
savedArticle = await newArticle.save()
}
console.log('Topic saved successfully', { topicId: savedArticle.id })
} catch (error) {
console.error('Failed to save topic', { error })
throw error
}
return NextResponse.json(
{
success: true,
data: savedArticle,
},
{ status: 200 }
)
} catch (error) {
console.error('Error processing topic post:', {
error,
message: error.message,
})
return NextResponse.json(
{ success: false, message: 'Failed to process topic post', error: error.message },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const featured = searchParams.get('featured') === 'true'
const isDraft = searchParams.get('isDraft')
const search = searchParams.get('search')
const tag = searchParams.get('tag')
// Build query (like dev-portfolio)
const query: any = {}
// Featured filter
if (featured) query.featured = true
// Draft filter (only if explicitly set)
if (isDraft !== null) query.isDraft = isDraft === 'true'
// Search functionality (like dev-portfolio)
if (search) {
query.$or = [
{ title: { $regex: search, $options: 'i' } },
{ excerpt: { $regex: search, $options: 'i' } },
{ content: { $regex: search, $options: 'i' } },
]
}
// Tag filtering (like dev-portfolio)
if (tag) {
query['tags.name'] = { $regex: tag, $options: 'i' }
}
const skip = (page - 1) * limit
const [topics, total] = await Promise.all([
TopicModel.find(query).sort({ publishedAt: -1 }).skip(skip).limit(limit),
TopicModel.countDocuments(query),
])
return NextResponse.json({
success: true,
data: {
topics,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
},
})
} catch (error) {
console.error('Error fetching topics:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch topics', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}
// PUT /api/topic - Update existing topic (like dev-portfolio)
export async function PUT(request: NextRequest) {
try {
console.log('Starting topic update processing')
// Authentication required
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const formData = await request.formData()
const topicId = formData.get('topicId') as string
if (!topicId) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic ID is required for updates', code: 'MISSING_TOPIC_ID' },
},
{ status: 400 }
)
}
// Find existing topic and check ownership
const existingTopic = await TopicModel.findOne({ id: topicId })
if (!existingTopic) {
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
}
// Check authorization - user can only update their own topics
if (existingTopic.authorId !== user.id) {
return NextResponse.json(
{
success: false,
error: { message: 'Not authorized to update this topic', code: 'UNAUTHORIZED' },
},
{ status: 403 }
)
}
// Use the same logic as POST but for updates
// This reuses the form processing logic from POST
const url = new URL(request.url)
url.searchParams.set('topicId', topicId)
// Create new request for POST handler with topicId parameter
const updateRequest = new NextRequest(url.toString(), {
method: 'POST',
body: formData,
headers: request.headers,
})
return POST(updateRequest)
} catch (error) {
console.error('Error updating topic post:', error)
return NextResponse.json(
{ success: false, message: 'Failed to update topic post', error: error.message },
{ status: 500 }
)
}
}
// DELETE /api/topic - Delete topic (like dev-portfolio)
export async function DELETE(request: NextRequest) {
try {
console.log('Starting topic deletion')
// Authentication required
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const topicId = searchParams.get('id')
if (!topicId) {
return NextResponse.json(
{ success: false, error: { message: 'Topic ID is required', code: 'MISSING_TOPIC_ID' } },
{ status: 400 }
)
}
// Find existing topic and check ownership
const existingTopic = await TopicModel.findOne({ id: topicId })
if (!existingTopic) {
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
}
// Check authorization - user can only delete their own topics
if (existingTopic.authorId !== user.id) {
return NextResponse.json(
{
success: false,
error: { message: 'Not authorized to delete this topic', code: 'UNAUTHORIZED' },
},
{ status: 403 }
)
}
console.log('Deleting topic:', { topicId, title: existingTopic.title?.substring(0, 50) })
// Delete the topic
await TopicModel.deleteOne({ id: topicId })
console.log('Topic deleted successfully:', topicId)
// TODO: Implement image cleanup like dev-portfolio
// if (existingTopic.coverImageKey && existingTopic.coverImageKey !== 'placeholder-key') {
// await deleteFile(existingTopic.coverImageKey)
// }
// if (existingTopic.contentImages && existingTopic.contentImages.length > 0) {
// for (const imagePath of existingTopic.contentImages) {
// await deleteFile(imagePath)
// }
// }
return NextResponse.json(
{
success: true,
message: 'Topic deleted successfully',
data: { id: topicId },
},
{ status: 200 }
)
} catch (error) {
console.error('Error deleting topic:', error)
return NextResponse.json(
{ success: false, message: 'Failed to delete topic', error: error.message },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from 'next/server'
import TopicModel, { transformToTopic } from '@/models/topic'
import { authMiddleware } from '@/lib/auth-middleware'
// GET /api/topic/slug/[slug] - Get topic by slug (for public viewing)
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await params
if (!slug) {
return NextResponse.json(
{
success: false,
error: { message: 'Slug parameter is required', code: 'MISSING_SLUG' },
},
{ status: 400 }
)
}
const topic = await TopicModel.findOne({ slug }).lean()
if (!topic) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found', code: 'NOT_FOUND' },
},
{ status: 404 }
)
}
// Check if topic is draft and user is not the owner
if (topic.isDraft) {
const user = await authMiddleware(request)
if (!user || user.id !== topic.authorId) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found', code: 'NOT_FOUND' },
},
{ status: 404 }
)
}
}
const transformedTopic = transformToTopic(topic)
if (!transformedTopic) {
return NextResponse.json(
{
success: false,
error: { message: 'Failed to process topic data', code: 'PROCESSING_ERROR' },
},
{ status: 500 }
)
}
return NextResponse.json(
{
success: true,
data: transformedTopic,
},
{ status: 200 }
)
} catch (error) {
console.error('Error fetching topic by slug:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch topic', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import TopicModel, { transformToTopics } from '@/models/topic'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
await connectDB()
const { slug } = await params
const searchParams = request.nextUrl.searchParams
const limit = parseInt(searchParams.get('limit') || '2')
if (!slug) {
return NextResponse.json(
{
success: false,
error: {
message: 'Topic slug is required',
code: 'SLUG_REQUIRED',
},
},
{ status: 400 }
)
}
// First, get the current post to find its ID and tags
const currentPost = await TopicModel.findOne({
slug,
isDraft: false
}).lean()
if (!currentPost) {
return NextResponse.json({
success: true,
data: [],
})
}
// Find related posts by excluding the current post and sorting by publishedAt
const relatedPosts = await TopicModel.find({
id: { $ne: currentPost.id },
isDraft: false,
})
.sort({ publishedAt: -1 })
.limit(limit)
.lean()
// Transform to proper format
const transformedPosts = transformToTopics(relatedPosts)
return NextResponse.json({
success: true,
data: transformedPosts,
})
} catch (error) {
console.error('Error fetching related posts:', error)
return NextResponse.json(
{
success: false,
error: {
message: 'Failed to fetch related posts',
code: 'SERVER_ERROR',
},
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import TopicModel, { transformToTopic } from '@/models/topic'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
await connectDB()
const { slug } = await params
if (!slug) {
return NextResponse.json(
{
success: false,
error: {
message: 'Topic slug is required',
code: 'SLUG_REQUIRED',
},
},
{ status: 400 }
)
}
// Find the topic post by slug (only published posts)
const post = await TopicModel.findOne({
slug,
isDraft: false
}).lean()
if (!post) {
return NextResponse.json(
{
success: false,
error: {
message: 'Topic post not found',
code: 'POST_NOT_FOUND',
},
},
{ status: 404 }
)
}
// Transform to proper format
const transformedPost = transformToTopic(post)
if (!transformedPost) {
return NextResponse.json(
{
success: false,
error: {
message: 'Failed to process topic post',
code: 'PROCESSING_ERROR',
},
},
{ status: 500 }
)
}
return NextResponse.json({
success: true,
data: transformedPost,
})
} catch (error) {
console.error('Error fetching topic post:', error)
return NextResponse.json(
{
success: false,
error: {
message: 'Failed to fetch topic post',
code: 'SERVER_ERROR',
},
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import TopicModel from '@/models/topic'
// Track topic view (increment view count and daily analytics)
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await params
if (!slug) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic slug is required', code: 'MISSING_SLUG' },
},
{ status: 400 }
)
}
await connectDB()
// Get current date in YYYY-MM-DD format
const today = new Date().toISOString().split('T')[0]
// Find and update the topic
const topic = await TopicModel.findOneAndUpdate(
{ slug: slug, isDraft: false }, // Only track views for published posts
{
$inc: { views: 1 }, // Increment total views
$push: {
viewHistory: {
$each: [{ date: today, count: 1 }],
$slice: -30, // Keep only last 30 days
},
},
},
{
new: true,
projection: { views: 1, slug: 1, title: 1, authorId: 1 } // Include authorId for cache invalidation
}
)
if (!topic) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic not found or is a draft', code: 'TOPIC_NOT_FOUND' },
},
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: {
slug: topic.slug,
title: topic.title,
views: topic.views,
},
})
} catch (error) {
console.error('View tracking error:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to track view', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

142
app/api/topics/route.ts Normal file
View File

@@ -0,0 +1,142 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import TopicModel, { transformToTopics } from '@/models/topic'
import { authMiddleware } from '@/lib/auth-middleware'
import { ZodError } from 'zod'
export async function GET(request: NextRequest) {
try {
await connectDB()
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('q') || ''
const tag = searchParams.get('tag') || ''
const authorId = searchParams.get('authorId') || '' // For filtering by user's topics
const includeDrafts = searchParams.get('includeDrafts') === 'true' // For user's own topics
const fetchAll = searchParams.get('fetchAll') // Admin panel support
const skip = (page - 1) * limit
// Build query based on parameters
const query: any = {}
// Authentication check for private operations (drafts or admin)
let user = null
try {
user = await authMiddleware(request)
} catch (error) {
// Non-authenticated request - that's okay for public topic listing
}
// If requesting user's own topics with drafts, verify authentication
if (includeDrafts && !user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required to view drafts', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
// Filter by author if specified
if (authorId) {
query.authorId = authorId
}
// Handle draft filtering
if (fetchAll === 'true') {
// Admin panel - show all posts (drafts and published)
// No draft filtering applied
} else if (includeDrafts && user && (authorId === user.id || !authorId)) {
// User requesting their own topics (including drafts)
if (!authorId) {
query.authorId = user.id
}
} else {
// Public topic listing - only published topics
query.isDraft = false
}
// Search functionality
if (search) {
query.$or = [
{ title: { $regex: search, $options: 'i' } },
{ excerpt: { $regex: search, $options: 'i' } },
{ content: { $regex: search, $options: 'i' } },
]
}
// Filter by tag
if (tag) {
query['tags.name'] = { $regex: new RegExp(tag, 'i') }
}
// Get total count with same filters
const totalTopics = await TopicModel.countDocuments(query)
// Get paginated topics sorted by publishedAt
const topics = await TopicModel.find(query)
.sort({ publishedAt: -1 })
.skip(skip)
.limit(limit)
.lean()
if (!topics || topics.length === 0) {
return NextResponse.json(
{
success: true,
data: [],
pagination: {
total: 0,
page,
limit,
totalPages: 0,
},
},
{ status: 200 }
)
}
// Transform the topics using our utility function
const transformedTopics = transformToTopics(topics)
return NextResponse.json(
{
success: true,
data: transformedTopics,
pagination: {
total: totalTopics,
page,
limit,
totalPages: Math.ceil(totalTopics / limit),
},
},
{ status: 200 }
)
} catch (error) {
console.error('Error fetching topics:', error)
if (error instanceof ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Data validation failed',
code: 'VALIDATION_ERROR',
details: error.issues,
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch topics', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'
import connectDB from '@/lib/mongodb'
import TopicModel from '@/models/topic'
export async function GET(request: NextRequest) {
try {
await connectDB()
// Get all unique tags from published topics
const topics = await TopicModel.find({ isDraft: false }).select('tags').lean()
const tags = new Set<string>()
topics.forEach((topic) => {
topic.tags.forEach((tag: { id?: string; name: string }) => tags.add(tag.name))
})
const uniqueTags = Array.from(tags).map((name, index) => ({
id: String(index + 1),
name
}))
return NextResponse.json({
success: true,
data: uniqueTags,
})
} catch (error) {
console.error('Error fetching tags:', error)
return NextResponse.json(
{
success: false,
error: {
message: 'Failed to fetch tags',
code: 'TAGS_FETCH_ERROR',
},
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,147 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { Transaction } from '@/models/transaction'
import { Billing } from '@/models/billing'
// Query parameters schema
const TransactionQuerySchema = z.object({
page: z.string().optional().default('1'),
limit: z.string().optional().default('10'),
type: z.enum(['debit', 'credit', 'all']).optional().default('all'),
service: z.string().optional(),
})
export async function GET(request: NextRequest) {
try {
// Authenticate user
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
// Parse query parameters
const { searchParams } = new URL(request.url)
const queryParams = TransactionQuerySchema.parse({
page: searchParams.get('page') || undefined,
limit: searchParams.get('limit') || undefined,
type: searchParams.get('type') || undefined,
service: searchParams.get('service') || undefined,
})
const page = parseInt(queryParams.page)
const limit = Math.min(parseInt(queryParams.limit), 100) // Max 100 per page
const skip = (page - 1) * limit
// Build query filter
const filter: any = { email: user.email }
if (queryParams.type !== 'all') {
filter.type = queryParams.type
}
if (queryParams.service) {
filter.service = { $regex: queryParams.service, $options: 'i' }
}
// Build billing filter
const billingFilter: any = {
$or: [{ user: user.email }, { siliconId: user.id }],
}
// console.log('billingFilter', user.email)
if (queryParams.service) {
billingFilter.service = { $regex: queryParams.service, $options: 'i' }
}
// Get both transactions and billing records
const [transactions, billingRecords, transactionCount, billingCount] = await Promise.all([
Transaction.find(filter).sort({ createdAt: -1 }).lean().exec(),
Billing.find(billingFilter).sort({ createdAt: -1 }).lean().exec(),
Transaction.countDocuments(filter).exec(),
Billing.countDocuments(billingFilter).exec(),
])
// Transform billing records to transaction format
const transformedBillings = billingRecords.map((billing: any) => ({
_id: billing._id,
transactionId: billing.billing_id,
type: 'debit' as const,
amount: billing.amount,
service: billing.service,
serviceId: billing.serviceId,
description: billing.remarks || `${billing.service} - ${billing.cycle} billing`,
status: billing.status === 'pending' ? 'pending' : 'completed',
previousBalance: 0, // We don't have this data in billing
newBalance: 0, // We don't have this data in billing
createdAt: billing.createdAt,
updatedAt: billing.updatedAt,
email: billing.user,
userId: billing.siliconId,
}))
// Combine and sort all records
const allRecords = [...transactions, ...transformedBillings].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
// Apply type filter to combined records
const filteredRecords =
queryParams.type === 'all'
? allRecords
: allRecords.filter((record) => record.type === queryParams.type)
// Apply pagination to filtered records
const totalCount = filteredRecords.length
const paginatedRecords = filteredRecords.slice(skip, skip + limit)
const totalPages = Math.ceil(totalCount / limit)
return NextResponse.json({
success: true,
data: {
transactions: paginatedRecords,
pagination: {
page,
limit,
totalCount,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
},
})
} catch (error) {
console.error('Get transactions error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid query parameters',
code: 'VALIDATION_ERROR',
details: error.format(),
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch transactions', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import { moveToPermStorage, generateUniqueFilename, deleteFile } from '@/lib/file-vault'
import { z } from 'zod'
// Confirm upload request validation
const confirmSchema = z.object({
tempPath: z.string().min(1, 'Temporary path is required'),
permanentFolder: z.string().optional().default('uploads'),
filename: z.string().optional(),
})
export async function POST(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { tempPath, permanentFolder, filename } = confirmSchema.parse(body)
// Validate temp path format
if (!tempPath.startsWith('temp/')) {
return NextResponse.json({ error: 'Invalid temporary path' }, { status: 400 })
}
// Generate permanent path
const originalFilename = tempPath.split('/').pop() || 'file'
const finalFilename = filename ? generateUniqueFilename(filename) : originalFilename
const permanentPath = `${permanentFolder}/${finalFilename}`
// Move file from temp to permanent storage
await moveToPermStorage(tempPath, permanentPath)
// TODO: Save file metadata to database
// This would include:
// - permanentPath
// - originalFilename
// - uploadedBy (user.id)
// - uploadedAt
// - fileSize
// - mimeType
return NextResponse.json({
success: true,
data: {
permanentPath,
filename: finalFilename,
folder: permanentFolder,
confirmedBy: user.id,
confirmedAt: new Date().toISOString(),
},
})
} catch (error) {
console.error('Upload confirmation error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid request parameters', details: error.issues },
{ status: 400 }
)
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
// Delete temporary file (cleanup)
export async function DELETE(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const tempPath = searchParams.get('path')
if (!tempPath) {
return NextResponse.json({ error: 'Temporary path is required' }, { status: 400 })
}
// Validate temp path format
if (!tempPath.startsWith('temp/')) {
return NextResponse.json({ error: 'Invalid temporary path' }, { status: 400 })
}
// Delete temporary file
await deleteFile(tempPath)
return NextResponse.json({
success: true,
message: 'Temporary file deleted successfully',
})
} catch (error) {
console.error('Temporary file deletion error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

129
app/api/upload/route.ts Normal file
View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from '@/lib/auth-middleware'
import { uploadFile, validateFileType, validateFileSize } from '@/lib/file-vault'
import { z } from 'zod'
// Allowed file types and sizes
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
const ALLOWED_DOCUMENT_TYPES = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
]
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
// Upload request validation
const uploadSchema = z.object({
type: z.enum(['image', 'document']).optional().default('image'),
})
export async function POST(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Parse form data
const formData = await request.formData()
const file = formData.get('file') as File
const typeParam = formData.get('type') as string
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
}
// Validate request parameters
const { type } = uploadSchema.parse({ type: typeParam })
// Determine allowed file types based on upload type
const allowedTypes = type === 'image' ? ALLOWED_IMAGE_TYPES : ALLOWED_DOCUMENT_TYPES
// Validate file type
if (!validateFileType(file.type, allowedTypes)) {
return NextResponse.json(
{
error: `Invalid file type. Allowed types: ${allowedTypes.join(', ')}`,
},
{ status: 400 }
)
}
// Validate file size
if (!validateFileSize(file.size, MAX_FILE_SIZE)) {
return NextResponse.json(
{
error: `File size too large. Max size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`,
},
{ status: 400 }
)
}
// Convert file to buffer
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
// Upload file to external API
const uploadResult = await uploadFile(buffer, file.name, file.type, user.id)
// Return upload information
return NextResponse.json({
success: true,
data: {
tempPath: uploadResult.url, // Use URL as tempPath for compatibility
filename: uploadResult.filename,
size: file.size,
type: file.type,
uploadedBy: user.id,
uploadedAt: new Date().toISOString(),
url: uploadResult.url, // Also include the direct URL
},
})
} catch (error) {
console.error('Upload error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid request parameters', details: error.issues },
{ status: 400 }
)
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
// Get file information
export async function GET(request: NextRequest) {
try {
// Check authentication
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const filePath = searchParams.get('path')
if (!filePath) {
return NextResponse.json({ error: 'File path is required' }, { status: 400 })
}
// TODO: Add file metadata retrieval from database
// For now, return basic info
return NextResponse.json({
success: true,
data: {
path: filePath,
// Additional metadata would be retrieved from database
},
})
} catch (error) {
console.error('File retrieval error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
// Schema for balance response
const BalanceResponseSchema = z.object({
success: z.boolean(),
data: z.object({
balance: z.number(),
currency: z.string().default('INR'),
lastUpdated: z.string(),
}),
})
// Get user's current balance
export async function GET(request: NextRequest) {
try {
// Authenticate user
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
// Get user's current balance from database
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
const responseData = {
success: true,
data: {
balance: userData.balance || 0,
currency: 'INR',
lastUpdated: userData.updatedAt?.toISOString() || new Date().toISOString(),
},
}
// Validate response format
const validatedResponse = BalanceResponseSchema.parse(responseData)
return NextResponse.json(validatedResponse, { status: 200 })
} catch (error) {
console.error('Balance API error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: { message: 'Invalid response format', code: 'VALIDATION_ERROR' },
},
{ status: 500 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch balance', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}