underrate commited on
Commit
d53e5b4
·
verified ·
1 Parent(s): 3a3c181

Initial commit

Browse files
Files changed (43) hide show
  1. .dockerignore +46 -0
  2. .gitignore +105 -0
  3. Dockerfile +58 -0
  4. check-db.ts +19 -0
  5. npm-install.log +0 -0
  6. package-lock.json +2079 -0
  7. package.json +45 -0
  8. prisma/dev.db +0 -0
  9. prisma/migrations/20260217145622_init_backend_final/migration.sql +96 -0
  10. prisma/migrations/20260217152351_add_token_version/migration.sql +19 -0
  11. prisma/migrations/migration_lock.toml +3 -0
  12. prisma/schema.prisma +99 -0
  13. prisma/seed-blog.ts +127 -0
  14. prisma/seed-portfolio.ts +72 -0
  15. prisma/seed.ts +37 -0
  16. prisma/tsconfig.json +15 -0
  17. scripts/check-role.ts +21 -0
  18. src/controllers/admin-blog.controller.ts +134 -0
  19. src/controllers/admin-inquiry.controller.ts +76 -0
  20. src/controllers/auth.controller.ts +110 -0
  21. src/controllers/blog-related.controller.ts +80 -0
  22. src/controllers/blog.controller.ts +64 -0
  23. src/controllers/portfolio.controller.ts +138 -0
  24. src/controllers/testimonial.controller.ts +129 -0
  25. src/controllers/upload.controller.ts +18 -0
  26. src/index.ts +55 -0
  27. src/middleware/auth.middleware.ts +39 -0
  28. src/middleware/csrf.ts +47 -0
  29. src/middleware/rate-limiter.ts +35 -0
  30. src/middleware/validate.ts +46 -0
  31. src/models/prisma.ts +5 -0
  32. src/routes/admin-blog.routes.ts +17 -0
  33. src/routes/admin-inquiry.routes.ts +10 -0
  34. src/routes/auth.routes.ts +11 -0
  35. src/routes/blog.routes.ts +11 -0
  36. src/routes/inquiry.routes.ts +84 -0
  37. src/routes/portfolio.routes.ts +22 -0
  38. src/routes/public-portfolio.routes.ts +37 -0
  39. src/routes/public-testimonials.routes.ts +29 -0
  40. src/routes/testimonial.routes.ts +24 -0
  41. src/routes/upload.routes.ts +59 -0
  42. src/services/email.service.ts +117 -0
  43. tsconfig.json +21 -0
.dockerignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Build output
8
+ dist
9
+ build
10
+
11
+ # Database
12
+ *.db
13
+ *.db-journal
14
+ prisma/dev.db
15
+ prisma/*.db
16
+
17
+ # Testing
18
+ coverage
19
+
20
+ # IDE
21
+ .idea
22
+ .vscode
23
+ *.swp
24
+ *.swo
25
+
26
+ # OS
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Git
31
+ .git
32
+ .gitignore
33
+
34
+ # Docker
35
+ Dockerfile
36
+ .dockerignore
37
+ docker-compose*
38
+
39
+ # Uploads (will be created at runtime)
40
+ uploads/*
41
+
42
+ # Misc
43
+ *.log
44
+ *.md
45
+ .env*
46
+ !.env.example
.gitignore ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+
9
+ # Diagnostic reports (https://nodejs.org/api/report.html)
10
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11
+
12
+ # Runtime data
13
+ pids
14
+ *.pid
15
+ *.seed
16
+ *.pid.lock
17
+
18
+ # Directory for instrumented libs generated by jscoverage/JSCover
19
+ lib-cov
20
+
21
+ # Coverage directory used by tools like istanbul
22
+ coverage
23
+ *.lcov
24
+
25
+ # nyc test coverage
26
+ .nyc_output
27
+
28
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29
+ .grunt
30
+
31
+ # Bower dependency directory (https://bower.io/)
32
+ bower_components
33
+
34
+ # node-waf configuration
35
+ .lock-wscript
36
+
37
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
38
+ build/Release
39
+
40
+ # Dependency directories
41
+ node_modules/
42
+ jspm_packages/
43
+
44
+ # TypeScript v1 declaration files
45
+ typings/
46
+
47
+ # TypeScript cache
48
+ *.tsbuildinfo
49
+
50
+ # Optional npm cache directory
51
+ .npm
52
+
53
+ # Optional eslint cache
54
+ .eslintcache
55
+
56
+ # Microbundle cache
57
+ .rpt2_cache/
58
+ .rts2_cache_cjs/
59
+ .rts2_cache_es/
60
+ .rts2_cache_umd/
61
+
62
+ # Optional REPL history
63
+ .node_repl_history
64
+
65
+ # Output of 'npm pack'
66
+ *.tgz
67
+
68
+ # Yarn Integrity file
69
+ .yarn-integrity
70
+
71
+ # dotenv environment variables file
72
+ .env
73
+ .env.test
74
+
75
+ # parcel-bundler cache (https://parceljs.org/)
76
+ .cache
77
+
78
+ # next.js build output
79
+ .next
80
+
81
+ # nuxt.js build output
82
+ .nuxt
83
+
84
+ # vuepress build output
85
+ .vuepress/dist
86
+
87
+ # serverless directories
88
+ .serverless/
89
+
90
+ # FuseBox cache
91
+ .fusebox/
92
+
93
+ # DynamoDB Local files
94
+ .dynamodb/
95
+
96
+ # TernJS port file
97
+ .tern-port
98
+
99
+ # dist/build folders
100
+ dist/
101
+ dist-*
102
+
103
+ # Prisma
104
+ /prisma/data.db
105
+ /lib/generated/prisma
Dockerfile ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package files
7
+ COPY package.json package-lock.json* ./
8
+
9
+ # Install dependencies
10
+ RUN npm ci
11
+
12
+ # Copy Prisma schema
13
+ COPY prisma ./prisma/
14
+
15
+ # Generate Prisma client
16
+ RUN npx prisma generate
17
+
18
+ # Copy source files
19
+ COPY . .
20
+
21
+ # Build TypeScript
22
+ RUN npm run build
23
+
24
+ # Production stage
25
+ FROM node:20-alpine AS runner
26
+
27
+ WORKDIR /app
28
+
29
+ # Set to production
30
+ ENV NODE_ENV=production
31
+
32
+ # Create non-root user for security
33
+ RUN addgroup --system --gid 1001 nodejs
34
+ RUN adduser --system --uid 1001 appuser
35
+
36
+ # Copy necessary files from builder
37
+ COPY --from=builder /app/dist ./dist
38
+ COPY --from=builder /app/node_modules ./node_modules
39
+ COPY --from=builder /app/package.json ./
40
+ COPY --from=builder /app/prisma ./prisma
41
+
42
+ # Create uploads directory
43
+ RUN mkdir -p uploads && chown -R appuser:nodejs uploads
44
+
45
+ # Set correct ownership
46
+ RUN chown -R appuser:nodejs /app
47
+
48
+ # Switch to non-root user
49
+ USER appuser
50
+
51
+ # Expose port (Hugging Face Spaces uses 7860)
52
+ EXPOSE 7860
53
+
54
+ # Set port
55
+ ENV PORT=7860
56
+
57
+ # Start the application
58
+ CMD ["node", "dist/index.js"]
check-db.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ async function main() {
6
+ console.log("Fetching all blog posts...");
7
+ const posts = await prisma.blogPost.findMany();
8
+ console.log(`Total posts in DB: ${posts.length}`);
9
+
10
+ if (posts.length > 0) {
11
+ console.log("First post sample:", JSON.stringify(posts[0], null, 2));
12
+ } else {
13
+ console.log("No posts found. Seed failed or ran on wrong DB?");
14
+ }
15
+ }
16
+
17
+ main()
18
+ .catch(console.error)
19
+ .finally(() => prisma.$disconnect());
npm-install.log ADDED
Binary file (534 Bytes). View file
 
package-lock.json ADDED
@@ -0,0 +1,2079 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "web-services-backend",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "web-services-backend",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@prisma/client": "^5.10.0",
13
+ "bcryptjs": "^2.4.3",
14
+ "cookie-parser": "^1.4.7",
15
+ "cors": "^2.8.5",
16
+ "dotenv": "^16.4.5",
17
+ "express": "^4.18.2",
18
+ "express-rate-limit": "^8.2.1",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "multer": "^2.0.2",
21
+ "nodemailer": "^8.0.1",
22
+ "slugify": "^1.6.6",
23
+ "xss": "^1.0.15",
24
+ "zod": "^3.25.76"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bcryptjs": "^2.4.6",
28
+ "@types/cookie-parser": "^1.4.10",
29
+ "@types/cors": "^2.8.17",
30
+ "@types/express": "^4.17.21",
31
+ "@types/jsonwebtoken": "^9.0.5",
32
+ "@types/multer": "^2.0.0",
33
+ "@types/node": "^20.11.19",
34
+ "@types/nodemailer": "^7.0.10",
35
+ "prisma": "^5.10.0",
36
+ "tsx": "^4.21.0",
37
+ "typescript": "^5.3.3"
38
+ }
39
+ },
40
+ "node_modules/@esbuild/aix-ppc64": {
41
+ "version": "0.27.3",
42
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
43
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
44
+ "cpu": [
45
+ "ppc64"
46
+ ],
47
+ "dev": true,
48
+ "license": "MIT",
49
+ "optional": true,
50
+ "os": [
51
+ "aix"
52
+ ],
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ },
57
+ "node_modules/@esbuild/android-arm": {
58
+ "version": "0.27.3",
59
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
60
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
61
+ "cpu": [
62
+ "arm"
63
+ ],
64
+ "dev": true,
65
+ "license": "MIT",
66
+ "optional": true,
67
+ "os": [
68
+ "android"
69
+ ],
70
+ "engines": {
71
+ "node": ">=18"
72
+ }
73
+ },
74
+ "node_modules/@esbuild/android-arm64": {
75
+ "version": "0.27.3",
76
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
77
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
78
+ "cpu": [
79
+ "arm64"
80
+ ],
81
+ "dev": true,
82
+ "license": "MIT",
83
+ "optional": true,
84
+ "os": [
85
+ "android"
86
+ ],
87
+ "engines": {
88
+ "node": ">=18"
89
+ }
90
+ },
91
+ "node_modules/@esbuild/android-x64": {
92
+ "version": "0.27.3",
93
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
94
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
95
+ "cpu": [
96
+ "x64"
97
+ ],
98
+ "dev": true,
99
+ "license": "MIT",
100
+ "optional": true,
101
+ "os": [
102
+ "android"
103
+ ],
104
+ "engines": {
105
+ "node": ">=18"
106
+ }
107
+ },
108
+ "node_modules/@esbuild/darwin-arm64": {
109
+ "version": "0.27.3",
110
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
111
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
112
+ "cpu": [
113
+ "arm64"
114
+ ],
115
+ "dev": true,
116
+ "license": "MIT",
117
+ "optional": true,
118
+ "os": [
119
+ "darwin"
120
+ ],
121
+ "engines": {
122
+ "node": ">=18"
123
+ }
124
+ },
125
+ "node_modules/@esbuild/darwin-x64": {
126
+ "version": "0.27.3",
127
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
128
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
129
+ "cpu": [
130
+ "x64"
131
+ ],
132
+ "dev": true,
133
+ "license": "MIT",
134
+ "optional": true,
135
+ "os": [
136
+ "darwin"
137
+ ],
138
+ "engines": {
139
+ "node": ">=18"
140
+ }
141
+ },
142
+ "node_modules/@esbuild/freebsd-arm64": {
143
+ "version": "0.27.3",
144
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
145
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
146
+ "cpu": [
147
+ "arm64"
148
+ ],
149
+ "dev": true,
150
+ "license": "MIT",
151
+ "optional": true,
152
+ "os": [
153
+ "freebsd"
154
+ ],
155
+ "engines": {
156
+ "node": ">=18"
157
+ }
158
+ },
159
+ "node_modules/@esbuild/freebsd-x64": {
160
+ "version": "0.27.3",
161
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
162
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
163
+ "cpu": [
164
+ "x64"
165
+ ],
166
+ "dev": true,
167
+ "license": "MIT",
168
+ "optional": true,
169
+ "os": [
170
+ "freebsd"
171
+ ],
172
+ "engines": {
173
+ "node": ">=18"
174
+ }
175
+ },
176
+ "node_modules/@esbuild/linux-arm": {
177
+ "version": "0.27.3",
178
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
179
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
180
+ "cpu": [
181
+ "arm"
182
+ ],
183
+ "dev": true,
184
+ "license": "MIT",
185
+ "optional": true,
186
+ "os": [
187
+ "linux"
188
+ ],
189
+ "engines": {
190
+ "node": ">=18"
191
+ }
192
+ },
193
+ "node_modules/@esbuild/linux-arm64": {
194
+ "version": "0.27.3",
195
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
196
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
197
+ "cpu": [
198
+ "arm64"
199
+ ],
200
+ "dev": true,
201
+ "license": "MIT",
202
+ "optional": true,
203
+ "os": [
204
+ "linux"
205
+ ],
206
+ "engines": {
207
+ "node": ">=18"
208
+ }
209
+ },
210
+ "node_modules/@esbuild/linux-ia32": {
211
+ "version": "0.27.3",
212
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
213
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
214
+ "cpu": [
215
+ "ia32"
216
+ ],
217
+ "dev": true,
218
+ "license": "MIT",
219
+ "optional": true,
220
+ "os": [
221
+ "linux"
222
+ ],
223
+ "engines": {
224
+ "node": ">=18"
225
+ }
226
+ },
227
+ "node_modules/@esbuild/linux-loong64": {
228
+ "version": "0.27.3",
229
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
230
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
231
+ "cpu": [
232
+ "loong64"
233
+ ],
234
+ "dev": true,
235
+ "license": "MIT",
236
+ "optional": true,
237
+ "os": [
238
+ "linux"
239
+ ],
240
+ "engines": {
241
+ "node": ">=18"
242
+ }
243
+ },
244
+ "node_modules/@esbuild/linux-mips64el": {
245
+ "version": "0.27.3",
246
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
247
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
248
+ "cpu": [
249
+ "mips64el"
250
+ ],
251
+ "dev": true,
252
+ "license": "MIT",
253
+ "optional": true,
254
+ "os": [
255
+ "linux"
256
+ ],
257
+ "engines": {
258
+ "node": ">=18"
259
+ }
260
+ },
261
+ "node_modules/@esbuild/linux-ppc64": {
262
+ "version": "0.27.3",
263
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
264
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
265
+ "cpu": [
266
+ "ppc64"
267
+ ],
268
+ "dev": true,
269
+ "license": "MIT",
270
+ "optional": true,
271
+ "os": [
272
+ "linux"
273
+ ],
274
+ "engines": {
275
+ "node": ">=18"
276
+ }
277
+ },
278
+ "node_modules/@esbuild/linux-riscv64": {
279
+ "version": "0.27.3",
280
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
281
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
282
+ "cpu": [
283
+ "riscv64"
284
+ ],
285
+ "dev": true,
286
+ "license": "MIT",
287
+ "optional": true,
288
+ "os": [
289
+ "linux"
290
+ ],
291
+ "engines": {
292
+ "node": ">=18"
293
+ }
294
+ },
295
+ "node_modules/@esbuild/linux-s390x": {
296
+ "version": "0.27.3",
297
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
298
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
299
+ "cpu": [
300
+ "s390x"
301
+ ],
302
+ "dev": true,
303
+ "license": "MIT",
304
+ "optional": true,
305
+ "os": [
306
+ "linux"
307
+ ],
308
+ "engines": {
309
+ "node": ">=18"
310
+ }
311
+ },
312
+ "node_modules/@esbuild/linux-x64": {
313
+ "version": "0.27.3",
314
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
315
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
316
+ "cpu": [
317
+ "x64"
318
+ ],
319
+ "dev": true,
320
+ "license": "MIT",
321
+ "optional": true,
322
+ "os": [
323
+ "linux"
324
+ ],
325
+ "engines": {
326
+ "node": ">=18"
327
+ }
328
+ },
329
+ "node_modules/@esbuild/netbsd-arm64": {
330
+ "version": "0.27.3",
331
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
332
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
333
+ "cpu": [
334
+ "arm64"
335
+ ],
336
+ "dev": true,
337
+ "license": "MIT",
338
+ "optional": true,
339
+ "os": [
340
+ "netbsd"
341
+ ],
342
+ "engines": {
343
+ "node": ">=18"
344
+ }
345
+ },
346
+ "node_modules/@esbuild/netbsd-x64": {
347
+ "version": "0.27.3",
348
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
349
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
350
+ "cpu": [
351
+ "x64"
352
+ ],
353
+ "dev": true,
354
+ "license": "MIT",
355
+ "optional": true,
356
+ "os": [
357
+ "netbsd"
358
+ ],
359
+ "engines": {
360
+ "node": ">=18"
361
+ }
362
+ },
363
+ "node_modules/@esbuild/openbsd-arm64": {
364
+ "version": "0.27.3",
365
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
366
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
367
+ "cpu": [
368
+ "arm64"
369
+ ],
370
+ "dev": true,
371
+ "license": "MIT",
372
+ "optional": true,
373
+ "os": [
374
+ "openbsd"
375
+ ],
376
+ "engines": {
377
+ "node": ">=18"
378
+ }
379
+ },
380
+ "node_modules/@esbuild/openbsd-x64": {
381
+ "version": "0.27.3",
382
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
383
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
384
+ "cpu": [
385
+ "x64"
386
+ ],
387
+ "dev": true,
388
+ "license": "MIT",
389
+ "optional": true,
390
+ "os": [
391
+ "openbsd"
392
+ ],
393
+ "engines": {
394
+ "node": ">=18"
395
+ }
396
+ },
397
+ "node_modules/@esbuild/openharmony-arm64": {
398
+ "version": "0.27.3",
399
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
400
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
401
+ "cpu": [
402
+ "arm64"
403
+ ],
404
+ "dev": true,
405
+ "license": "MIT",
406
+ "optional": true,
407
+ "os": [
408
+ "openharmony"
409
+ ],
410
+ "engines": {
411
+ "node": ">=18"
412
+ }
413
+ },
414
+ "node_modules/@esbuild/sunos-x64": {
415
+ "version": "0.27.3",
416
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
417
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
418
+ "cpu": [
419
+ "x64"
420
+ ],
421
+ "dev": true,
422
+ "license": "MIT",
423
+ "optional": true,
424
+ "os": [
425
+ "sunos"
426
+ ],
427
+ "engines": {
428
+ "node": ">=18"
429
+ }
430
+ },
431
+ "node_modules/@esbuild/win32-arm64": {
432
+ "version": "0.27.3",
433
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
434
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
435
+ "cpu": [
436
+ "arm64"
437
+ ],
438
+ "dev": true,
439
+ "license": "MIT",
440
+ "optional": true,
441
+ "os": [
442
+ "win32"
443
+ ],
444
+ "engines": {
445
+ "node": ">=18"
446
+ }
447
+ },
448
+ "node_modules/@esbuild/win32-ia32": {
449
+ "version": "0.27.3",
450
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
451
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
452
+ "cpu": [
453
+ "ia32"
454
+ ],
455
+ "dev": true,
456
+ "license": "MIT",
457
+ "optional": true,
458
+ "os": [
459
+ "win32"
460
+ ],
461
+ "engines": {
462
+ "node": ">=18"
463
+ }
464
+ },
465
+ "node_modules/@esbuild/win32-x64": {
466
+ "version": "0.27.3",
467
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
468
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
469
+ "cpu": [
470
+ "x64"
471
+ ],
472
+ "dev": true,
473
+ "license": "MIT",
474
+ "optional": true,
475
+ "os": [
476
+ "win32"
477
+ ],
478
+ "engines": {
479
+ "node": ">=18"
480
+ }
481
+ },
482
+ "node_modules/@prisma/client": {
483
+ "version": "5.22.0",
484
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
485
+ "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
486
+ "hasInstallScript": true,
487
+ "license": "Apache-2.0",
488
+ "engines": {
489
+ "node": ">=16.13"
490
+ },
491
+ "peerDependencies": {
492
+ "prisma": "*"
493
+ },
494
+ "peerDependenciesMeta": {
495
+ "prisma": {
496
+ "optional": true
497
+ }
498
+ }
499
+ },
500
+ "node_modules/@prisma/debug": {
501
+ "version": "5.22.0",
502
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
503
+ "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
504
+ "devOptional": true,
505
+ "license": "Apache-2.0"
506
+ },
507
+ "node_modules/@prisma/engines": {
508
+ "version": "5.22.0",
509
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
510
+ "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
511
+ "devOptional": true,
512
+ "hasInstallScript": true,
513
+ "license": "Apache-2.0",
514
+ "dependencies": {
515
+ "@prisma/debug": "5.22.0",
516
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
517
+ "@prisma/fetch-engine": "5.22.0",
518
+ "@prisma/get-platform": "5.22.0"
519
+ }
520
+ },
521
+ "node_modules/@prisma/engines-version": {
522
+ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
523
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
524
+ "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
525
+ "devOptional": true,
526
+ "license": "Apache-2.0"
527
+ },
528
+ "node_modules/@prisma/fetch-engine": {
529
+ "version": "5.22.0",
530
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
531
+ "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
532
+ "devOptional": true,
533
+ "license": "Apache-2.0",
534
+ "dependencies": {
535
+ "@prisma/debug": "5.22.0",
536
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
537
+ "@prisma/get-platform": "5.22.0"
538
+ }
539
+ },
540
+ "node_modules/@prisma/get-platform": {
541
+ "version": "5.22.0",
542
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
543
+ "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
544
+ "devOptional": true,
545
+ "license": "Apache-2.0",
546
+ "dependencies": {
547
+ "@prisma/debug": "5.22.0"
548
+ }
549
+ },
550
+ "node_modules/@types/bcryptjs": {
551
+ "version": "2.4.6",
552
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
553
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
554
+ "dev": true,
555
+ "license": "MIT"
556
+ },
557
+ "node_modules/@types/body-parser": {
558
+ "version": "1.19.6",
559
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
560
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
561
+ "dev": true,
562
+ "license": "MIT",
563
+ "dependencies": {
564
+ "@types/connect": "*",
565
+ "@types/node": "*"
566
+ }
567
+ },
568
+ "node_modules/@types/connect": {
569
+ "version": "3.4.38",
570
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
571
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
572
+ "dev": true,
573
+ "license": "MIT",
574
+ "dependencies": {
575
+ "@types/node": "*"
576
+ }
577
+ },
578
+ "node_modules/@types/cookie-parser": {
579
+ "version": "1.4.10",
580
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
581
+ "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
582
+ "dev": true,
583
+ "license": "MIT",
584
+ "peerDependencies": {
585
+ "@types/express": "*"
586
+ }
587
+ },
588
+ "node_modules/@types/cors": {
589
+ "version": "2.8.19",
590
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
591
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
592
+ "dev": true,
593
+ "license": "MIT",
594
+ "dependencies": {
595
+ "@types/node": "*"
596
+ }
597
+ },
598
+ "node_modules/@types/express": {
599
+ "version": "4.17.25",
600
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
601
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
602
+ "dev": true,
603
+ "license": "MIT",
604
+ "dependencies": {
605
+ "@types/body-parser": "*",
606
+ "@types/express-serve-static-core": "^4.17.33",
607
+ "@types/qs": "*",
608
+ "@types/serve-static": "^1"
609
+ }
610
+ },
611
+ "node_modules/@types/express-serve-static-core": {
612
+ "version": "4.19.8",
613
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
614
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
615
+ "dev": true,
616
+ "license": "MIT",
617
+ "dependencies": {
618
+ "@types/node": "*",
619
+ "@types/qs": "*",
620
+ "@types/range-parser": "*",
621
+ "@types/send": "*"
622
+ }
623
+ },
624
+ "node_modules/@types/http-errors": {
625
+ "version": "2.0.5",
626
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
627
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
628
+ "dev": true,
629
+ "license": "MIT"
630
+ },
631
+ "node_modules/@types/jsonwebtoken": {
632
+ "version": "9.0.10",
633
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
634
+ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
635
+ "dev": true,
636
+ "license": "MIT",
637
+ "dependencies": {
638
+ "@types/ms": "*",
639
+ "@types/node": "*"
640
+ }
641
+ },
642
+ "node_modules/@types/mime": {
643
+ "version": "1.3.5",
644
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
645
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
646
+ "dev": true,
647
+ "license": "MIT"
648
+ },
649
+ "node_modules/@types/ms": {
650
+ "version": "2.1.0",
651
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
652
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
653
+ "dev": true,
654
+ "license": "MIT"
655
+ },
656
+ "node_modules/@types/multer": {
657
+ "version": "2.0.0",
658
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
659
+ "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
660
+ "dev": true,
661
+ "license": "MIT",
662
+ "dependencies": {
663
+ "@types/express": "*"
664
+ }
665
+ },
666
+ "node_modules/@types/node": {
667
+ "version": "20.19.33",
668
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
669
+ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
670
+ "dev": true,
671
+ "license": "MIT",
672
+ "dependencies": {
673
+ "undici-types": "~6.21.0"
674
+ }
675
+ },
676
+ "node_modules/@types/nodemailer": {
677
+ "version": "7.0.10",
678
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.10.tgz",
679
+ "integrity": "sha512-tP+9WggTFN22Zxh0XFyst7239H0qwiRCogsk7v9aQS79sYAJY+WEbTHbNYcxUMaalHKmsNpxmoTe35hBEMMd6g==",
680
+ "dev": true,
681
+ "license": "MIT",
682
+ "dependencies": {
683
+ "@types/node": "*"
684
+ }
685
+ },
686
+ "node_modules/@types/qs": {
687
+ "version": "6.14.0",
688
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
689
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
690
+ "dev": true,
691
+ "license": "MIT"
692
+ },
693
+ "node_modules/@types/range-parser": {
694
+ "version": "1.2.7",
695
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
696
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
697
+ "dev": true,
698
+ "license": "MIT"
699
+ },
700
+ "node_modules/@types/send": {
701
+ "version": "1.2.1",
702
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
703
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
704
+ "dev": true,
705
+ "license": "MIT",
706
+ "dependencies": {
707
+ "@types/node": "*"
708
+ }
709
+ },
710
+ "node_modules/@types/serve-static": {
711
+ "version": "1.15.10",
712
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
713
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
714
+ "dev": true,
715
+ "license": "MIT",
716
+ "dependencies": {
717
+ "@types/http-errors": "*",
718
+ "@types/node": "*",
719
+ "@types/send": "<1"
720
+ }
721
+ },
722
+ "node_modules/@types/serve-static/node_modules/@types/send": {
723
+ "version": "0.17.6",
724
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
725
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
726
+ "dev": true,
727
+ "license": "MIT",
728
+ "dependencies": {
729
+ "@types/mime": "^1",
730
+ "@types/node": "*"
731
+ }
732
+ },
733
+ "node_modules/accepts": {
734
+ "version": "1.3.8",
735
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
736
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
737
+ "license": "MIT",
738
+ "dependencies": {
739
+ "mime-types": "~2.1.34",
740
+ "negotiator": "0.6.3"
741
+ },
742
+ "engines": {
743
+ "node": ">= 0.6"
744
+ }
745
+ },
746
+ "node_modules/append-field": {
747
+ "version": "1.0.0",
748
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
749
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
750
+ "license": "MIT"
751
+ },
752
+ "node_modules/array-flatten": {
753
+ "version": "1.1.1",
754
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
755
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
756
+ "license": "MIT"
757
+ },
758
+ "node_modules/bcryptjs": {
759
+ "version": "2.4.3",
760
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
761
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
762
+ "license": "MIT"
763
+ },
764
+ "node_modules/body-parser": {
765
+ "version": "1.20.4",
766
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
767
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
768
+ "license": "MIT",
769
+ "dependencies": {
770
+ "bytes": "~3.1.2",
771
+ "content-type": "~1.0.5",
772
+ "debug": "2.6.9",
773
+ "depd": "2.0.0",
774
+ "destroy": "~1.2.0",
775
+ "http-errors": "~2.0.1",
776
+ "iconv-lite": "~0.4.24",
777
+ "on-finished": "~2.4.1",
778
+ "qs": "~6.14.0",
779
+ "raw-body": "~2.5.3",
780
+ "type-is": "~1.6.18",
781
+ "unpipe": "~1.0.0"
782
+ },
783
+ "engines": {
784
+ "node": ">= 0.8",
785
+ "npm": "1.2.8000 || >= 1.4.16"
786
+ }
787
+ },
788
+ "node_modules/buffer-equal-constant-time": {
789
+ "version": "1.0.1",
790
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
791
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
792
+ "license": "BSD-3-Clause"
793
+ },
794
+ "node_modules/buffer-from": {
795
+ "version": "1.1.2",
796
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
797
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
798
+ "license": "MIT"
799
+ },
800
+ "node_modules/busboy": {
801
+ "version": "1.6.0",
802
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
803
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
804
+ "dependencies": {
805
+ "streamsearch": "^1.1.0"
806
+ },
807
+ "engines": {
808
+ "node": ">=10.16.0"
809
+ }
810
+ },
811
+ "node_modules/bytes": {
812
+ "version": "3.1.2",
813
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
814
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
815
+ "license": "MIT",
816
+ "engines": {
817
+ "node": ">= 0.8"
818
+ }
819
+ },
820
+ "node_modules/call-bind-apply-helpers": {
821
+ "version": "1.0.2",
822
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
823
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
824
+ "license": "MIT",
825
+ "dependencies": {
826
+ "es-errors": "^1.3.0",
827
+ "function-bind": "^1.1.2"
828
+ },
829
+ "engines": {
830
+ "node": ">= 0.4"
831
+ }
832
+ },
833
+ "node_modules/call-bound": {
834
+ "version": "1.0.4",
835
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
836
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
837
+ "license": "MIT",
838
+ "dependencies": {
839
+ "call-bind-apply-helpers": "^1.0.2",
840
+ "get-intrinsic": "^1.3.0"
841
+ },
842
+ "engines": {
843
+ "node": ">= 0.4"
844
+ },
845
+ "funding": {
846
+ "url": "https://github.com/sponsors/ljharb"
847
+ }
848
+ },
849
+ "node_modules/commander": {
850
+ "version": "2.20.3",
851
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
852
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
853
+ "license": "MIT"
854
+ },
855
+ "node_modules/concat-stream": {
856
+ "version": "2.0.0",
857
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
858
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
859
+ "engines": [
860
+ "node >= 6.0"
861
+ ],
862
+ "license": "MIT",
863
+ "dependencies": {
864
+ "buffer-from": "^1.0.0",
865
+ "inherits": "^2.0.3",
866
+ "readable-stream": "^3.0.2",
867
+ "typedarray": "^0.0.6"
868
+ }
869
+ },
870
+ "node_modules/content-disposition": {
871
+ "version": "0.5.4",
872
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
873
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
874
+ "license": "MIT",
875
+ "dependencies": {
876
+ "safe-buffer": "5.2.1"
877
+ },
878
+ "engines": {
879
+ "node": ">= 0.6"
880
+ }
881
+ },
882
+ "node_modules/content-type": {
883
+ "version": "1.0.5",
884
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
885
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
886
+ "license": "MIT",
887
+ "engines": {
888
+ "node": ">= 0.6"
889
+ }
890
+ },
891
+ "node_modules/cookie": {
892
+ "version": "0.7.2",
893
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
894
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
895
+ "license": "MIT",
896
+ "engines": {
897
+ "node": ">= 0.6"
898
+ }
899
+ },
900
+ "node_modules/cookie-parser": {
901
+ "version": "1.4.7",
902
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
903
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
904
+ "license": "MIT",
905
+ "dependencies": {
906
+ "cookie": "0.7.2",
907
+ "cookie-signature": "1.0.6"
908
+ },
909
+ "engines": {
910
+ "node": ">= 0.8.0"
911
+ }
912
+ },
913
+ "node_modules/cookie-parser/node_modules/cookie-signature": {
914
+ "version": "1.0.6",
915
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
916
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
917
+ "license": "MIT"
918
+ },
919
+ "node_modules/cookie-signature": {
920
+ "version": "1.0.7",
921
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
922
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
923
+ "license": "MIT"
924
+ },
925
+ "node_modules/cors": {
926
+ "version": "2.8.6",
927
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
928
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
929
+ "license": "MIT",
930
+ "dependencies": {
931
+ "object-assign": "^4",
932
+ "vary": "^1"
933
+ },
934
+ "engines": {
935
+ "node": ">= 0.10"
936
+ },
937
+ "funding": {
938
+ "type": "opencollective",
939
+ "url": "https://opencollective.com/express"
940
+ }
941
+ },
942
+ "node_modules/cssfilter": {
943
+ "version": "0.0.10",
944
+ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
945
+ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==",
946
+ "license": "MIT"
947
+ },
948
+ "node_modules/debug": {
949
+ "version": "2.6.9",
950
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
951
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
952
+ "license": "MIT",
953
+ "dependencies": {
954
+ "ms": "2.0.0"
955
+ }
956
+ },
957
+ "node_modules/depd": {
958
+ "version": "2.0.0",
959
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
960
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
961
+ "license": "MIT",
962
+ "engines": {
963
+ "node": ">= 0.8"
964
+ }
965
+ },
966
+ "node_modules/destroy": {
967
+ "version": "1.2.0",
968
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
969
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
970
+ "license": "MIT",
971
+ "engines": {
972
+ "node": ">= 0.8",
973
+ "npm": "1.2.8000 || >= 1.4.16"
974
+ }
975
+ },
976
+ "node_modules/dotenv": {
977
+ "version": "16.6.1",
978
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
979
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
980
+ "license": "BSD-2-Clause",
981
+ "engines": {
982
+ "node": ">=12"
983
+ },
984
+ "funding": {
985
+ "url": "https://dotenvx.com"
986
+ }
987
+ },
988
+ "node_modules/dunder-proto": {
989
+ "version": "1.0.1",
990
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
991
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
992
+ "license": "MIT",
993
+ "dependencies": {
994
+ "call-bind-apply-helpers": "^1.0.1",
995
+ "es-errors": "^1.3.0",
996
+ "gopd": "^1.2.0"
997
+ },
998
+ "engines": {
999
+ "node": ">= 0.4"
1000
+ }
1001
+ },
1002
+ "node_modules/ecdsa-sig-formatter": {
1003
+ "version": "1.0.11",
1004
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
1005
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
1006
+ "license": "Apache-2.0",
1007
+ "dependencies": {
1008
+ "safe-buffer": "^5.0.1"
1009
+ }
1010
+ },
1011
+ "node_modules/ee-first": {
1012
+ "version": "1.1.1",
1013
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
1014
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
1015
+ "license": "MIT"
1016
+ },
1017
+ "node_modules/encodeurl": {
1018
+ "version": "2.0.0",
1019
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
1020
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
1021
+ "license": "MIT",
1022
+ "engines": {
1023
+ "node": ">= 0.8"
1024
+ }
1025
+ },
1026
+ "node_modules/es-define-property": {
1027
+ "version": "1.0.1",
1028
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1029
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1030
+ "license": "MIT",
1031
+ "engines": {
1032
+ "node": ">= 0.4"
1033
+ }
1034
+ },
1035
+ "node_modules/es-errors": {
1036
+ "version": "1.3.0",
1037
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1038
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1039
+ "license": "MIT",
1040
+ "engines": {
1041
+ "node": ">= 0.4"
1042
+ }
1043
+ },
1044
+ "node_modules/es-object-atoms": {
1045
+ "version": "1.1.1",
1046
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1047
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1048
+ "license": "MIT",
1049
+ "dependencies": {
1050
+ "es-errors": "^1.3.0"
1051
+ },
1052
+ "engines": {
1053
+ "node": ">= 0.4"
1054
+ }
1055
+ },
1056
+ "node_modules/esbuild": {
1057
+ "version": "0.27.3",
1058
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
1059
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
1060
+ "dev": true,
1061
+ "hasInstallScript": true,
1062
+ "license": "MIT",
1063
+ "bin": {
1064
+ "esbuild": "bin/esbuild"
1065
+ },
1066
+ "engines": {
1067
+ "node": ">=18"
1068
+ },
1069
+ "optionalDependencies": {
1070
+ "@esbuild/aix-ppc64": "0.27.3",
1071
+ "@esbuild/android-arm": "0.27.3",
1072
+ "@esbuild/android-arm64": "0.27.3",
1073
+ "@esbuild/android-x64": "0.27.3",
1074
+ "@esbuild/darwin-arm64": "0.27.3",
1075
+ "@esbuild/darwin-x64": "0.27.3",
1076
+ "@esbuild/freebsd-arm64": "0.27.3",
1077
+ "@esbuild/freebsd-x64": "0.27.3",
1078
+ "@esbuild/linux-arm": "0.27.3",
1079
+ "@esbuild/linux-arm64": "0.27.3",
1080
+ "@esbuild/linux-ia32": "0.27.3",
1081
+ "@esbuild/linux-loong64": "0.27.3",
1082
+ "@esbuild/linux-mips64el": "0.27.3",
1083
+ "@esbuild/linux-ppc64": "0.27.3",
1084
+ "@esbuild/linux-riscv64": "0.27.3",
1085
+ "@esbuild/linux-s390x": "0.27.3",
1086
+ "@esbuild/linux-x64": "0.27.3",
1087
+ "@esbuild/netbsd-arm64": "0.27.3",
1088
+ "@esbuild/netbsd-x64": "0.27.3",
1089
+ "@esbuild/openbsd-arm64": "0.27.3",
1090
+ "@esbuild/openbsd-x64": "0.27.3",
1091
+ "@esbuild/openharmony-arm64": "0.27.3",
1092
+ "@esbuild/sunos-x64": "0.27.3",
1093
+ "@esbuild/win32-arm64": "0.27.3",
1094
+ "@esbuild/win32-ia32": "0.27.3",
1095
+ "@esbuild/win32-x64": "0.27.3"
1096
+ }
1097
+ },
1098
+ "node_modules/escape-html": {
1099
+ "version": "1.0.3",
1100
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
1101
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
1102
+ "license": "MIT"
1103
+ },
1104
+ "node_modules/etag": {
1105
+ "version": "1.8.1",
1106
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
1107
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
1108
+ "license": "MIT",
1109
+ "engines": {
1110
+ "node": ">= 0.6"
1111
+ }
1112
+ },
1113
+ "node_modules/express": {
1114
+ "version": "4.22.1",
1115
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
1116
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
1117
+ "license": "MIT",
1118
+ "dependencies": {
1119
+ "accepts": "~1.3.8",
1120
+ "array-flatten": "1.1.1",
1121
+ "body-parser": "~1.20.3",
1122
+ "content-disposition": "~0.5.4",
1123
+ "content-type": "~1.0.4",
1124
+ "cookie": "~0.7.1",
1125
+ "cookie-signature": "~1.0.6",
1126
+ "debug": "2.6.9",
1127
+ "depd": "2.0.0",
1128
+ "encodeurl": "~2.0.0",
1129
+ "escape-html": "~1.0.3",
1130
+ "etag": "~1.8.1",
1131
+ "finalhandler": "~1.3.1",
1132
+ "fresh": "~0.5.2",
1133
+ "http-errors": "~2.0.0",
1134
+ "merge-descriptors": "1.0.3",
1135
+ "methods": "~1.1.2",
1136
+ "on-finished": "~2.4.1",
1137
+ "parseurl": "~1.3.3",
1138
+ "path-to-regexp": "~0.1.12",
1139
+ "proxy-addr": "~2.0.7",
1140
+ "qs": "~6.14.0",
1141
+ "range-parser": "~1.2.1",
1142
+ "safe-buffer": "5.2.1",
1143
+ "send": "~0.19.0",
1144
+ "serve-static": "~1.16.2",
1145
+ "setprototypeof": "1.2.0",
1146
+ "statuses": "~2.0.1",
1147
+ "type-is": "~1.6.18",
1148
+ "utils-merge": "1.0.1",
1149
+ "vary": "~1.1.2"
1150
+ },
1151
+ "engines": {
1152
+ "node": ">= 0.10.0"
1153
+ },
1154
+ "funding": {
1155
+ "type": "opencollective",
1156
+ "url": "https://opencollective.com/express"
1157
+ }
1158
+ },
1159
+ "node_modules/express-rate-limit": {
1160
+ "version": "8.2.1",
1161
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
1162
+ "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
1163
+ "license": "MIT",
1164
+ "dependencies": {
1165
+ "ip-address": "10.0.1"
1166
+ },
1167
+ "engines": {
1168
+ "node": ">= 16"
1169
+ },
1170
+ "funding": {
1171
+ "url": "https://github.com/sponsors/express-rate-limit"
1172
+ },
1173
+ "peerDependencies": {
1174
+ "express": ">= 4.11"
1175
+ }
1176
+ },
1177
+ "node_modules/finalhandler": {
1178
+ "version": "1.3.2",
1179
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
1180
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
1181
+ "license": "MIT",
1182
+ "dependencies": {
1183
+ "debug": "2.6.9",
1184
+ "encodeurl": "~2.0.0",
1185
+ "escape-html": "~1.0.3",
1186
+ "on-finished": "~2.4.1",
1187
+ "parseurl": "~1.3.3",
1188
+ "statuses": "~2.0.2",
1189
+ "unpipe": "~1.0.0"
1190
+ },
1191
+ "engines": {
1192
+ "node": ">= 0.8"
1193
+ }
1194
+ },
1195
+ "node_modules/forwarded": {
1196
+ "version": "0.2.0",
1197
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1198
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1199
+ "license": "MIT",
1200
+ "engines": {
1201
+ "node": ">= 0.6"
1202
+ }
1203
+ },
1204
+ "node_modules/fresh": {
1205
+ "version": "0.5.2",
1206
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
1207
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
1208
+ "license": "MIT",
1209
+ "engines": {
1210
+ "node": ">= 0.6"
1211
+ }
1212
+ },
1213
+ "node_modules/fsevents": {
1214
+ "version": "2.3.3",
1215
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1216
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1217
+ "dev": true,
1218
+ "hasInstallScript": true,
1219
+ "license": "MIT",
1220
+ "optional": true,
1221
+ "os": [
1222
+ "darwin"
1223
+ ],
1224
+ "engines": {
1225
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1226
+ }
1227
+ },
1228
+ "node_modules/function-bind": {
1229
+ "version": "1.1.2",
1230
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1231
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1232
+ "license": "MIT",
1233
+ "funding": {
1234
+ "url": "https://github.com/sponsors/ljharb"
1235
+ }
1236
+ },
1237
+ "node_modules/get-intrinsic": {
1238
+ "version": "1.3.0",
1239
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1240
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1241
+ "license": "MIT",
1242
+ "dependencies": {
1243
+ "call-bind-apply-helpers": "^1.0.2",
1244
+ "es-define-property": "^1.0.1",
1245
+ "es-errors": "^1.3.0",
1246
+ "es-object-atoms": "^1.1.1",
1247
+ "function-bind": "^1.1.2",
1248
+ "get-proto": "^1.0.1",
1249
+ "gopd": "^1.2.0",
1250
+ "has-symbols": "^1.1.0",
1251
+ "hasown": "^2.0.2",
1252
+ "math-intrinsics": "^1.1.0"
1253
+ },
1254
+ "engines": {
1255
+ "node": ">= 0.4"
1256
+ },
1257
+ "funding": {
1258
+ "url": "https://github.com/sponsors/ljharb"
1259
+ }
1260
+ },
1261
+ "node_modules/get-proto": {
1262
+ "version": "1.0.1",
1263
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1264
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1265
+ "license": "MIT",
1266
+ "dependencies": {
1267
+ "dunder-proto": "^1.0.1",
1268
+ "es-object-atoms": "^1.0.0"
1269
+ },
1270
+ "engines": {
1271
+ "node": ">= 0.4"
1272
+ }
1273
+ },
1274
+ "node_modules/get-tsconfig": {
1275
+ "version": "4.13.6",
1276
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
1277
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
1278
+ "dev": true,
1279
+ "license": "MIT",
1280
+ "dependencies": {
1281
+ "resolve-pkg-maps": "^1.0.0"
1282
+ },
1283
+ "funding": {
1284
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
1285
+ }
1286
+ },
1287
+ "node_modules/gopd": {
1288
+ "version": "1.2.0",
1289
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1290
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1291
+ "license": "MIT",
1292
+ "engines": {
1293
+ "node": ">= 0.4"
1294
+ },
1295
+ "funding": {
1296
+ "url": "https://github.com/sponsors/ljharb"
1297
+ }
1298
+ },
1299
+ "node_modules/has-symbols": {
1300
+ "version": "1.1.0",
1301
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1302
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1303
+ "license": "MIT",
1304
+ "engines": {
1305
+ "node": ">= 0.4"
1306
+ },
1307
+ "funding": {
1308
+ "url": "https://github.com/sponsors/ljharb"
1309
+ }
1310
+ },
1311
+ "node_modules/hasown": {
1312
+ "version": "2.0.2",
1313
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1314
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1315
+ "license": "MIT",
1316
+ "dependencies": {
1317
+ "function-bind": "^1.1.2"
1318
+ },
1319
+ "engines": {
1320
+ "node": ">= 0.4"
1321
+ }
1322
+ },
1323
+ "node_modules/http-errors": {
1324
+ "version": "2.0.1",
1325
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
1326
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
1327
+ "license": "MIT",
1328
+ "dependencies": {
1329
+ "depd": "~2.0.0",
1330
+ "inherits": "~2.0.4",
1331
+ "setprototypeof": "~1.2.0",
1332
+ "statuses": "~2.0.2",
1333
+ "toidentifier": "~1.0.1"
1334
+ },
1335
+ "engines": {
1336
+ "node": ">= 0.8"
1337
+ },
1338
+ "funding": {
1339
+ "type": "opencollective",
1340
+ "url": "https://opencollective.com/express"
1341
+ }
1342
+ },
1343
+ "node_modules/iconv-lite": {
1344
+ "version": "0.4.24",
1345
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1346
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1347
+ "license": "MIT",
1348
+ "dependencies": {
1349
+ "safer-buffer": ">= 2.1.2 < 3"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">=0.10.0"
1353
+ }
1354
+ },
1355
+ "node_modules/inherits": {
1356
+ "version": "2.0.4",
1357
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1358
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1359
+ "license": "ISC"
1360
+ },
1361
+ "node_modules/ip-address": {
1362
+ "version": "10.0.1",
1363
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
1364
+ "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
1365
+ "license": "MIT",
1366
+ "engines": {
1367
+ "node": ">= 12"
1368
+ }
1369
+ },
1370
+ "node_modules/ipaddr.js": {
1371
+ "version": "1.9.1",
1372
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1373
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1374
+ "license": "MIT",
1375
+ "engines": {
1376
+ "node": ">= 0.10"
1377
+ }
1378
+ },
1379
+ "node_modules/jsonwebtoken": {
1380
+ "version": "9.0.3",
1381
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
1382
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
1383
+ "license": "MIT",
1384
+ "dependencies": {
1385
+ "jws": "^4.0.1",
1386
+ "lodash.includes": "^4.3.0",
1387
+ "lodash.isboolean": "^3.0.3",
1388
+ "lodash.isinteger": "^4.0.4",
1389
+ "lodash.isnumber": "^3.0.3",
1390
+ "lodash.isplainobject": "^4.0.6",
1391
+ "lodash.isstring": "^4.0.1",
1392
+ "lodash.once": "^4.0.0",
1393
+ "ms": "^2.1.1",
1394
+ "semver": "^7.5.4"
1395
+ },
1396
+ "engines": {
1397
+ "node": ">=12",
1398
+ "npm": ">=6"
1399
+ }
1400
+ },
1401
+ "node_modules/jsonwebtoken/node_modules/ms": {
1402
+ "version": "2.1.3",
1403
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1404
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1405
+ "license": "MIT"
1406
+ },
1407
+ "node_modules/jwa": {
1408
+ "version": "2.0.1",
1409
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
1410
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
1411
+ "license": "MIT",
1412
+ "dependencies": {
1413
+ "buffer-equal-constant-time": "^1.0.1",
1414
+ "ecdsa-sig-formatter": "1.0.11",
1415
+ "safe-buffer": "^5.0.1"
1416
+ }
1417
+ },
1418
+ "node_modules/jws": {
1419
+ "version": "4.0.1",
1420
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
1421
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
1422
+ "license": "MIT",
1423
+ "dependencies": {
1424
+ "jwa": "^2.0.1",
1425
+ "safe-buffer": "^5.0.1"
1426
+ }
1427
+ },
1428
+ "node_modules/lodash.includes": {
1429
+ "version": "4.3.0",
1430
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1431
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
1432
+ "license": "MIT"
1433
+ },
1434
+ "node_modules/lodash.isboolean": {
1435
+ "version": "3.0.3",
1436
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1437
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
1438
+ "license": "MIT"
1439
+ },
1440
+ "node_modules/lodash.isinteger": {
1441
+ "version": "4.0.4",
1442
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1443
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
1444
+ "license": "MIT"
1445
+ },
1446
+ "node_modules/lodash.isnumber": {
1447
+ "version": "3.0.3",
1448
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1449
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
1450
+ "license": "MIT"
1451
+ },
1452
+ "node_modules/lodash.isplainobject": {
1453
+ "version": "4.0.6",
1454
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1455
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
1456
+ "license": "MIT"
1457
+ },
1458
+ "node_modules/lodash.isstring": {
1459
+ "version": "4.0.1",
1460
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1461
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
1462
+ "license": "MIT"
1463
+ },
1464
+ "node_modules/lodash.once": {
1465
+ "version": "4.1.1",
1466
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1467
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
1468
+ "license": "MIT"
1469
+ },
1470
+ "node_modules/math-intrinsics": {
1471
+ "version": "1.1.0",
1472
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1473
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1474
+ "license": "MIT",
1475
+ "engines": {
1476
+ "node": ">= 0.4"
1477
+ }
1478
+ },
1479
+ "node_modules/media-typer": {
1480
+ "version": "0.3.0",
1481
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1482
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1483
+ "license": "MIT",
1484
+ "engines": {
1485
+ "node": ">= 0.6"
1486
+ }
1487
+ },
1488
+ "node_modules/merge-descriptors": {
1489
+ "version": "1.0.3",
1490
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1491
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1492
+ "license": "MIT",
1493
+ "funding": {
1494
+ "url": "https://github.com/sponsors/sindresorhus"
1495
+ }
1496
+ },
1497
+ "node_modules/methods": {
1498
+ "version": "1.1.2",
1499
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1500
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1501
+ "license": "MIT",
1502
+ "engines": {
1503
+ "node": ">= 0.6"
1504
+ }
1505
+ },
1506
+ "node_modules/mime": {
1507
+ "version": "1.6.0",
1508
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1509
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1510
+ "license": "MIT",
1511
+ "bin": {
1512
+ "mime": "cli.js"
1513
+ },
1514
+ "engines": {
1515
+ "node": ">=4"
1516
+ }
1517
+ },
1518
+ "node_modules/mime-db": {
1519
+ "version": "1.52.0",
1520
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1521
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1522
+ "license": "MIT",
1523
+ "engines": {
1524
+ "node": ">= 0.6"
1525
+ }
1526
+ },
1527
+ "node_modules/mime-types": {
1528
+ "version": "2.1.35",
1529
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1530
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1531
+ "license": "MIT",
1532
+ "dependencies": {
1533
+ "mime-db": "1.52.0"
1534
+ },
1535
+ "engines": {
1536
+ "node": ">= 0.6"
1537
+ }
1538
+ },
1539
+ "node_modules/minimist": {
1540
+ "version": "1.2.8",
1541
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
1542
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1543
+ "license": "MIT",
1544
+ "funding": {
1545
+ "url": "https://github.com/sponsors/ljharb"
1546
+ }
1547
+ },
1548
+ "node_modules/ms": {
1549
+ "version": "2.0.0",
1550
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1551
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1552
+ "license": "MIT"
1553
+ },
1554
+ "node_modules/multer": {
1555
+ "version": "2.0.2",
1556
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
1557
+ "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
1558
+ "license": "MIT",
1559
+ "dependencies": {
1560
+ "append-field": "^1.0.0",
1561
+ "busboy": "^1.6.0",
1562
+ "concat-stream": "^2.0.0",
1563
+ "mkdirp": "^0.5.6",
1564
+ "object-assign": "^4.1.1",
1565
+ "type-is": "^1.6.18",
1566
+ "xtend": "^4.0.2"
1567
+ },
1568
+ "engines": {
1569
+ "node": ">= 10.16.0"
1570
+ }
1571
+ },
1572
+ "node_modules/multer/node_modules/mkdirp": {
1573
+ "version": "0.5.6",
1574
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
1575
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
1576
+ "license": "MIT",
1577
+ "dependencies": {
1578
+ "minimist": "^1.2.6"
1579
+ },
1580
+ "bin": {
1581
+ "mkdirp": "bin/cmd.js"
1582
+ }
1583
+ },
1584
+ "node_modules/negotiator": {
1585
+ "version": "0.6.3",
1586
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1587
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1588
+ "license": "MIT",
1589
+ "engines": {
1590
+ "node": ">= 0.6"
1591
+ }
1592
+ },
1593
+ "node_modules/nodemailer": {
1594
+ "version": "8.0.1",
1595
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
1596
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
1597
+ "license": "MIT-0",
1598
+ "engines": {
1599
+ "node": ">=6.0.0"
1600
+ }
1601
+ },
1602
+ "node_modules/object-assign": {
1603
+ "version": "4.1.1",
1604
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1605
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1606
+ "license": "MIT",
1607
+ "engines": {
1608
+ "node": ">=0.10.0"
1609
+ }
1610
+ },
1611
+ "node_modules/object-inspect": {
1612
+ "version": "1.13.4",
1613
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1614
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1615
+ "license": "MIT",
1616
+ "engines": {
1617
+ "node": ">= 0.4"
1618
+ },
1619
+ "funding": {
1620
+ "url": "https://github.com/sponsors/ljharb"
1621
+ }
1622
+ },
1623
+ "node_modules/on-finished": {
1624
+ "version": "2.4.1",
1625
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1626
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1627
+ "license": "MIT",
1628
+ "dependencies": {
1629
+ "ee-first": "1.1.1"
1630
+ },
1631
+ "engines": {
1632
+ "node": ">= 0.8"
1633
+ }
1634
+ },
1635
+ "node_modules/parseurl": {
1636
+ "version": "1.3.3",
1637
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1638
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1639
+ "license": "MIT",
1640
+ "engines": {
1641
+ "node": ">= 0.8"
1642
+ }
1643
+ },
1644
+ "node_modules/path-to-regexp": {
1645
+ "version": "0.1.12",
1646
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1647
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1648
+ "license": "MIT"
1649
+ },
1650
+ "node_modules/prisma": {
1651
+ "version": "5.22.0",
1652
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
1653
+ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
1654
+ "devOptional": true,
1655
+ "hasInstallScript": true,
1656
+ "license": "Apache-2.0",
1657
+ "dependencies": {
1658
+ "@prisma/engines": "5.22.0"
1659
+ },
1660
+ "bin": {
1661
+ "prisma": "build/index.js"
1662
+ },
1663
+ "engines": {
1664
+ "node": ">=16.13"
1665
+ },
1666
+ "optionalDependencies": {
1667
+ "fsevents": "2.3.3"
1668
+ }
1669
+ },
1670
+ "node_modules/proxy-addr": {
1671
+ "version": "2.0.7",
1672
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1673
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1674
+ "license": "MIT",
1675
+ "dependencies": {
1676
+ "forwarded": "0.2.0",
1677
+ "ipaddr.js": "1.9.1"
1678
+ },
1679
+ "engines": {
1680
+ "node": ">= 0.10"
1681
+ }
1682
+ },
1683
+ "node_modules/qs": {
1684
+ "version": "6.14.2",
1685
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
1686
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
1687
+ "license": "BSD-3-Clause",
1688
+ "dependencies": {
1689
+ "side-channel": "^1.1.0"
1690
+ },
1691
+ "engines": {
1692
+ "node": ">=0.6"
1693
+ },
1694
+ "funding": {
1695
+ "url": "https://github.com/sponsors/ljharb"
1696
+ }
1697
+ },
1698
+ "node_modules/range-parser": {
1699
+ "version": "1.2.1",
1700
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1701
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1702
+ "license": "MIT",
1703
+ "engines": {
1704
+ "node": ">= 0.6"
1705
+ }
1706
+ },
1707
+ "node_modules/raw-body": {
1708
+ "version": "2.5.3",
1709
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
1710
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
1711
+ "license": "MIT",
1712
+ "dependencies": {
1713
+ "bytes": "~3.1.2",
1714
+ "http-errors": "~2.0.1",
1715
+ "iconv-lite": "~0.4.24",
1716
+ "unpipe": "~1.0.0"
1717
+ },
1718
+ "engines": {
1719
+ "node": ">= 0.8"
1720
+ }
1721
+ },
1722
+ "node_modules/readable-stream": {
1723
+ "version": "3.6.2",
1724
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
1725
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
1726
+ "license": "MIT",
1727
+ "dependencies": {
1728
+ "inherits": "^2.0.3",
1729
+ "string_decoder": "^1.1.1",
1730
+ "util-deprecate": "^1.0.1"
1731
+ },
1732
+ "engines": {
1733
+ "node": ">= 6"
1734
+ }
1735
+ },
1736
+ "node_modules/resolve-pkg-maps": {
1737
+ "version": "1.0.0",
1738
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
1739
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
1740
+ "dev": true,
1741
+ "license": "MIT",
1742
+ "funding": {
1743
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1744
+ }
1745
+ },
1746
+ "node_modules/safe-buffer": {
1747
+ "version": "5.2.1",
1748
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1749
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1750
+ "funding": [
1751
+ {
1752
+ "type": "github",
1753
+ "url": "https://github.com/sponsors/feross"
1754
+ },
1755
+ {
1756
+ "type": "patreon",
1757
+ "url": "https://www.patreon.com/feross"
1758
+ },
1759
+ {
1760
+ "type": "consulting",
1761
+ "url": "https://feross.org/support"
1762
+ }
1763
+ ],
1764
+ "license": "MIT"
1765
+ },
1766
+ "node_modules/safer-buffer": {
1767
+ "version": "2.1.2",
1768
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1769
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1770
+ "license": "MIT"
1771
+ },
1772
+ "node_modules/semver": {
1773
+ "version": "7.7.4",
1774
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1775
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1776
+ "license": "ISC",
1777
+ "bin": {
1778
+ "semver": "bin/semver.js"
1779
+ },
1780
+ "engines": {
1781
+ "node": ">=10"
1782
+ }
1783
+ },
1784
+ "node_modules/send": {
1785
+ "version": "0.19.2",
1786
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
1787
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
1788
+ "license": "MIT",
1789
+ "dependencies": {
1790
+ "debug": "2.6.9",
1791
+ "depd": "2.0.0",
1792
+ "destroy": "1.2.0",
1793
+ "encodeurl": "~2.0.0",
1794
+ "escape-html": "~1.0.3",
1795
+ "etag": "~1.8.1",
1796
+ "fresh": "~0.5.2",
1797
+ "http-errors": "~2.0.1",
1798
+ "mime": "1.6.0",
1799
+ "ms": "2.1.3",
1800
+ "on-finished": "~2.4.1",
1801
+ "range-parser": "~1.2.1",
1802
+ "statuses": "~2.0.2"
1803
+ },
1804
+ "engines": {
1805
+ "node": ">= 0.8.0"
1806
+ }
1807
+ },
1808
+ "node_modules/send/node_modules/ms": {
1809
+ "version": "2.1.3",
1810
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1811
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1812
+ "license": "MIT"
1813
+ },
1814
+ "node_modules/serve-static": {
1815
+ "version": "1.16.3",
1816
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
1817
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
1818
+ "license": "MIT",
1819
+ "dependencies": {
1820
+ "encodeurl": "~2.0.0",
1821
+ "escape-html": "~1.0.3",
1822
+ "parseurl": "~1.3.3",
1823
+ "send": "~0.19.1"
1824
+ },
1825
+ "engines": {
1826
+ "node": ">= 0.8.0"
1827
+ }
1828
+ },
1829
+ "node_modules/setprototypeof": {
1830
+ "version": "1.2.0",
1831
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1832
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1833
+ "license": "ISC"
1834
+ },
1835
+ "node_modules/side-channel": {
1836
+ "version": "1.1.0",
1837
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1838
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1839
+ "license": "MIT",
1840
+ "dependencies": {
1841
+ "es-errors": "^1.3.0",
1842
+ "object-inspect": "^1.13.3",
1843
+ "side-channel-list": "^1.0.0",
1844
+ "side-channel-map": "^1.0.1",
1845
+ "side-channel-weakmap": "^1.0.2"
1846
+ },
1847
+ "engines": {
1848
+ "node": ">= 0.4"
1849
+ },
1850
+ "funding": {
1851
+ "url": "https://github.com/sponsors/ljharb"
1852
+ }
1853
+ },
1854
+ "node_modules/side-channel-list": {
1855
+ "version": "1.0.0",
1856
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1857
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1858
+ "license": "MIT",
1859
+ "dependencies": {
1860
+ "es-errors": "^1.3.0",
1861
+ "object-inspect": "^1.13.3"
1862
+ },
1863
+ "engines": {
1864
+ "node": ">= 0.4"
1865
+ },
1866
+ "funding": {
1867
+ "url": "https://github.com/sponsors/ljharb"
1868
+ }
1869
+ },
1870
+ "node_modules/side-channel-map": {
1871
+ "version": "1.0.1",
1872
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1873
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1874
+ "license": "MIT",
1875
+ "dependencies": {
1876
+ "call-bound": "^1.0.2",
1877
+ "es-errors": "^1.3.0",
1878
+ "get-intrinsic": "^1.2.5",
1879
+ "object-inspect": "^1.13.3"
1880
+ },
1881
+ "engines": {
1882
+ "node": ">= 0.4"
1883
+ },
1884
+ "funding": {
1885
+ "url": "https://github.com/sponsors/ljharb"
1886
+ }
1887
+ },
1888
+ "node_modules/side-channel-weakmap": {
1889
+ "version": "1.0.2",
1890
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1891
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1892
+ "license": "MIT",
1893
+ "dependencies": {
1894
+ "call-bound": "^1.0.2",
1895
+ "es-errors": "^1.3.0",
1896
+ "get-intrinsic": "^1.2.5",
1897
+ "object-inspect": "^1.13.3",
1898
+ "side-channel-map": "^1.0.1"
1899
+ },
1900
+ "engines": {
1901
+ "node": ">= 0.4"
1902
+ },
1903
+ "funding": {
1904
+ "url": "https://github.com/sponsors/ljharb"
1905
+ }
1906
+ },
1907
+ "node_modules/slugify": {
1908
+ "version": "1.6.6",
1909
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
1910
+ "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
1911
+ "license": "MIT",
1912
+ "engines": {
1913
+ "node": ">=8.0.0"
1914
+ }
1915
+ },
1916
+ "node_modules/statuses": {
1917
+ "version": "2.0.2",
1918
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1919
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1920
+ "license": "MIT",
1921
+ "engines": {
1922
+ "node": ">= 0.8"
1923
+ }
1924
+ },
1925
+ "node_modules/streamsearch": {
1926
+ "version": "1.1.0",
1927
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1928
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1929
+ "engines": {
1930
+ "node": ">=10.0.0"
1931
+ }
1932
+ },
1933
+ "node_modules/string_decoder": {
1934
+ "version": "1.3.0",
1935
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1936
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1937
+ "license": "MIT",
1938
+ "dependencies": {
1939
+ "safe-buffer": "~5.2.0"
1940
+ }
1941
+ },
1942
+ "node_modules/toidentifier": {
1943
+ "version": "1.0.1",
1944
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1945
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1946
+ "license": "MIT",
1947
+ "engines": {
1948
+ "node": ">=0.6"
1949
+ }
1950
+ },
1951
+ "node_modules/tsx": {
1952
+ "version": "4.21.0",
1953
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
1954
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
1955
+ "dev": true,
1956
+ "license": "MIT",
1957
+ "dependencies": {
1958
+ "esbuild": "~0.27.0",
1959
+ "get-tsconfig": "^4.7.5"
1960
+ },
1961
+ "bin": {
1962
+ "tsx": "dist/cli.mjs"
1963
+ },
1964
+ "engines": {
1965
+ "node": ">=18.0.0"
1966
+ },
1967
+ "optionalDependencies": {
1968
+ "fsevents": "~2.3.3"
1969
+ }
1970
+ },
1971
+ "node_modules/type-is": {
1972
+ "version": "1.6.18",
1973
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1974
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1975
+ "license": "MIT",
1976
+ "dependencies": {
1977
+ "media-typer": "0.3.0",
1978
+ "mime-types": "~2.1.24"
1979
+ },
1980
+ "engines": {
1981
+ "node": ">= 0.6"
1982
+ }
1983
+ },
1984
+ "node_modules/typedarray": {
1985
+ "version": "0.0.6",
1986
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1987
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1988
+ "license": "MIT"
1989
+ },
1990
+ "node_modules/typescript": {
1991
+ "version": "5.9.3",
1992
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1993
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1994
+ "dev": true,
1995
+ "license": "Apache-2.0",
1996
+ "bin": {
1997
+ "tsc": "bin/tsc",
1998
+ "tsserver": "bin/tsserver"
1999
+ },
2000
+ "engines": {
2001
+ "node": ">=14.17"
2002
+ }
2003
+ },
2004
+ "node_modules/undici-types": {
2005
+ "version": "6.21.0",
2006
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2007
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2008
+ "dev": true,
2009
+ "license": "MIT"
2010
+ },
2011
+ "node_modules/unpipe": {
2012
+ "version": "1.0.0",
2013
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2014
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2015
+ "license": "MIT",
2016
+ "engines": {
2017
+ "node": ">= 0.8"
2018
+ }
2019
+ },
2020
+ "node_modules/util-deprecate": {
2021
+ "version": "1.0.2",
2022
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2023
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2024
+ "license": "MIT"
2025
+ },
2026
+ "node_modules/utils-merge": {
2027
+ "version": "1.0.1",
2028
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
2029
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
2030
+ "license": "MIT",
2031
+ "engines": {
2032
+ "node": ">= 0.4.0"
2033
+ }
2034
+ },
2035
+ "node_modules/vary": {
2036
+ "version": "1.1.2",
2037
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2038
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2039
+ "license": "MIT",
2040
+ "engines": {
2041
+ "node": ">= 0.8"
2042
+ }
2043
+ },
2044
+ "node_modules/xss": {
2045
+ "version": "1.0.15",
2046
+ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz",
2047
+ "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
2048
+ "license": "MIT",
2049
+ "dependencies": {
2050
+ "commander": "^2.20.3",
2051
+ "cssfilter": "0.0.10"
2052
+ },
2053
+ "bin": {
2054
+ "xss": "bin/xss"
2055
+ },
2056
+ "engines": {
2057
+ "node": ">= 0.10.0"
2058
+ }
2059
+ },
2060
+ "node_modules/xtend": {
2061
+ "version": "4.0.2",
2062
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
2063
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
2064
+ "license": "MIT",
2065
+ "engines": {
2066
+ "node": ">=0.4"
2067
+ }
2068
+ },
2069
+ "node_modules/zod": {
2070
+ "version": "3.25.76",
2071
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
2072
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
2073
+ "license": "MIT",
2074
+ "funding": {
2075
+ "url": "https://github.com/sponsors/colinhacks"
2076
+ }
2077
+ }
2078
+ }
2079
+ }
package.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "web-services-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backend API for WebServices",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "start": "node dist/index.js",
8
+ "dev": "tsx watch src/index.ts",
9
+ "build": "tsc",
10
+ "lint": "eslint src/**/*.ts",
11
+ "prisma:generate": "prisma generate",
12
+ "prisma:migrate": "prisma migrate dev"
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "@prisma/client": "^5.10.0",
19
+ "bcryptjs": "^2.4.3",
20
+ "cookie-parser": "^1.4.7",
21
+ "cors": "^2.8.5",
22
+ "dotenv": "^16.4.5",
23
+ "express": "^4.18.2",
24
+ "express-rate-limit": "^8.2.1",
25
+ "jsonwebtoken": "^9.0.2",
26
+ "multer": "^2.0.2",
27
+ "nodemailer": "^8.0.1",
28
+ "slugify": "^1.6.6",
29
+ "xss": "^1.0.15",
30
+ "zod": "^3.25.76"
31
+ },
32
+ "devDependencies": {
33
+ "@types/bcryptjs": "^2.4.6",
34
+ "@types/cookie-parser": "^1.4.10",
35
+ "@types/cors": "^2.8.17",
36
+ "@types/express": "^4.17.21",
37
+ "@types/jsonwebtoken": "^9.0.5",
38
+ "@types/multer": "^2.0.0",
39
+ "@types/node": "^20.11.19",
40
+ "@types/nodemailer": "^7.0.10",
41
+ "prisma": "^5.10.0",
42
+ "tsx": "^4.21.0",
43
+ "typescript": "^5.3.3"
44
+ }
45
+ }
prisma/dev.db ADDED
Binary file (90.1 kB). View file
 
