diff --git a/.gitignore b/.gitignore index 1f03032..efe7f68 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ android/key.properties .idea/ *.keystore android/app/google-services.json +web/.env +api/.env +.env diff --git a/api/package.json b/api/package.json index 84e21bc..94ef734 100644 --- a/api/package.json +++ b/api/package.json @@ -17,7 +17,10 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "seed": "node dist/seed.js", + "seed:build": "npm run build && npm run seed", + "seed:docker": "echo '๐ŸŒฑ Starting database seeding...' && node dist/seed.js && echo 'โœ… Database seeding completed!'" }, "dependencies": { "@nest-modules/mailer": "^1.3.22", diff --git a/api/scripts/fix-database.js b/api/scripts/fix-database.js new file mode 100644 index 0000000..86bc799 --- /dev/null +++ b/api/scripts/fix-database.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +const { NestFactory } = require('@nestjs/core'); +const { AppModule } = require('../dist/app.module'); +const { getConnectionToken } = require('@nestjs/mongoose'); + +async function fixDatabase() { + console.log('๐Ÿ”ง Fixing database schema issues...'); + + try { + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); + + const connection = app.get(getConnectionToken()); + const db = connection.db; + + console.log('๐Ÿ“‹ Checking current plans...'); + const plans = await db.collection('plans').find({}).toArray(); + console.log('Current plans:', plans.map(p => ({ name: p.name, polarProductId: p.polarProductId }))); + + console.log('๐Ÿงน Removing polarProductId from existing plans...'); + await db.collection('plans').updateMany( + { name: { $in: ['free', 'mega'] } }, + { $unset: { polarProductId: "" } } + ); + console.log('โœ… Removed polarProductId from basic plans'); + + console.log('๐Ÿ—‘๏ธ Dropping unique index on polarProductId...'); + try { + await db.collection('plans').dropIndex('polarProductId_1'); + console.log('โœ… Unique index dropped successfully'); + } catch (error) { + if (error.code === 27) { + console.log('โ„น๏ธ Index does not exist, continuing...'); + } else { + console.log('โš ๏ธ Error dropping index:', error.message); + } + } + + console.log('๐Ÿ†” Creating new sparse index on polarProductId...'); + try { + await db.collection('plans').createIndex( + { polarProductId: 1 }, + { + unique: true, + sparse: true, // This allows multiple null/undefined values + name: 'polarProductId_sparse_1' + } + ); + console.log('โœ… New sparse index created'); + } catch (error) { + console.log('โš ๏ธ Index creation error (may already exist):', error.message); + } + + console.log('๐Ÿงน Cleaning up any duplicate plans...'); + const megaPlans = await db.collection('plans').find({ name: 'mega' }).toArray(); + if (megaPlans.length > 1) { + console.log(`Found ${megaPlans.length} mega plans, keeping the first one...`); + for (let i = 1; i < megaPlans.length; i++) { + await db.collection('plans').deleteOne({ _id: megaPlans[i]._id }); + } + } + + console.log('๐Ÿ“‹ Final plans check...'); + const finalPlans = await db.collection('plans').find({}).toArray(); + console.log('Final plans:', finalPlans.map(p => ({ name: p.name, polarProductId: p.polarProductId }))); + + console.log('โœ… Database schema fixed successfully!'); + await app.close(); + process.exit(0); + } catch (error) { + console.error('โŒ Database fix failed:', error); + process.exit(1); + } +} + +fixDatabase(); \ No newline at end of file diff --git a/api/scripts/seed-database.js b/api/scripts/seed-database.js new file mode 100644 index 0000000..81c14f9 --- /dev/null +++ b/api/scripts/seed-database.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +const { NestFactory } = require('@nestjs/core'); +const { AppModule } = require('../dist/app.module'); +const { AdminSeed } = require('../dist/seeds/admin.seed'); + +async function bootstrap() { + console.log('๐ŸŒฑ Starting database seeding...'); + + try { + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); + + console.log('๐Ÿ“ฆ Application created, getting seeder...'); + const seeder = app.get(AdminSeed); + + console.log('๐Ÿš€ Running seed process...'); + await seeder.seed(); + + console.log('โœ… Database seeding completed successfully!'); + await app.close(); + process.exit(0); + } catch (error) { + console.error('โŒ Database seeding failed:', error); + process.exit(1); + } +} + +bootstrap(); \ No newline at end of file diff --git a/api/scripts/seed-database.ts b/api/scripts/seed-database.ts new file mode 100644 index 0000000..34f651d --- /dev/null +++ b/api/scripts/seed-database.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import { NestFactory } from '@nestjs/core'; +import { AppModule } from '../src/app.module'; +import { AdminSeed } from '../src/seeds/admin.seed'; + +async function bootstrap() { + console.log('๐ŸŒฑ Starting database seeding...'); + + try { + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); + + console.log('๐Ÿ“ฆ Application created, getting seeder...'); + const seeder = app.get(AdminSeed); + + console.log('๐Ÿš€ Running seed process...'); + await seeder.seed(); + + console.log('โœ… Database seeding completed successfully!'); + await app.close(); + process.exit(0); + } catch (error) { + console.error('โŒ Database seeding failed:', error); + process.exit(1); + } +} + +bootstrap(); \ No newline at end of file diff --git a/api/scripts/seed.sh b/api/scripts/seed.sh new file mode 100755 index 0000000..9198de0 --- /dev/null +++ b/api/scripts/seed.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "๐ŸŒฑ TextBee Database Seeding Script" +echo "==================================" + +# Check if we're in the right directory +if [ ! -f "package.json" ]; then + echo "โŒ Error: package.json not found. Please run this script from the API directory." + exit 1 +fi + +# Check if dist directory exists (compiled TypeScript) +if [ ! -d "dist" ]; then + echo "๐Ÿ“ฆ Building the application..." + npm run build + if [ $? -ne 0 ]; then + echo "โŒ Build failed. Please check your code for errors." + exit 1 + fi +fi + +echo "๐Ÿš€ Starting database seeding..." + +# Run the seeding script +node dist/seed.js + +if [ $? -eq 0 ]; then + echo "โœ… Database seeding completed successfully!" + echo "" + echo "๐Ÿ“‹ What was created:" + echo " โ€ข Admin user: ${ADMIN_EMAIL:-admin@example.com}" + echo " โ€ข Free plan: 10 daily, 100 monthly messages" + echo " โ€ข Mega plan: Unlimited messages" + echo " โ€ข Admin user assigned to Mega plan" +else + echo "โŒ Database seeding failed. Check the logs above for details." + exit 1 +fi \ No newline at end of file diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 722e158..6721ad6 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -19,6 +19,7 @@ import { BillingModule } from './billing/billing.module' import { ConfigModule, ConfigService } from '@nestjs/config' import { BullModule } from '@nestjs/bull' import { SupportModule } from './support/support.module' +import { SeedModule } from './seeds/seed.module' @Injectable() export class LoggerMiddleware implements NestMiddleware { @@ -58,6 +59,7 @@ export class LoggerMiddleware implements NestMiddleware { WebhookModule, BillingModule, SupportModule, + SeedModule, ], controllers: [], providers: [ diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts index d4fbb8f..670de9b 100644 --- a/api/src/auth/auth.controller.ts +++ b/api/src/auth/auth.controller.ts @@ -43,12 +43,12 @@ export class AuthController { return { data } } - @ApiOperation({ summary: 'Register' }) - @Post('/register') - async register(@Body() input: RegisterInputDTO) { - const data = await this.authService.register(input) - return { data } - } + // @ApiOperation({ summary: 'Register' }) + // @Post('/register') + // async register(@Body() input: RegisterInputDTO) { + // const data = await this.authService.register(input) + // return { data } + // } @ApiOperation({ summary: 'Get current logged in user' }) @ApiBearerAuth() diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts index c1f804f..cbfd41b 100644 --- a/api/src/billing/billing.service.ts +++ b/api/src/billing/billing.service.ts @@ -199,7 +199,8 @@ export class BillingService { const plans = await this.planModel.find() const customPlans = plans.filter((plan) => plan.name?.startsWith('custom')) - const proPlan = plans.find((plan) => plan.name === 'pro') + console.log('plans', plans); + const megaPlan = plans.find((plan) => plan.name === 'mega') const freePlan = plans.find((plan) => plan.name === 'free') const customPlanSubscription = await this.subscriptionModel.findOne({ @@ -212,14 +213,14 @@ export class BillingService { return customPlanSubscription.populate('plan') } - const proPlanSubscription = await this.subscriptionModel.findOne({ + const megaPlanSubscription = await this.subscriptionModel.findOne({ user: user._id, - plan: proPlan._id, + plan: megaPlan._id, isActive: true, }) - if (proPlanSubscription) { - return proPlanSubscription.populate('plan') + if (megaPlanSubscription) { + return megaPlanSubscription.populate('plan') } const freePlanSubscription = await this.subscriptionModel.findOne({ @@ -562,4 +563,4 @@ export class BillingService { productName, }) } -} +} \ No newline at end of file diff --git a/api/src/billing/schemas/plan.schema.ts b/api/src/billing/schemas/plan.schema.ts index e91cbe9..50f04b9 100644 --- a/api/src/billing/schemas/plan.schema.ts +++ b/api/src/billing/schemas/plan.schema.ts @@ -23,13 +23,13 @@ export class Plan { @Prop({}) yearlyPrice: number // in cents - @Prop({ type: String, unique: true }) + @Prop({ type: String }) polarProductId?: string - @Prop({ type: String, unique: true }) + @Prop({ type: String }) polarMonthlyProductId?: string - @Prop({ type: String, unique: true }) + @Prop({ type: String }) polarYearlyProductId?: string @Prop({ type: Boolean, default: true }) diff --git a/api/src/gateway/gateway.controller.ts b/api/src/gateway/gateway.controller.ts index f052519..d7ee714 100644 --- a/api/src/gateway/gateway.controller.ts +++ b/api/src/gateway/gateway.controller.ts @@ -47,6 +47,7 @@ export class GatewayController { @ApiOperation({ summary: 'Register device' }) @Post('/devices') async registerDevice(@Body() input: RegisterDeviceInputDTO, @Request() req) { + console.log('Hello World 2') const data = await this.gatewayService.registerDevice(input, req.user) return { data } } @@ -55,6 +56,7 @@ export class GatewayController { @ApiOperation({ summary: 'List of registered devices' }) @Get('/devices') async getDevices(@Request() req) { + console.log('Hello World 1') const data = await this.gatewayService.getDevicesForUser(req.user) return { data } } @@ -66,6 +68,7 @@ export class GatewayController { @Param('id') deviceId: string, @Body() input: RegisterDeviceInputDTO, ) { + console.log('Hello World') const data = await this.gatewayService.updateDevice(deviceId, input) return { data } } diff --git a/api/src/main.ts b/api/src/main.ts index 2ef2cc4..c3b51f5 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -5,7 +5,8 @@ import { AppModule } from './app.module' import * as firebase from 'firebase-admin' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' import * as express from 'express' -import { NestExpressApplication } from '@nestjs/platform-express' +import { NestExpressApplication } from '@nestjs/platform-express'; + async function bootstrap() { const app: NestExpressApplication = await NestFactory.create(AppModule) @@ -58,6 +59,7 @@ async function bootstrap() { ) app.useBodyParser('json', { limit: '2mb' }); app.enableCors() + await app.listen(PORT) } -bootstrap() +bootstrap() \ No newline at end of file diff --git a/api/src/seed.ts b/api/src/seed.ts new file mode 100644 index 0000000..5531015 --- /dev/null +++ b/api/src/seed.ts @@ -0,0 +1,12 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { AdminSeed } from './seeds/admin.seed'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const seeder = app.get(AdminSeed); + await seeder.seed(); + await app.close(); +} + +bootstrap(); diff --git a/api/src/seeds/admin.seed.ts b/api/src/seeds/admin.seed.ts new file mode 100644 index 0000000..ff7d36a --- /dev/null +++ b/api/src/seeds/admin.seed.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { UsersService } from '../users/users.service'; +import { UserRole } from '../users/user-roles.enum'; +import * as bcrypt from 'bcryptjs'; +import { AuthService } from '../auth/auth.service'; +import { BillingService } from '../billing/billing.service'; +import { InjectModel } from '@nestjs/mongoose'; +import { Plan, PlanDocument } from '../billing/schemas/plan.schema'; +import { Model } from 'mongoose'; +import { PlanSeed } from './plan.seed'; + +@Injectable() +export class AdminSeed { + constructor( + private readonly usersService: UsersService, + private readonly authService: AuthService, + private readonly billingService: BillingService, + @InjectModel(Plan.name) private planModel: Model, + private readonly planSeed: PlanSeed, + ) {} + + async seed() { + await this.planSeed.seed(); + const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com'; + let adminUser = await this.usersService.findOne({ email: adminEmail }); + + if (!adminUser) { + const adminPassword = process.env.ADMIN_PASSWORD || 'password'; + const adminName = 'Admin'; + + // Register the user using AuthService.register + const { user } = await this.authService.register({ + name: adminName, + email: adminEmail, + password: adminPassword, + }); + + // Assign ADMIN role + user.role = UserRole.ADMIN; + adminUser = await user.save(); + + console.log('Admin user created successfully.'); + } + + // Check if the user has an active subscription + const subscription = await this.billingService.getActiveSubscription(adminUser._id.toString()); + if (subscription && subscription.plan.name !== 'free') { + return; + } + + // Assign the best plan + const bestPlan = await this.planModel.findOne({ name: 'mega' }); + if (bestPlan) { + await this.billingService.switchPlan({ + userId: adminUser._id.toString(), + newPlanName: bestPlan.name, + status: 'active', + amount: bestPlan.monthlyPrice, + }); + console.log(`Admin user subscribed to ${bestPlan.name} plan.`); + } else { + console.warn('No unlimited plan found to assign to admin user. Defaulting to free plan.'); + } + } +} \ No newline at end of file diff --git a/api/src/seeds/plan.seed.ts b/api/src/seeds/plan.seed.ts new file mode 100644 index 0000000..51c7d4f --- /dev/null +++ b/api/src/seeds/plan.seed.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Plan, PlanDocument } from '../billing/schemas/plan.schema'; + +@Injectable() +export class PlanSeed { + constructor( + @InjectModel(Plan.name) private planModel: Model, + ) {} + + async seed() { + // Check if free plan exists, if not create it + const existingFreePlan = await this.planModel.findOne({ name: 'free' }); + if (!existingFreePlan) { + await this.planModel.create({ + name: 'free', + dailyLimit: 10, + monthlyLimit: 100, + bulkSendLimit: 10, + isActive: true, + monthlyPrice: 0, + yearlyPrice: 0, + polarProductId: 'textbee-free-plan', + polarMonthlyProductId: 'free', + polarYearlyProductId: 'free' + }); + console.log('Free plan created successfully.'); + } else { + // Update existing free plan + await this.planModel.updateOne({ name: 'free' }, { + dailyLimit: 10, + monthlyLimit: 100, + bulkSendLimit: 10, + isActive: true, + monthlyPrice: 0, + yearlyPrice: 0, + polarProductId: 'textbee-free-plan', + polarMonthlyProductId: 'free', + polarYearlyProductId: 'free' + }); + console.log('Free plan updated successfully.'); + } + + // Check if mega plan exists, if not create it + const existingMegaPlan = await this.planModel.findOne({ name: 'mega' }); + if (!existingMegaPlan) { + console.log("Mega creating") + await this.planModel.create({ + name: 'mega', + dailyLimit: -1, // unlimited + monthlyLimit: -1, // unlimited + bulkSendLimit: -1, // unlimited + isActive: true, + monthlyPrice: 99900, // $999.00 + yearlyPrice: 999000, // $9990.00 + polarProductId: 'textbee-mega-plan', + polarMonthlyProductId: 'mega', + polarYearlyProductId: 'mega' + }); + console.log('Mega plan created successfully.'); + } else { + // Update existing mega plan + console.log("Mega updating") + await this.planModel.updateOne({ name: 'mega' }, { + dailyLimit: -1, + monthlyLimit: -1, + bulkSendLimit: -1, + isActive: true, + monthlyPrice: 99900, + yearlyPrice: 999000, + polarProductId: 'textbee-mega-plan', + polarMonthlyProductId: 'mega', + polarYearlyProductId: 'mega' + }); + console.log('Mega plan updated successfully.'); + } + } +} diff --git a/api/src/seeds/seed.module.ts b/api/src/seeds/seed.module.ts new file mode 100644 index 0000000..5172c6d --- /dev/null +++ b/api/src/seeds/seed.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { AdminSeed } from './admin.seed'; +import { UsersModule } from '../users/users.module'; +import { AuthModule } from '../auth/auth.module'; +import { BillingModule } from '../billing/billing.module'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Plan, PlanSchema } from '../billing/schemas/plan.schema'; +import { PlanSeed } from './plan.seed'; + +@Module({ + imports: [ + UsersModule, + AuthModule, + BillingModule, + MongooseModule.forFeature([ + { name: Plan.name, schema: PlanSchema }, + ]), + ], + providers: [AdminSeed, PlanSeed], + exports: [AdminSeed, PlanSeed], +}) +export class SeedModule {} \ No newline at end of file diff --git a/api/tsconfig.build.json b/api/tsconfig.build.json index 64f86c6..bdc6320 100644 --- a/api/tsconfig.build.json +++ b/api/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "include": ["src/**/*"] } diff --git a/api/tsconfig.json b/api/tsconfig.json index c512908..bdea8a9 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -18,5 +18,6 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false - } + }, + "include": ["src/**/*"] } diff --git a/docker-compose.yaml b/docker-compose.yaml index 084be20..7e2b626 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,8 +5,8 @@ services: image: mongo:latest restart: always environment: - - MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USER:-adminUser} - - MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASS:-adminPassword} + - MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USER} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASS} - MONGO_INITDB_DATABASE=textbee volumes: # - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro @@ -30,8 +30,8 @@ services: ports: - "${MONGO_EXPRESS_PORT:-8081}:8081" environment: - - ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_ROOT_USER:-adminUser} - - ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_ROOT_PASS:-adminPassword} + - ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_ROOT_USER} + - ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_ROOT_PASS} - ME_CONFIG_MONGODB_SERVER=textbee-db depends_on: textbee-db: @@ -48,12 +48,18 @@ services: dockerfile: Dockerfile restart: always ports: - - "${PORT:-3001}:3001" + - "${API_PORT}:4001" env_file: - ./api/.env environment: - - PORT=${PORT:-3001} - - REDIS_URL=${REDIS_URL:-redis://textbee-redis:6379} + - PORT=${API_PORT} + - REDIS_URL=redis://textbee-redis:6379 + healthcheck: + test: ["CMD", "ps", "aux", "| grep", "node"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s depends_on: textbee-db: @@ -70,12 +76,12 @@ services: dockerfile: Dockerfile restart: always ports: - - "${PORT:-3000}:3000" + - "${WEB_PORT}:3000" env_file: - ./web/.env environment: - - PORT=${PORT:-3000} - - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001/api/v1} + - PORT=3000 + - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} depends_on: - textbee-api diff --git a/web/lib/httpServerClient.ts b/web/lib/httpServerClient.ts index d4c6177..bc77e2c 100644 --- a/web/lib/httpServerClient.ts +++ b/web/lib/httpServerClient.ts @@ -9,7 +9,7 @@ const getServerSideBaseUrl = () => { // When running server-side in Docker, use the service name from docker-compose if (process.env.CONTAINER_RUNTIME === 'docker') { console.log('Running in Docker container') - return 'http://textbee-api:3001/api/v1' + return 'http://textbee-api:4001/api/v1' } // Otherwise use the public URL return process.env.NEXT_PUBLIC_API_BASE_URL || ''