feat: Initialize NestJS backend project with Prisma, core modules, configuration, and common utilities.
This commit is contained in:
72
.env.example
Normal file
72
.env.example
Normal file
@@ -0,0 +1,72 @@
|
||||
# ===========================================
|
||||
# Real Estate Agent Platform - Backend Environment Variables
|
||||
# Copy this file to .env and fill in your values
|
||||
# ===========================================
|
||||
|
||||
# Application
|
||||
NODE_ENV=development
|
||||
PORT=3001
|
||||
APP_NAME="Real Estate Agent Platform"
|
||||
API_URL=http://localhost:3001
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
ADMIN_URL=http://localhost:3002
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/real_estate_db?schema=public"
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
JWT_ACCESS_EXPIRATION=15m
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
|
||||
# Password Hashing
|
||||
BCRYPT_SALT_ROUNDS=12
|
||||
|
||||
# Google OAuth
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
GOOGLE_CALLBACK_URL=http://localhost:3001/auth/google/callback
|
||||
|
||||
# Facebook OAuth
|
||||
FACEBOOK_APP_ID=your-facebook-app-id
|
||||
FACEBOOK_APP_SECRET=your-facebook-app-secret
|
||||
FACEBOOK_CALLBACK_URL=http://localhost:3001/auth/facebook/callback
|
||||
|
||||
# AWS S3 (File Storage)
|
||||
AWS_ACCESS_KEY_ID=your-aws-access-key
|
||||
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
||||
AWS_REGION=us-east-1
|
||||
AWS_S3_BUCKET=your-s3-bucket-name
|
||||
|
||||
# Email (SMTP / SendGrid)
|
||||
MAIL_HOST=smtp.sendgrid.net
|
||||
MAIL_PORT=587
|
||||
MAIL_USER=apikey
|
||||
MAIL_PASSWORD=your-sendgrid-api-key
|
||||
MAIL_FROM="Real Estate Platform <noreply@yourdomain.com>"
|
||||
|
||||
# Stripe (Payments)
|
||||
STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key
|
||||
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_your-stripe-publishable-key
|
||||
|
||||
# Firebase (Push Notifications)
|
||||
FIREBASE_PROJECT_ID=your-firebase-project-id
|
||||
FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com
|
||||
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYour-Private-Key\n-----END PRIVATE KEY-----"
|
||||
|
||||
# Redis (Caching & Queue)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Rate Limiting
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=100
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:3002
|
||||
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Prisma
|
||||
/generated/prisma
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Debug
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
98
README.md
Normal file
98
README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
20178
package-lock.json
generated
Normal file
20178
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
143
package.json
Normal file
143
package.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"name": "real-estate-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Real Estate Agent Platform - Backend API",
|
||||
"author": "SuperLabs",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"db:generate": "npx prisma generate",
|
||||
"db:migrate": "npx prisma migrate dev",
|
||||
"db:migrate:prod": "npx prisma migrate deploy",
|
||||
"db:push": "npx prisma db push",
|
||||
"db:seed": "npx prisma db seed",
|
||||
"db:studio": "npx prisma studio",
|
||||
"db:reset": "npx prisma migrate reset"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.954.0",
|
||||
"@aws-sdk/lib-storage": "^3.954.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bull": "^11.0.4",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/cqrs": "^11.0.3",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-socket.io": "^11.1.9",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
"@nestjs/swagger": "^11.2.3",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@nestjs/websockets": "^11.1.9",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@sendgrid/mail": "^8.1.6",
|
||||
"argon2": "^0.44.0",
|
||||
"axios": "^1.13.2",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bull": "^4.16.5",
|
||||
"bullmq": "^5.66.1",
|
||||
"cache-manager": "^7.2.7",
|
||||
"cache-manager-redis-yet": "^5.1.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cross-env": "^10.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"decimal.js": "^10.6.0",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"google-auth-library": "^10.5.0",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"joi": "^18.0.2",
|
||||
"mime-types": "^3.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"nest-winston": "^1.10.2",
|
||||
"nestjs-pino": "^4.5.0",
|
||||
"nodemailer": "^7.0.11",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.16.3",
|
||||
"pino-http": "^11.0.0",
|
||||
"prisma": "^7.2.0",
|
||||
"redis": "^5.10.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.34.5",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.8.1",
|
||||
"stripe": "^20.1.0",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.19.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@types/passport-google-oauth20": "^2.0.17",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"autocannon": "^8.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file was generated by Prisma, and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
},
|
||||
});
|
||||
147
prisma/schema.prisma
Normal file
147
prisma/schema.prisma
Normal file
@@ -0,0 +1,147 @@
|
||||
// Real Estate Agent Platform - Database Schema
|
||||
// Milestone 1: Foundation & Authentication
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// ENUMS
|
||||
// ===========================================
|
||||
|
||||
enum UserRole {
|
||||
USER
|
||||
AGENT
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
SUSPENDED
|
||||
PENDING_VERIFICATION
|
||||
}
|
||||
|
||||
enum AuthProvider {
|
||||
LOCAL
|
||||
GOOGLE
|
||||
FACEBOOK
|
||||
}
|
||||
|
||||
enum VerificationStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// USER & AUTHENTICATION (Milestone 1)
|
||||
// ===========================================
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String? // Null for social login users
|
||||
firstName String?
|
||||
lastName String?
|
||||
phone String?
|
||||
avatar String?
|
||||
role UserRole @default(USER)
|
||||
status UserStatus @default(ACTIVE)
|
||||
emailVerified Boolean @default(false)
|
||||
emailVerifiedAt DateTime?
|
||||
|
||||
// Social Auth
|
||||
authProvider AuthProvider @default(LOCAL)
|
||||
googleId String? @unique
|
||||
facebookId String? @unique
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lastLoginAt DateTime?
|
||||
|
||||
// Relations (will be added in later milestones)
|
||||
agentProfile AgentProfile?
|
||||
sessions Session[]
|
||||
passwordResets PasswordReset[]
|
||||
|
||||
@@index([email])
|
||||
@@index([role])
|
||||
@@index([status])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
token String @unique
|
||||
refreshToken String? @unique
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([token])
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
model PasswordReset {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
token String @unique
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([token])
|
||||
@@map("password_resets")
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// AGENT PROFILE (Milestone 2 - Placeholder)
|
||||
// ===========================================
|
||||
|
||||
model AgentProfile {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
slug String @unique
|
||||
bio String?
|
||||
headline String?
|
||||
location String?
|
||||
city String?
|
||||
state String?
|
||||
country String?
|
||||
yearsOfExperience Int?
|
||||
licenseNumber String?
|
||||
|
||||
// Verification
|
||||
verificationStatus VerificationStatus @default(PENDING)
|
||||
verifiedAt DateTime?
|
||||
|
||||
// Profile completeness
|
||||
isProfileComplete Boolean @default(false)
|
||||
profileCompleteness Int @default(0)
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([slug])
|
||||
@@index([verificationStatus])
|
||||
@@index([city, state])
|
||||
@@map("agent_profiles")
|
||||
}
|
||||
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
23
src/app.controller.ts
Normal file
23
src/app.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get API welcome message' })
|
||||
@ApiResponse({ status: 200, description: 'API is running' })
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: 'Health check endpoint' })
|
||||
@ApiResponse({ status: 200, description: 'Service is healthy' })
|
||||
getHealth() {
|
||||
return this.appService.getHealth();
|
||||
}
|
||||
}
|
||||
54
src/app.module.ts
Normal file
54
src/app.module.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
||||
import { configuration } from './config';
|
||||
import { PrismaModule } from './prisma';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Configuration
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
}),
|
||||
|
||||
// Rate Limiting
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60000, // 60 seconds
|
||||
limit: 100, // 100 requests per minute
|
||||
},
|
||||
]),
|
||||
|
||||
// Task Scheduling
|
||||
ScheduleModule.forRoot(),
|
||||
|
||||
// Event Emitter
|
||||
EventEmitterModule.forRoot(),
|
||||
|
||||
// Database
|
||||
PrismaModule,
|
||||
|
||||
// Feature Modules (will be added as we develop them)
|
||||
// AuthModule,
|
||||
// UsersModule,
|
||||
// AgentsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
AppService,
|
||||
// Global Rate Limiting Guard
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
38
src/app.service.ts
Normal file
38
src/app.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from './prisma';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
getHello(): string {
|
||||
return 'Real Estate Agent Platform API v1.0';
|
||||
}
|
||||
|
||||
async getHealth() {
|
||||
const dbHealthy = await this.checkDatabaseHealth();
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: this.configService.get<string>('app.env'),
|
||||
version: '1.0.0',
|
||||
services: {
|
||||
database: dbHealthy ? 'healthy' : 'unhealthy',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async checkDatabaseHealth(): Promise<boolean> {
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/common/filters/http-exception.filter.ts
Normal file
58
src/common/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AllExceptionsFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let status: number;
|
||||
let message: string | object;
|
||||
let error: string;
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'object') {
|
||||
message = (exceptionResponse as any).message || exception.message;
|
||||
error = (exceptionResponse as any).error || 'Error';
|
||||
} else {
|
||||
message = exceptionResponse;
|
||||
error = 'Error';
|
||||
}
|
||||
} else {
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
message = 'Internal server error';
|
||||
error = 'Internal Server Error';
|
||||
|
||||
// Log unexpected errors
|
||||
this.logger.error(
|
||||
`Unexpected error: ${exception}`,
|
||||
exception instanceof Error ? exception.stack : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const errorResponse = {
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
error,
|
||||
message,
|
||||
};
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
1
src/common/filters/index.ts
Normal file
1
src/common/filters/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './http-exception.filter';
|
||||
1
src/common/interceptors/index.ts
Normal file
1
src/common/interceptors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './transform.interceptor';
|
||||
32
src/common/interceptors/transform.interceptor.ts
Normal file
32
src/common/interceptors/transform.interceptor.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface Response<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TransformInterceptor<T>
|
||||
implements NestInterceptor<T, Response<T>>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<Response<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
100
src/config/configuration.ts
Normal file
100
src/config/configuration.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
export default () => ({
|
||||
// Application
|
||||
app: {
|
||||
name: process.env.APP_NAME || 'Real Estate Agent Platform',
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT || '3001', 10),
|
||||
apiUrl: process.env.API_URL || 'http://localhost:3001',
|
||||
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||
adminUrl: process.env.ADMIN_URL || 'http://localhost:3002',
|
||||
},
|
||||
|
||||
// Database
|
||||
database: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
|
||||
// JWT Authentication
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'super-secret-key',
|
||||
accessExpiration: process.env.JWT_ACCESS_EXPIRATION || '15m',
|
||||
refreshExpiration: process.env.JWT_REFRESH_EXPIRATION || '7d',
|
||||
},
|
||||
|
||||
// Password Hashing
|
||||
bcrypt: {
|
||||
saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS || '12', 10),
|
||||
},
|
||||
|
||||
// Google OAuth
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackUrl: process.env.GOOGLE_CALLBACK_URL,
|
||||
},
|
||||
|
||||
// Facebook OAuth
|
||||
facebook: {
|
||||
appId: process.env.FACEBOOK_APP_ID,
|
||||
appSecret: process.env.FACEBOOK_APP_SECRET,
|
||||
callbackUrl: process.env.FACEBOOK_CALLBACK_URL,
|
||||
},
|
||||
|
||||
// AWS S3
|
||||
aws: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
s3Bucket: process.env.AWS_S3_BUCKET,
|
||||
},
|
||||
|
||||
// Email
|
||||
mail: {
|
||||
host: process.env.MAIL_HOST,
|
||||
port: parseInt(process.env.MAIL_PORT || '587', 10),
|
||||
user: process.env.MAIL_USER,
|
||||
password: process.env.MAIL_PASSWORD,
|
||||
from: process.env.MAIL_FROM,
|
||||
},
|
||||
|
||||
// Stripe
|
||||
stripe: {
|
||||
secretKey: process.env.STRIPE_SECRET_KEY,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
|
||||
},
|
||||
|
||||
// Firebase
|
||||
firebase: {
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
||||
},
|
||||
|
||||
// Redis
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db: parseInt(process.env.REDIS_DB || '0', 10),
|
||||
},
|
||||
|
||||
// Rate Limiting
|
||||
throttle: {
|
||||
ttl: parseInt(process.env.THROTTLE_TTL || '60', 10),
|
||||
limit: parseInt(process.env.THROTTLE_LIMIT || '100', 10),
|
||||
},
|
||||
|
||||
// Logging
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || 'debug',
|
||||
},
|
||||
|
||||
// CORS
|
||||
cors: {
|
||||
origins: process.env.CORS_ORIGINS?.split(',') || [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3002',
|
||||
],
|
||||
},
|
||||
});
|
||||
1
src/config/index.ts
Normal file
1
src/config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as configuration } from './configuration';
|
||||
91
src/main.ts
Normal file
91
src/main.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import helmet from 'helmet';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { AppModule } from './app.module';
|
||||
import { AllExceptionsFilter } from './common/filters';
|
||||
import { TransformInterceptor } from './common/interceptors';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
const port = configService.get<number>('app.port') || 3001;
|
||||
const appEnv = configService.get<string>('app.env') || 'development';
|
||||
const corsOrigins = configService.get<string[]>('cors.origins') || [];
|
||||
|
||||
// Security
|
||||
app.use(helmet());
|
||||
app.use(cookieParser());
|
||||
|
||||
// CORS
|
||||
app.enableCors({
|
||||
origin: corsOrigins,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||
});
|
||||
|
||||
// Global prefix
|
||||
app.setGlobalPrefix('api/v1');
|
||||
|
||||
// Global pipes
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Global filters
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
|
||||
// Global interceptors
|
||||
app.useGlobalInterceptors(new TransformInterceptor());
|
||||
|
||||
// Swagger documentation (only in development)
|
||||
if (appEnv !== 'production') {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Real Estate Agent Platform API')
|
||||
.setDescription('API documentation for the Real Estate Agent Platform')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
name: 'JWT',
|
||||
description: 'Enter JWT token',
|
||||
in: 'header',
|
||||
},
|
||||
'JWT-auth',
|
||||
)
|
||||
.addTag('Auth', 'Authentication endpoints')
|
||||
.addTag('Users', 'User management endpoints')
|
||||
.addTag('Agents', 'Agent management endpoints')
|
||||
.addTag('Health', 'Health check endpoints')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.log(`Swagger documentation available at http://localhost:${port}/api/docs`);
|
||||
}
|
||||
|
||||
await app.listen(port);
|
||||
logger.log(`Application is running on: http://localhost:${port}`);
|
||||
logger.log(`Environment: ${appEnv}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
2
src/prisma/index.ts
Normal file
2
src/prisma/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './prisma.module';
|
||||
export * from './prisma.service';
|
||||
9
src/prisma/prisma.module.ts
Normal file
9
src/prisma/prisma.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
45
src/prisma/prisma.service.ts
Normal file
45
src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
constructor() {
|
||||
super({
|
||||
log:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? ['query', 'info', 'warn', 'error']
|
||||
: ['error'],
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
|
||||
async cleanDatabase() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error('cleanDatabase is not allowed in production');
|
||||
}
|
||||
|
||||
const models = Reflect.ownKeys(this).filter(
|
||||
(key) => typeof key === 'string' && !key.startsWith('_') && !key.startsWith('$'),
|
||||
);
|
||||
|
||||
return Promise.all(
|
||||
models.map((modelKey) => {
|
||||
const model = this[modelKey as string];
|
||||
if (model && typeof model.deleteMany === 'function') {
|
||||
return model.deleteMany();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
test/app.e2e-spec.ts
Normal file
25
test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user