prisma/migrations/20260217145622_init_backend_final/migration.sql ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- CreateTable
2
+ CREATE TABLE "User" (
3
+ "id" TEXT NOT NULL PRIMARY KEY,
4
+ "email" TEXT NOT NULL,
5
+ "password" TEXT NOT NULL,
6
+ "name" TEXT,
7
+ "role" TEXT NOT NULL DEFAULT 'VIEWER',
8
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
+ "updatedAt" DATETIME NOT NULL
10
+ );
11
+
12
+ -- CreateTable
13
+ CREATE TABLE "PortfolioItem" (
14
+ "id" TEXT NOT NULL PRIMARY KEY,
15
+ "title" TEXT NOT NULL,
16
+ "slug" TEXT NOT NULL,
17
+ "description" TEXT NOT NULL,
18
+ "technologies" TEXT,
19
+ "imageUrl" TEXT,
20
+ "clientName" TEXT,
21
+ "industry" TEXT,
22
+ "testimonials" TEXT,
23
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+ "updatedAt" DATETIME NOT NULL,
25
+ "authorId" TEXT,
26
+ CONSTRAINT "PortfolioItem_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
27
+ );
28
+
29
+ -- CreateTable
30
+ CREATE TABLE "BlogPost" (
31
+ "id" TEXT NOT NULL PRIMARY KEY,
32
+ "title" TEXT NOT NULL,
33
+ "slug" TEXT NOT NULL,
34
+ "content" TEXT NOT NULL,
35
+ "excerpt" TEXT,
36
+ "featuredImage" TEXT,
37
+ "tags" TEXT,
38
+ "status" TEXT NOT NULL DEFAULT 'DRAFT',
39
+ "publishedAt" DATETIME,
40
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+ "updatedAt" DATETIME NOT NULL,
42
+ "authorId" TEXT NOT NULL,
43
+ CONSTRAINT "BlogPost_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
44
+ );
45
+
46
+ -- CreateTable
47
+ CREATE TABLE "Inquiry" (
48
+ "id" TEXT NOT NULL PRIMARY KEY,
49
+ "name" TEXT NOT NULL,
50
+ "email" TEXT NOT NULL,
51
+ "phone" TEXT,
52
+ "company" TEXT,
53
+ "projectType" TEXT,
54
+ "budget" TEXT,
55
+ "timeline" TEXT,
56
+ "description" TEXT NOT NULL,
57
+ "attachments" TEXT,
58
+ "status" TEXT NOT NULL DEFAULT 'NEW',
59
+ "notes" TEXT,
60
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
61
+ "updatedAt" DATETIME NOT NULL,
62
+ "managedById" TEXT,
63
+ CONSTRAINT "Inquiry_managedById_fkey" FOREIGN KEY ("managedById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
64
+ );
65
+
66
+ -- CreateTable
67
+ CREATE TABLE "Testimonial" (
68
+ "id" TEXT NOT NULL PRIMARY KEY,
69
+ "clientName" TEXT NOT NULL,
70
+ "company" TEXT,
71
+ "content" TEXT NOT NULL,
72
+ "rating" INTEGER NOT NULL,
73
+ "imageUrl" TEXT,
74
+ "approved" BOOLEAN NOT NULL DEFAULT false,
75
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
76
+ );
77
+
78
+ -- CreateTable
79
+ CREATE TABLE "ServicePackage" (
80
+ "id" TEXT NOT NULL PRIMARY KEY,
81
+ "name" TEXT NOT NULL,
82
+ "description" TEXT NOT NULL,
83
+ "features" TEXT NOT NULL,
84
+ "startingPrice" REAL NOT NULL,
85
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
86
+ "isActive" BOOLEAN NOT NULL DEFAULT true
87
+ );
88
+
89
+ -- CreateIndex
90
+ CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
91
+
92
+ -- CreateIndex
93
+ CREATE UNIQUE INDEX "PortfolioItem_slug_key" ON "PortfolioItem"("slug");
94
+
95
+ -- CreateIndex
96
+ CREATE UNIQUE INDEX "BlogPost_slug_key" ON "BlogPost"("slug");
prisma/migrations/20260217152351_add_token_version/migration.sql ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- RedefineTables
2
+ PRAGMA defer_foreign_keys=ON;
3
+ PRAGMA foreign_keys=OFF;
4
+ CREATE TABLE "new_User" (
5
+ "id" TEXT NOT NULL PRIMARY KEY,
6
+ "email" TEXT NOT NULL,
7
+ "password" TEXT NOT NULL,
8
+ "name" TEXT,
9
+ "role" TEXT NOT NULL DEFAULT 'VIEWER',
10
+ "tokenVersion" INTEGER NOT NULL DEFAULT 0,
11
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
+ "updatedAt" DATETIME NOT NULL
13
+ );
14
+ INSERT INTO "new_User" ("createdAt", "email", "id", "name", "password", "role", "updatedAt") SELECT "createdAt", "email", "id", "name", "password", "role", "updatedAt" FROM "User";
15
+ DROP TABLE "User";
16
+ ALTER TABLE "new_User" RENAME TO "User";
17
+ CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
18
+ PRAGMA foreign_keys=ON;
19
+ PRAGMA defer_foreign_keys=OFF;
prisma/migrations/migration_lock.toml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (i.e. Git)
3
+ provider = "sqlite"
prisma/schema.prisma ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "sqlite"
7
+ url = "file:./dev.db"
8
+ }
9
+
10
+ model User {
11
+ id String @id @default(cuid())
12
+ email String @unique
13
+ password String
14
+ name String?
15
+ role String @default("VIEWER")
16
+ tokenVersion Int @default(0)
17
+ createdAt DateTime @default(now())
18
+ updatedAt DateTime @updatedAt
19
+
20
+ inquiries Inquiry[]
21
+ blogPosts BlogPost[]
22
+ portfolioItems PortfolioItem[]
23
+ }
24
+
25
+ model PortfolioItem {
26
+ id String @id @default(cuid())
27
+ title String
28
+ slug String @unique
29
+ description String
30
+ technologies String?
31
+ imageUrl String?
32
+ clientName String?
33
+ industry String?
34
+ testimonials String?
35
+ createdAt DateTime @default(now())
36
+ updatedAt DateTime @updatedAt
37
+
38
+ authorId String?
39
+ author User? @relation(fields: [authorId], references: [id])
40
+ }
41
+
42
+ model BlogPost {
43
+ id String @id @default(cuid())
44
+ title String
45
+ slug String @unique
46
+ content String
47
+ excerpt String?
48
+ featuredImage String?
49
+ tags String?
50
+ status String @default("DRAFT")
51
+ publishedAt DateTime?
52
+ createdAt DateTime @default(now())
53
+ updatedAt DateTime @updatedAt
54
+
55
+ authorId String
56
+ author User @relation(fields: [authorId], references: [id])
57
+ }
58
+
59
+ model Inquiry {
60
+ id String @id @default(cuid())
61
+ name String
62
+ email String
63
+ phone String?
64
+ company String?
65
+ projectType String?
66
+ budget String?
67
+ timeline String?
68
+ description String
69
+ attachments String?
70
+ status String @default("NEW")
71
+ notes String?
72
+ createdAt DateTime @default(now())
73
+ updatedAt DateTime @updatedAt
74
+
75
+ managedById String?
76
+ managedBy User? @relation(fields: [managedById], references: [id])
77
+ }
78
+
79
+ model Testimonial {
80
+ id String @id @default(cuid())
81
+ clientName String
82
+ company String?
83
+ content String
84
+ rating Int
85
+ imageUrl String?
86
+ approved Boolean @default(false)
87
+ createdAt DateTime @default(now())
88
+ }
89
+
90
+
91
+ model ServicePackage {
92
+ id String @id @default(cuid())
93
+ name String
94
+ description String
95
+ features String
96
+ startingPrice Float
97
+ sortOrder Int @default(0)
98
+ isActive Boolean @default(true)
99
+ }
prisma/seed-blog.ts ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ async function main() {
6
+ console.log('Start seeding blog posts...')
7
+
8
+ let defaultAuthor = await prisma.user.findFirst({
9
+ where: { role: 'ADMIN' }
10
+ });
11
+
12
+ if (!defaultAuthor) {
13
+ console.log("No admin user found. Creating a default admin user...");
14
+ // This is a plain text password in DB for a dummy test user - only for seeding.
15
+ defaultAuthor = await prisma.user.create({
16
+ data: {
17
+ name: 'NexaFlow Admin',
18
+ email: 'admin@nexaflow.com',
19
+ password: 'hashedpassword_placeholder', // Dummy
20
+ role: 'ADMIN'
21
+ }
22
+ });
23
+ }
24
+
25
+ const posts = [
26
+ {
27
+ title: 'Why Next.js 16 Changes Everything',
28
+ slug: 'why-nextjs-16-changes-everything',
29
+ content: `
30
+ <h2>The Era of Turbopack</h2>
31
+ <p>The latest release of Next.js brings Turbopack to production, dramatically improving build times and developer experience. Gone are the days of waiting for Webpack to compile massive enterprise applications.</p>
32
+ <h3>Key Features</h3>
33
+ <ul>
34
+ <li>Stable Turbopack for both dev and build</li>
35
+ <li>Improved Server Actions security</li>
36
+ <li>Enhanced caching semantics</li>
37
+ </ul>
38
+ <p>In our experience, migrating to Next.js 16 has reduced initial load times by up to 40% on complex dashboard routes.</p>
39
+ `,
40
+ excerpt: 'The latest release of Next.js brings Turbopack to production, dramatically improving build times and developer experience.',
41
+ featuredImage: 'https://images.unsplash.com/photo-1555066931-bf19f8fd1085?q=80&w=1200',
42
+ tags: JSON.stringify(['Engineering', 'React', 'Next.js']),
43
+ status: 'PUBLISHED',
44
+ publishedAt: new Date(new Date().setDate(new Date().getDate() - 5)),
45
+ authorId: defaultAuthor.id
46
+ },
47
+ {
48
+ title: 'The Power of Design Systems',
49
+ slug: 'power-of-design-systems',
50
+ content: `
51
+ <h2>Consistency at Scale</h2>
52
+ <p>How a well-structured design system can save your team hundreds of hours and ensure consistency across products. A design system isn't just a UI kit—it's a shared language between designers and developers.</p>
53
+ <h3>Getting Started</h3>
54
+ <ol>
55
+ <li>Audit your existing components</li>
56
+ <li>Define base tokens (colors, typography, spacing)</li>
57
+ <li>Build primitive components</li>
58
+ <li>Compose complex patterns</li>
59
+ </ol>
60
+ <p>Tools like Tailwind CSS and Radix UI make implementing accessible design systems easier than ever.</p>
61
+ `,
62
+ excerpt: 'How a well-structured design system can save your team hundreds of hours and ensure consistency across products.',
63
+ featuredImage: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?q=80&w=1200',
64
+ tags: JSON.stringify(['Design', 'UI/UX', 'Tailwind']),
65
+ status: 'PUBLISHED',
66
+ publishedAt: new Date(new Date().setDate(new Date().getDate() - 10)),
67
+ authorId: defaultAuthor.id
68
+ },
69
+ {
70
+ title: 'Building Accessible Web Apps',
71
+ slug: 'building-accessible-web-apps',
72
+ content: `
73
+ <h2>Inclusivity by Default</h2>
74
+ <p>Accessibility isn't optional — it's a requirement. Here's our guide to making your web applications inclusive for everyone. Over 1 billion people worldwide experience some form of disability.</p>
75
+ <h3>Checklist</h3>
76
+ <ul>
77
+ <li>Semantic HTML tags</li>
78
+ <li>ARAI labels where necessary</li>
79
+ <li>Keyboard navigability</li>
80
+ <li>Sufficient color contrast</li>
81
+ </ul>
82
+ <p>Remember: semantic HTML gets you 80% of the way there.</p>
83
+ `,
84
+ excerpt: "Accessibility isn't optional — it's a requirement. Here's our guide to making your web applications inclusive.",
85
+ featuredImage: 'https://images.unsplash.com/photo-1573164713988-8665fc963095?q=80&w=1200',
86
+ tags: JSON.stringify(['Engineering', 'Accessibility', 'HTML']),
87
+ status: 'PUBLISHED',
88
+ publishedAt: new Date(new Date().setDate(new Date().getDate() - 15)),
89
+ authorId: defaultAuthor.id
90
+ },
91
+ {
92
+ title: 'The Future of Serverless Backend',
93
+ slug: 'future-of-serverless-backend',
94
+ content: `
95
+ <h2>Edge Computing Rising</h2>
96
+ <p>Serverless architecture has evolved. Edge functions now allow developers to run backend code closer to the user, reducing latency to near zero.</p>
97
+ <p>This paradigm shift forces us to rethink connection pooling, state management, and localized caching.</p>
98
+ `,
99
+ excerpt: 'Serverless architecture has evolved. Edge functions now allow developers to run backend code closer to the user.',
100
+ featuredImage: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=1200',
101
+ tags: JSON.stringify(['Engineering', 'Serverless', 'Cloud']),
102
+ status: 'PUBLISHED',
103
+ publishedAt: new Date(new Date().setDate(new Date().getDate() - 20)),
104
+ authorId: defaultAuthor.id
105
+ }
106
+ ];
107
+
108
+ for (const post of posts) {
109
+ // Upsert by slug so we can run this multiple times safely
110
+ await prisma.blogPost.upsert({
111
+ where: { slug: post.slug },
112
+ update: post,
113
+ create: post
114
+ });
115
+ }
116
+
117
+ console.log('Seeding finished.')
118
+ }
119
+
120
+ main()
121
+ .catch((e) => {
122
+ console.error(e)
123
+ process.exit(1)
124
+ })
125
+ .finally(async () => {
126
+ await prisma.$disconnect()
127
+ })
prisma/seed-portfolio.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="node" />
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ const projects = [
7
+ {
8
+ title: "E-Commerce Platform",
9
+ slug: "e-commerce-platform",
10
+ description: "A full-stack e-commerce solution with custom checkout, inventory management, and real-time analytics.",
11
+ technologies: "Next.js, Stripe, Tailwind, PostgreSQL",
12
+ industry: "Web",
13
+ },
14
+ {
15
+ title: "Health Tracker App",
16
+ slug: "health-tracker-app",
17
+ description: "Cross-platform health tracking app with wearable integration, goal setting, and social features.",
18
+ technologies: "React Native, Firebase, HealthKit, Google Fit",
19
+ industry: "Mobile",
20
+ },
21
+ {
22
+ title: "Financial Dashboard",
23
+ slug: "financial-dashboard",
24
+ description: "Enterprise dashboard for financial analytics with real-time data visualization and reporting.",
25
+ technologies: "Figma, Design System, D3.js, React",
26
+ industry: "Design",
27
+ },
28
+ {
29
+ title: "Learning Platform",
30
+ slug: "learning-platform",
31
+ description: "Online learning platform with video streaming, quizzes, progress tracking, and certification.",
32
+ technologies: "Next.js, AWS, PostgreSQL, Mux",
33
+ industry: "Web",
34
+ },
35
+ {
36
+ title: "Food Delivery App",
37
+ slug: "food-delivery-app",
38
+ description: "Real-time food delivery app with live order tracking, restaurant management, and payments.",
39
+ technologies: "Flutter, Node.js, Maps API, Razorpay",
40
+ industry: "Mobile",
41
+ },
42
+ {
43
+ title: "SaaS Dashboard",
44
+ slug: "saas-dashboard",
45
+ description: "Multi-tenant SaaS platform with role-based access, usage analytics, and billing integration.",
46
+ technologies: "React, Stripe, Tailwind, Figma",
47
+ industry: "Design",
48
+ }
49
+ ];
50
+
51
+ async function main() {
52
+ console.log(`Start seeding portfolio items...`);
53
+ for (const p of projects) {
54
+ const item = await prisma.portfolioItem.upsert({
55
+ where: { slug: p.slug },
56
+ update: p,
57
+ create: p,
58
+ });
59
+ console.log(`Created/Updated portfolio item with slug: ${item.slug}`);
60
+ }
61
+ console.log(`Seeding finished.`);
62
+ }
63
+
64
+ main()
65
+ .then(async () => {
66
+ await prisma.$disconnect();
67
+ })
68
+ .catch(async (e) => {
69
+ console.error(e);
70
+ await prisma.$disconnect();
71
+ process.exit(1);
72
+ });
prisma/seed.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ async function main() {
6
+ const email = 'admin@example.com'
7
+ const password = 'password123'
8
+
9
+ const existingUser = await prisma.user.findUnique({
10
+ where: { email },
11
+ })
12
+
13
+ if (!existingUser) {
14
+ console.log(`Creating user: ${email}`)
15
+ await prisma.user.create({
16
+ data: {
17
+ email,
18
+ password,
19
+ name: 'Admin User',
20
+ role: 'SUPER_ADMIN',
21
+ },
22
+ })
23
+ console.log('User created.')
24
+ } else {
25
+ console.log('User already exists.')
26
+ }
27
+ }
28
+
29
+ main()
30
+ .then(async () => {
31
+ await prisma.$disconnect()
32
+ })
33
+ .catch(async (e) => {
34
+ console.error(e)
35
+ await prisma.$disconnect()
36
+ process.exit(1)
37
+ })
prisma/tsconfig.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es2016",
5
+ "strict": true,
6
+ "esModuleInterop": true,
7
+ "skipLibCheck": true,
8
+ "types": [
9
+ "node"
10
+ ]
11
+ },
12
+ "include": [
13
+ "*.ts"
14
+ ]
15
+ }
scripts/check-role.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ async function main() {
6
+ const email = 'admin@example.com'
7
+ const user = await prisma.user.findUnique({
8
+ where: { email },
9
+ })
10
+ console.log(`User: ${email}, Role: ${user?.role}`)
11
+ }
12
+
13
+ main()
14
+ .then(async () => {
15
+ await prisma.$disconnect()
16
+ })
17
+ .catch(async (e) => {
18
+ console.error(e)
19
+ await prisma.$disconnect()
20
+ process.exit(1)
21
+ })
src/controllers/admin-blog.controller.ts ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+ import slugify from 'slugify';
4
+
5
+ // GET /api/admin/blog
6
+ export const getAdminBlogPosts = async (req: Request, res: Response) => {
7
+ try {
8
+ const posts = await prisma.blogPost.findMany({
9
+ orderBy: { createdAt: 'desc' },
10
+ include: { author: { select: { name: true, email: true } } }
11
+ });
12
+ return res.status(200).json(posts);
13
+ } catch (error) {
14
+ console.error('Error fetching admin blog posts:', error);
15
+ return res.status(500).json({ error: 'Internal server error' });
16
+ }
17
+ };
18
+
19
+ // GET /api/admin/blog/:id
20
+ export const getAdminBlogPost = async (req: Request, res: Response) => {
21
+ try {
22
+ const { id } = req.params;
23
+ const post = await prisma.blogPost.findUnique({
24
+ where: { id },
25
+ include: { author: { select: { name: true, email: true } } }
26
+ });
27
+ if (!post) {
28
+ return res.status(404).json({ error: 'Post not found' });
29
+ }
30
+ return res.status(200).json(post);
31
+ } catch (error) {
32
+ console.error('Error fetching admin blog post:', error);
33
+ return res.status(500).json({ error: 'Internal server error' });
34
+ }
35
+ };
36
+
37
+ // POST /api/admin/blog
38
+ export const createAdminBlogPost = async (req: Request, res: Response) => {
39
+ try {
40
+ const userId = (req as any).user.userId;
41
+ const { title, slug, content, excerpt, featuredImage, tags, status, publishedAt } = req.body;
42
+
43
+ if (!title || !content) {
44
+ return res.status(400).json({ error: 'Title and content are required' });
45
+ }
46
+
47
+ const generatedSlug = slug ? slugify(slug, { lower: true, strict: true }) : slugify(title, { lower: true, strict: true });
48
+
49
+ // Check for existing slug
50
+ const existing = await prisma.blogPost.findUnique({ where: { slug: generatedSlug } });
51
+ if (existing) {
52
+ return res.status(400).json({ error: 'A post with this slug already exists' });
53
+ }
54
+
55
+ const newPost = await prisma.blogPost.create({
56
+ data: {
57
+ title,
58
+ slug: generatedSlug,
59
+ content,
60
+ excerpt: excerpt || null,
61
+ featuredImage: featuredImage || null,
62
+ tags: tags || null,
63
+ status: status || 'DRAFT',
64
+ publishedAt: publishedAt ? new Date(publishedAt) : null,
65
+ authorId: userId,
66
+ }
67
+ });
68
+
69
+ return res.status(201).json(newPost);
70
+ } catch (error) {
71
+ console.error('Error creating admin blog post:', error);
72
+ return res.status(500).json({ error: 'Internal server error' });
73
+ }
74
+ };
75
+
76
+ // PUT /api/admin/blog/:id
77
+ export const updateAdminBlogPost = async (req: Request, res: Response) => {
78
+ try {
79
+ const { id } = req.params;
80
+ const { title, slug, content, excerpt, featuredImage, tags, status, publishedAt } = req.body;
81
+
82
+ const existingPost = await prisma.blogPost.findUnique({ where: { id } });
83
+ if (!existingPost) {
84
+ return res.status(404).json({ error: 'Post not found' });
85
+ }
86
+
87
+ let updateSlug = existingPost.slug;
88
+ if (slug && slug !== existingPost.slug) {
89
+ updateSlug = slugify(slug, { lower: true, strict: true });
90
+ const checkSlug = await prisma.blogPost.findUnique({ where: { slug: updateSlug } });
91
+ if (checkSlug) {
92
+ return res.status(400).json({ error: 'A post with this slug already exists' });
93
+ }
94
+ }
95
+
96
+ const updatedPost = await prisma.blogPost.update({
97
+ where: { id },
98
+ data: {
99
+ title: title !== undefined ? title : existingPost.title,
100
+ slug: updateSlug,
101
+ content: content !== undefined ? content : existingPost.content,
102
+ excerpt: excerpt !== undefined ? excerpt : existingPost.excerpt,
103
+ featuredImage: featuredImage !== undefined ? featuredImage : existingPost.featuredImage,
104
+ tags: tags !== undefined ? tags : existingPost.tags,
105
+ status: status !== undefined ? status : existingPost.status,
106
+ publishedAt: publishedAt !== undefined ? (publishedAt ? new Date(publishedAt) : null) : existingPost.publishedAt,
107
+ }
108
+ });
109
+
110
+ return res.status(200).json(updatedPost);
111
+ } catch (error) {
112
+ console.error('Error updating admin blog post:', error);
113
+ return res.status(500).json({ error: 'Internal server error' });
114
+ }
115
+ };
116
+
117
+ // DELETE /api/admin/blog/:id
118
+ export const deleteAdminBlogPost = async (req: Request, res: Response) => {
119
+ try {
120
+ const { id } = req.params;
121
+
122
+ const existingPost = await prisma.blogPost.findUnique({ where: { id } });
123
+ if (!existingPost) {
124
+ return res.status(404).json({ error: 'Post not found' });
125
+ }
126
+
127
+ await prisma.blogPost.delete({ where: { id } });
128
+
129
+ return res.status(200).json({ message: 'Post deleted successfully' });
130
+ } catch (error) {
131
+ console.error('Error deleting admin blog post:', error);
132
+ return res.status(500).json({ error: 'Internal server error' });
133
+ }
134
+ };
src/controllers/admin-inquiry.controller.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+
4
+ // GET /api/admin/inquiries
5
+ export const getInquiries = async (req: Request, res: Response) => {
6
+ try {
7
+ const inquiries = await prisma.inquiry.findMany({
8
+ orderBy: { createdAt: 'desc' },
9
+ });
10
+
11
+ return res.status(200).json(inquiries);
12
+ } catch (error) {
13
+ console.error('Error fetching inquiries:', error);
14
+ return res.status(500).json({ error: 'Internal server error' });
15
+ }
16
+ };
17
+
18
+ // GET /api/admin/inquiries/:id
19
+ export const getInquiryById = async (req: Request, res: Response) => {
20
+ try {
21
+ const { id } = req.params;
22
+
23
+ const inquiry = await prisma.inquiry.findUnique({
24
+ where: { id },
25
+ });
26
+
27
+ if (!inquiry) {
28
+ return res.status(404).json({ error: 'Inquiry not found' });
29
+ }
30
+
31
+ return res.status(200).json(inquiry);
32
+ } catch (error) {
33
+ console.error('Error fetching inquiry:', error);
34
+ return res.status(500).json({ error: 'Internal server error' });
35
+ }
36
+ };
37
+
38
+ // PATCH /api/admin/inquiries/:id
39
+ export const updateInquiry = async (req: Request, res: Response) => {
40
+ try {
41
+ const { id } = req.params;
42
+ const { status, notes } = req.body;
43
+
44
+ const currentInquiry = await prisma.inquiry.findUnique({ where: { id } });
45
+ if (!currentInquiry) {
46
+ return res.status(404).json({ error: 'Inquiry not found' });
47
+ }
48
+
49
+ const updateData: any = {};
50
+
51
+ if (status !== undefined) {
52
+ // Validate status
53
+ const validStatuses = ['NEW', 'IN_PROGRESS', 'QUOTED', 'WON', 'LOST'];
54
+ if (!validStatuses.includes(status)) {
55
+ return res.status(400).json({ error: 'Invalid status value' });
56
+ }
57
+ updateData.status = status;
58
+ }
59
+
60
+ if (notes !== undefined) {
61
+ // Strip script tags basic sanitization
62
+ const sanitize = (str: string) => str.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
63
+ updateData.notes = sanitize(notes);
64
+ }
65
+
66
+ const updatedInquiry = await prisma.inquiry.update({
67
+ where: { id },
68
+ data: updateData,
69
+ });
70
+
71
+ return res.status(200).json(updatedInquiry);
72
+ } catch (error) {
73
+ console.error('Error updating inquiry:', error);
74
+ return res.status(500).json({ error: 'Internal server error' });
75
+ }
76
+ };
src/controllers/auth.controller.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+ import bcrypt from 'bcryptjs';
4
+ // import jwt from 'jsonwebtoken';
5
+
6
+ // Mock token storage from the previous implementation, but adapted for backend
7
+ // In a real app we'd use a DB table or Redis
8
+ const resetTokens = new Map<string, { email: string; expires: Date }>();
9
+
10
+ export const login = async (req: Request, res: Response) => {
11
+ try {
12
+ const { email, password } = req.body;
13
+
14
+ // Find user
15
+ const user = await prisma.user.findUnique({ where: { email } });
16
+
17
+ if (!user) {
18
+ return res.status(401).json({ error: "Invalid credentials" });
19
+ }
20
+
21
+ // Check password
22
+ // In production: await bcrypt.compare(password, user.password);
23
+ // For now (legacy mock data compatibility): simple string compare or bcrypt
24
+ const isValid = password === user.password;
25
+
26
+ if (!isValid) {
27
+ return res.status(401).json({ error: "Invalid credentials" });
28
+ }
29
+
30
+ // Increment tokenVersion to invalidate old sessions (if we check it)
31
+ // Or just track it for new session
32
+ const updatedUser = await prisma.user.update({
33
+ where: { id: user.id },
34
+ data: { tokenVersion: { increment: 1 } }
35
+ });
36
+
37
+ // Generate JWT (Mock for now, or use real one)
38
+ // const token = jwt.sign({ id: user.id, role: user.role }, process.env.AUTH_SECRET!);
39
+
40
+ // Return user info (frontend NextAuth will handle session mostly,
41
+ // but if we move fully to backend auth, we return a token)
42
+ return res.json({
43
+ user: {
44
+ id: updatedUser.id,
45
+ email: updatedUser.email,
46
+ name: updatedUser.name,
47
+ role: updatedUser.role,
48
+ tokenVersion: updatedUser.tokenVersion
49
+ }
50
+ // token
51
+ });
52
+
53
+ } catch (error) {
54
+ console.error("Login Error:", error);
55
+ return res.status(500).json({ error: "Internal server error" });
56
+ }
57
+ };
58
+
59
+ export const forgotPassword = async (req: Request, res: Response) => {
60
+ try {
61
+ const { email } = req.body;
62
+ if (!email) return res.status(400).json({ error: "Email required" });
63
+
64
+ const user = await prisma.user.findUnique({ where: { email } });
65
+
66
+ if (user) {
67
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
68
+ const expires = new Date(Date.now() + 3600 * 1000); // 1 hour
69
+
70
+ resetTokens.set(token, { email, expires });
71
+
72
+ console.log("----------------------------------------------------------------");
73
+ console.log(`[Backend Email] Password Reset Requested for ${email}`);
74
+ console.log(`[Link] http://localhost:3000/admin/reset-password?token=${token}`);
75
+ console.log("----------------------------------------------------------------");
76
+ }
77
+
78
+ return res.json({ message: "If account exists, email sent" });
79
+ } catch (error) {
80
+ return res.status(500).json({ error: "Internal server error" });
81
+ }
82
+ };
83
+
84
+ export const resetPassword = async (req: Request, res: Response) => {
85
+ try {
86
+ const { token, password } = req.body;
87
+
88
+ const tokenData = resetTokens.get(token);
89
+ if (!tokenData) return res.status(400).json({ error: "Invalid token" });
90
+
91
+ if (new Date() > tokenData.expires) {
92
+ resetTokens.delete(token);
93
+ return res.status(400).json({ error: "Expired token" });
94
+ }
95
+
96
+ // Update password
97
+ await prisma.user.update({
98
+ where: { email: tokenData.email },
99
+ data: { password } // In real app: hash it
100
+ });
101
+
102
+ resetTokens.delete(token);
103
+ console.log(`[Backend] Password updated for ${tokenData.email}`);
104
+
105
+ return res.json({ message: "Password updated successfully" });
106
+
107
+ } catch (error) {
108
+ return res.status(500).json({ error: "Internal server error" });
109
+ }
110
+ };
src/controllers/blog-related.controller.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+
4
+ // GET /api/blog/:slug/related (Public)
5
+ export const getRelatedPosts = async (req: Request, res: Response) => {
6
+ try {
7
+ const { slug } = req.params;
8
+
9
+ // Find the current post to get its tags
10
+ const currentPost = await prisma.blogPost.findFirst({
11
+ where: { slug }
12
+ });
13
+
14
+ if (!currentPost) {
15
+ return res.status(404).json({ error: 'Post not found' });
16
+ }
17
+
18
+ // Parse tags
19
+ let tags: string[] = [];
20
+ try {
21
+ if (currentPost.tags) tags = JSON.parse(currentPost.tags);
22
+ } catch (e) {
23
+ // ignore
24
+ }
25
+
26
+ // If no tags, just return 3 latest posts excluding current
27
+ if (tags.length === 0) {
28
+ const latest = await prisma.blogPost.findMany({
29
+ where: {
30
+ status: 'PUBLISHED',
31
+ slug: { not: slug },
32
+ publishedAt: { lte: new Date() }
33
+ },
34
+ orderBy: { publishedAt: 'desc' },
35
+ take: 3,
36
+ include: { author: { select: { name: true } } }
37
+ });
38
+ return res.status(200).json(latest);
39
+ }
40
+
41
+ // SQLite doesn't natively support querying inside JSON arrays easily without raw queries or specific extensions,
42
+ // so we'll fetch recently published posts and manually filter/score them by tag overlap.
43
+ // For a production app with Postgres, you'd use a raw query or better JSON filtering.
44
+
45
+ const allOtherPosts = await prisma.blogPost.findMany({
46
+ where: {
47
+ status: 'PUBLISHED',
48
+ slug: { not: slug },
49
+ publishedAt: { lte: new Date() }
50
+ },
51
+ orderBy: {
52
+ publishedAt: 'desc'
53
+ },
54
+ take: 50, // Limit to recent 50 to avoid massive memory usage
55
+ include: { author: { select: { name: true } } }
56
+ });
57
+
58
+ // Score based on shared tags
59
+ const scoredPosts = allOtherPosts.map(post => {
60
+ let postTags: string[] = [];
61
+ try {
62
+ if (post.tags) postTags = JSON.parse(post.tags);
63
+ } catch (e) { }
64
+
65
+ const intersection = tags.filter(t => postTags.includes(t));
66
+ return { post, score: intersection.length };
67
+ });
68
+
69
+ // Sort by score (desc), then date (desc) implicitly from the DB query
70
+ scoredPosts.sort((a, b) => b.score - a.score);
71
+
72
+ // Take top 3
73
+ const related = scoredPosts.slice(0, 3).map(s => s.post);
74
+
75
+ return res.status(200).json(related);
76
+ } catch (error) {
77
+ console.error('Error fetching related posts:', error);
78
+ return res.status(500).json({ error: 'Internal server error' });
79
+ }
80
+ }
src/controllers/blog.controller.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+
4
+ // GET /api/blog (Public)
5
+ export const getPublishedPosts = async (req: Request, res: Response) => {
6
+ try {
7
+ const posts = await prisma.blogPost.findMany({
8
+ where: {
9
+ status: 'PUBLISHED',
10
+ publishedAt: {
11
+ lte: new Date(),
12
+ },
13
+ },
14
+ orderBy: {
15
+ publishedAt: 'desc',
16
+ },
17
+ include: {
18
+ author: {
19
+ select: {
20
+ name: true,
21
+ },
22
+ },
23
+ },
24
+ });
25
+
26
+ return res.status(200).json(posts);
27
+ } catch (error) {
28
+ console.error('Error fetching blog posts:', error);
29
+ return res.status(500).json({ error: 'Internal server error' });
30
+ }
31
+ };
32
+
33
+ // GET /api/blog/:slug (Public)
34
+ export const getPostBySlug = async (req: Request, res: Response) => {
35
+ try {
36
+ const { slug } = req.params;
37
+
38
+ const post = await prisma.blogPost.findFirst({
39
+ where: {
40
+ slug,
41
+ status: 'PUBLISHED',
42
+ publishedAt: {
43
+ lte: new Date(), // ensure it's not a scheduled post in the future
44
+ },
45
+ },
46
+ include: {
47
+ author: {
48
+ select: {
49
+ name: true,
50
+ },
51
+ },
52
+ },
53
+ });
54
+
55
+ if (!post) {
56
+ return res.status(404).json({ error: 'Post not found' });
57
+ }
58
+
59
+ return res.status(200).json(post);
60
+ } catch (error) {
61
+ console.error('Error fetching blog post:', error);
62
+ return res.status(500).json({ error: 'Internal server error' });
63
+ }
64
+ };
src/controllers/portfolio.controller.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+ import slugify from 'slugify';
4
+ import { z } from 'zod';
5
+
6
+ const portfolioSchema = z.object({
7
+ title: z.string().min(1, 'Title is required'),
8
+ slug: z.string().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must be lowercase letters, numbers, and hyphens only'),
9
+ description: z.string().min(1, 'Description is required'),
10
+ technologies: z.string().nullable().optional(),
11
+ imageUrl: z.string().nullable().optional(),
12
+ clientName: z.string().nullable().optional(),
13
+ industry: z.string().nullable().optional(),
14
+ testimonials: z.string().nullable().optional(),
15
+ });
16
+
17
+
18
+ // GET /api/admin/portfolio — List all portfolio items
19
+ export const getAllPortfolioItems = async (_req: Request, res: Response) => {
20
+ try {
21
+ const items = await prisma.portfolioItem.findMany({
22
+ orderBy: { createdAt: 'desc' },
23
+ include: { author: { select: { name: true, email: true } } },
24
+ });
25
+ return res.json(items);
26
+ } catch (error) {
27
+ console.error('Get portfolio items error:', error);
28
+ return res.status(500).json({ error: 'Internal server error' });
29
+ }
30
+ };
31
+
32
+ // GET /api/admin/portfolio/:id — Get single portfolio item
33
+ export const getPortfolioItem = async (req: Request, res: Response) => {
34
+ try {
35
+ const item = await prisma.portfolioItem.findUnique({
36
+ where: { id: req.params.id },
37
+ include: { author: { select: { name: true, email: true } } },
38
+ });
39
+ if (!item) return res.status(404).json({ error: 'Not found' });
40
+ return res.json(item);
41
+ } catch (error) {
42
+ console.error('Get portfolio item error:', error);
43
+ return res.status(500).json({ error: 'Internal server error' });
44
+ }
45
+ };
46
+
47
+ // POST /api/admin/portfolio — Create portfolio item
48
+ export const createPortfolioItem = async (req: Request, res: Response) => {
49
+ try {
50
+ const validatedData = portfolioSchema.parse(req.body);
51
+ const { title, slug, description, technologies, imageUrl, clientName, industry, testimonials } = validatedData;
52
+
53
+ // Check slug uniqueness
54
+ const existing = await prisma.portfolioItem.findUnique({ where: { slug } });
55
+ if (existing) {
56
+ return res.status(409).json({ error: 'Slug already exists' });
57
+ }
58
+
59
+ const item = await prisma.portfolioItem.create({
60
+ data: {
61
+ title,
62
+ slug,
63
+ description,
64
+ technologies: technologies || null,
65
+ imageUrl: imageUrl || null,
66
+ clientName: clientName || null,
67
+ industry: industry || null,
68
+ testimonials: testimonials || null,
69
+ },
70
+ });
71
+
72
+ return res.status(201).json(item);
73
+ } catch (error: any) {
74
+ if (error instanceof z.ZodError) {
75
+ return res.status(400).json({ error: error.errors[0].message });
76
+ }
77
+ console.error('Create portfolio item error:', error);
78
+ return res.status(500).json({ error: 'Internal server error' });
79
+ }
80
+ };
81
+
82
+ // PATCH /api/admin/portfolio/:id — Update portfolio item
83
+ export const updatePortfolioItem = async (req: Request, res: Response) => {
84
+ try {
85
+ const { id } = req.params;
86
+ const validatedData = portfolioSchema.partial().parse(req.body);
87
+ const { title, slug, description, technologies, imageUrl, clientName, industry, testimonials } = validatedData;
88
+
89
+ // Check item exists
90
+ const existing = await prisma.portfolioItem.findUnique({ where: { id } });
91
+ if (!existing) return res.status(404).json({ error: 'Not found' });
92
+
93
+ // Check slug uniqueness if changed
94
+ if (slug && slug !== existing.slug) {
95
+ const slugExists = await prisma.portfolioItem.findUnique({ where: { slug } });
96
+ if (slugExists) return res.status(409).json({ error: 'Slug already exists' });
97
+ }
98
+
99
+ const item = await prisma.portfolioItem.update({
100
+ where: { id },
101
+ data: {
102
+ ...(title !== undefined && { title }),
103
+ ...(slug !== undefined && { slug }),
104
+ ...(description !== undefined && { description }),
105
+ ...(technologies !== undefined && { technologies }),
106
+ ...(imageUrl !== undefined && { imageUrl }),
107
+ ...(clientName !== undefined && { clientName }),
108
+ ...(industry !== undefined && { industry }),
109
+ ...(testimonials !== undefined && { testimonials }),
110
+ },
111
+ });
112
+
113
+ return res.json(item);
114
+ } catch (error: any) {
115
+ if (error instanceof z.ZodError) {
116
+ return res.status(400).json({ error: error.errors[0].message });
117
+ }
118
+ console.error('Update portfolio item error:', error);
119
+ return res.status(500).json({ error: 'Internal server error' });
120
+ }
121
+ };
122
+
123
+ // DELETE /api/admin/portfolio/:id — Delete portfolio item
124
+ export const deletePortfolioItem = async (req: Request, res: Response) => {
125
+ try {
126
+ const { id } = req.params;
127
+
128
+ const existing = await prisma.portfolioItem.findUnique({ where: { id } });
129
+ if (!existing) return res.status(404).json({ error: 'Not found' });
130
+
131
+ await prisma.portfolioItem.delete({ where: { id } });
132
+
133
+ return res.json({ message: 'Deleted successfully' });
134
+ } catch (error) {
135
+ console.error('Delete portfolio item error:', error);
136
+ return res.status(500).json({ error: 'Internal server error' });
137
+ }
138
+ };
src/controllers/testimonial.controller.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+
4
+ // GET /api/admin/testimonials — List all testimonials
5
+ export const getAllTestimonials = async (_req: Request, res: Response) => {
6
+ try {
7
+ const items = await prisma.testimonial.findMany({
8
+ orderBy: { createdAt: 'desc' },
9
+ });
10
+ return res.json(items);
11
+ } catch (error) {
12
+ console.error('Get testimonials error:', error);
13
+ return res.status(500).json({ error: 'Internal server error' });
14
+ }
15
+ };
16
+
17
+ // GET /api/admin/testimonials/:id — Get single testimonial
18
+ export const getTestimonial = async (req: Request, res: Response) => {
19
+ try {
20
+ const item = await prisma.testimonial.findUnique({
21
+ where: { id: req.params.id },
22
+ });
23
+ if (!item) return res.status(404).json({ error: 'Not found' });
24
+ return res.json(item);
25
+ } catch (error) {
26
+ console.error('Get testimonial error:', error);
27
+ return res.status(500).json({ error: 'Internal server error' });
28
+ }
29
+ };
30
+
31
+ // POST /api/admin/testimonials — Create testimonial
32
+ export const createTestimonial = async (req: Request, res: Response) => {
33
+ try {
34
+ const { clientName, company, content, rating, imageUrl, approved } = req.body;
35
+
36
+ if (!clientName || !content) {
37
+ return res.status(400).json({ error: 'Client name and content are required' });
38
+ }
39
+
40
+ if (rating !== undefined && (rating < 1 || rating > 5)) {
41
+ return res.status(400).json({ error: 'Rating must be between 1 and 5' });
42
+ }
43
+
44
+ const item = await prisma.testimonial.create({
45
+ data: {
46
+ clientName,
47
+ company: company || null,
48
+ content,
49
+ rating: rating || 5,
50
+ imageUrl: imageUrl || null,
51
+ approved: approved ?? false,
52
+ },
53
+ });
54
+
55
+ return res.status(201).json(item);
56
+ } catch (error) {
57
+ console.error('Create testimonial error:', error);
58
+ return res.status(500).json({ error: 'Internal server error' });
59
+ }
60
+ };
61
+
62
+ // PATCH /api/admin/testimonials/:id — Update testimonial
63
+ export const updateTestimonial = async (req: Request, res: Response) => {
64
+ try {
65
+ const { id } = req.params;
66
+ const { clientName, company, content, rating, imageUrl, approved } = req.body;
67
+
68
+ const existing = await prisma.testimonial.findUnique({ where: { id } });
69
+ if (!existing) return res.status(404).json({ error: 'Not found' });
70
+
71
+ if (rating !== undefined && (rating < 1 || rating > 5)) {
72
+ return res.status(400).json({ error: 'Rating must be between 1 and 5' });
73
+ }
74
+
75
+ const item = await prisma.testimonial.update({
76
+ where: { id },
77
+ data: {
78
+ ...(clientName !== undefined && { clientName }),
79
+ ...(company !== undefined && { company }),
80
+ ...(content !== undefined && { content }),
81
+ ...(rating !== undefined && { rating }),
82
+ ...(imageUrl !== undefined && { imageUrl }),
83
+ ...(approved !== undefined && { approved }),
84
+ },
85
+ });
86
+
87
+ return res.json(item);
88
+ } catch (error) {
89
+ console.error('Update testimonial error:', error);
90
+ return res.status(500).json({ error: 'Internal server error' });
91
+ }
92
+ };
93
+
94
+ // PATCH /api/admin/testimonials/:id/toggle — Toggle approval status
95
+ export const toggleApproval = async (req: Request, res: Response) => {
96
+ try {
97
+ const { id } = req.params;
98
+
99
+ const existing = await prisma.testimonial.findUnique({ where: { id } });
100
+ if (!existing) return res.status(404).json({ error: 'Not found' });
101
+
102
+ const item = await prisma.testimonial.update({
103
+ where: { id },
104
+ data: { approved: !existing.approved },
105
+ });
106
+
107
+ return res.json(item);
108
+ } catch (error) {
109
+ console.error('Toggle approval error:', error);
110
+ return res.status(500).json({ error: 'Internal server error' });
111
+ }
112
+ };
113
+
114
+ // DELETE /api/admin/testimonials/:id — Delete testimonial
115
+ export const deleteTestimonial = async (req: Request, res: Response) => {
116
+ try {
117
+ const { id } = req.params;
118
+
119
+ const existing = await prisma.testimonial.findUnique({ where: { id } });
120
+ if (!existing) return res.status(404).json({ error: 'Not found' });
121
+
122
+ await prisma.testimonial.delete({ where: { id } });
123
+
124
+ return res.json({ message: 'Deleted successfully' });
125
+ } catch (error) {
126
+ console.error('Delete testimonial error:', error);
127
+ return res.status(500).json({ error: 'Internal server error' });
128
+ }
129
+ };
src/controllers/upload.controller.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from 'express';
2
+
3
+ export const uploadImage = (req: Request, res: Response) => {
4
+ try {
5
+ if (!req.file) {
6
+ return res.status(400).json({ error: 'No file uploaded' });
7
+ }
8
+
9
+ // Return the URL path
10
+ // In local development, this translates to the public URL served by Express static
11
+ const imageUrl = `/uploads/${req.file.filename}`;
12
+
13
+ return res.status(200).json({ url: imageUrl });
14
+ } catch (error) {
15
+ console.error('Upload Error:', error);
16
+ return res.status(500).json({ error: 'Internal server error during file upload' });
17
+ }
18
+ };
src/index.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import dotenv from "dotenv";
4
+
5
+ dotenv.config();
6
+
7
+ const app = express();
8
+ const port = process.env.PORT || 5000;
9
+
10
+ app.use(cors({
11
+ origin: process.env.FRONTEND_URL || "http://localhost:3000",
12
+ credentials: true,
13
+ }));
14
+ app.use(express.json());
15
+
16
+ import cookieParser from "cookie-parser";
17
+ app.use(cookieParser());
18
+
19
+ import authRoutes from "./routes/auth.routes";
20
+ import portfolioRoutes from "./routes/portfolio.routes";
21
+ import testimonialRoutes from "./routes/testimonial.routes";
22
+ import publicTestimonialRoutes from "./routes/public-testimonials.routes";
23
+ import inquiryRoutes from "./routes/inquiry.routes";
24
+ import adminInquiryRoutes from "./routes/admin-inquiry.routes";
25
+ import blogRoutes from "./routes/blog.routes";
26
+ import adminBlogRoutes from "./routes/admin-blog.routes";
27
+ import uploadRoutes from "./routes/upload.routes";
28
+ import publicPortfolioRoutes from "./routes/public-portfolio.routes";
29
+ import { getCsrfToken, csrfProtection } from "./middleware/csrf";
30
+ import path from "path";
31
+
32
+ app.get("/", (req, res) => {
33
+ res.send("WebServices API is running");
34
+ });
35
+
36
+ // Provide CSRF token to frontend
37
+ app.get("/api/csrf-token", csrfProtection, getCsrfToken);
38
+
39
+ app.use("/api/auth", authRoutes);
40
+ app.use("/api/inquiries", inquiryRoutes);
41
+ app.use("/api/testimonials", publicTestimonialRoutes);
42
+ app.use("/api/blog", blogRoutes);
43
+ app.use("/api/admin/portfolio", portfolioRoutes);
44
+ app.use("/api/admin/testimonials", testimonialRoutes);
45
+ app.use("/api/admin/inquiries", adminInquiryRoutes);
46
+ app.use("/api/admin/blog", adminBlogRoutes);
47
+ app.use("/api/portfolio", publicPortfolioRoutes);
48
+ app.use("/api/upload", uploadRoutes);
49
+
50
+ // Serve uploaded local files
51
+ app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
52
+
53
+ app.listen(port, () => {
54
+ console.log(`[server]: Server is running at http://localhost:${port}`);
55
+ });
src/middleware/auth.middleware.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+
3
+ /**
4
+ * Simple auth middleware that checks for a bearer token or session cookie.
5
+ * For now, this is a lightweight check — in production, verify JWT signature.
6
+ *
7
+ * The frontend (NextAuth) manages actual session validation.
8
+ * This middleware ensures only authenticated requests reach admin API routes.
9
+ */
10
+ export const requireAuth = (req: Request, res: Response, next: NextFunction) => {
11
+ const authHeader = req.headers.authorization;
12
+
13
+ // Check for Bearer token (from frontend API calls)
14
+ if (authHeader && authHeader.startsWith('Bearer ')) {
15
+ const token = authHeader.split(' ')[1];
16
+ if (token && token.length > 0) {
17
+ (req as any).user = { userId: token };
18
+ return next();
19
+ }
20
+ }
21
+
22
+ // Check for session cookie (NextAuth passes it)
23
+ if (req.headers.cookie && req.headers.cookie.includes('authjs.session-token')) {
24
+ return next();
25
+ }
26
+
27
+ return res.status(401).json({ error: 'Unauthorized' });
28
+ };
29
+
30
+ /**
31
+ * Admin auth middleware.
32
+ * For this implementation, since NextAuth secures the frontend routes,
33
+ * this simply ensures the request is authenticated.
34
+ */
35
+ export const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
36
+ // Relying on requireAuth to have run first.
37
+ // Real implementation would decode JWT or read database role.
38
+ return next();
39
+ };
src/middleware/csrf.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import crypto from 'crypto';
3
+
4
+ /**
5
+ * Middleware to generate a CSRF token for the session / request.
6
+ * If the request doesn't have a token cookie, one is generated and attached.
7
+ */
8
+ export const csrfProtection = (req: Request, res: Response, next: NextFunction) => {
9
+ // Determine the token
10
+ let token = req.cookies['csrf-token'];
11
+
12
+ if (!token) {
13
+ token = crypto.randomBytes(32).toString('hex');
14
+ res.cookie('csrf-token', token, {
15
+ httpOnly: false, // Must be readable by frontend JS to set header
16
+ sameSite: 'lax',
17
+ secure: process.env.NODE_ENV === 'production',
18
+ path: '/',
19
+ });
20
+ }
21
+
22
+ // Attach to request object for use in endpoints if needed
23
+ (req as any).csrfToken = token;
24
+
25
+ // Fast-path for GET / OPTIONS / HEAD
26
+ if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
27
+ return next();
28
+ }
29
+
30
+ // For POST/PATCH/DELETE require the token in the headers
31
+ const clientToken = req.headers['x-csrf-token'];
32
+
33
+ if (!clientToken || clientToken !== token) {
34
+ return res.status(403).json({
35
+ error: 'CSRF token missing or invalid',
36
+ });
37
+ }
38
+
39
+ next();
40
+ };
41
+
42
+ /**
43
+ * Endpoint to explicitly fetch the CSRF token
44
+ */
45
+ export const getCsrfToken = (req: Request, res: Response) => {
46
+ res.status(200).json({ csrfToken: (req as any).csrfToken });
47
+ };
src/middleware/rate-limiter.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import rateLimit from 'express-rate-limit';
2
+
3
+ const windowMs = process.env.RATE_LIMIT_WINDOW_MS ? parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) : 15 * 60 * 1000;
4
+ const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) : 5;
5
+
6
+ // Public form submissions limiter (e.g., quotes/contact)
7
+ export const publicFormLimiter = rateLimit({
8
+ windowMs, // Default: 15 minutes
9
+ max: maxRequests, // Default: limit each IP to 5 requests per windowMs
10
+ standardHeaders: true,
11
+ legacyHeaders: false,
12
+ keyGenerator: (req) => {
13
+ // Fallback to connection.remoteAddress if IP is missing
14
+ return req.ip || req.connection.remoteAddress || 'unknown';
15
+ },
16
+ message: {
17
+ error: "Too many requests",
18
+ details: "Please try again later after 15 minutes",
19
+ }
20
+ });
21
+
22
+ // Stricter limiter for Auth / Logins
23
+ export const authLimiter = rateLimit({
24
+ windowMs: 15 * 60 * 1000, // 15 minutes
25
+ max: 10, // Limit each IP to 10 login attempts per window
26
+ standardHeaders: true,
27
+ legacyHeaders: false,
28
+ keyGenerator: (req) => {
29
+ return req.ip || req.connection.remoteAddress || 'unknown';
30
+ },
31
+ message: {
32
+ error: "Too many login attempts",
33
+ details: "Account temporarily locked for 15 minutes due to too many failed attempts"
34
+ }
35
+ });
src/middleware/validate.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { AnyZodObject, ZodError } from 'zod';
3
+ import xss from 'xss';
4
+
5
+ /**
6
+ * Validates request body, query, or params against a Zod schema.
7
+ * Automatically sanitizes strings in `req.body` using the `xss` library before validation.
8
+ */
9
+ export const validateRequest = (schema: AnyZodObject) => {
10
+ return async (req: Request, res: Response, next: NextFunction) => {
11
+ try {
12
+ // Traverse and sanitize string fields in req.body
13
+ if (req.body && typeof req.body === 'object') {
14
+ for (const key of Object.keys(req.body)) {
15
+ if (typeof req.body[key] === 'string') {
16
+ req.body[key] = xss(req.body[key], {
17
+ whiteList: {}, // completely strip all HTML tags
18
+ stripIgnoreTag: true,
19
+ stripIgnoreTagBody: ['script'] // ensures inner content of <script> is ripped out
20
+ });
21
+ }
22
+ }
23
+ }
24
+
25
+ // Validate against Zod Schema
26
+ await schema.parseAsync({
27
+ body: req.body,
28
+ query: req.query,
29
+ params: req.params,
30
+ });
31
+
32
+ return next();
33
+ } catch (error) {
34
+ if (error instanceof ZodError) {
35
+ return res.status(400).json({
36
+ error: 'Validation failed',
37
+ details: error.errors.map(err => ({
38
+ path: err.path.join('.'),
39
+ message: err.message
40
+ }))
41
+ });
42
+ }
43
+ return res.status(500).json({ error: 'Internal server error during validation' });
44
+ }
45
+ };
46
+ };
src/models/prisma.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const prisma = new PrismaClient();
4
+
5
+ export default prisma;
src/routes/admin-blog.routes.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { getAdminBlogPosts, getAdminBlogPost, createAdminBlogPost, updateAdminBlogPost, deleteAdminBlogPost } from '../controllers/admin-blog.controller';
3
+ import { requireAuth, requireAdmin } from '../middleware/auth.middleware';
4
+
5
+ const router = Router();
6
+
7
+ // All admin blog routes require authentication and admin privileges
8
+ router.use(requireAuth, requireAdmin);
9
+
10
+ router.get('/', getAdminBlogPosts);
11
+ router.post('/', createAdminBlogPost);
12
+
13
+ router.get('/:id', getAdminBlogPost);
14
+ router.put('/:id', updateAdminBlogPost);
15
+ router.delete('/:id', deleteAdminBlogPost);
16
+
17
+ export default router;
src/routes/admin-inquiry.routes.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { getInquiries, getInquiryById, updateInquiry } from '../controllers/admin-inquiry.controller';
3
+
4
+ const router = Router();
5
+
6
+ router.get('/', getInquiries);
7
+ router.get('/:id', getInquiryById);
8
+ router.patch('/:id', updateInquiry);
9
+
10
+ export default router;
src/routes/auth.routes.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { login, forgotPassword, resetPassword } from '../controllers/auth.controller';
3
+ import { authLimiter } from '../middleware/rate-limiter';
4
+
5
+ const router = Router();
6
+
7
+ router.post('/login', authLimiter, login);
8
+ router.post('/forgot-password', forgotPassword);
9
+ router.post('/reset-password', resetPassword);
10
+
11
+ export default router;
src/routes/blog.routes.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { getPublishedPosts, getPostBySlug } from '../controllers/blog.controller';
3
+ import { getRelatedPosts } from '../controllers/blog-related.controller';
4
+
5
+ const router = Router();
6
+
7
+ router.get('/', getPublishedPosts);
8
+ router.get('/:slug/related', getRelatedPosts);
9
+ router.get('/:slug', getPostBySlug);
10
+
11
+ export default router;
src/routes/inquiry.routes.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router, Request, Response } from 'express';
2
+ import { z } from 'zod';
3
+ import prisma from '../models/prisma';
4
+ import { sendQuoteConfirmation, sendAdminNotification } from '../services/email.service';
5
+ import { validateRequest } from '../middleware/validate';
6
+ import { publicFormLimiter } from '../middleware/rate-limiter';
7
+ import { csrfProtection } from '../middleware/csrf';
8
+
9
+ const router = Router();
10
+
11
+ // Zod Schema for Quote Request Form
12
+ const inquirySchema = z.object({
13
+ body: z.object({
14
+ name: z.string().min(2, 'Name is required (min 2 characters)').max(100),
15
+ email: z.string().email('A valid email is required'),
16
+ phone: z.string().max(20).optional().nullable(),
17
+ company: z.string().max(100).optional().nullable(),
18
+ projectType: z.string().max(50).optional().nullable(),
19
+ budget: z.string().max(50).optional().nullable(),
20
+ timeline: z.string().max(50).optional().nullable(),
21
+ description: z.string().min(10, 'Project description is required (min 10 characters)').max(2000),
22
+ })
23
+ });
24
+
25
+ // POST /api/inquiries — Public: submit a quote request
26
+ router.post(
27
+ '/',
28
+ publicFormLimiter,
29
+ csrfProtection,
30
+ validateRequest(inquirySchema),
31
+ async (req: Request, res: Response) => {
32
+ try {
33
+ const { name, email, phone, company, projectType, budget, timeline, description } = req.body;
34
+
35
+ const inquiry = await prisma.inquiry.create({
36
+ data: {
37
+ name,
38
+ email: email.toLowerCase(),
39
+ phone,
40
+ company,
41
+ projectType,
42
+ budget,
43
+ timeline,
44
+ description,
45
+ status: 'NEW',
46
+ },
47
+ });
48
+
49
+ // Prepare data for email templates
50
+ const inquiryData = {
51
+ referenceNumber: inquiry.id.split('-')[0].toUpperCase(), // Short ref
52
+ name: inquiry.name,
53
+ email: inquiry.email,
54
+ projectType: inquiry.projectType || 'General Inquiry',
55
+ budget: inquiry.budget || 'N/A',
56
+ timeline: inquiry.timeline || 'N/A',
57
+ description: inquiry.description,
58
+ company: inquiry.company || 'N/A',
59
+ phone: inquiry.phone || 'N/A',
60
+ };
61
+
62
+ // Dispatch emails asynchronously
63
+ sendQuoteConfirmation(inquiryData).catch(err => console.error('Failed to send confirmation email', err));
64
+ sendAdminNotification(inquiryData).catch(err => console.error('Failed to send admin notification', err));
65
+
66
+ // Log for development
67
+ console.log('────────────────────────────────────────────');
68
+ console.log(`[New Inquiry] ${inquiryData.referenceNumber}`);
69
+ console.log(` Name: ${inquiry.name}`);
70
+ console.log(` Email: ${inquiry.email}`);
71
+ console.log(` Type: ${inquiryData.projectType}`);
72
+ console.log('────────────────────────────────────────────');
73
+
74
+ return res.status(201).json({
75
+ id: inquiryData.referenceNumber, // Return the short ref for the success screen
76
+ message: 'Quote request submitted successfully',
77
+ });
78
+ } catch (error) {
79
+ console.error('Create inquiry error:', error);
80
+ return res.status(500).json({ error: 'Internal server error' });
81
+ }
82
+ });
83
+
84
+ export default router;
src/routes/portfolio.routes.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { requireAuth, requireAdmin } from '../middleware/auth.middleware';
3
+ import {
4
+ getAllPortfolioItems,
5
+ getPortfolioItem,
6
+ createPortfolioItem,
7
+ updatePortfolioItem,
8
+ deletePortfolioItem,
9
+ } from '../controllers/portfolio.controller';
10
+
11
+ const router = Router();
12
+
13
+ // All routes require authentication and admin
14
+ router.use(requireAuth, requireAdmin);
15
+
16
+ router.get('/', getAllPortfolioItems);
17
+ router.get('/:id', getPortfolioItem);
18
+ router.post('/', createPortfolioItem);
19
+ router.patch('/:id', updatePortfolioItem);
20
+ router.delete('/:id', deletePortfolioItem);
21
+
22
+ export default router;
src/routes/public-portfolio.routes.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import prisma from '../models/prisma';
3
+
4
+ export const getPublicPortfolioItems = async (req: any, res: any) => {
5
+ try {
6
+ const items = await prisma.portfolioItem.findMany({
7
+ orderBy: { createdAt: 'desc' },
8
+ include: { author: { select: { name: true } } },
9
+ });
10
+ return res.json(items);
11
+ } catch (error) {
12
+ console.error('Get public portfolio items error:', error);
13
+ return res.status(500).json({ error: 'Internal server error' });
14
+ }
15
+ };
16
+
17
+ export const getPublicPortfolioItem = async (req: any, res: any) => {
18
+ try {
19
+ const { slug } = req.params;
20
+ const item = await prisma.portfolioItem.findUnique({
21
+ where: { slug },
22
+ include: { author: { select: { name: true } } },
23
+ });
24
+ if (!item) return res.status(404).json({ error: 'Not found' });
25
+ return res.json(item);
26
+ } catch (error) {
27
+ console.error('Get public portfolio item error:', error);
28
+ return res.status(500).json({ error: 'Internal server error' });
29
+ }
30
+ };
31
+
32
+ const router = Router();
33
+
34
+ router.get('/', getPublicPortfolioItems);
35
+ router.get('/:slug', getPublicPortfolioItem);
36
+
37
+ export default router;
src/routes/public-testimonials.routes.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router, Request, Response } from 'express';
2
+ import prisma from '../models/prisma';
3
+
4
+ const router = Router();
5
+
6
+ // GET /api/testimonials — Public: only return approved testimonials
7
+ router.get('/', async (_req: Request, res: Response) => {
8
+ try {
9
+ const items = await prisma.testimonial.findMany({
10
+ where: { approved: true },
11
+ orderBy: { createdAt: 'desc' },
12
+ select: {
13
+ id: true,
14
+ clientName: true,
15
+ company: true,
16
+ content: true,
17
+ rating: true,
18
+ imageUrl: true,
19
+ createdAt: true,
20
+ },
21
+ });
22
+ return res.json(items);
23
+ } catch (error) {
24
+ console.error('Get public testimonials error:', error);
25
+ return res.status(500).json({ error: 'Internal server error' });
26
+ }
27
+ });
28
+
29
+ export default router;
src/routes/testimonial.routes.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { requireAuth } from '../middleware/auth.middleware';
3
+ import {
4
+ getAllTestimonials,
5
+ getTestimonial,
6
+ createTestimonial,
7
+ updateTestimonial,
8
+ toggleApproval,
9
+ deleteTestimonial,
10
+ } from '../controllers/testimonial.controller';
11
+
12
+ const router = Router();
13
+
14
+ // All routes require authentication
15
+ router.use(requireAuth);
16
+
17
+ router.get('/', getAllTestimonials);
18
+ router.get('/:id', getTestimonial);
19
+ router.post('/', createTestimonial);
20
+ router.patch('/:id', updateTestimonial);
21
+ router.patch('/:id/toggle', toggleApproval);
22
+ router.delete('/:id', deleteTestimonial);
23
+
24
+ export default router;
src/routes/upload.routes.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import multer from 'multer';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { requireAuth, requireAdmin } from '../middleware/auth.middleware';
6
+ import { uploadImage } from '../controllers/upload.controller';
7
+
8
+ const router = Router();
9
+
10
+ // Ensure the uploads directory exists
11
+ const uploadDir = path.join(process.cwd(), 'uploads');
12
+ if (!fs.existsSync(uploadDir)) {
13
+ fs.mkdirSync(uploadDir, { recursive: true });
14
+ }
15
+
16
+ // Multer layout configuration
17
+ const storage = multer.diskStorage({
18
+ destination: (req, file, cb) => {
19
+ cb(null, uploadDir);
20
+ },
21
+ filename: (req, file, cb) => {
22
+ // Create unique filename: timestamp-random-originalName
23
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
24
+ const ext = path.extname(file.originalname);
25
+ const name = path.basename(file.originalname, ext).replace(/[^a-zA-Z0-9]/g, '-');
26
+ cb(null, `${name}-${uniqueSuffix}${ext}`);
27
+ }
28
+ });
29
+
30
+ // File filter to restrict to images
31
+ const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
32
+ if (file.mimetype.startsWith('image/')) {
33
+ cb(null, true);
34
+ } else {
35
+ cb(new Error('Only images are allowed'));
36
+ }
37
+ };
38
+
39
+ const upload = multer({
40
+ storage,
41
+ fileFilter,
42
+ limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit
43
+ });
44
+
45
+ // Endpoint: POST /api/upload
46
+ // Requires Auth, and Admin
47
+ router.post('/', requireAuth, requireAdmin, upload.single('file'), uploadImage);
48
+
49
+ // Handle multer errors specifically
50
+ router.use((err: any, req: any, res: any, next: any) => {
51
+ if (err instanceof multer.MulterError) {
52
+ return res.status(400).json({ error: err.message });
53
+ } else if (err) {
54
+ return res.status(400).json({ error: err.message });
55
+ }
56
+ next();
57
+ });
58
+
59
+ export default router;
src/services/email.service.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import nodemailer from 'nodemailer';
2
+
3
+ // Email configuration
4
+ const smtpHost = process.env.SMTP_HOST || '';
5
+ const smtpPort = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587;
6
+ const smtpUser = process.env.SMTP_USER || '';
7
+ const smtpPass = process.env.SMTP_PASS || '';
8
+ const fromEmail = process.env.FROM_EMAIL || 'noreply@nexaflow.com';
9
+ const adminEmail = process.env.ADMIN_EMAIL || 'admin@nexaflow.com';
10
+
11
+ const isConfigured = Boolean(smtpHost && smtpUser && smtpPass);
12
+
13
+ // Create reusable transporter object using the default SMTP transport
14
+ const transporter = nodemailer.createTransport({
15
+ host: smtpHost,
16
+ port: smtpPort,
17
+ secure: smtpPort === 465, // true for 465, false for other ports
18
+ auth: {
19
+ user: smtpUser, // generated ethereal user
20
+ pass: smtpPass, // generated ethereal password
21
+ },
22
+ });
23
+
24
+ interface InquiryData {
25
+ referenceNumber: string;
26
+ name: string;
27
+ email: string;
28
+ projectType: string;
29
+ budget?: string;
30
+ timeline?: string;
31
+ description: string;
32
+ company?: string;
33
+ phone?: string;
34
+ }
35
+
36
+ export const sendQuoteConfirmation = async (inquiry: InquiryData) => {
37
+ const targetEmail = inquiry.email;
38
+ const subject = `Quote Request Received - Ref: ${inquiry.referenceNumber}`;
39
+
40
+ const htmlBody = `
41
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
42
+ <h2>Hello ${inquiry.name},</h2>
43
+ <p>Thank you for requesting a quote from NexaFlow! We have received your inquiry.</p>
44
+ <p><strong>Your Reference Number:</strong> ${inquiry.referenceNumber}</p>
45
+ <hr />
46
+ <p><strong>Project Details Summarized:</strong></p>
47
+ <ul>
48
+ <li><strong>Type:</strong> ${inquiry.projectType}</li>
49
+ <li><strong>Budget:</strong> ${inquiry.budget || 'Not specified'}</li>
50
+ <li><strong>Timeline:</strong> ${inquiry.timeline || 'Not specified'}</li>
51
+ </ul>
52
+ <p>Our team will review your requirements and get back to you within 24 hours with a detailed proposal or further questions.</p>
53
+ <p>Best regards,<br/>The NexaFlow Team</p>
54
+ </div>
55
+ `;
56
+
57
+ return sendEmail(targetEmail, subject, htmlBody);
58
+ };
59
+
60
+ export const sendAdminNotification = async (inquiry: InquiryData) => {
61
+ const subject = `New Quote Request - ${inquiry.projectType.toUpperCase()} - Ref: ${inquiry.referenceNumber}`;
62
+
63
+ const htmlBody = `
64
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
65
+ <h2>New Quote Request Received</h2>
66
+ <p>A new inquiry has been submitted via the website form.</p>
67
+ <hr />
68
+ <p><strong>Reference Number:</strong> ${inquiry.referenceNumber}</p>
69
+ <p><strong>Client Details:</strong></p>
70
+ <ul>
71
+ <li><strong>Name:</strong> ${inquiry.name}</li>
72
+ <li><strong>Email:</strong> ${inquiry.email}</li>
73
+ <li><strong>Phone:</strong> ${inquiry.phone || 'Not provided'}</li>
74
+ <li><strong>Company:</strong> ${inquiry.company || 'Not provided'}</li>
75
+ </ul>
76
+ <p><strong>Project Details:</strong></p>
77
+ <ul>
78
+ <li><strong>Type:</strong> ${inquiry.projectType}</li>
79
+ <li><strong>Budget:</strong> ${inquiry.budget || 'Not specified'}</li>
80
+ <li><strong>Timeline:</strong> ${inquiry.timeline || 'Not specified'}</li>
81
+ </ul>
82
+ <p><strong>Description:</strong></p>
83
+ <p>${inquiry.description}</p>
84
+ </div>
85
+ `;
86
+
87
+ return sendEmail(adminEmail, subject, htmlBody);
88
+ };
89
+
90
+ const sendEmail = async (to: string, subject: string, html: string) => {
91
+ if (!isConfigured) {
92
+ // Fallback for local development or when SMTP is not configured
93
+ console.log('-------------------------------------------------------');
94
+ console.log('📧 EMAIL MOCK (SMTP not configured)');
95
+ console.log(`To: ${to}`);
96
+ console.log(`From: ${fromEmail}`);
97
+ console.log(`Subject: ${subject}`);
98
+ console.log('Body HTML preview:');
99
+ console.log(html);
100
+ console.log('-------------------------------------------------------');
101
+ return true;
102
+ }
103
+
104
+ try {
105
+ const info = await transporter.sendMail({
106
+ from: `"NexaFlow" <${fromEmail}>`, // sender address
107
+ to, // list of receivers
108
+ subject, // Subject line
109
+ html, // html body
110
+ });
111
+ console.log("Message sent: %s", info.messageId);
112
+ return true;
113
+ } catch (error) {
114
+ console.error("Error sending email:", error);
115
+ return false;
116
+ }
117
+ };
tsconfig.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2016",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "types": [
12
+ "node"
13
+ ]
14
+ },
15
+ "include": [
16
+ "src/**/*"
17
+ ],
18
+ "exclude": [
19
+ "node_modules"
20
+ ]
21
+ }