initial commit
This commit is contained in:
311
models/billing.ts
Normal file
311
models/billing.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import mongoose, { Schema, Document } from 'mongoose'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Zod schema for validation
|
||||
export const BillingSchema = z.object({
|
||||
billing_id: z.string().min(1, 'Billing ID is required'),
|
||||
service: z.string().min(1, 'Service name is required'),
|
||||
service_type: z.enum([
|
||||
'vps',
|
||||
'kubernetes',
|
||||
'developer_hire',
|
||||
'vpn',
|
||||
'hosting',
|
||||
'storage',
|
||||
'database',
|
||||
'ai_service',
|
||||
'custom',
|
||||
]),
|
||||
amount: z.number().min(0, 'Amount must be non-negative'),
|
||||
currency: z.enum(['INR', 'USD']).default('INR'),
|
||||
user_email: z.string().email('Invalid email format'),
|
||||
silicon_id: z.string().min(1, 'Silicon ID is required'),
|
||||
user_id: z.string().min(1, 'User ID is required'),
|
||||
status: z.enum(['pending', 'active', 'completed', 'cancelled', 'failed', 'refunded']),
|
||||
payment_status: z.enum(['pending', 'paid', 'failed', 'refunded']).default('pending'),
|
||||
cycle: z.enum(['onetime', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly']),
|
||||
service_id: z.string().optional(),
|
||||
cluster_id: z.string().optional(),
|
||||
instance_id: z.string().optional(),
|
||||
service_name: z.string().optional(),
|
||||
service_config: z.record(z.string(), z.any()).optional(),
|
||||
billing_period_start: z.date().optional(),
|
||||
billing_period_end: z.date().optional(),
|
||||
next_billing_date: z.date().optional(),
|
||||
auto_renew: z.boolean().default(false),
|
||||
discount_applied: z.number().min(0).default(0),
|
||||
tax_amount: z.number().min(0).default(0),
|
||||
total_amount: z.number().min(0),
|
||||
transaction_id: z.string().optional(),
|
||||
invoice_id: z.string().optional(),
|
||||
remarks: z.string().optional(),
|
||||
metadata: z.record(z.string(), z.any()).optional(),
|
||||
service_status: z.number().min(0).max(2).default(0), // 0: inactive, 1: active, 2: suspended
|
||||
created_by: z.string().optional(),
|
||||
updated_by: z.string().optional(),
|
||||
})
|
||||
|
||||
export type BillingType = z.infer<typeof BillingSchema>
|
||||
|
||||
// Mongoose interface
|
||||
export interface IBilling extends Document {
|
||||
billing_id: string
|
||||
service: string
|
||||
service_type:
|
||||
| 'vps'
|
||||
| 'kubernetes'
|
||||
| 'developer_hire'
|
||||
| 'vpn'
|
||||
| 'hosting'
|
||||
| 'storage'
|
||||
| 'database'
|
||||
| 'ai_service'
|
||||
| 'custom'
|
||||
amount: number
|
||||
currency: 'INR' | 'USD'
|
||||
user_email: string
|
||||
silicon_id: string
|
||||
user_id: string
|
||||
status: 'pending' | 'active' | 'completed' | 'cancelled' | 'failed' | 'refunded'
|
||||
payment_status: 'pending' | 'paid' | 'failed' | 'refunded'
|
||||
cycle: 'onetime' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'
|
||||
service_id?: string
|
||||
cluster_id?: string
|
||||
instance_id?: string
|
||||
service_name?: string
|
||||
service_config?: Record<string, any>
|
||||
billing_period_start?: Date
|
||||
billing_period_end?: Date
|
||||
next_billing_date?: Date
|
||||
auto_renew: boolean
|
||||
discount_applied: number
|
||||
tax_amount: number
|
||||
total_amount: number
|
||||
transaction_id?: string
|
||||
invoice_id?: string
|
||||
remarks?: string
|
||||
metadata?: Record<string, any>
|
||||
service_status: number
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
const billingSchema: Schema = new Schema<IBilling>(
|
||||
{
|
||||
billing_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
service: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
service_type: {
|
||||
type: String,
|
||||
enum: [
|
||||
'vps',
|
||||
'kubernetes',
|
||||
'developer_hire',
|
||||
'vpn',
|
||||
'hosting',
|
||||
'storage',
|
||||
'database',
|
||||
'ai_service',
|
||||
'custom',
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
currency: {
|
||||
type: String,
|
||||
enum: ['INR', 'USD'],
|
||||
default: 'INR',
|
||||
},
|
||||
user_email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
silicon_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
user_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'active', 'completed', 'cancelled', 'failed', 'refunded'],
|
||||
default: 'pending',
|
||||
},
|
||||
payment_status: {
|
||||
type: String,
|
||||
enum: ['pending', 'paid', 'failed', 'refunded'],
|
||||
default: 'pending',
|
||||
},
|
||||
cycle: {
|
||||
type: String,
|
||||
enum: ['onetime', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
|
||||
required: true,
|
||||
},
|
||||
service_id: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
cluster_id: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
instance_id: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
service_name: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
service_config: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
billing_period_start: {
|
||||
type: Date,
|
||||
},
|
||||
billing_period_end: {
|
||||
type: Date,
|
||||
},
|
||||
next_billing_date: {
|
||||
type: Date,
|
||||
},
|
||||
auto_renew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
discount_applied: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
tax_amount: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
total_amount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
transaction_id: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
invoice_id: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
remarks: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
metadata: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
service_status: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 2,
|
||||
default: 0,
|
||||
},
|
||||
created_by: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
updated_by: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Indexes for performance
|
||||
billingSchema.index({ user_email: 1, createdAt: -1 })
|
||||
billingSchema.index({ silicon_id: 1, createdAt: -1 })
|
||||
billingSchema.index({ user_id: 1, createdAt: -1 })
|
||||
billingSchema.index({ service_type: 1, status: 1 })
|
||||
billingSchema.index({ payment_status: 1 })
|
||||
billingSchema.index({ next_billing_date: 1, auto_renew: 1 })
|
||||
billingSchema.index({ service_id: 1 })
|
||||
billingSchema.index({ cluster_id: 1 })
|
||||
billingSchema.index({ instance_id: 1 })
|
||||
|
||||
// Pre-save middleware to calculate total_amount
|
||||
billingSchema.pre('save', function (next) {
|
||||
if (
|
||||
this.isModified('amount') ||
|
||||
this.isModified('tax_amount') ||
|
||||
this.isModified('discount_applied')
|
||||
) {
|
||||
const amount = Number(this.amount) || 0
|
||||
const taxAmount = Number(this.tax_amount) || 0
|
||||
const discountApplied = Number(this.discount_applied) || 0
|
||||
this.total_amount = amount + taxAmount - discountApplied
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// Transform method to format response
|
||||
billingSchema.methods.toJSON = function () {
|
||||
const billing = this.toObject()
|
||||
return billing
|
||||
}
|
||||
|
||||
// Static methods for common queries
|
||||
billingSchema.statics.findByUser = function (userEmail: string, siliconId: string) {
|
||||
return this.find({
|
||||
$or: [{ user_email: userEmail }, { silicon_id: siliconId }],
|
||||
}).sort({ createdAt: -1 })
|
||||
}
|
||||
|
||||
billingSchema.statics.findActiveServices = function (userEmail: string, siliconId: string) {
|
||||
return this.find({
|
||||
$or: [{ user_email: userEmail }, { silicon_id: siliconId }],
|
||||
status: 'active',
|
||||
service_status: 1,
|
||||
}).sort({ createdAt: -1 })
|
||||
}
|
||||
|
||||
billingSchema.statics.findByServiceType = function (
|
||||
serviceType: string,
|
||||
userEmail?: string,
|
||||
siliconId?: string
|
||||
) {
|
||||
const query: any = { service_type: serviceType }
|
||||
if (userEmail || siliconId) {
|
||||
query.$or = []
|
||||
if (userEmail) query.$or.push({ user_email: userEmail })
|
||||
if (siliconId) query.$or.push({ silicon_id: siliconId })
|
||||
}
|
||||
return this.find(query).sort({ createdAt: -1 })
|
||||
}
|
||||
|
||||
// Check if model already exists before compiling it
|
||||
export const Billing =
|
||||
(mongoose.models?.Billing as mongoose.Model<IBilling>) ||
|
||||
mongoose.model<IBilling>('Billing', billingSchema)
|
||||
114
models/developer-request.ts
Normal file
114
models/developer-request.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose'
|
||||
|
||||
export interface IDeveloperRequest extends Document {
|
||||
userId: mongoose.Types.ObjectId
|
||||
planId: string
|
||||
planName: string
|
||||
planPrice: number
|
||||
requirements: string
|
||||
contactInfo: {
|
||||
name: string
|
||||
email: string
|
||||
phone?: string
|
||||
}
|
||||
status: 'pending' | 'approved' | 'in_progress' | 'completed' | 'cancelled'
|
||||
paymentStatus: 'pending' | 'paid' | 'failed'
|
||||
transactionId?: mongoose.Types.ObjectId
|
||||
assignedDeveloper?: {
|
||||
name: string
|
||||
email: string
|
||||
skills: string[]
|
||||
}
|
||||
estimatedStartDate?: Date
|
||||
estimatedCompletionDate?: Date
|
||||
actualStartDate?: Date
|
||||
actualCompletionDate?: Date
|
||||
notes?: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
const DeveloperRequestSchema = new Schema<IDeveloperRequest>(
|
||||
{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
planId: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['hourly', 'daily', 'monthly'],
|
||||
},
|
||||
planName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
planPrice: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
requirements: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 10,
|
||||
maxlength: 5000,
|
||||
},
|
||||
contactInfo: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'approved', 'in_progress', 'completed', 'cancelled'],
|
||||
default: 'pending',
|
||||
},
|
||||
paymentStatus: {
|
||||
type: String,
|
||||
enum: ['pending', 'paid', 'failed'],
|
||||
default: 'pending',
|
||||
},
|
||||
transactionId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Transaction',
|
||||
},
|
||||
assignedDeveloper: {
|
||||
name: String,
|
||||
email: String,
|
||||
skills: [String],
|
||||
},
|
||||
estimatedStartDate: Date,
|
||||
estimatedCompletionDate: Date,
|
||||
actualStartDate: Date,
|
||||
actualCompletionDate: Date,
|
||||
notes: {
|
||||
type: String,
|
||||
maxlength: 2000,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Indexes for better query performance
|
||||
DeveloperRequestSchema.index({ userId: 1, createdAt: -1 })
|
||||
DeveloperRequestSchema.index({ status: 1 })
|
||||
DeveloperRequestSchema.index({ paymentStatus: 1 })
|
||||
|
||||
export const DeveloperRequest =
|
||||
mongoose.models.DeveloperRequest ||
|
||||
mongoose.model<IDeveloperRequest>('DeveloperRequest', DeveloperRequestSchema)
|
||||
45
models/system-settings.ts
Normal file
45
models/system-settings.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import mongoose, { Schema, Document } from 'mongoose'
|
||||
|
||||
export interface ISystemSettings extends Document {
|
||||
maintenanceMode: boolean
|
||||
registrationEnabled: boolean
|
||||
emailVerificationRequired: boolean
|
||||
maxUserBalance: number
|
||||
defaultUserRole: 'user' | 'admin'
|
||||
systemMessage: string
|
||||
paymentGatewayEnabled: boolean
|
||||
developerHireEnabled: boolean
|
||||
vpsDeploymentEnabled: boolean
|
||||
kubernetesDeploymentEnabled: boolean
|
||||
vpnServiceEnabled: boolean
|
||||
lastUpdated: Date
|
||||
updatedBy: string
|
||||
}
|
||||
|
||||
const SystemSettingsSchema = new Schema<ISystemSettings>(
|
||||
{
|
||||
maintenanceMode: { type: Boolean, default: false },
|
||||
registrationEnabled: { type: Boolean, default: true },
|
||||
emailVerificationRequired: { type: Boolean, default: true },
|
||||
maxUserBalance: { type: Number, default: 1000000 },
|
||||
defaultUserRole: { type: String, enum: ['user', 'admin'], default: 'user' },
|
||||
systemMessage: { type: String, default: '' },
|
||||
paymentGatewayEnabled: { type: Boolean, default: true },
|
||||
developerHireEnabled: { type: Boolean, default: true },
|
||||
vpsDeploymentEnabled: { type: Boolean, default: true },
|
||||
kubernetesDeploymentEnabled: { type: Boolean, default: true },
|
||||
vpnServiceEnabled: { type: Boolean, default: true },
|
||||
lastUpdated: { type: Date, default: Date.now },
|
||||
updatedBy: { type: String, default: 'System' },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Ensure only one settings document exists
|
||||
SystemSettingsSchema.index({}, { unique: true })
|
||||
|
||||
export const SystemSettings =
|
||||
mongoose.models.SystemSettings ||
|
||||
mongoose.model<ISystemSettings>('SystemSettings', SystemSettingsSchema)
|
||||
222
models/topic.ts
Normal file
222
models/topic.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import mongoose, { Schema, models, Document, Model } from 'mongoose'
|
||||
import { z } from 'zod'
|
||||
import readingTime from 'reading-time'
|
||||
|
||||
// Define tag schema
|
||||
const tagSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
})
|
||||
|
||||
// Zod schema for topic validation and transformation
|
||||
export const topicSchema = z.object({
|
||||
id: z.string(),
|
||||
publishedAt: z.number(),
|
||||
title: z.string(),
|
||||
excerpt: z.string(),
|
||||
coverImage: z.string(),
|
||||
coverImageKey: z.string().optional(),
|
||||
author: z.string(),
|
||||
authorId: z.string(), // NEW: User ownership field
|
||||
slug: z.string(),
|
||||
content: z.string(),
|
||||
contentRTE: z.unknown(),
|
||||
contentImages: z.array(z.string()).optional(),
|
||||
tags: z.array(tagSchema),
|
||||
featured: z.boolean(),
|
||||
isDraft: z.boolean().optional(),
|
||||
views: z.number().optional().default(0), // NEW: View tracking
|
||||
viewHistory: z.array(z.object({
|
||||
date: z.string(),
|
||||
count: z.number()
|
||||
})).optional().default([]), // NEW: Daily analytics
|
||||
readingTime: z.object({
|
||||
text: z.string(),
|
||||
minutes: z.number(),
|
||||
time: z.number(),
|
||||
words: z.number(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
// Type inference from Zod schema
|
||||
export type ITopic = z.infer<typeof topicSchema>
|
||||
|
||||
// Type for Mongoose document with ITopic
|
||||
type TopicDocument = Document & ITopic
|
||||
|
||||
// Type for Mongoose model
|
||||
type TopicModel = Model<ITopic>
|
||||
|
||||
// Generic utility function to transform and validate Mongoose document to any type
|
||||
export const transformToType = <T>(
|
||||
doc: Document | Record<string, unknown> | null,
|
||||
schema: z.ZodType<T>
|
||||
): T | null => {
|
||||
if (!doc) return null
|
||||
|
||||
// Handle both Mongoose documents and plain objects
|
||||
const data = 'toObject' in doc && typeof doc.toObject === 'function' ? doc.toObject() : doc
|
||||
return schema.parse(data)
|
||||
}
|
||||
|
||||
// Utility function to transform and validate Mongoose document to ITopic
|
||||
export const transformToTopic = (doc: Document | Record<string, unknown> | null): ITopic | null => {
|
||||
return transformToType(doc, topicSchema)
|
||||
}
|
||||
|
||||
// Utility function to transform and validate multiple Mongoose documents to ITopic array
|
||||
export const transformToTopics = (docs: (TopicDocument | Record<string, unknown>)[]): ITopic[] => {
|
||||
return docs
|
||||
.map((doc) => transformToType(doc, topicSchema))
|
||||
.filter((topic): topic is ITopic => topic !== null)
|
||||
}
|
||||
|
||||
const TopicSchema = new Schema<ITopic>(
|
||||
{
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
publishedAt: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
coverImage: {
|
||||
type: String,
|
||||
required: [true, 'Please provide a cover image for this topic'],
|
||||
},
|
||||
coverImageKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: [true, 'Please provide a title for this topic'],
|
||||
maxlength: [100, 'Title cannot be more than 100 characters'],
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
required: [true, 'Please provide an author for this topic'],
|
||||
},
|
||||
authorId: {
|
||||
type: String,
|
||||
required: [true, 'Please provide an author ID for this topic'],
|
||||
index: true, // NEW: Index for efficient queries by user
|
||||
},
|
||||
featured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDraft: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
excerpt: {
|
||||
type: String,
|
||||
required: [true, 'Please provide a mini description for this topic'],
|
||||
maxlength: [200, 'Mini description cannot be more than 200 characters'],
|
||||
},
|
||||
tags: {
|
||||
type: [
|
||||
{
|
||||
id: { type: String },
|
||||
name: { type: String, required: true },
|
||||
},
|
||||
],
|
||||
required: [true, 'Please provide tags for this topic'],
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true, // This allows the field to be unique only if it exists
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: [true, 'Please provide content for this topic'],
|
||||
},
|
||||
contentRTE: {
|
||||
type: Schema.Types.Mixed,
|
||||
default: [],
|
||||
},
|
||||
contentImages: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
readingTime: {
|
||||
text: { type: String },
|
||||
minutes: { type: Number },
|
||||
time: { type: Number },
|
||||
words: { type: Number },
|
||||
},
|
||||
views: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
index: true, // Index for efficient sorting by popularity
|
||||
},
|
||||
viewHistory: [{
|
||||
date: { type: String, required: true }, // YYYY-MM-DD format
|
||||
count: { type: Number, required: true, default: 1 },
|
||||
_id: false, // Disable automatic _id generation for subdocuments
|
||||
}],
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
minimize: false,
|
||||
}
|
||||
)
|
||||
|
||||
// Auto-generate slug from title if not provided
|
||||
TopicSchema.pre('save', async function (next) {
|
||||
if (!this.slug && this.title) {
|
||||
// Create base slug from title
|
||||
const baseSlug = this.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace any sequence of non-alphanumeric chars with a single dash
|
||||
.replace(/^-|-$/g, '') // Remove leading and trailing dashes
|
||||
|
||||
// First try using the base slug
|
||||
let slug = baseSlug
|
||||
|
||||
// Check if an article with this slug already exists
|
||||
const TopicModel = this.constructor as TopicModel
|
||||
const existingTopic = await TopicModel.findOne({ slug: slug })
|
||||
|
||||
// If the slug already exists, append a timestamp to make it unique
|
||||
if (existingTopic) {
|
||||
// Generate a timestamp suffix (last 6 digits for brevity)
|
||||
const timestamp = Date.now().toString().slice(-6)
|
||||
slug = `${baseSlug}-${timestamp}`
|
||||
}
|
||||
|
||||
this.slug = slug
|
||||
}
|
||||
|
||||
// Auto-calculate reading time from content
|
||||
if (this.content && this.isModified('content')) {
|
||||
const stats = readingTime(this.content)
|
||||
this.readingTime = {
|
||||
text: stats.text,
|
||||
minutes: Math.ceil(stats.minutes),
|
||||
time: stats.time,
|
||||
words: stats.words,
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// Index for efficient queries by user ownership
|
||||
TopicSchema.index({ authorId: 1, publishedAt: -1 })
|
||||
TopicSchema.index({ publishedAt: -1 }) // For public topic listing
|
||||
TopicSchema.index({ slug: 1 }) // For individual topic lookup
|
||||
TopicSchema.index({ 'tags.name': 1 }) // For tag-based filtering
|
||||
TopicSchema.index({ isDraft: 1, authorId: 1 }) // For user's draft management
|
||||
TopicSchema.index({ views: -1 }) // For sorting by popularity
|
||||
TopicSchema.index({ authorId: 1, views: -1 }) // For user's most viewed posts
|
||||
|
||||
// Check if the model exists before creating a new one
|
||||
// This prevents "Cannot overwrite model once compiled" errors
|
||||
const TopicModel = (models.topics as TopicModel) || mongoose.model<ITopic>('topics', TopicSchema)
|
||||
|
||||
export default TopicModel
|
||||
121
models/transaction.ts
Normal file
121
models/transaction.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import mongoose, { Document, Model } from 'mongoose'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Zod schema for validation
|
||||
export const TransactionSchema = z.object({
|
||||
transactionId: z.string().min(1, 'Transaction ID is required'),
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
email: z.string().email('Invalid email format'),
|
||||
type: z.enum(['debit', 'credit']),
|
||||
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(),
|
||||
status: z.enum(['pending', 'completed', 'failed']).default('completed'),
|
||||
previousBalance: z.number().min(0, 'Previous balance cannot be negative'),
|
||||
newBalance: z.number().min(0, 'New balance cannot be negative'),
|
||||
metadata: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
|
||||
export type TransactionType = z.infer<typeof TransactionSchema>
|
||||
|
||||
// Mongoose interface
|
||||
export interface ITransaction extends Document {
|
||||
transactionId: string
|
||||
userId: string
|
||||
email: string
|
||||
type: 'debit' | 'credit'
|
||||
amount: number
|
||||
service: string
|
||||
serviceId?: string
|
||||
description?: string
|
||||
status: 'pending' | 'completed' | 'failed'
|
||||
previousBalance: number
|
||||
newBalance: number
|
||||
metadata?: Record<string, any>
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
// Mongoose schema
|
||||
const transactionSchema = new mongoose.Schema<ITransaction>(
|
||||
{
|
||||
transactionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['debit', 'credit'],
|
||||
required: true,
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
service: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
serviceId: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'completed', 'failed'],
|
||||
default: 'completed',
|
||||
},
|
||||
previousBalance: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
newBalance: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
metadata: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Indexes for performance
|
||||
transactionSchema.index({ userId: 1, createdAt: -1 })
|
||||
transactionSchema.index({ email: 1, createdAt: -1 })
|
||||
transactionSchema.index({ service: 1, createdAt: -1 })
|
||||
transactionSchema.index({ type: 1, createdAt: -1 })
|
||||
|
||||
// Transform method to format response
|
||||
transactionSchema.methods.toJSON = function () {
|
||||
const transaction = this.toObject()
|
||||
return transaction
|
||||
}
|
||||
|
||||
export const Transaction: Model<ITransaction> =
|
||||
mongoose.models.Transaction || mongoose.model<ITransaction>('Transaction', transactionSchema)
|
||||
125
models/user.ts
Normal file
125
models/user.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import mongoose, { Document, Model } from 'mongoose'
|
||||
import { z } from 'zod'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
// Zod schema for validation
|
||||
export const UserSchema = z.object({
|
||||
email: z.string().email('Invalid email format'),
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
siliconId: z.string().optional(),
|
||||
role: z.enum(['user', 'admin']).optional().default('user'),
|
||||
isVerified: z.boolean().optional().default(false),
|
||||
provider: z.string().optional(),
|
||||
providerId: z.string().optional(),
|
||||
avatar: z.string().url().optional(),
|
||||
})
|
||||
|
||||
export type UserType = z.infer<typeof UserSchema>
|
||||
|
||||
// Mongoose interface
|
||||
export interface IUser extends Document {
|
||||
email: string
|
||||
name: string
|
||||
password?: string
|
||||
siliconId: string
|
||||
role: 'user' | 'admin'
|
||||
isVerified: boolean
|
||||
provider: 'local' | 'google' | 'github'
|
||||
providerId?: string
|
||||
avatar?: string
|
||||
refreshToken?: string
|
||||
balance?: number
|
||||
lastLogin: Date
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
comparePassword(candidatePassword: string): Promise<boolean>
|
||||
}
|
||||
|
||||
// Mongoose schema
|
||||
const userSchema = new mongoose.Schema<IUser>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
siliconId: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: function (this: any) {
|
||||
return !this.provider // Password required only if not using OAuth
|
||||
},
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['user', 'admin'],
|
||||
default: 'user',
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
enum: ['local', 'google', 'github'],
|
||||
default: 'local',
|
||||
},
|
||||
providerId: String,
|
||||
avatar: String,
|
||||
refreshToken: String,
|
||||
lastLogin: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Index for performance
|
||||
userSchema.index({ email: 1 }, { unique: true })
|
||||
userSchema.index({ siliconId: 1 }, { unique: true })
|
||||
userSchema.index({ providerId: 1, provider: 1 })
|
||||
|
||||
// Hash password before saving
|
||||
userSchema.pre('save', async function (next) {
|
||||
if (!this.isModified('password')) return next()
|
||||
|
||||
if (this.password) {
|
||||
this.password = await bcrypt.hash(this.password, 12)
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// Compare password method
|
||||
userSchema.methods.comparePassword = async function (candidatePassword: string): Promise<boolean> {
|
||||
if (!this.password) return false
|
||||
return bcrypt.compare(candidatePassword, this.password)
|
||||
}
|
||||
|
||||
// Transform method to remove sensitive fields
|
||||
userSchema.methods.toJSON = function () {
|
||||
const user = this.toObject()
|
||||
delete user.password
|
||||
delete user.refreshToken
|
||||
return user
|
||||
}
|
||||
|
||||
export const User: Model<IUser> = mongoose.models.User || mongoose.model<IUser>('User', userSchema)
|
||||
Reference in New Issue
Block a user