commit 7219108342f69a2051d92b54bd6c40b77e885027 Author: Kar k1 Date: Sat Aug 30 18:18:57 2025 +0530 initial commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a661fea --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Bash(yarn:*)", + "Bash(mkdir:*)", + "Bash(ls:*)", + "Bash(touch:*)", + "Bash(NODE_ENV=test yarn build)", + "Bash(npx tsc:*)", + "Bash(yarn audit)", + "Bash(npm audit:*)", + "Bash(find:*)" + ], + "deny": [] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..57c863f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.next +Dockerfile +docker-compose* +.git +.gitignore +.env* +README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ed453a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,json,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..af3ad12 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..4d25b23 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,156 @@ +name: Deploy SiliconPin +on: + push: + branches: [ main ] # Change this to match your main branch name if different + workflow_dispatch: # Allows manual workflow runs + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Cache Yarn dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/yarn + node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Create .env file + run: | + echo "Creating .env file with secrets..." + echo "MONGODB_URI=${{ secrets.MONGODB_URI }}" > .env + echo "REDIS_URL=${{ secrets.REDIS_URL }}" >> .env + echo "SESSION_SECRET=${{ secrets.SESSION_SECRET }}" >> .env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env + echo "JWT_REFRESH_SECRET=${{ secrets.JWT_REFRESH_SECRET }}" >> .env + echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env + echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env + echo "GOOGLE_REDIRECT_URI=${{ secrets.GOOGLE_REDIRECT_URI }}" >> .env + echo "NEXT_PUBLIC_APP_URL=${{ secrets.NEXT_PUBLIC_APP_URL }}" >> .env + echo "NODE_ENV=production" >> .env + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: siliconpin:latest + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=docker,dest=/tmp/siliconpin-image.tar + + - name: Load Docker image + run: docker load -i /tmp/siliconpin-image.tar + + - name: Set up SSH + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.SSH_PRIVATE_KEY }} + known_hosts: 'just-a-placeholder' + + - name: Add remote host to known hosts + run: | + mkdir -p ~/.ssh + echo "Adding ${{ secrets.SSH_HOST }} to known hosts..." + ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts + + - name: Compress Docker image + run: | + echo "Compressing Docker image..." + gzip /tmp/siliconpin-image.tar + mv /tmp/siliconpin-image.tar.gz siliconpin-image.tar.gz + + - name: Transfer Docker image to server + run: | + echo "Transferring Docker image to server..." + scp -o StrictHostKeyChecking=no siliconpin-image.tar.gz ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/root/services/siliconpin-image.tar.gz + scp -o StrictHostKeyChecking=no docker-compose.yml ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/root/services/siliconpin/docker-compose.yml + + - name: Deploy to production + run: | + ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF' + # Force using bash instead of fish + bash -c ' + # Navigate to the services directory + cd /root/services/ + + # Create siliconpin directory if it doesn not exist + mkdir -p siliconpin + cd siliconpin + + # Stop the current container + docker compose down || true + + # Load the new Docker image + echo "Loading Docker image..." + docker load < /root/services/siliconpin-image.tar.gz + + # Remove the transferred image file + rm -f /root/services/siliconpin-image.tar.gz + + # Start the container using the new image + echo "Starting container..." + docker compose up -d + + # Wait for container to be fully running + echo "Waiting for container to be ready..." + attempt=1 + max_attempts=30 + + until [ $attempt -gt $max_attempts ] || docker container inspect siliconpin --format="{{.State.Running}}" 2>/dev/null | grep -q "true"; do + echo "Attempt $attempt/$max_attempts: Container not ready yet, waiting..." + sleep 2 + attempt=$((attempt+1)) + done + + if [ $attempt -gt $max_attempts ]; then + echo "Container failed to start properly within time limit!" + docker compose logs + exit 1 + fi + + # Further verification of application health + echo "Verifying application health..." + timeout=120 + start_time=$(date +%s) + end_time=$((start_time + timeout)) + while [ $(date +%s) -lt $end_time ]; do + if curl -s http://localhost:4023/ > /dev/null; then + echo "Application is responding!" + break + fi + echo "Waiting for application to respond..." + sleep 10 + done + + if ! curl -s http://localhost:4023/ > /dev/null; then + echo "Application failed to respond within timeout!" + docker compose logs + exit 1 + fi + + # Clean up is already done above + + # Verify deployment + echo "Deployment successful! Container is running and application is responding." + docker ps | grep siliconpin + ' + EOF \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21e6373 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz + +# Testing +coverage/ + +# Next.js +.next/ +out/ + +# Service Worker (auto-generated) +public/sw.js +public/workbox-*.js + +# Production +build/ +dist/ +Backups/ +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env.prod +.env +.env* +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +Thumbs.db diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..3723623 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +yarn lint-staged diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ff0633c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,462 @@ +# CLAUDE.md - AI Assistant Context + +## Project Overview + +SiliconPin is a comprehensive web platform built with Next.js 15 that offers multiple services and tools for modern web development and hosting needs. The application provides: + +- **Topic Management System**: Rich content creation and management with user ownership +- **Admin Dashboard**: Comprehensive analytics, user management, and service administration +- **Web Services**: Cloud hosting, VPN, Kubernetes deployment, and developer hiring +- **PWA Capabilities**: Full Progressive Web App with offline functionality +- **Speech Tools**: Text-to-speech and voice recognition utilities +- **Payment System**: Balance management and billing integration +- **Authentication**: JWT-based auth with Redis sessions and OAuth integration + +## Technology Stack + +### Core Framework + +- **Next.js 15** - React framework with App Router +- **TypeScript** - Type safety and better developer experience +- **React 19** - Latest React with concurrent features + +### UI & Styling + +- **Tailwind CSS 4** - Utility-first CSS framework +- **Custom UI Components** - Reusable React components with Tailwind styling +- **next-themes** - Dark/light theme support +- **Lucide React** - Beautiful, customizable icons +- **BlockNote Editor** - Rich text editor with @blocknote/mantine (v0.25.2) + +### Authentication & Sessions + +- **JWT** - JSON Web Tokens for authentication +- **bcryptjs** - Password hashing +- **Redis** - Session storage (high performance) +- **express-session** - Session management +- **connect-redis** - Redis session store + +### Database & Validation + +- **MongoDB** - Document database with Mongoose ODM +- **Mongoose** - MongoDB object modeling +- **Zod** - Schema validation and type inference +- **Topic System** - User-owned content with rich text editing and BlockNote editor + +### State Management + +- **TanStack Query v5** - Server state management +- **React Context** - Client state management +- **React Hook Form** - Form handling and validation + +### Development Tools + +- **ESLint** - Code linting with Next.js config +- **Prettier** - Code formatting with Tailwind plugin +- **Husky** - Git hooks for code quality +- **lint-staged** - Run linters on staged files + +## Project Structure + +``` +siliconpin/ +├── app/ # Next.js App Router +│ ├── api/ # Comprehensive API routes (auth, admin, topics, services, payments) +│ ├── admin/ # Admin dashboard with analytics and management +│ ├── auth/ # Authentication pages +│ ├── topics/ # Topic management system (main content) +│ ├── tools/ # Speech tools and utilities +│ ├── services/ # Web services (hosting, VPN, K8s) +│ ├── dashboard/ # User dashboard +│ ├── globals.css # Global styles and Tailwind +│ ├── layout.tsx # Root layout with providers +│ └── page.tsx # Home page +├── components/ # React components +│ ├── admin/ # Admin-specific components +│ ├── auth/ # Authentication forms and UI +│ ├── topics/ # Topic/content components +│ ├── tools/ # Tool-specific components +│ ├── BlockNoteEditor/ # Rich text editor component +│ ├── ui/ # Reusable UI library +│ ├── header.tsx # Header with navigation +│ ├── footer.tsx # Footer component +│ └── theme-*.tsx # Theme components +├── contexts/ # React contexts +│ ├── AuthContext.tsx # Authentication state +│ └── QueryProvider.tsx # TanStack Query provider +├── lib/ # Utility libraries +│ ├── auth-middleware.ts # API authentication middleware +│ ├── jwt.ts # JWT utilities +│ ├── mongodb.ts # Database connection +│ ├── redis.ts # Redis connection +│ ├── session.ts # Session configuration +│ └── utils.ts # General utilities +├── models/ # Database models +│ ├── user.ts # User model with auth methods +│ ├── topic.ts # Topic model with user ownership +│ ├── billing.ts # Billing and payment models +│ └── transaction.ts # Transaction tracking +└── hooks/ # Custom React hooks +``` + +## Key Features + +### Authentication System + +- JWT-based authentication with refresh tokens +- Secure HTTP-only cookies +- Redis session storage for high performance +- Protected routes and API middleware +- User registration, login, logout, token refresh +- User ownership model for content (blogs, etc.) + +### API Routes + +- `POST /api/auth/register` - Create new user account +- `POST /api/auth/login` - Sign in user +- `POST /api/auth/logout` - Sign out user +- `POST /api/auth/refresh` - Refresh access token +- `GET /api/auth/me` - Get current user info (protected) + +### Security Features + +- Secure HTTP-only cookies +- Password hashing with bcrypt +- CSRF protection headers +- Input validation with Zod +- Protected API middleware +- Session management with Redis + +### Core Application Features + +- **Topic Management** - User-owned content creation with rich text editing +- **Admin Dashboard** - Comprehensive analytics, user management, and service administration +- **Web Services** - Cloud hosting, VPN services, Kubernetes deployment, developer hiring +- **Payment System** - Balance management, billing integration, and transaction tracking +- **Speech Tools** - Text-to-speech and voice recognition utilities +- **PWA Support** - Full Progressive Web App with offline capabilities and installability + +## Development Commands + +```bash +# Development +yarn dev # Start development server with hot reload +yarn build # Build for production +yarn start # Start production server + +# Code quality +yarn lint # Run ESLint with auto-fix +yarn typecheck # Run TypeScript compiler (no output) +yarn prepare # Set up Husky pre-commit hooks +``` + +## Environment Variables + +Required environment variables (create `.env.local`): + +```env +# Database +MONGODB_URI=mongodb://localhost:27017/siliconpin + +# Redis Session Store +REDIS_URL=redis://localhost:6379 + +# Authentication Secrets +SESSION_SECRET=your-session-secret-must-be-at-least-32-characters-long +JWT_SECRET=your-jwt-secret-change-in-production +JWT_REFRESH_SECRET=your-jwt-refresh-secret-change-in-production + +# MinIO Storage (for topic images and file uploads) +MINIO_ENDPOINT=your-minio-endpoint +MINIO_PORT=9000 +MINIO_ACCESS_KEY=your-access-key +MINIO_SECRET_KEY=your-secret-key +MINIO_BUCKET=your-bucket-name + +# Optional +NODE_ENV=development +PORT=3006 +``` + +## Common Development Tasks + +### Adding New API Routes + +1. Create new route file in `app/api/` +2. Use the auth middleware for protected routes: + + ```typescript + import { authMiddleware } from '@/lib/auth-middleware' + + export async function GET(request: Request) { + const user = await authMiddleware(request) + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }) + } + // Your protected logic here + } + ``` + +### Creating New Components + +1. Add component in `components/` directory +2. Use custom UI components from components/ui/ when possible +3. Follow existing patterns for styling and props +4. Create index.ts barrel export for cleaner imports +5. Use dynamic imports for heavy components (e.g., BlockNote) + +### Adding New Database Models + +1. Create model in `models/` directory +2. Define Mongoose schema with proper types +3. Add Zod validation schema +4. Export both model and validation schema + +### Form Handling Pattern + +```typescript +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' + +const schema = z.object({ + // Define your schema +}) + +export function MyForm() { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + // Set defaults + }, + }) + + // Handle form submission +} +``` + +## AI Assistant Instructions + +When working on this codebase: + +### DO: + +- Use TypeScript for all new code +- Follow existing component patterns +- Use custom UI components from components/ui/ when building UI +- Implement proper error handling +- Add Zod validation for API inputs +- Follow the established folder structure +- Use the existing authentication patterns +- Implement responsive design with Tailwind +- Add proper loading and error states + +### DON'T: + +- Mix different authentication patterns +- Skip TypeScript types +- Create components without proper props interface +- Forget to handle loading/error states +- Skip validation on API routes +- Use different state management patterns +- Add dependencies without checking existing ones + +### Code Style + +- Use functional components with hooks +- Prefer composition over inheritance +- Use descriptive variable and function names +- Add JSDoc comments for complex functions +- Follow Prettier formatting rules +- Use consistent import ordering + +### Key Pages + +- `/auth` - Authentication and user management +- `/dashboard` - User analytics and account overview +- `/topics` - Public topic listing and content browsing +- `/topics/new` - Create new topic (protected) +- `/topics/[slug]` - View individual topic content +- `/admin` - Admin dashboard with comprehensive management +- `/services` - Web services (hosting, VPN, Kubernetes) +- `/tools` - Speech tools and utilities + +## 🔥 **CRITICAL: PWA Build vs Development Guidelines** + +### **When You Need `yarn build` (Production Build Required):** + +- ✅ **Service Worker Changes** - New caching strategies, SW updates +- ✅ **Workbox Configuration** - Changes to `next.config.js` PWA settings +- ✅ **Production PWA Testing** - Final validation before deployment +- ✅ **Performance Optimization** - Testing minified/optimized PWA bundle + +### **When `yarn dev` is Sufficient (No Build Needed):** + +- ✅ **Manifest Changes** - Icons, name, description, screenshots, theme colors +- ✅ **PWA Validation** - Chrome DevTools Application tab checks +- ✅ **Install Button Testing** - Browser install prompts +- ✅ **Icon Updates** - New icon files or sizes +- ✅ **Most PWA Features** - Installability, offline detection, etc. + +### **Quick PWA Development Workflow:** + +```bash +# 1. Start development (most PWA features work) +yarn dev + +# 2. Make manifest/icon changes +# 3. Refresh browser - changes appear immediately +# 4. Validate in Chrome DevTools → Application → Manifest + +# 5. Only build when deploying or testing service worker +yarn build && yarn start +``` + +**🚨 KEY INSIGHT**: PWA manifest changes hot-reload in development mode, making `yarn build` unnecessary for most PWA validation and testing! + +--- + +## Troubleshooting + +### Common Issues + +1. **BlockNote Editor SideMenu Error** + - **Solution**: Ensure using `@blocknote/mantine` v0.25.2 (not newer versions) + - Import from `@blocknote/mantine` not `@blocknote/react` for BlockNoteView + - Create index.ts barrel export in BlockNoteEditor folder + - Clear cache: `rm -rf .next node_modules/.cache` + - Clean install: `rm -rf node_modules yarn.lock && yarn install` + +2. **MongoDB Connection Issues** + - Ensure MongoDB is running locally or connection string is correct + - Check network connectivity for cloud databases + +3. **Redis Connection Issues** + - Verify Redis server is running locally + - Check REDIS_URL format: `redis://localhost:6379` + +4. **Authentication Not Working** + - Check JWT secrets are set in environment variables + - Verify cookies are being set correctly + - Check browser developer tools for errors + +5. **Build Errors** + - Run `yarn typecheck` to identify TypeScript errors + - Check for unused imports or variables + - Verify all environment variables are set + +6. **Development Server Issues** + - Clear Next.js cache: `rm -rf .next` + - Reinstall dependencies: `rm -rf node_modules && yarn install` + +## Deployment Notes + +- This application is optimized for Vercel deployment +- Environment variables must be set in production +- Ensure MongoDB and Redis instances are accessible from production +- Use secure secrets for JWT and session keys +- Enable HTTPS in production for secure cookies + +## Contributing + +When extending this application: + +1. Maintain the established patterns +2. Add proper TypeScript types +3. Include validation schemas +4. Update documentation as needed +5. Test authentication flows +6. Follow security best practices + +--- + +## Application Status + +### ✅ **PRODUCTION READY - COMPREHENSIVE PLATFORM** + +SiliconPin has evolved into a complete web platform with the following implemented features: + +#### **🔐 Authentication & User Management** + +- JWT-based authentication with refresh tokens +- OAuth integration (Google, GitHub) +- Redis session management +- Protected routes and API middleware +- User profile management + +#### **📝 Topic Management System** + +- Rich text editing with BlockNote editor +- User-owned content creation and management +- Tag system with search and filtering +- Image upload with MinIO integration +- Draft/publish workflows +- SEO optimization with metadata + +#### **🛠️ Admin Dashboard** + +- Comprehensive analytics and reporting +- User management and moderation +- Billing and transaction tracking +- Service administration +- System settings and configuration +- PDF report generation + +#### **🌐 Web Services** + +- Cloud hosting deployment and management +- VPN service configuration +- Kubernetes orchestration +- Developer hiring marketplace +- Service monitoring and health checks + +#### **🎤 Speech Tools** + +- Text-to-speech synthesis with multiple voices +- Voice recognition and transcription +- Web Speech API integration +- Audio processing utilities + +#### **💰 Payment & Billing System** + +- Balance management and top-up +- Transaction history and tracking +- Service billing and invoicing +- Payment gateway integration +- Refund processing + +#### **📱 PWA Implementation** + +- Full Progressive Web App capabilities +- Offline functionality with service worker +- App installation and native feel +- Responsive design across devices +- Push notification support + +### **🚀 Technical Architecture** + +#### **Backend Infrastructure** + +- **50+ API Endpoints**: Comprehensive REST API coverage +- **8 Database Models**: User, Topic, Billing, Transaction, etc. +- **MongoDB Integration**: Full CRUD operations with Mongoose +- **Redis Caching**: High-performance session and data caching +- **MinIO Storage**: File and image management + +#### **Frontend Excellence** + +- **150+ Component Library**: Reusable React components +- **TypeScript**: 100% type safety with strict mode +- **Tailwind CSS**: Modern, responsive design system +- **Next.js 15**: Latest framework with App Router +- **PWA Ready**: Installable with offline capabilities + +#### **Development Quality** + +- **Code Quality**: ESLint, Prettier, and Husky pre-commit hooks +- **Security**: Input validation, CSRF protection, secure cookies +- **Performance**: Optimized bundle, lazy loading, caching strategies +- **Scalability**: Microservice-ready architecture +- **Documentation**: Comprehensive developer documentation + +**Status**: **PRODUCTION DEPLOYMENT READY** with full feature set and enterprise-grade architecture. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..577c32b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# ---------- Stage 1: Dependencies ---------- +FROM node:20-alpine AS deps +WORKDIR /app + +# Install dependencies needed to compile some Node modules +RUN apk add --no-cache libc6-compat + +# Copy dependency declarations +COPY package.json yarn.lock ./ + +# Install production and build dependencies +RUN yarn install --frozen-lockfile --network-timeout 402300 + +# ---------- Stage 2: Build ---------- +FROM node:20-alpine AS builder +WORKDIR /app + +ENV NEXT_TELEMETRY_DISABLED=1 \ + SKIP_ENV_VALIDATION=true \ + NODE_ENV=production + +# Copy installed deps +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/package.json ./ +COPY --from=deps /app/yarn.lock ./ + +# Copy all source files +COPY . . + +# Build the application +RUN yarn build + +# ---------- Stage 3: Runner ---------- +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=4023 \ + HOSTNAME=0.0.0.0 + +# Create a non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -u 1001 -S nextjs -G nodejs + +# Copy the minimal standalone app +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +# Ensure correct ownership and drop root +RUN chown -R nextjs:nodejs /app +USER nextjs + +EXPOSE 4023 + +CMD ["node", "server.js"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e9e9e11 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +# Makefile for SiliconPin + +COMPOSE_PROD=docker-compose.prod.yml +COMPOSE_DEV=docker-compose.dev.yml + +# Production commands +up: + docker-compose -f $(COMPOSE_PROD) up -d + +build: + docker-compose -f $(COMPOSE_PROD) up -d --build + +down: + docker-compose -f $(COMPOSE_PROD) down + +down-volumes: + docker-compose -f $(COMPOSE_PROD) down -v + +logs: + docker-compose -f $(COMPOSE_PROD) logs -f + +ps: + docker-compose -f $(COMPOSE_PROD) ps + +restart: + docker-compose -f $(COMPOSE_PROD) restart + +health: + docker inspect --format='{{.Name}}: {{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' $$(docker ps -q) + +# Development commands +dev: + docker-compose -f $(COMPOSE_DEV) up -d + +dev-down: + docker-compose -f $(COMPOSE_DEV) down + +dev-logs: + docker-compose -f $(COMPOSE_DEV) logs -f + +#Backup commands +backup-mongo-dev: + docker exec siliconpin-mongo-dev mongodump --archive=/data/db/sp_mongo_dev.archive && mkdir -p Backups && docker cp siliconpin-mongo-dev:/data/db/sp_mongo_dev.archive ./Backups/sp_mongo_dev.archive + +backup-mongo-prod: + docker exec mongo_sp mongodump --archive=/data/db/sp_mongo_prod.archive && mkdir -p Backups && docker cp mongo_sp:/data/db/sp_mongo_prod.archive ./Backups/sp_mongo_prod.archive diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c4f0b7 --- /dev/null +++ b/README.md @@ -0,0 +1,348 @@ +# SiliconPin + +A comprehensive web platform offering topic management, admin dashboards, web services, payment processing, and Progressive Web App capabilities. + +## 🚀 Quick Start + +```bash +# Start services and development +make dev && yarn && yarn dev +``` + +## ✨ Features + +### 🔐 **Authentication & User Management** + +- JWT-based authentication with refresh tokens +- OAuth integration (Google, GitHub) +- Redis session storage for high performance +- Protected routes and comprehensive API middleware +- User profile management and account settings + +### 📝 **Topic Management System** + +- Rich text editing with BlockNote editor +- User-owned content creation and management +- Dynamic tag system with search and filtering +- Image upload with MinIO integration +- Draft/publish workflows with version control +- SEO optimization with metadata and structured data + +### 🛠️ **Admin Dashboard** + +- Comprehensive analytics and reporting +- User management and moderation tools +- Billing and transaction tracking +- Service administration and monitoring +- System settings and configuration +- PDF report generation + +### 🌐 **Web Services** + +- Cloud hosting deployment and management +- VPN service configuration and setup +- Kubernetes orchestration and container management +- Developer hiring marketplace +- Service monitoring and health checks + +### 🎤 **Speech Tools** + +- Text-to-speech synthesis with multiple voices +- Voice recognition and transcription +- Web Speech API integration +- Audio processing utilities + +### 💰 **Payment & Billing System** + +- Balance management and top-up functionality +- Transaction history and detailed tracking +- Service billing and automated invoicing +- Payment gateway integration +- Refund processing and management + +### 📱 **Progressive Web App (PWA)** + +- Full PWA capabilities with offline functionality +- App installation and native-like experience +- Service worker for caching and background sync +- Responsive design across all devices +- Push notification support + +### 🎨 **Modern UI/UX** + +- Next.js 15 with App Router architecture +- TypeScript for complete type safety +- Tailwind CSS with custom design system +- Radix UI components for accessibility +- Dark/light theme support +- Mobile-first responsive design + +## 🛠️ Installation & Setup + +### Prerequisites + +- Node.js 18+ +- MongoDB (local or cloud) +- Redis (local or cloud) +- Yarn package manager + +### Development Setup + +1. **Clone and install:** + +```bash +git clone +cd siliconpin +yarn install +``` + +2. **Environment configuration:** + +```bash +cp .env.example .env.local +``` + +Configure `.env.local`: + +```env +# Database +MONGODB_URI=mongodb://localhost:27017/siliconpin + +# Redis Session Store +REDIS_URL=redis://localhost:6379 + +# Authentication +SESSION_SECRET=your-secure-32-character-session-secret +JWT_SECRET=your-jwt-secret-change-in-production +JWT_REFRESH_SECRET=your-jwt-refresh-secret-change-in-production + +# MinIO Storage (for file uploads) +MINIO_ENDPOINT=your-minio-endpoint +MINIO_PORT=9000 +MINIO_ACCESS_KEY=your-access-key +MINIO_SECRET_KEY=your-secret-key +MINIO_BUCKET=your-bucket-name +``` + +3. **Start development:** + +```bash +# Start MongoDB and Redis +make dev + +# Start Next.js development server +yarn dev +``` + +4. **Access the application:** + +- Main site: [http://localhost:4023](http://localhost:4023) +- Authentication: [http://localhost:4023/auth](http://localhost:4023/auth) +- Admin dashboard: [http://localhost:4023/admin](http://localhost:4023/admin) +- Topics: [http://localhost:4023/topics](http://localhost:4023/topics) +- Tools: [http://localhost:4023/tools](http://localhost:4023/tools) + +## 🏗️ Architecture + +### Project Structure + +``` +siliconpin/ +├── app/ # Next.js App Router +│ ├── api/ # Comprehensive API (50+ endpoints) +│ │ ├── auth/ # Authentication endpoints +│ │ ├── admin/ # Admin management +│ │ ├── topics/ # Content management +│ │ ├── services/ # Service deployment +│ │ ├── payments/ # Payment processing +│ │ └── tools/ # Utility APIs +│ ├── admin/ # Admin dashboard +│ ├── auth/ # Authentication pages +│ ├── topics/ # Topic management +│ ├── services/ # Web services +│ ├── tools/ # Speech tools +│ ├── dashboard/ # User dashboard +│ └── layout.tsx # Root layout +├── components/ # React components (150+) +│ ├── admin/ # Admin-specific UI +│ ├── auth/ # Authentication forms +│ ├── topics/ # Content components +│ ├── tools/ # Tool interfaces +│ ├── ui/ # Reusable UI library +│ └── BlockNoteEditor/ # Rich text editor +├── lib/ # Utilities and services +│ ├── mongodb.ts # Database connection +│ ├── redis.ts # Redis caching +│ ├── minio.ts # File storage +│ ├── auth-middleware.ts # API security +│ └── billing-service.ts # Payment logic +├── models/ # Database models (8 schemas) +│ ├── user.ts # User accounts +│ ├── topic.ts # Content model +│ ├── billing.ts # Payment data +│ └── transaction.ts # Transaction records +├── contexts/ # React contexts +├── hooks/ # Custom React hooks +└── docs/ # Documentation +``` + +## 🔗 API Overview + +### Core APIs (50+ endpoints) + +**Authentication & Users** + +- `POST /api/auth/register` - User registration +- `POST /api/auth/login` - User authentication +- `POST /api/auth/refresh` - Token refresh +- `GET /api/auth/me` - Current user profile +- `POST /api/auth/google` - OAuth authentication + +**Topic Management** + +- `GET /api/topics` - List topics with search/filter +- `POST /api/topics` - Create new topic +- `GET /api/topics/[slug]` - Get specific topic +- `PUT /api/topic/[id]` - Update topic +- `DELETE /api/topic/[id]` - Delete topic + +**Admin Dashboard** + +- `GET /api/admin/dashboard` - Analytics data +- `GET /api/admin/users` - User management +- `GET /api/admin/billing` - Billing overview +- `GET /api/admin/reports` - Generate reports + +**Services & Deployment** + +- `POST /api/services/deploy-kubernetes` - K8s deployment +- `POST /api/services/deploy-vpn` - VPN setup +- `POST /api/services/deploy-cloude` - Cloud instances +- `POST /api/services/hire-developer` - Developer requests + +**Payment & Billing** + +- `POST /api/payments/initiate` - Start payment +- `GET /api/user/balance` - User balance +- `POST /api/balance/add` - Add funds +- `GET /api/transactions` - Transaction history + +**File Management** + +- `POST /api/upload` - File upload to MinIO +- `POST /api/topic-content-image` - Topic images + +## 📦 Development Scripts + +```bash +# Development +yarn dev # Start development server (port 4023) +yarn build # Build for production +yarn start # Start production server + +# Code Quality +yarn lint # Run ESLint with auto-fix +yarn typecheck # TypeScript compilation check +yarn format # Format code with Prettier + +# Database Management +yarn db:seed # Seed database with initial data +yarn db:reset # Reset database to clean state + +# Docker +make dev # Start MongoDB & Redis containers +docker-compose up # Full containerized deployment +``` + +## 🔒 Security & Architecture + +### Security Features + +- JWT authentication with HTTP-only cookies +- OAuth integration (Google, GitHub) +- Password hashing with bcryptjs +- CSRF protection headers +- Input validation with Zod schemas +- Protected API routes with middleware +- Redis session management +- Content Security Policy headers +- Rate limiting ready for production + +### Technical Stack + +- **Frontend**: Next.js 15, React 19, TypeScript +- **Styling**: Tailwind CSS, Radix UI, Custom components +- **Backend**: Node.js, MongoDB, Redis, MinIO +- **Authentication**: JWT, OAuth, Sessions +- **State**: TanStack Query, React Context +- **Validation**: Zod schemas, React Hook Form +- **PWA**: Service Worker, Workbox, Manifest + +## 🚀 Deployment & Production + +### Supported Platforms + +- **Vercel** (Recommended) - Zero-config Next.js deployment +- **Railway** - Full-stack with managed databases +- **DigitalOcean App Platform** - Container deployment +- **Docker** - Self-hosted containerized deployment +- **AWS ECS/Lambda** - Enterprise cloud deployment + +### Production Checklist + +- [ ] Environment variables configured +- [ ] MongoDB Atlas or production database +- [ ] Redis Cloud or managed instance +- [ ] MinIO or S3 for file storage +- [ ] SSL/HTTPS certificates +- [ ] Domain and DNS configuration +- [ ] Security headers and CORS +- [ ] Rate limiting implementation +- [ ] Error tracking (Sentry) +- [ ] Analytics and monitoring + +### Docker Deployment + +```bash +# Production deployment +docker-compose --profile production up -d + +# With custom environment +docker-compose -f docker-compose.prod.yml up -d +``` + +## 📚 Documentation + +Comprehensive documentation is available in the `docs/` directory: + +- **[API Documentation](docs/API.md)** - Complete API reference +- **[Deployment Guide](docs/DEPLOYMENT.md)** - Platform-specific deployment +- **[Code Patterns](docs/PATTERNS.md)** - Development patterns and examples + +## 🤝 Contributing + +### Development Guidelines + +1. Follow TypeScript strict mode +2. Use Zod for validation +3. Implement proper error handling +4. Add tests for new features +5. Update documentation +6. Follow existing code patterns + +### Code Quality + +Pre-commit hooks ensure: + +- ESLint compliance +- Prettier formatting +- TypeScript compilation +- Test passing (when applicable) + +## 📄 License + +MIT License - Open source and free for commercial use. + +--- + +**SiliconPin** - A comprehensive web platform combining topic management, admin dashboards, web services, payment processing, and Progressive Web App capabilities in a modern, scalable architecture. diff --git a/Screenshot.png b/Screenshot.png new file mode 100644 index 0000000..c0ef52d Binary files /dev/null and b/Screenshot.png differ diff --git a/app/about/page.tsx b/app/about/page.tsx new file mode 100644 index 0000000..3d6cce2 --- /dev/null +++ b/app/about/page.tsx @@ -0,0 +1,190 @@ +import { Metadata } from 'next' +import { generateMetadata } from '@/lib/seo' +import { Header } from '@/components/header' +import { Footer } from '@/components/footer' +import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Shield, Zap, Users, Globe, Server, Cpu } from 'lucide-react' + +export const metadata: Metadata = generateMetadata({ + title: 'About Us - SiliconPin', + description: + 'Learn more about SiliconPin, our mission, values, and the team behind our high-performance hosting solutions and developer services.', +}) + +export default function AboutPage() { + const coreValues = [ + { + title: 'Reliability', + description: 'We understand that downtime means lost business. That\'s why we prioritize reliability in everything we do, from our infrastructure to our customer service.', + icon: Server, + color: 'text-blue-600' + }, + { + title: 'Security', + description: 'In an increasingly digital world, security is paramount. We implement robust security measures to protect our clients\' data and applications.', + icon: Shield, + color: 'text-green-600' + }, + { + title: 'Innovation', + description: 'We continuously explore new technologies and methodologies to improve our services and provide our clients with cutting-edge solutions.', + icon: Zap, + color: 'text-purple-600' + }, + { + title: 'Customer Focus', + description: 'Our clients\' success is our success. We work closely with our clients to understand their needs and provide tailored solutions that help them achieve their goals.', + icon: Users, + color: 'text-orange-600' + } + ] + + const whyChooseUs = [ + '24/7 expert technical support', + '99.9% uptime guarantee', + 'Scalable infrastructure to grow with your business', + 'Comprehensive security measures including DDoS protection and SSL certificates', + 'Expertise across various technologies including PHP, Node.js, Python, and Kubernetes', + 'Commitment to customer success through personalized service' + ] + + return ( +
+
+
+ {/* Page Header */} +
+

