From 7219108342f69a2051d92b54bd6c40b77e885027 Mon Sep 17 00:00:00 2001 From: Kar k1 Date: Sat, 30 Aug 2025 18:18:57 +0530 Subject: [PATCH] initial commit --- .claude/settings.local.json | 16 + .dockerignore | 8 + .editorconfig | 10 + .gitattributes | 4 + .github/workflows/deploy.yml | 156 + .gitignore | 57 + .husky/pre-commit | 1 + CLAUDE.md | 462 + Dockerfile | 57 + Makefile | 46 + README.md | 348 + Screenshot.png | Bin 0 -> 353432 bytes app/about/page.tsx | 190 + app/admin/analytics/page.tsx | 28 + app/admin/billing/page.tsx | 19 + app/admin/page.tsx | 256 + app/admin/reports/page.tsx | 19 + app/admin/services/page.tsx | 21 + app/admin/settings/page.tsx | 19 + app/admin/users/page.tsx | 19 + app/api/admin/billing/route.ts | 183 + app/api/admin/dashboard/route.ts | 155 + app/api/admin/reports/route.ts | 187 + app/api/admin/services/route.ts | 200 + app/api/admin/settings/route.ts | 117 + app/api/admin/users/route.ts | 163 + app/api/auth/forgot-password/route.ts | 120 + app/api/auth/google/callback/route.ts | 113 + app/api/auth/google/route.ts | 22 + app/api/auth/login/route.ts | 98 + app/api/auth/logout/route.ts | 62 + app/api/auth/me/route.ts | 33 + app/api/auth/refresh/route.ts | 99 + app/api/auth/register/route.ts | 109 + app/api/balance/add/route.ts | 237 + app/api/balance/deduct/route.ts | 169 + app/api/balance/failure/route.ts | 147 + app/api/balance/success/route.ts | 132 + app/api/billing/route.ts | 102 + app/api/billing/stats/route.ts | 57 + app/api/contact/route.ts | 80 + app/api/dashboard/route.ts | 138 + app/api/debug/topics/route.ts | 39 + app/api/feedback/route.ts | 95 + app/api/health/route.ts | 63 + app/api/payments/failure/route.ts | 115 + app/api/payments/initiate/route.ts | 112 + app/api/payments/success/route.ts | 110 + app/api/services/deploy-cloude/route.ts | 198 + app/api/services/deploy-kubernetes/route.ts | 247 + app/api/services/deploy-vpn/route.ts | 162 + .../services/download-hosting-conf/route.ts | 108 + app/api/services/download-kubernetes/route.ts | 54 + app/api/services/hire-developer/route.ts | 285 + app/api/startup-test/route.ts | 52 + app/api/tags/route.ts | 76 + app/api/tools/openai-chat/route.ts | 71 + app/api/topic-content-image/route.ts | 166 + app/api/topic/[id]/route.ts | 261 + app/api/topic/route.ts | 427 + app/api/topic/slug/[slug]/route.ts | 78 + app/api/topics/[slug]/related/route.ts | 71 + app/api/topics/[slug]/route.ts | 79 + app/api/topics/[slug]/view/route.ts | 75 + app/api/topics/route.ts | 142 + app/api/topics/tags/route.ts | 39 + app/api/transactions/route.ts | 147 + app/api/upload/confirm/route.ts | 102 + app/api/upload/route.ts | 129 + app/api/user/balance/route.ts | 79 + app/auth/page.tsx | 217 + app/balance/page.tsx | 107 + app/billing/page.tsx | 51 + app/contact/contact-client.tsx | 316 + app/contact/page.tsx | 12 + app/dashboard/page.tsx | 462 + app/error.tsx | 107 + app/features/page.tsx | 227 + app/feedback/feedback-client.tsx | 322 + app/feedback/page.tsx | 12 + app/forgot-password/page.tsx | 163 + app/globals.css | 231 + app/icon.tsx | 36 + app/layout.tsx | 90 + app/legal-agreement/page.tsx | 371 + app/not-found.tsx | 71 + app/page.tsx | 1008 + app/payment/failed/page.tsx | 303 + app/payment/success/page.tsx | 198 + app/privacy/page.tsx | 337 + app/profile/page.tsx | 182 + app/refund-policy/page.tsx | 260 + app/robots.ts | 47 + app/services/cloud-instance/page.tsx | 719 + app/services/hosting-control-panel/page.tsx | 426 + app/services/human-developer/page.tsx | 582 + app/services/kubernetes/page.tsx | 642 + app/services/page.tsx | 202 + app/services/vpn/page.tsx | 431 + app/sitemap.ts | 162 + app/sw.js | 24 + app/terms/page.tsx | 299 + app/tools/page.tsx | 223 + app/tools/speech-to-text/page.tsx | 36 + app/tools/text-to-speech/page.tsx | 38 + app/tools/web-speech/page.tsx | 1008 + app/topics/[slug]/edit/page.tsx | 601 + app/topics/[slug]/not-found.tsx | 50 + app/topics/[slug]/page.tsx | 289 + app/topics/edit/[id]/page.tsx | 4 + app/topics/new/page.tsx | 553 + app/topics/page.tsx | 383 + .../BlockNoteEditor/BlockNoteEditor.tsx | 88 + components/BlockNoteEditor/index.ts | 1 + components/BlockNoteEditor/styles.css | 5 + components/Features.tsx | 81 + components/Hero.tsx | 94 + components/Logo.tsx | 19 + components/PWAInstallPrompt.tsx | 137 + components/RecentTopics.tsx | 136 + components/StartupTest.tsx | 70 + components/Testimonials.tsx | 90 + components/admin/AdminLayout.tsx | 188 + components/admin/BillingManagement.tsx | 567 + components/admin/DashboardStats.tsx | 130 + components/admin/RecentActivity.tsx | 137 + components/admin/ReportsManagement.tsx | 284 + components/admin/ServiceManagement.tsx | 665 + components/admin/SystemSettings.tsx | 592 + components/admin/UserManagement.tsx | 586 + components/auth/GitHubSignInButton.tsx | 47 + components/auth/GoogleSignInButton.tsx | 63 + components/auth/LoginForm.tsx | 175 + components/auth/RegisterForm.tsx | 308 + components/auth/RequireAuth.tsx | 43 + components/balance/BalanceCard.tsx | 261 + components/balance/BalanceDisplay.tsx | 61 + components/balance/TransactionHistory.tsx | 278 + components/balance/index.ts | 3 + components/billing/BillingHistory.tsx | 308 + components/footer.tsx | 165 + components/header.tsx | 241 + components/profile/ProfileCard.tsx | 189 + components/profile/ProfileSettings.tsx | 1170 ++ components/seo/SEO.tsx | 200 + components/theme-provider.tsx | 70 + components/theme-toggle.tsx | 54 + components/tools/speech-to-text-client.tsx | 1101 ++ components/tools/text-to-speech.tsx | 302 + components/topics/ImageUpload.tsx | 276 + components/topics/SearchableTagSelect.tsx | 273 + components/topics/SimpleShareButtons.tsx | 130 + components/topics/TopicCard.tsx | 101 + components/topics/TopicClientComponents.tsx | 7 + components/topics/TopicContent.tsx | 53 + components/topics/TopicEditButton.tsx | 34 + components/topics/ViewTracker.tsx | 41 + components/ui/Link.tsx | 22 + components/ui/accordion.tsx | 52 + components/ui/alert.tsx | 49 + components/ui/avatar.tsx | 45 + components/ui/badge.tsx | 33 + components/ui/breadcrumb.tsx | 101 + components/ui/button.tsx | 65 + components/ui/card.tsx | 75 + components/ui/checkbox.tsx | 26 + components/ui/command.tsx | 155 + components/ui/custom-solution-cta.tsx | 39 + components/ui/dialog.tsx | 129 + components/ui/dropdown-menu.tsx | 228 + components/ui/form.tsx | 152 + components/ui/hover-card.tsx | 27 + components/ui/input.tsx | 21 + components/ui/label.tsx | 21 + components/ui/navigation-menu.tsx | 120 + components/ui/pagination.tsx | 117 + components/ui/popover.tsx | 29 + components/ui/progress.tsx | 23 + components/ui/radio-group.tsx | 63 + components/ui/scroll-area.tsx | 44 + components/ui/select.tsx | 170 + components/ui/separator.tsx | 24 + components/ui/sheet.tsx | 130 + components/ui/skeleton.tsx | 7 + components/ui/switch.tsx | 27 + components/ui/tabs.tsx | 53 + components/ui/textarea.tsx | 18 + components/ui/toast.tsx | 122 + components/ui/toaster.tsx | 33 + components/ui/tooltip.tsx | 28 + contexts/AuthContext.tsx | 337 + contexts/QueryProvider.tsx | 35 + cookies.txt | 6 + docker-compose.dev.yml | 19 + docker-compose.prod.yml | 55 + docker-compose.yml | 18 + docs/API.md | 503 + docs/DEPLOYMENT.md | 593 + docs/PATTERNS.md | 1124 ++ eslint.config.mjs | 23 + hooks/use-toast.ts | 188 + hooks/useProfileData.ts | 57 + lib/admin-middleware.ts | 75 + lib/analytics.tsx | 310 + lib/auth-middleware.ts | 101 + lib/balance-service.ts | 141 + lib/billing-service.ts | 375 + lib/cache.ts | 141 + lib/env.ts | 199 + lib/file-vault.ts | 152 + lib/google-oauth.ts | 82 + lib/jwt.ts | 47 + lib/lazy-loading.tsx | 193 + lib/minio.ts | 186 + lib/mongodb.ts | 87 + lib/proxy.ts | 126 + lib/redis.ts | 49 + lib/seo.ts | 321 + lib/session.ts | 55 + lib/siliconId.ts | 36 + lib/startup.ts | 48 + lib/storage.ts | 229 + lib/structured-data.ts | 162 + lib/system-settings.ts | 81 + lib/utils.ts | 6 + models/billing.ts | 311 + models/developer-request.ts | 114 + models/system-settings.ts | 45 + models/topic.ts | 222 + models/transaction.ts | 121 + models/user.ts | 125 + next.config.js | 171 + package-lock.json | 16101 ++++++++++++++++ package.json | 119 + postcss.config.js | 6 + prettier.config.js | 9 + public/favicon.ico | Bin 0 -> 33566 bytes public/icons/icon-192x192.png | Bin 0 -> 4634 bytes public/icons/icon-32x32.png | Bin 0 -> 513 bytes public/icons/icon-512x512.png | Bin 0 -> 14587 bytes public/manifest.json | 45 + public/robots.txt | 2 + public/screenshots/desktop-wide.png | Bin 0 -> 26167 bytes public/screenshots/mobile.png | Bin 0 -> 18715 bytes pwa-test.html | 143 + scripts/create-admin.ts | 42 + scripts/init-mongo.js | 33 + scripts/reset-db.ts | 104 + scripts/seed.ts | 112 + tailwind.config.js | 142 + templates/README.md | 216 + templates/api-route.template.ts | 298 + templates/component.template.tsx | 151 + templates/hook.template.ts | 267 + templates/model.template.ts | 342 + templates/page.template.tsx | 66 + todo/CODE_QUALITY_IMPROVEMENTS.md | 382 + todo/PERFORMANCE_OPTIMIZATION.md | 327 + todo/README.md | 202 + todo/SECURITY_IMPROVEMENTS.md | 292 + todo/authentication-apis.md | 79 + todo/blog-system-remaining-features.md | 603 + todo/blogs-to-topics-migration.md | 246 + todo/profile-enhancement.md | 149 + todo/profile-tabs-routing.md | 224 + todo/services-backend-integration.md | 232 + tsconfig.json | 45 + types/react-speech-recognition.d.ts | 22 + types/speech-recognition.d.ts | 81 + yarn.lock | 9955 ++++++++++ 270 files changed, 70221 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 Screenshot.png create mode 100644 app/about/page.tsx create mode 100644 app/admin/analytics/page.tsx create mode 100644 app/admin/billing/page.tsx create mode 100644 app/admin/page.tsx create mode 100644 app/admin/reports/page.tsx create mode 100644 app/admin/services/page.tsx create mode 100644 app/admin/settings/page.tsx create mode 100644 app/admin/users/page.tsx create mode 100644 app/api/admin/billing/route.ts create mode 100644 app/api/admin/dashboard/route.ts create mode 100644 app/api/admin/reports/route.ts create mode 100644 app/api/admin/services/route.ts create mode 100644 app/api/admin/settings/route.ts create mode 100644 app/api/admin/users/route.ts create mode 100644 app/api/auth/forgot-password/route.ts create mode 100644 app/api/auth/google/callback/route.ts create mode 100644 app/api/auth/google/route.ts create mode 100644 app/api/auth/login/route.ts create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/api/auth/me/route.ts create mode 100644 app/api/auth/refresh/route.ts create mode 100644 app/api/auth/register/route.ts create mode 100644 app/api/balance/add/route.ts create mode 100644 app/api/balance/deduct/route.ts create mode 100644 app/api/balance/failure/route.ts create mode 100644 app/api/balance/success/route.ts create mode 100644 app/api/billing/route.ts create mode 100644 app/api/billing/stats/route.ts create mode 100644 app/api/contact/route.ts create mode 100644 app/api/dashboard/route.ts create mode 100644 app/api/debug/topics/route.ts create mode 100644 app/api/feedback/route.ts create mode 100644 app/api/health/route.ts create mode 100644 app/api/payments/failure/route.ts create mode 100644 app/api/payments/initiate/route.ts create mode 100644 app/api/payments/success/route.ts create mode 100644 app/api/services/deploy-cloude/route.ts create mode 100644 app/api/services/deploy-kubernetes/route.ts create mode 100644 app/api/services/deploy-vpn/route.ts create mode 100644 app/api/services/download-hosting-conf/route.ts create mode 100644 app/api/services/download-kubernetes/route.ts create mode 100644 app/api/services/hire-developer/route.ts create mode 100644 app/api/startup-test/route.ts create mode 100644 app/api/tags/route.ts create mode 100644 app/api/tools/openai-chat/route.ts create mode 100644 app/api/topic-content-image/route.ts create mode 100644 app/api/topic/[id]/route.ts create mode 100644 app/api/topic/route.ts create mode 100644 app/api/topic/slug/[slug]/route.ts create mode 100644 app/api/topics/[slug]/related/route.ts create mode 100644 app/api/topics/[slug]/route.ts create mode 100644 app/api/topics/[slug]/view/route.ts create mode 100644 app/api/topics/route.ts create mode 100644 app/api/topics/tags/route.ts create mode 100644 app/api/transactions/route.ts create mode 100644 app/api/upload/confirm/route.ts create mode 100644 app/api/upload/route.ts create mode 100644 app/api/user/balance/route.ts create mode 100644 app/auth/page.tsx create mode 100644 app/balance/page.tsx create mode 100644 app/billing/page.tsx create mode 100644 app/contact/contact-client.tsx create mode 100644 app/contact/page.tsx create mode 100644 app/dashboard/page.tsx create mode 100644 app/error.tsx create mode 100644 app/features/page.tsx create mode 100644 app/feedback/feedback-client.tsx create mode 100644 app/feedback/page.tsx create mode 100644 app/forgot-password/page.tsx create mode 100644 app/globals.css create mode 100644 app/icon.tsx create mode 100644 app/layout.tsx create mode 100644 app/legal-agreement/page.tsx create mode 100644 app/not-found.tsx create mode 100644 app/page.tsx create mode 100644 app/payment/failed/page.tsx create mode 100644 app/payment/success/page.tsx create mode 100644 app/privacy/page.tsx create mode 100644 app/profile/page.tsx create mode 100644 app/refund-policy/page.tsx create mode 100644 app/robots.ts create mode 100644 app/services/cloud-instance/page.tsx create mode 100644 app/services/hosting-control-panel/page.tsx create mode 100644 app/services/human-developer/page.tsx create mode 100644 app/services/kubernetes/page.tsx create mode 100644 app/services/page.tsx create mode 100644 app/services/vpn/page.tsx create mode 100644 app/sitemap.ts create mode 100644 app/sw.js create mode 100644 app/terms/page.tsx create mode 100644 app/tools/page.tsx create mode 100644 app/tools/speech-to-text/page.tsx create mode 100644 app/tools/text-to-speech/page.tsx create mode 100644 app/tools/web-speech/page.tsx create mode 100644 app/topics/[slug]/edit/page.tsx create mode 100644 app/topics/[slug]/not-found.tsx create mode 100644 app/topics/[slug]/page.tsx create mode 100644 app/topics/edit/[id]/page.tsx create mode 100644 app/topics/new/page.tsx create mode 100644 app/topics/page.tsx create mode 100644 components/BlockNoteEditor/BlockNoteEditor.tsx create mode 100644 components/BlockNoteEditor/index.ts create mode 100644 components/BlockNoteEditor/styles.css create mode 100644 components/Features.tsx create mode 100644 components/Hero.tsx create mode 100644 components/Logo.tsx create mode 100644 components/PWAInstallPrompt.tsx create mode 100644 components/RecentTopics.tsx create mode 100644 components/StartupTest.tsx create mode 100644 components/Testimonials.tsx create mode 100644 components/admin/AdminLayout.tsx create mode 100644 components/admin/BillingManagement.tsx create mode 100644 components/admin/DashboardStats.tsx create mode 100644 components/admin/RecentActivity.tsx create mode 100644 components/admin/ReportsManagement.tsx create mode 100644 components/admin/ServiceManagement.tsx create mode 100644 components/admin/SystemSettings.tsx create mode 100644 components/admin/UserManagement.tsx create mode 100644 components/auth/GitHubSignInButton.tsx create mode 100644 components/auth/GoogleSignInButton.tsx create mode 100644 components/auth/LoginForm.tsx create mode 100644 components/auth/RegisterForm.tsx create mode 100644 components/auth/RequireAuth.tsx create mode 100644 components/balance/BalanceCard.tsx create mode 100644 components/balance/BalanceDisplay.tsx create mode 100644 components/balance/TransactionHistory.tsx create mode 100644 components/balance/index.ts create mode 100644 components/billing/BillingHistory.tsx create mode 100644 components/footer.tsx create mode 100644 components/header.tsx create mode 100644 components/profile/ProfileCard.tsx create mode 100644 components/profile/ProfileSettings.tsx create mode 100644 components/seo/SEO.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/theme-toggle.tsx create mode 100644 components/tools/speech-to-text-client.tsx create mode 100644 components/tools/text-to-speech.tsx create mode 100644 components/topics/ImageUpload.tsx create mode 100644 components/topics/SearchableTagSelect.tsx create mode 100644 components/topics/SimpleShareButtons.tsx create mode 100644 components/topics/TopicCard.tsx create mode 100644 components/topics/TopicClientComponents.tsx create mode 100644 components/topics/TopicContent.tsx create mode 100644 components/topics/TopicEditButton.tsx create mode 100644 components/topics/ViewTracker.tsx create mode 100644 components/ui/Link.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/custom-solution-cta.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 contexts/AuthContext.tsx create mode 100644 contexts/QueryProvider.tsx create mode 100644 cookies.txt create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 docs/API.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/PATTERNS.md create mode 100644 eslint.config.mjs create mode 100644 hooks/use-toast.ts create mode 100644 hooks/useProfileData.ts create mode 100644 lib/admin-middleware.ts create mode 100644 lib/analytics.tsx create mode 100644 lib/auth-middleware.ts create mode 100644 lib/balance-service.ts create mode 100644 lib/billing-service.ts create mode 100644 lib/cache.ts create mode 100644 lib/env.ts create mode 100644 lib/file-vault.ts create mode 100644 lib/google-oauth.ts create mode 100644 lib/jwt.ts create mode 100644 lib/lazy-loading.tsx create mode 100644 lib/minio.ts create mode 100644 lib/mongodb.ts create mode 100644 lib/proxy.ts create mode 100644 lib/redis.ts create mode 100644 lib/seo.ts create mode 100644 lib/session.ts create mode 100644 lib/siliconId.ts create mode 100644 lib/startup.ts create mode 100644 lib/storage.ts create mode 100644 lib/structured-data.ts create mode 100644 lib/system-settings.ts create mode 100644 lib/utils.ts create mode 100644 models/billing.ts create mode 100644 models/developer-request.ts create mode 100644 models/system-settings.ts create mode 100644 models/topic.ts create mode 100644 models/transaction.ts create mode 100644 models/user.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 public/favicon.ico create mode 100644 public/icons/icon-192x192.png create mode 100644 public/icons/icon-32x32.png create mode 100644 public/icons/icon-512x512.png create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100644 public/screenshots/desktop-wide.png create mode 100644 public/screenshots/mobile.png create mode 100644 pwa-test.html create mode 100644 scripts/create-admin.ts create mode 100644 scripts/init-mongo.js create mode 100644 scripts/reset-db.ts create mode 100644 scripts/seed.ts create mode 100644 tailwind.config.js create mode 100644 templates/README.md create mode 100644 templates/api-route.template.ts create mode 100644 templates/component.template.tsx create mode 100644 templates/hook.template.ts create mode 100644 templates/model.template.ts create mode 100644 templates/page.template.tsx create mode 100644 todo/CODE_QUALITY_IMPROVEMENTS.md create mode 100644 todo/PERFORMANCE_OPTIMIZATION.md create mode 100644 todo/README.md create mode 100644 todo/SECURITY_IMPROVEMENTS.md create mode 100644 todo/authentication-apis.md create mode 100644 todo/blog-system-remaining-features.md create mode 100644 todo/blogs-to-topics-migration.md create mode 100644 todo/profile-enhancement.md create mode 100644 todo/profile-tabs-routing.md create mode 100644 todo/services-backend-integration.md create mode 100644 tsconfig.json create mode 100644 types/react-speech-recognition.d.ts create mode 100644 types/speech-recognition.d.ts create mode 100644 yarn.lock 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 0000000000000000000000000000000000000000..c0ef52d17574a5bb1922ea30137b415737c7928d GIT binary patch literal 353432 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

N#KnAxtpl2#|`>}gO_EJZQEen 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

dCRx=A8)~2qlvV%MQ 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

JDe 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 literal 0 HcmV?d00001 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}

+ )} +
+
+ +
+ +