Skip to content

Commit bac22d0

Browse files
committed
resolve merge conflicts with dev
2 parents 68ae90e + 5171b0a commit bac22d0

73 files changed

Lines changed: 2445 additions & 2057 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cd.yml

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,47 @@
1+
# name: CD
2+
3+
# on:
4+
# workflow_run:
5+
# workflows: ["CI"]
6+
# types:
7+
# - completed
8+
9+
# jobs:
10+
# deploy-core-api:
11+
# if: ${{ github.event.workflow_run.conclusion == 'success' }}
12+
# runs-on: ubuntu-latest
13+
14+
# steps:
15+
# - name: Deploy to EC2
16+
# uses: appleboy/ssh-action@v1.0.3
17+
# with:
18+
# host: ${{ secrets.EC2_HOST }}
19+
# username: ${{ secrets.EC2_USER }}
20+
# key: ${{ secrets.EC2_SSH_KEY }}
21+
# script: |
22+
# cd ${{ secrets.EC2_APP_PATH }}
23+
24+
# git pull origin main
25+
26+
# corepack enable
27+
# corepack prepare pnpm@latest --activate
28+
29+
# pnpm install --frozen-lockfile
30+
# pnpm --filter core-api build:prod
31+
32+
# pm2 restart core-api
33+
134
name: CD
235

336
on:
437
workflow_run:
538
workflows: ["CI"]
39+
branches: [main]
640
types:
741
- completed
842

943
jobs:
10-
deploy-core-api:
44+
deploy:
1145
if: ${{ github.event.workflow_run.conclusion == 'success' }}
1246
runs-on: ubuntu-latest
1347

@@ -18,15 +52,41 @@ jobs:
1852
host: ${{ secrets.EC2_HOST }}
1953
username: ${{ secrets.EC2_USER }}
2054
key: ${{ secrets.EC2_SSH_KEY }}
55+
2156
script: |
22-
cd ${{ secrets.EC2_APP_PATH }}
57+
set -e
2358
59+
cd ~/apps/noillin
60+
61+
echo "Pulling latest code..."
2462
git pull origin main
2563
64+
echo "Enabling corepack..."
2665
corepack enable
2766
corepack prepare pnpm@latest --activate
2867
68+
echo "Installing dependencies..."
2969
pnpm install --frozen-lockfile
30-
pnpm --filter core-api build:prod
3170
32-
pm2 restart core-api
71+
echo "Building shared..."
72+
pnpm --filter shared build
73+
74+
echo "Building core-api..."
75+
pnpm --filter core-api build
76+
77+
echo "Building realtime..."
78+
pnpm --filter realtime build
79+
80+
echo "Building worker..."
81+
pnpm --filter worker build
82+
83+
echo "Building web..."
84+
pnpm --filter web build
85+
86+
echo "Restarting PM2 services..."
87+
pm2 reload ecosystem.config.js --update-env || pm2 start ecosystem.config.js
88+
89+
echo "Saving PM2..."
90+
pm2 save
91+
92+
echo "Deployment completed successfully."

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@ node_modules
33
.env.*
44
dist
55
.next
6-
!.env.example
7-
seed_test.ts
6+
!.env.example
7+
data.ms/
8+
data.ms/
9+
*.mdb

DEPLOYMENT.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Noillin Production Deployment Runbook
2+
3+
## Production URL
4+
5+
- App: https://noillin.in
6+
- API health: https://noillin.in/api/health
7+
- Socket.IO: https://noillin.in/socket.io/
8+
9+
## Infrastructure
10+
11+
- Server: AWS EC2 Ubuntu 24.04
12+
- Domain: noillin.in
13+
- DNS: GoDaddy → Elastic IP
14+
- Reverse proxy: Nginx
15+
- SSL: Let's Encrypt Certbot
16+
- Process manager: PM2
17+
- Database: MongoDB Atlas
18+
- Storage: AWS S3
19+
- Cache: Redis
20+
- Queue: RabbitMQ
21+
- Package manager: pnpm
22+
23+
## Services
24+
25+
| Service | Port | PM2 Name |
26+
| ------------------ | -------: | -------- |
27+
| Next.js web | 3000 | web |
28+
| Core API | 5000 | core-api |
29+
| Realtime Socket.IO | 6001 | realtime |
30+
| Worker | internal | worker |
31+
32+
## Important Server Paths
33+
34+
```bash
35+
/home/ubuntu/apps/noillin
36+
/etc/nginx/sites-available/noillin
37+
/etc/nginx/sites-enabled/noillin
38+
/home/ubuntu/.pm2
39+
/home/ubuntu/backups
40+
```