About SiliconPin

+

+ We promote awareness about digital freedom by providing software and hardware development and research +

+
+ +
+ {/* Our Story */} + + + Our Story + + +

+ SiliconPin was founded in 2021 with a clear mission: to provide businesses of all sizes with reliable, + high-performance hosting solutions that enable growth and innovation. What started as a small team of + passionate developers and system administrators has grown into a trusted hosting provider serving clients + across various industries. +

+

+ Based in Habra, West Bengal, India, our team combines technical expertise with a deep understanding of + business needs to deliver hosting solutions that are not just technically sound but also aligned with + our clients' business objectives. +

+
+
+ + {/* Our Mission */} + + + Our Mission + + +

+ At SiliconPin, our mission is to empower businesses through technology by providing reliable, secure, + and scalable hosting solutions. We believe that technology should enable businesses to focus on what + they do best, without worrying about infrastructure management or technical complexities. +

+
+
+ + {/* Our Values */} + + + Our Values + + +
+ {coreValues.map((value, index) => { + const Icon = value.icon + return ( +
+
+
+ +
+

{value.title}

+
+

+ {value.description} +

+
+ ) + })} +
+
+
+ + {/* Why Choose Us */} + + + Why Choose SiliconPin? + + +
+ {whyChooseUs.map((reason, index) => ( +
+
+ {reason} +
+ ))} +
+
+
+ + {/* Location & Contact */} + + + + + Our Location + + + +
+
+

