Skip to content

Commit 8044a45

Browse files
authored
Merge pull request #686 from sparrowapp-dev/development
merge to release v2
2 parents 844affb + 0eb16b0 commit 8044a45

File tree

79 files changed

+7852
-1160
lines changed

Some content is hidden

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

79 files changed

+7852
-1160
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ AZURE_OPENAI_API_VERSION= # API version (e.g., "2023-05-15")
8989
AZURE_OPENAI_MAX_TOKENS= # Maximum tokens per AI response
9090
AZURE_OPENAI_MONTHLY_TOKEN_LIMIT= # Monthly token quota for AI services
9191
AZURE_OPENAI_ASSISTANT_ID= # OpenAI assistant ID
92+
ENCRYPTION_SECRET= # Encryption key for LLM key's Encryption
9293

9394
# Deepseek AI Configuration
9495
DEEPSEEK_ENDPOINT= # Deepseek API endpoint
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Inject, Injectable, OnModuleInit } from "@nestjs/common";
2+
import { Db, ObjectId } from "mongodb";
3+
import { v4 as uuidv4 } from "uuid";
4+
import { Collections } from "@src/modules/common/enum/database.collection.enum";
5+
6+
@Injectable()
7+
export class AuthToAuthProfilesMigration implements OnModuleInit {
8+
private hasRun = false;
9+
10+
constructor(@Inject("DATABASE_CONNECTION") private db: Db) {}
11+
12+
async onModuleInit(): Promise<void> {
13+
if (this.hasRun) return;
14+
15+
try {
16+
console.log(`\n\x1b[32m[Nest]\x1b[0m \x1b[32mRunning AuthToAuthProfilesMigration...`);
17+
18+
const collection = this.db.collection(Collections.COLLECTION);
19+
20+
const documents = await collection
21+
.find({
22+
$or: [
23+
{ auth: { $type: "object" }, "auth.0": { $exists: false } }, // Valid auth object (not array)
24+
{ auth: { $exists: false } }, // No auth field
25+
{ auth: null }, // Null auth
26+
],
27+
})
28+
.toArray();
29+
30+
if (documents.length === 0) {
31+
console.log("No documents needing authProfiles migration.");
32+
this.hasRun = true;
33+
return;
34+
}
35+
36+
const now = new Date();
37+
38+
for (const doc of documents) {
39+
const authObject = doc.auth;
40+
41+
// Case 1: auth is missing or invalid
42+
if (!authObject || typeof authObject !== "object" || Array.isArray(authObject)) {
43+
const update: any = {};
44+
45+
if (!Array.isArray(doc.authProfiles)) {
46+
update.$set = { authProfiles: [] };
47+
48+
await collection.updateOne(
49+
{ _id: new ObjectId(doc._id) },
50+
update
51+
);
52+
53+
console.log(`Set empty authProfiles for document: ${doc._id}`);
54+
} else {
55+
console.log(`authProfiles already exists for document: ${doc._id}`);
56+
}
57+
58+
continue;
59+
}
60+
61+
// Case 2: auth is a valid object, migrate to authProfiles
62+
let authType = "";
63+
const { bearerToken, basicAuth = {}, apiKey = {} } = authObject;
64+
65+
const hasBearer = bearerToken && bearerToken.trim() !== "";
66+
const hasBasic = basicAuth.username?.trim() || basicAuth.password?.trim();
67+
const hasApiKey = apiKey.authKey?.trim() || apiKey.authValue?.trim();
68+
69+
if (hasBearer) {
70+
authType = "Bearer Token";
71+
} else if (hasBasic) {
72+
authType = "Basic Auth";
73+
} else if (hasApiKey) {
74+
authType = "API Key";
75+
} else {
76+
console.warn(`Skipping ${doc._id}: no valid auth data.`);
77+
continue;
78+
}
79+
80+
const newAuthProfile = {
81+
name: "New-Auth-Profile",
82+
description: "",
83+
authType,
84+
auth: {
85+
bearerToken: bearerToken || "",
86+
basicAuth: {
87+
username: basicAuth.username || "",
88+
password: basicAuth.password || "",
89+
},
90+
apiKey: {
91+
authKey: apiKey.authKey || "",
92+
authValue: apiKey.authValue || "",
93+
addTo: apiKey.addTo || "Header",
94+
},
95+
},
96+
defaultKey: false,
97+
createdAt: now,
98+
authId: uuidv4(),
99+
};
100+
101+
const update: any = {};
102+
103+
if (Array.isArray(doc.authProfiles)) {
104+
update.$push = { authProfiles: newAuthProfile };
105+
} else {
106+
update.$set = { authProfiles: [newAuthProfile] };
107+
}
108+
109+
await collection.updateOne(
110+
{ _id: new ObjectId(doc._id) },
111+
update
112+
);
113+
114+
console.log(`Updated authProfiles for document: ${doc._id}`);
115+
}
116+
117+
console.log(`Migration completed. Total processed: ${documents.length}`);
118+
this.hasRun = true;
119+
} catch (error) {
120+
console.error("Error during AuthToAuthProfilesMigration:", error);
121+
}
122+
}
123+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Inject, Injectable, OnModuleInit } from "@nestjs/common";
2+
import { Db, ObjectId } from "mongodb";
3+
import { Collections } from "@src/modules/common/enum/database.collection.enum";
4+
5+
@Injectable()
6+
export class AuthToAuthProfilesMigration implements OnModuleInit {
7+
private hasRun = false;
8+
9+
constructor(@Inject("DATABASE_CONNECTION") private db: Db) {}
10+
11+
async onModuleInit(): Promise<void> {
12+
if (this.hasRun) return;
13+
14+
try {
15+
console.log(`\n\x1b[32m[Nest]\x1b[0m \x1b[32mRunning AddEmptyAuthProfilesMigration...`);
16+
17+
const collection = this.db.collection(Collections.COLLECTION);
18+
19+
const documents = await collection
20+
.find({
21+
$or: [
22+
{ authProfiles: { $exists: false } },
23+
{ authProfiles: { $not: { $type: "array" } } },
24+
],
25+
})
26+
.toArray();
27+
28+
if (documents.length === 0) {
29+
console.log("No documents needing authProfiles initialization.");
30+
this.hasRun = true;
31+
return;
32+
}
33+
34+
for (const doc of documents) {
35+
await collection.updateOne(
36+
{ _id: new ObjectId(doc._id) },
37+
{ $set: { authProfiles: [] } }
38+
);
39+
console.log(`Set empty authProfiles for document: ${doc._id}`);
40+
}
41+
42+
console.log(`Migration completed. Total updated: ${documents.length}`);
43+
this.hasRun = true;
44+
} catch (error) {
45+
console.error("Error during AddEmptyAuthProfilesMigration:", error);
46+
}
47+
}
48+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
2+
import { Db, ObjectId } from 'mongodb';
3+
import { ConfigService } from '@nestjs/config';
4+
import * as CryptoJS from 'crypto-js';
5+
6+
@Injectable()
7+
export class EncryptLlmApiKeysMigration implements OnModuleInit {
8+
private hasRun = false;
9+
private readonly secret: string;
10+
private readonly iv = CryptoJS.enc.Hex.parse('00000000000000000000000000000000');
11+
12+
constructor(
13+
@Inject('DATABASE_CONNECTION') private readonly db: Db,
14+
private readonly configService: ConfigService,
15+
) {
16+
this.secret = this.configService.get('ai.encryptionSecret');
17+
}
18+
19+
async onModuleInit(): Promise<void> {
20+
if (this.hasRun) return;
21+
22+
try {
23+
console.log('\x1b[34m[Migration]\x1b[0m Encrypting API keys in llmconversation...');
24+
25+
const llmConversationCollection = this.db.collection('llmconversation');
26+
const fields = ['openai', 'anthropic', 'deepseek', 'google'];
27+
28+
const documents = await llmConversationCollection.find({}).toArray();
29+
30+
for (const doc of documents) {
31+
const updates: Record<string, any> = {};
32+
let needsUpdate = false;
33+
34+
for (const field of fields) {
35+
if (Array.isArray(doc[field])) {
36+
const updatedArray = doc[field].map((entry: any) => {
37+
if (entry?.value && !this.isEncrypted(entry.value)) {
38+
const encrypted = this.encrypt(entry.value);
39+
needsUpdate = true;
40+
return { ...entry, value: encrypted };
41+
}
42+
return entry;
43+
});
44+
updates[field] = updatedArray;
45+
}
46+
}
47+
48+
if (needsUpdate) {
49+
await llmConversationCollection.updateOne(
50+
{ _id: new ObjectId(doc._id) },
51+
{ $set: updates },
52+
);
53+
console.log(`Updated document _id: ${doc._id}`);
54+
}
55+
}
56+
57+
this.hasRun = true;
58+
console.log('\x1b[32m[Migration]\x1b[0m Encryption completed successfully.');
59+
} catch (err) {
60+
console.error('[Migration Error]', err);
61+
}
62+
}
63+
64+
private encrypt(text: string): string {
65+
const key = CryptoJS.enc.Utf8.parse(this.secret.padEnd(32, ' '));
66+
const encrypted = CryptoJS.AES.encrypt(text, key, {
67+
iv: this.iv,
68+
mode: CryptoJS.mode.CBC,
69+
padding: CryptoJS.pad.Pkcs7,
70+
});
71+
72+
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
73+
}
74+
75+
private isEncrypted(value: string): boolean {
76+
// Basic check: OpenAI key pattern usually starts with 'sk-'
77+
return /^[A-Za-z0-9+/=]{32,}$/.test(value) && !value.startsWith('sk-');
78+
}
79+
}

migrations/update-hub-billing.migration.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ export class UpdateHubBillingMigration implements OnModuleInit {
7373
amount_billed: 119.88,
7474
currency: "usd",
7575
status: "active",
76-
collection_method: "charge_manually",
77-
paid_at: new Date("2025-07-08T13:04:28.000Z"),
7876
billingType: "paid",
7977
updatedBy: "system-admin",
8078
in_trial: false,

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@azure/service-bus": "^7.9.1",
4444
"@azure/storage-blob": "^12.18.0",
4545
"@blazity/nest-file-fastify": "^1.0.0",
46+
"@dqbd/tiktoken": "^1.0.21",
4647
"@fastify/helmet": "^11.0.0",
4748
"@fastify/multipart": "^8.0.0",
4849
"@fastify/rate-limit": "^8.0.3",
@@ -73,6 +74,7 @@
7374
"class-transformer": "0.5.1",
7475
"class-transformer-validator": "^0.9.1",
7576
"class-validator": "^0.14.0",
77+
"crypto-js": "^4.2.0",
7678
"curlconverter": "^4.9.0",
7779
"dotenv": "16.0.1",
7880
"fastify": "4.28.1",
@@ -91,6 +93,7 @@
9193
"passport": "0.6.0",
9294
"passport-google-oauth20": "^2.0.0",
9395
"passport-jwt": "^4.0.1",
96+
"pdf-parse": "^1.1.1",
9497
"pino": "^8.15.3",
9598
"pino-pretty": "^10.2.0",
9699
"prom-client": "^15.1.3",
@@ -109,13 +112,15 @@
109112
"@jest/types": "^29.6.3",
110113
"@nestjs/testing": "^10.1.3",
111114
"@types/chai": "^4.3.5",
115+
"@types/crypto-js": "^4.2.2",
112116
"@types/gravatar": "1.8.3",
113117
"@types/jest": "^29.5.3",
114118
"@types/js-yaml": "^4.0.7",
115119
"@types/jsonwebtoken": "^9.0.9",
116120
"@types/mime-types": "^3.0.1",
117121
"@types/node": "16.11.56",
118122
"@types/passport-jwt": "^3.0.9",
123+
"@types/pdf-parse": "^1.1.5",
119124
"@types/supertest": "^2.0.12",
120125
"@types/uuid": "^9.0.5",
121126
"@types/ws": "^8.5.13",
@@ -140,6 +145,6 @@
140145
},
141146
"packageManager": "[email protected]",
142147
"optionalDependencies": {
143-
"@sparrowapp-dev/stripe-billing": "^1.1.1"
148+
"@sparrowapp-dev/stripe-billing": "^1.1.7"
144149
}
145150
}

0 commit comments

Comments
 (0)