apps/core-api/.env.example

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
1-
PORT=5000
2-
REDIS_URL=redis://localhost:6379
3-
RABBIT_URL=amqp://localhost:5672
4-
MEILI_HOST=http://localhost:7700
5-
MONGO_URI=mongodb://localhost:27017/noillin
1+
PORT=5000
2+
REDIS_URL=redis://localhost:6379
3+
RABBIT_URL=amqp://localhost:5672
4+
MEILI_HOST=http://localhost:7700
5+
MEILI_KEY=your_master_key
6+
MONGO_URI=mongodb://localhost:27017/noillin
7+
JWT_ACCESS_SECRET=your_access_secret
8+
JWT_REFRESH_SECRET=your_refresh_secret
9+
10+
# Email
11+
EMAIL_USER=your_email@gmail.com
12+
EMAIL_PASS=your_email_password
13+
14+
# AWS S3
15+
AWS_ACCESS_KEY_ID=your_access_key
16+
AWS_SECRET_ACCESS_KEY=your_secret_key
17+
AWS_REGION=your_region
18+
AWS_BUCKET_NAME=your_bucket_name
19+
20+
# Payments
21+
STRIPE_SECRET_KEY=your_stripe_secret_key
22+
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
23+
STRIPE_MOCK_PAYOUT=true
24+
25+
# Feature Flags
26+
ENABLE_WEB_PUSH=true

apps/core-api/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
"type": "module",
77
"scripts": {
88
"dev": "nodemon --watch src --ext ts --exec tsx src/server.ts",
9-
"build": "tsc --noEmit",
10-
"build:prod": "tsc",
9+
"build": "tsc",
1110
"start": "ts-node src/server.ts",
11+
"start:prod": "node dist/server.js",
12+
"typecheck": "tsc --noEmit",
1213
"test": "echo \"Error: no test specified\" && exit 1"
1314
},
1415
"keywords": [],
@@ -24,6 +25,7 @@
2425
"cookie-parser": "^1.4.7",
2526
"dotenv": "^17.2.3",
2627
"express": "^5.2.1",
28+
"express-rate-limit": "^8.5.1",
2729
"ioredis": "^5.10.1",
2830
"jsonwebtoken": "^9.0.3",
2931
"meilisearch": "^0.55.0",

apps/core-api/src/cache/redis.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import IORedis from "ioredis";
2-
import type {Redis} from "ioredis";
1+
import { Redis } from "ioredis";
32

43
import { logger } from "../utils/logger.js";
54
const redis_url = process.env.REDIS_URL
65
if (!redis_url){
76
throw new Error("REDIS_URL is not defined in environment variables")
87
}
9-
export const redis:Redis = new IORedis.default(redis_url)
8+
export const redis = new Redis(redis_url);
109
redis.on("connect", ()=>{
1110
logger.info("Redis connected successfully")
1211
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { rateLimit, Options } from 'express-rate-limit';
3+
4+
export const authLimiter = rateLimit({
5+
windowMs: 15 * 60 * 1000, // 15 minutes
6+
max: 10, // Limit each IP to 10 requests per windowMs
7+
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
8+
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
9+
message: {
10+
success: false,
11+
message: "Too many authentication attempts. Please try again later.",
12+
},
13+
handler: (req: Request, res: Response, next: NextFunction, options: Options) => {
14+
res.status(429).json(options.message);
15+
},
16+
});

apps/core-api/src/models/gig.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ const GigSchema = new Schema<GigDocument>(
137137
type: Number,
138138
default: 0,
139139
index: true
140+
},
141+
bannerUrl: {
142+
type: String,
143+
default: ""
140144
}
141145
},
142146

apps/core-api/src/repositories/chat.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const aggregateConversations = async (userId: string, role?: string) => {
108108
$cond: [
109109
{ $eq: ["$user.role", "INFLUENCER"] },
110110
{ $arrayElemAt: ["$influencerProfile.profileImageUrl", 0] },
111-
null,
111+
{ $arrayElemAt: ["$brandProfile.profileImageUrl", 0] },
112112
],
113113
},
114114
},

apps/core-api/src/repositories/gig-request.repository.ts

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Types } from "mongoose";
2+
13
import { GigRequestModel } from "../models/gig-request.model.js";
24