Headquarters

+

+ 121 Lalbari, GourBongo Road
+ Habra, West Bengal 743271
+ India +

+
+
+

Get in Touch

+
+

Phone: +91-700-160-1485

+

Email: support@siliconpin.com

+
+ Founded 2021 + India Based + 24/7 Support +
+
+
+
+
+
+ + {/* CTA Section */} + +
+
+
+
+ ) +} diff --git a/app/admin/analytics/page.tsx b/app/admin/analytics/page.tsx new file mode 100644 index 0000000..cf4fe68 --- /dev/null +++ b/app/admin/analytics/page.tsx @@ -0,0 +1,28 @@ +'use client' + +import AdminLayout from '@/components/admin/AdminLayout' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export default function AdminAnalyticsPage() { + return ( + +
+
+

Analytics

+

Advanced analytics and insights

+
+ + + + Analytics Dashboard + + +
+

Analytics dashboard coming soon...

+
+
+
+
+
+ ) +} diff --git a/app/admin/billing/page.tsx b/app/admin/billing/page.tsx new file mode 100644 index 0000000..f116ccb --- /dev/null +++ b/app/admin/billing/page.tsx @@ -0,0 +1,19 @@ +'use client' + +import AdminLayout from '@/components/admin/AdminLayout' +import BillingManagement from '@/components/admin/BillingManagement' + +export default function AdminBillingPage() { + return ( + +
+
+

Billing Management

+

Manage billing records, payments, and refunds

+
+ + +
+
+ ) +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..de5acb2 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,256 @@ +'use client' + +import { useEffect, useState } from 'react' +import AdminLayout from '@/components/admin/AdminLayout' +import DashboardStats from '@/components/admin/DashboardStats' +import RecentActivity from '@/components/admin/RecentActivity' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Skeleton } from '@/components/ui/skeleton' +import { toast } from '@/hooks/use-toast' + +interface DashboardData { + users: { + total: number + active: number + newThisMonth: number + verified: number + verificationRate: string + } + revenue: { + total: number + monthly: number + weekly: number + } + transactions: { + total: number + monthly: number + } + services: { + total: number + active: number + breakdown: Array<{ + _id: string + count: number + revenue: number + }> + } + developerRequests: { + total: number + pending: number + } + recentActivity: { + users: Array<{ + _id: string + name: string + email: string + siliconId: string + createdAt: string + }> + transactions: Array<{ + _id: string + type: string + amount: number + description: string + status: string + createdAt: string + userId: { + name: string + email: string + siliconId: string + } + }> + } +} + +export default function AdminDashboard() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchDashboardData() + }, []) + + const fetchDashboardData = async () => { + try { + const token = + localStorage.getItem('token') || + document.cookie + .split('; ') + .find((row) => row.startsWith('token=')) + ?.split('=')[1] + + const response = await fetch('/api/admin/dashboard', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + if (!response.ok) { + throw new Error('Failed to fetch dashboard data') + } + + const dashboardData = await response.json() + setData(dashboardData) + } catch (error) { + console.error('Dashboard fetch error:', error) + toast({ + title: 'Error', + description: 'Failed to load dashboard data', + variant: 'destructive', + }) + } finally { + setLoading(false) + } + } + + if (loading) { + return ( + +
+
+

Dashboard

+

Overview of your SiliconPin platform

+
+ + {/* Loading skeletons */} +
+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + + + + + + ))} +
+ +
+ + + + + +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+
+
+ + + + + +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+
+
+
+
+
+ ) + } + + if (!data) { + return ( + +
+

Failed to load dashboard data

+
+
+ ) + } + + return ( + +
+ {/* Header */} +
+
+
+ 📊 +
+
+

Dashboard

+

+ Overview of your SiliconPin platform +

+
+
+
+ + {/* Stats Cards */} + + + {/* Service Breakdown */} + + + + 🔧 + Service Breakdown + + + +
+ {data.services.breakdown.map((service, index) => { + const colors = [ + 'from-blue-500 to-blue-600', + 'from-indigo-500 to-indigo-600', + 'from-purple-500 to-purple-600', + 'from-pink-500 to-pink-600', + 'from-red-500 to-red-600', + 'from-orange-500 to-orange-600', + 'from-yellow-500 to-yellow-600', + 'from-green-500 to-green-600', + 'from-teal-500 to-teal-600', + 'from-cyan-500 to-cyan-600', + ] + const colorClass = colors[index % colors.length] + + return ( +
+

+ {service._id.replace('_', ' ')} +

+

{service.count}

+

+ ₹{service.revenue.toLocaleString()} revenue +

+
+ ) + })} +
+
+
+ + {/* Recent Activity */} + +
+
+ ) +} diff --git a/app/admin/reports/page.tsx b/app/admin/reports/page.tsx new file mode 100644 index 0000000..01ac121 --- /dev/null +++ b/app/admin/reports/page.tsx @@ -0,0 +1,19 @@ +'use client' + +import AdminLayout from '@/components/admin/AdminLayout' +import ReportsManagement from '@/components/admin/ReportsManagement' + +export default function AdminReportsPage() { + return ( + +
+
+

Reports & Analytics

+

Generate and export comprehensive system reports

+
+ + +
+
+ ) +} diff --git a/app/admin/services/page.tsx b/app/admin/services/page.tsx new file mode 100644 index 0000000..d26a5ba --- /dev/null +++ b/app/admin/services/page.tsx @@ -0,0 +1,21 @@ +'use client' + +import AdminLayout from '@/components/admin/AdminLayout' +import ServiceManagement from '@/components/admin/ServiceManagement' + +export default function AdminServicesPage() { + return ( + +
+
+

Service Management

+

+ Manage deployments, developer requests, and service status +

+
+ + +
+
+ ) +} diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx new file mode 100644 index 0000000..70b5856 --- /dev/null +++ b/app/admin/settings/page.tsx @@ -0,0 +1,19 @@ +'use client' + +import AdminLayout from '@/components/admin/AdminLayout' +import SystemSettings from '@/components/admin/SystemSettings' + +export default function AdminSettingsPage() { + return ( + +
+
+

System Settings

+

Configure system-wide settings and preferences

+
+ + +
+
+ ) +} diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx new file mode 100644 index 0000000..dffbb81 --- /dev/null +++ b/app/admin/users/page.tsx @@ -0,0 +1,19 @@ +'use client' + +import AdminLayout from '@/components/admin/AdminLayout' +import UserManagement from '@/components/admin/UserManagement' + +export default function AdminUsersPage() { + return ( + +
+
+

User Management

+

Manage users, roles, and permissions

+
+ + +
+
+ ) +} diff --git a/app/api/admin/billing/route.ts b/app/api/admin/billing/route.ts new file mode 100644 index 0000000..8139444 --- /dev/null +++ b/app/api/admin/billing/route.ts @@ -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 }) + } + }) +} diff --git a/app/api/admin/dashboard/route.ts b/app/api/admin/dashboard/route.ts new file mode 100644 index 0000000..916255b --- /dev/null +++ b/app/api/admin/dashboard/route.ts @@ -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 }) + } + }) +} diff --git a/app/api/admin/reports/route.ts b/app/api/admin/reports/route.ts new file mode 100644 index 0000000..cf0e92c --- /dev/null +++ b/app/api/admin/reports/route.ts @@ -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') +} diff --git a/app/api/admin/services/route.ts b/app/api/admin/services/route.ts new file mode 100644 index 0000000..c9be333 --- /dev/null +++ b/app/api/admin/services/route.ts @@ -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 }) + } + }) +} diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts new file mode 100644 index 0000000..968e10c --- /dev/null +++ b/app/api/admin/settings/route.ts @@ -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 }) + } + }) +} diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts new file mode 100644 index 0000000..426952f --- /dev/null +++ b/app/api/admin/users/route.ts @@ -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 }) + } + }) +} diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..bd9be87 --- /dev/null +++ b/app/api/auth/forgot-password/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/google/callback/route.ts b/app/api/auth/google/callback/route.ts new file mode 100644 index 0000000..b7120b0 --- /dev/null +++ b/app/api/auth/google/callback/route.ts @@ -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)) + } +} diff --git a/app/api/auth/google/route.ts b/app/api/auth/google/route.ts new file mode 100644 index 0000000..afa355e --- /dev/null +++ b/app/api/auth/google/route.ts @@ -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 } + ) + } +} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..9f4b3bd --- /dev/null +++ b/app/api/auth/login/route.ts @@ -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 } + ) + } +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..ed35271 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -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 + } +} diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts new file mode 100644 index 0000000..a824c65 --- /dev/null +++ b/app/api/auth/me/route.ts @@ -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 } + ) + } +}) diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts new file mode 100644 index 0000000..559018a --- /dev/null +++ b/app/api/auth/refresh/route.ts @@ -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 } + ) + } +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..4cc87fc --- /dev/null +++ b/app/api/auth/register/route.ts @@ -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 } + ) + } +} diff --git a/app/api/balance/add/route.ts b/app/api/balance/add/route.ts new file mode 100644 index 0000000..d8c4622 --- /dev/null +++ b/app/api/balance/add/route.ts @@ -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', + }, + ] +} diff --git a/app/api/balance/deduct/route.ts b/app/api/balance/deduct/route.ts new file mode 100644 index 0000000..7783fe5 --- /dev/null +++ b/app/api/balance/deduct/route.ts @@ -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 } + ) + } +} diff --git a/app/api/balance/failure/route.ts b/app/api/balance/failure/route.ts new file mode 100644 index 0000000..3daa2da --- /dev/null +++ b/app/api/balance/failure/route.ts @@ -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 = {} + + // 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 } +} \ No newline at end of file diff --git a/app/api/balance/success/route.ts b/app/api/balance/success/route.ts new file mode 100644 index 0000000..0769d7b --- /dev/null +++ b/app/api/balance/success/route.ts @@ -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 = {} + + // 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 } +} \ No newline at end of file diff --git a/app/api/billing/route.ts b/app/api/billing/route.ts new file mode 100644 index 0000000..f71f3c3 --- /dev/null +++ b/app/api/billing/route.ts @@ -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 } + ) + } +} diff --git a/app/api/billing/stats/route.ts b/app/api/billing/stats/route.ts new file mode 100644 index 0000000..4d1b341 --- /dev/null +++ b/app/api/billing/stats/route.ts @@ -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 } + ) + } +} diff --git a/app/api/contact/route.ts b/app/api/contact/route.ts new file mode 100644 index 0000000..fae1a67 --- /dev/null +++ b/app/api/contact/route.ts @@ -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) + }, { status: 400 }) + } + + return NextResponse.json({ + success: false, + message: 'Internal server error. Please try again later.' + }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/api/dashboard/route.ts b/app/api/dashboard/route.ts new file mode 100644 index 0000000..c0e42ab --- /dev/null +++ b/app/api/dashboard/route.ts @@ -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 + +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 } + ) + } +} \ No newline at end of file diff --git a/app/api/debug/topics/route.ts b/app/api/debug/topics/route.ts new file mode 100644 index 0000000..4f0bfe5 --- /dev/null +++ b/app/api/debug/topics/route.ts @@ -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 }) + } +} diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts new file mode 100644 index 0000000..c3a4b24 --- /dev/null +++ b/app/api/feedback/route.ts @@ -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) + }, { status: 400 }) + } + + return NextResponse.json({ + success: false, + message: 'Internal server error. Please try again later.' + }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..8f62e68 --- /dev/null +++ b/app/api/health/route.ts @@ -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 }) + } +} diff --git a/app/api/payments/failure/route.ts b/app/api/payments/failure/route.ts new file mode 100644 index 0000000..a7cdc6b --- /dev/null +++ b/app/api/payments/failure/route.ts @@ -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 = {} + + // 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 +} \ No newline at end of file diff --git a/app/api/payments/initiate/route.ts b/app/api/payments/initiate/route.ts new file mode 100644 index 0000000..ffea5a0 --- /dev/null +++ b/app/api/payments/initiate/route.ts @@ -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', + } +} diff --git a/app/api/payments/success/route.ts b/app/api/payments/success/route.ts new file mode 100644 index 0000000..b3c6973 --- /dev/null +++ b/app/api/payments/success/route.ts @@ -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 = {} + + // 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 } +} \ No newline at end of file diff --git a/app/api/services/deploy-cloude/route.ts b/app/api/services/deploy-cloude/route.ts new file mode 100644 index 0000000..24798d2 --- /dev/null +++ b/app/api/services/deploy-cloude/route.ts @@ -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 } + ) + } +} diff --git a/app/api/services/deploy-kubernetes/route.ts b/app/api/services/deploy-kubernetes/route.ts new file mode 100644 index 0000000..ce31c0f --- /dev/null +++ b/app/api/services/deploy-kubernetes/route.ts @@ -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 } + ) + } +} diff --git a/app/api/services/deploy-vpn/route.ts b/app/api/services/deploy-vpn/route.ts new file mode 100644 index 0000000..16686c5 --- /dev/null +++ b/app/api/services/deploy-vpn/route.ts @@ -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 } + ) + } +} diff --git a/app/api/services/download-hosting-conf/route.ts b/app/api/services/download-hosting-conf/route.ts new file mode 100644 index 0000000..f99300a --- /dev/null +++ b/app/api/services/download-hosting-conf/route.ts @@ -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 }) + } +} diff --git a/app/api/services/download-kubernetes/route.ts b/app/api/services/download-kubernetes/route.ts new file mode 100644 index 0000000..4792af5 --- /dev/null +++ b/app/api/services/download-kubernetes/route.ts @@ -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 }) + } +} \ No newline at end of file diff --git a/app/api/services/hire-developer/route.ts b/app/api/services/hire-developer/route.ts new file mode 100644 index 0000000..77fa44b --- /dev/null +++ b/app/api/services/hire-developer/route.ts @@ -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 } + ) + } +} diff --git a/app/api/startup-test/route.ts b/app/api/startup-test/route.ts new file mode 100644 index 0000000..d90951b --- /dev/null +++ b/app/api/startup-test/route.ts @@ -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 } + ) + } +} diff --git a/app/api/tags/route.ts b/app/api/tags/route.ts new file mode 100644 index 0000000..f297ffd --- /dev/null +++ b/app/api/tags/route.ts @@ -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() + + 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 } + ) + } +} diff --git a/app/api/tools/openai-chat/route.ts b/app/api/tools/openai-chat/route.ts new file mode 100644 index 0000000..3e8068b --- /dev/null +++ b/app/api/tools/openai-chat/route.ts @@ -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 }) + } +} diff --git a/app/api/topic-content-image/route.ts b/app/api/topic-content-image/route.ts new file mode 100644 index 0000000..9b95b80 --- /dev/null +++ b/app/api/topic-content-image/route.ts @@ -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 } + ) + } +} diff --git a/app/api/topic/[id]/route.ts b/app/api/topic/[id]/route.ts new file mode 100644 index 0000000..0f4d479 --- /dev/null +++ b/app/api/topic/[id]/route.ts @@ -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 } + ) + } +} diff --git a/app/api/topic/route.ts b/app/api/topic/route.ts new file mode 100644 index 0000000..7c381ac --- /dev/null +++ b/app/api/topic/route.ts @@ -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 } + ) + } +} diff --git a/app/api/topic/slug/[slug]/route.ts b/app/api/topic/slug/[slug]/route.ts new file mode 100644 index 0000000..112a1a2 --- /dev/null +++ b/app/api/topic/slug/[slug]/route.ts @@ -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 } + ) + } +} diff --git a/app/api/topics/[slug]/related/route.ts b/app/api/topics/[slug]/related/route.ts new file mode 100644 index 0000000..c58f3a8 --- /dev/null +++ b/app/api/topics/[slug]/related/route.ts @@ -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 } + ) + } +} diff --git a/app/api/topics/[slug]/route.ts b/app/api/topics/[slug]/route.ts new file mode 100644 index 0000000..fbb554e --- /dev/null +++ b/app/api/topics/[slug]/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/app/api/topics/[slug]/view/route.ts b/app/api/topics/[slug]/view/route.ts new file mode 100644 index 0000000..29b70da --- /dev/null +++ b/app/api/topics/[slug]/view/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/app/api/topics/route.ts b/app/api/topics/route.ts new file mode 100644 index 0000000..bfd9d37 --- /dev/null +++ b/app/api/topics/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/app/api/topics/tags/route.ts b/app/api/topics/tags/route.ts new file mode 100644 index 0000000..4e0bc13 --- /dev/null +++ b/app/api/topics/tags/route.ts @@ -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() + + 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 } + ) + } +} diff --git a/app/api/transactions/route.ts b/app/api/transactions/route.ts new file mode 100644 index 0000000..5841668 --- /dev/null +++ b/app/api/transactions/route.ts @@ -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 } + ) + } +} diff --git a/app/api/upload/confirm/route.ts b/app/api/upload/confirm/route.ts new file mode 100644 index 0000000..29c40e5 --- /dev/null +++ b/app/api/upload/confirm/route.ts @@ -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 }) + } +} diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 0000000..5d2bf02 --- /dev/null +++ b/app/api/upload/route.ts @@ -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 }) + } +} diff --git a/app/api/user/balance/route.ts b/app/api/user/balance/route.ts new file mode 100644 index 0000000..cce7c90 --- /dev/null +++ b/app/api/user/balance/route.ts @@ -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 } + ) + } +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000..98c6469 --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,217 @@ +'use client' +import { useState, useEffect, Suspense } from 'react' +import { useSearchParams, useRouter } from 'next/navigation' +import { LoginForm } from '@/components/auth/LoginForm' +import { RegisterForm } from '@/components/auth/RegisterForm' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { useAuth } from '@/contexts/AuthContext' +import { Header } from '@/components/header' +import { Footer } from '@/components/footer' +import Link from 'next/link' +import { BookOpen, Search, Settings, User, Edit, Tag } from 'lucide-react' + +// Main auth component with search params handling +function AuthPageContent() { + const searchParams = useSearchParams() + const router = useRouter() + const [mode, setMode] = useState<'login' | 'register'>('login') + const [shouldRedirect, setShouldRedirect] = useState(false) + + useEffect(() => { + const modeParam = searchParams.get('mode') + if (modeParam === 'register') { + setMode('register') + } + + // Only redirect if there's a returnUrl (user was redirected here) or if they just logged in + const returnUrl = searchParams.get('returnUrl') + if (returnUrl) { + setShouldRedirect(true) + } + }, [searchParams]) + const { user, logout, loading } = useAuth() + + // Only redirect if shouldRedirect is true (not just because user exists) + useEffect(() => { + if (!loading && user && shouldRedirect) { + const returnUrl = searchParams.get('returnUrl') + if (returnUrl) { + router.push(decodeURIComponent(returnUrl)) + } else { + router.push('/dashboard') + } + } + }, [user, loading, router, searchParams, shouldRedirect]) + + if (loading) { + return ( +
+
+
+
+
+

Loading...

+
+
+
+
+ ) + } + + if (user) { + return ( +
+
+
+
+ {/* Welcome Section */} + + + + + Welcome back, {user.name}! + + + You're logged in and ready to start creating amazing content + + + + + {/* Quick Actions */} +
+ + + + + Topic Management + + Create and manage your topics + + + + + + + + + + + + Account Settings + + Manage your account and preferences + + + + + + + +
+ + {/* User Info */} + + + Account Information + + +
+
+ Email: {user.email} +
+
+ Role: {user.role} +
+
+ Provider: {user.provider} +
+
+ Verified: {user.isVerified ? 'Yes' : 'No'} +
+
+
+
+
+
+
+
+ ) + } + + return ( +
+
+
+
+

Authentication Demo

+

+ Test the authentication system with login and registration +

+
+ + +
+
+ + {mode === 'login' ? ( + setMode('register')} /> + ) : ( + setMode('login')} /> + )} +
+
+
+ ) +} + +// Wrapper component with Suspense boundary +export default function AuthPage() { + return ( + +
+ + } + > + +
+ ) +} diff --git a/app/balance/page.tsx b/app/balance/page.tsx new file mode 100644 index 0000000..1ea4286 --- /dev/null +++ b/app/balance/page.tsx @@ -0,0 +1,107 @@ +'use client' + +import React from 'react' +import { useAuth } from '@/contexts/AuthContext' +import { BalanceCard, BalanceDisplay, TransactionHistory } from '@/components/balance' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { ArrowLeft, History } from 'lucide-react' +import Link from 'next/link' + +export default function BalancePage() { + const { user } = useAuth() + + if (!user) { + return ( +
+
+

Access Denied

+

Please log in to view your balance.

+ + + +
+
+ ) + } + + return ( +
+
+ + + +

Balance Management

+

+ Manage your account balance and view transaction history +

+
+ +
+ {/* Main Balance Card */} +
+ + + {/* Quick Balance Display Examples */} + + + Balance Display Variants + Different ways to show balance in your UI + + +
+

Default Display:

+ +
+ +
+

Compact Display:

+ +
+ +
+

Badge Display:

+ +
+
+
+
+ + {/* Transaction History */} +
+ + + {/* Account Info */} + + + Account Information + + +
+ Name: + {user.name} +
+
+ Email: + {user.email} +
+ {user.siliconId && ( +
+ Silicon ID: + {user.siliconId} +
+ )} +
+ Account Type: + {user.role} +
+
+
+
+
+
+ ) +} diff --git a/app/billing/page.tsx b/app/billing/page.tsx new file mode 100644 index 0000000..efada0b --- /dev/null +++ b/app/billing/page.tsx @@ -0,0 +1,51 @@ +'use client' + +import React from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import BillingHistory from '@/components/billing/BillingHistory' +import { BalanceCard } from '@/components/balance/BalanceCard' + +export default function BillingPage() { + return ( +
+
+

Billing & Usage

+

+ Manage your billing, view service usage, and track your spending across all SiliconPin + services. +

+
+ + + + Billing History + Balance Management + + + + + + + +
+ + + + Payment Methods + + Manage your payment methods and billing preferences + + + +
+ Payment methods management coming soon +
+
+
+
+
+
+
+ ) +} diff --git a/app/contact/contact-client.tsx b/app/contact/contact-client.tsx new file mode 100644 index 0000000..3bd0825 --- /dev/null +++ b/app/contact/contact-client.tsx @@ -0,0 +1,316 @@ +'use client' + +import { useState } from 'react' +import { Header } from '@/components/header' +import { Footer } from '@/components/footer' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Mail, Phone, MapPin, Clock, Send, CheckCircle, AlertCircle } from 'lucide-react' + +export function ContactPageClient() { + const [formData, setFormData] = useState({ + name: '', + email: '', + company: '', + service_intrest: '', + message: '' + }) + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsSubmitting(true) + setErrors({}) + + // Client-side validation + const newErrors: Record = {} + if (!formData.name.trim()) newErrors.name = 'Name is required' + if (!formData.email.trim()) newErrors.email = 'Email is required' + if (!formData.service_intrest) newErrors.service_intrest = 'Service interest is required' + if (!formData.message.trim()) newErrors.message = 'Message is required' + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors) + setIsSubmitting(false) + return + } + + try { + const response = await fetch('/api/contact', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) + + const result = await response.json() + + if (response.ok && result.success) { + setIsSuccess(true) + setFormData({ + name: '', + email: '', + company: '', + service_intrest: '', + message: '' + }) + } else { + if (result.errors) { + setErrors(result.errors) + } else { + setErrors({ submit: result.message || 'Error submitting form. Please try again.' }) + } + } + } catch (error) { + setErrors({ submit: 'Network error. Please try again.' }) + } finally { + setIsSubmitting(false) + } + } + + const handleInputChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })) + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })) + } + } + + return ( +
+
+
+ {/* Page Header */} +
+

Contact Us

+

+ Have questions about our hosting services? Get in touch with our team of experts. +

+
+ +
+ {/* Contact Form */} +
+ + + + + Send us a message + + + + {isSuccess && ( + + + + Thank you for your message! We'll get back to you soon. + + + )} + + {errors.submit && ( + + + + {errors.submit} + + + )} + +
+
+
+ + handleInputChange('name', e.target.value)} + className={errors.name ? 'border-red-500' : ''} + /> + {errors.name && ( +

{errors.name}

+ )} +
+ +
+ + handleInputChange('email', e.target.value)} + className={errors.email ? 'border-red-500' : ''} + /> + {errors.email && ( +

{errors.email}

+ )} +
+
+ +
+
+ + handleInputChange('company', e.target.value)} + /> +
+ +
+ + + {errors.service_intrest && ( +

{errors.service_intrest}

+ )} +
+
+ +
+ +