35
export class GigRequestRepository {
@@ -14,10 +16,46 @@ export class GigRequestRepository {
1416

1517
// Find by ID
1618
async findById(id: string) {
17-
return GigRequestModel.findById(id)
18-
.populate("gigId")
19-
.populate("brandId", "fullName profileImageUrl email")
20-
.populate("influencerId", "fullName profileImageUrl email");
19+
const results = await GigRequestModel.aggregate([
20+
{ $match: { _id: new Types.ObjectId(id) } },
21+
{
22+
$lookup: {
23+
from: "brandprofiles",
24+
localField: "brandId",
25+
foreignField: "userId",
26+
as: "brandProfile",
27+
},
28+
},
29+
{
30+
$lookup: {
31+
from: "influencerprofiles",
32+
localField: "influencerId",
33+
foreignField: "userId",
34+
as: "influencerProfile",
35+
},
36+
},
37+
{
38+
$lookup: {
39+
from: "gigs",
40+
localField: "gigId",
41+
foreignField: "_id",
42+
as: "gigData",
43+
},
44+
},
45+
{
46+
$project: {
47+
_id: 1,
48+
brandId: 1,
49+
influencerId: 1,
50+
gigId: { $arrayElemAt: ["$gigData", 0] },
51+
status: 1,
52+
note: 1,
53+
brandProfile: { $arrayElemAt: ["$brandProfile", 0] },
54+
influencerProfile: { $arrayElemAt: ["$influencerProfile", 0] },
55+
}
56+
}
57+
]);
58+
return results[0] || null;
2159
}
2260

2361
// Check existing request (prevent duplicate per gig)
@@ -36,15 +74,75 @@ export class GigRequestRepository {
3674

3775
// Get all requests for a user (brand or influencer)
3876
async findMyRequests(userId: string) {
39-
return GigRequestModel.find({
40-
$or: [
41-
{ brandId: userId },
42-
{ influencerId: userId },
43-
],
44-
})
45-
.populate("gigId")
46-
.populate("brandId", "fullName profileImageUrl")
47-
.populate("influencerId", "fullName profileImageUrl")
48-
.sort({ createdAt: -1 });
77+
return GigRequestModel.aggregate([
78+
{
79+
$match: {
80+
$or: [
81+
{ brandId: new Types.ObjectId(userId) },
82+
{ influencerId: new Types.ObjectId(userId) },
83+
],
84+
},
85+
},
86+
{
87+
$lookup: {
88+
from: "users",
89+
localField: "brandId",
90+
foreignField: "_id",
91+
as: "brandUser",
92+
},
93+
},
94+
{
95+
$lookup: {
96+
from: "brandprofiles",
97+
localField: "brandId",
98+
foreignField: "userId",
99+
as: "brandProfile",
100+
},
101+
},
102+
{
103+
$lookup: {
104+
from: "users",
105+
localField: "influencerId",
106+
foreignField: "_id",
107+
as: "influencerUser",
108+
},
109+
},
110+
{
111+
$lookup: {
112+
from: "influencerprofiles",
113+
localField: "influencerId",
114+
foreignField: "userId",
115+
as: "influencerProfile",
116+
},
117+
},
118+
{
119+
$lookup: {
120+
from: "gigs",
121+
localField: "gigId",
122+
foreignField: "_id",
123+
as: "gigData",
124+
},
125+
},
126+
{
127+
$project: {
128+
_id: 1,
129+
status: 1,
130+
note: 1,
131+
createdAt: 1,
132+
brandId: {
133+
_id: { $arrayElemAt: ["$brandUser._id", 0] },
134+
fullName: { $arrayElemAt: ["$brandProfile.companyName", 0] },
135+
profileImageUrl: { $arrayElemAt: ["$brandProfile.profileImageUrl", 0] },
136+
},
137+
influencerId: {
138+
_id: { $arrayElemAt: ["$influencerUser._id", 0] },
139+
fullName: { $arrayElemAt: ["$influencerProfile.fullName", 0] },
140+
profileImageUrl: { $arrayElemAt: ["$influencerProfile.profileImageUrl", 0] },
141+
},
142+
gigId: { $arrayElemAt: ["$gigData", 0] },
143+
},
144+
},
145+
{ $sort: { createdAt: -1 } },
146+
]);
49147
}
50148
}

0 commit comments

Comments
 (0)