Skip to content

Commit 2c868fb

Browse files
committed
fix(dx): clarify encryption key generation format
2 parents 76bebe2 + 1cf5a6e commit 2c868fb

24 files changed

Lines changed: 853 additions & 39 deletions

File tree

.env.example

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,31 @@ PORT=1234 # For Admin Server (Dashboard)
1010
USER_PORT=1235 # For User Server (Public API)
1111
NODE_ENV=development
1212

13-
14-
# ── Database & Cache ──────────────────────────────────────────────────────────
13+
# ── Database & Cache ────────────────────────────────────────────────────────
1514
# When using docker-compose, these are automatically overridden to point to
1615
# internal service names (mongo, redis). You do NOT need to change these.
1716
MONGO_URL=mongodb://mongo:27017/urbackend
1817
REDIS_URL=redis://redis:6379
1918

20-
# ── Authentication ────────────────────────────────────────────────────────────
21-
# Generate random strings for these values (e.g., `openssl rand -base64 32`)
19+
# ── Authentication ──────────────────────────────────────────────────────────
20+
# Generate random secrets for these values.
21+
# JWT_SECRET/API_KEY_SALT example: `openssl rand -base64 32`
22+
# ENCRYPTION_KEY must be 64 hex chars (32 bytes), example: `openssl rand -hex 32`
2223
JWT_SECRET=your_super_secret_jwt_key_min_32_chars
23-
ENCRYPTION_KEY=32_character_long_string_for_byod_creds
24+
ENCRYPTION_KEY=64_hex_characters_for_aes_256_gcm_key
2425
API_KEY_SALT=your_random_api_key_salt
2526

2627
# Public API userAuth token config (optional; defaults shown)
2728
PUBLIC_AUTH_ACCESS_TOKEN_TTL=15m
2829
PUBLIC_AUTH_REFRESH_TOKEN_TTL_SECONDS=604800
2930

30-
# ── External Storage (Supabase) ───────────────────────────────────────────────
31+
# ── External Storage (Supabase) ─────────────────────────────────────────────
3132
# Required for file upload/storage features.
3233
# Get from: https://app.supabase.com → Project Settings → API
3334
SUPABASE_URL=https://your-project.supabase.co
3435
SUPABASE_KEY=your-supabase-anon-key
3536

36-
# ── Email (Resend) ────────────────────────────────────────────────────────────
37+
# ── Email (Resend) ──────────────────────────────────────────────────────────
3738
# Required for OTP / email verification flow.
3839
# Get from: https://resend.com/api-keys
3940
RESEND_API_KEY=re_your_resend_api_key
@@ -42,11 +43,11 @@ EMAIL_FROM=onboarding@resend.dev
4243
# Get from: https://resend.com/webhooks → Create Endpoint
4344
RESEND_WEBHOOK_SECRET=whsec_your_resend_webhook_secret
4445

45-
# ── Frontend ──────────────────────────────────────────────────────────────────
46+
# ── Frontend ────────────────────────────────────────────────────────────────
4647
FRONTEND_URL=http://localhost:5173
4748
PUBLIC_API_URL=https://api.ub.bitbros.in
4849

49-
# ── Billing (Razorpay) ──────────────────────────────────────────────────────
50+
# ── Billing (Razorpay) ──────────────────────────────────────────────────────
5051
# Get from: https://dashboard.razorpay.com → Settings → API Keys
5152
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
5253
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxx
@@ -55,4 +56,10 @@ RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxx
5556
RAZORPAY_PLAN_ID=plan_xxxxxxxxxxxx
5657

5758
# Get from: https://dashboard.razorpay.com → Settings → Webhooks → Add Endpoint
58-
RAZORPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
59+
RAZORPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
60+
61+
# ── Internal Microservices (AI & Python) ────────────────────────────────────
62+
# Required for AI Query Builder and Python microservice communication
63+
PYTHON_SERVICE_URL=http://localhost:8000
64+
INTERNAL_SECRET=generate_a_random_32_char_secret_here
65+
GROQ_API_KEY=gsk_your_groq_api_key_here

.github/workflows/weekly-changelog.yml

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,11 @@ jobs:
7272
7373
# Create monthly file if it doesn't exist yet
7474
if [ ! -f "$FILE" ]; then
75-
cat > "$FILE" << MDXEOF
76-
---
77-
title: Changelog
78-
description: What's new in urBackend — new features, improvements, and fixes.
79-
---
80-
81-
MDXEOF
75+
echo "---" > "$FILE"
76+
echo "title: Changelog" >> "$FILE"
77+
echo "description: What's new in urBackend — new features, improvements, and fixes." >> "$FILE"
78+
echo "---" >> "$FILE"
79+
echo "" >> "$FILE"
8280
fi
8381
8482
# Insert new <Update> entry right after the closing --- of frontmatter
@@ -98,23 +96,17 @@ MDXEOF
9896
9997
echo "Updated: $FILE"
10098
101-
- name: Open Pull Request
102-
env:
103-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
104-
run: |
105-
BRANCH="changelog/week-of-$(date +%Y-%m-%d)-${{ github.run_id }}"
106-
107-
git config user.name "github-actions[bot]"
108-
git config user.email "github-actions[bot]@users.noreply.github.com"
109-
git checkout -b "$BRANCH"
110-
git add mintlify/docs/changelog/
111-
git diff --staged --quiet && echo "No changes to commit" && exit 0
112-
git commit -m "docs: weekly changelog update $(date +%Y-%m-%d)"
113-
git push origin "$BRANCH"
114-
gh pr create \
115-
--repo ${{ github.repository }} \
116-
--base main \
117-
--head "$BRANCH" \
118-
--title "docs: weekly changelog update $(date +%Y-%m-%d)" \
119-
--body "Auto-generated weekly changelog entry. Please review before merging." \
120-
--label "documentation"
99+
- name: Create Pull Request
100+
uses: peter-evans/create-pull-request@v7
101+
with:
102+
# Use a PAT if available. This bypasses repos where GITHUB_TOKEN is
103+
# restricted from creating PRs via GitHub Actions settings.
104+
token: ${{ secrets.CHANGELOG_PR_TOKEN || secrets.GITHUB_TOKEN }}
105+
branch: changelog/week-of-${{ github.run_id }}
106+
base: main
107+
commit-message: docs: weekly changelog update
108+
title: docs: weekly changelog update
109+
body: Auto-generated weekly changelog entry. Please review before merging.
110+
labels: documentation
111+
add-paths: |
112+
mintlify/docs/changelog/

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,12 @@ coverage/
99
.turbo/
1010
plan.md
1111
/.kiroo
12-
/xtemp/
12+
/xtemp/
13+
14+
# Python
15+
__pycache__/
16+
*.py[cod]
17+
*$py.class
18+
.pytest_cache/
19+
venv/
20+
env/

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
{
2+
"git.ignoreLimitWarning": true
23
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const axios = require('axios');
2+
const crypto = require('crypto');
3+
const { forwardToPythonService } = require('../utils/internalPythonClient');
4+
5+
describe('internalPythonClient', () => {
6+
let originalEnv;
7+
8+
beforeEach(() => {
9+
originalEnv = process.env;
10+
process.env = { ...originalEnv };
11+
jest.clearAllMocks();
12+
13+
// Mock Date.now to freeze timestamp for assertions
14+
jest.spyOn(Date, 'now').mockImplementation(() => 1609459200000); // 2021-01-01T00:00:00.000Z
15+
jest.spyOn(axios, 'post').mockResolvedValue({ data: { success: true } });
16+
});
17+
18+
afterEach(() => {
19+
process.env = originalEnv;
20+
jest.restoreAllMocks();
21+
});
22+
23+
test('throws error if INTERNAL_SECRET is missing', async () => {
24+
delete process.env.INTERNAL_SECRET;
25+
26+
await expect(forwardToPythonService('/test', {}))
27+
.rejects
28+
.toThrow("INTERNAL_SECRET is not defined in environment");
29+
});
30+
31+
test('generates correct HMAC signature and calls axios', async () => {
32+
process.env.INTERNAL_SECRET = 'test-secret';
33+
process.env.PYTHON_SERVICE_URL = 'http://test-python.local';
34+
35+
const path = '/ai/query-builder';
36+
const payload = { prompt: "test prompt" };
37+
const payloadString = JSON.stringify(payload);
38+
const timestamp = "1609459200000";
39+
40+
// Calculate expected signature manually
41+
const expectedSignature = crypto
42+
.createHmac('sha256', 'test-secret')
43+
.update(`${timestamp}.${payloadString}`)
44+
.digest('hex');
45+
46+
const result = await forwardToPythonService(path, payload);
47+
48+
expect(axios.post).toHaveBeenCalledTimes(1);
49+
expect(axios.post).toHaveBeenCalledWith(
50+
`http://test-python.local${path}`,
51+
payloadString,
52+
{
53+
headers: {
54+
'X-Internal-Signature': expectedSignature,
55+
'X-Timestamp': timestamp,
56+
'Content-Type': 'application/json'
57+
}
58+
}
59+
);
60+
expect(result).toEqual({ success: true });
61+
});
62+
});

apps/dashboard-api/src/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,11 @@ const analyticsRoute = require('./routes/analytics');
105105
const billingRoute = require('./routes/billing');
106106
const eventsRoute = require('./routes/events');
107107
const adminMetricsRoute = require('./routes/admin.metrics');
108+
const aiRoute = require('./routes/ai.routes');
108109

109110
app.use('/api/auth', authRoute);
110111
app.use('/api/projects', dashboardLimiter, projectRoute);
112+
app.use('/api/projects/:projectId/ai', dashboardLimiter, aiRoute);
111113
app.use('/api/projects', dashboardLimiter, webhookRoute);
112114
app.use('/api/releases', releaseRoute);
113115
app.use('/api/analytics', dashboardLimiter, analyticsRoute);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
const { Project } = require('@urbackend/common/src/models');
2+
const { forwardToPythonService } = require('../utils/internalPythonClient');
3+
const { AppError } = require('@urbackend/common');
4+
5+
/**
6+
* Controller to handle AI Query Builder requests.
7+
*/
8+
const queryBuilder = async (req, res, next) => {
9+
try {
10+
const { projectId } = req.params;
11+
const { collectionName, prompt } = req.body;
12+
13+
const mongoose = require('mongoose');
14+
if (!mongoose.Types.ObjectId.isValid(projectId)) {
15+
throw new AppError(400, "Invalid project ID");
16+
}
17+
18+
if (typeof collectionName !== 'string' || typeof prompt !== 'string') {
19+
throw new AppError(400, "Collection name and prompt must be strings");
20+
}
21+
22+
const safeCollectionName = collectionName.trim();
23+
const safePrompt = prompt.trim();
24+
25+
if (!safeCollectionName || !safePrompt) {
26+
throw new AppError(400, "Collection name and prompt are required");
27+
}
28+
29+
if (safeCollectionName === 'users') {
30+
throw new AppError(403, "Cannot query the users collection via AI");
31+
}
32+
33+
// 1. Fetch the project and specifically the requested collection schema
34+
const project = await Project.findOne(
35+
{ _id: projectId, owner: req.user._id, "collections.name": safeCollectionName },
36+
{ "collections.$": 1 }
37+
);
38+
39+
if (!project || !project.collections || project.collections.length === 0) {
40+
throw new AppError(404, "Collection not found or access denied");
41+
}
42+
43+
const collection = project.collections[0];
44+
const allowedFields = new Set([
45+
...collection.model.map(field => field.key),
46+
'_id',
47+
'createdAt',
48+
'updatedAt'
49+
]);
50+
51+
// 2. Extract simplified schema fields for the LLM
52+
// We only send key and type to save tokens and prevent confusion
53+
const schemaFields = collection.model.map(field => ({
54+
key: field.key,
55+
type: field.type
56+
}));
57+
58+
// Add implicit MongoDB fields
59+
schemaFields.push(
60+
{ key: "_id", type: "OBJECTID" },
61+
{ key: "createdAt", type: "DATE" },
62+
{ key: "updatedAt", type: "DATE" }
63+
);
64+
65+
// 3. Forward request to Python Service
66+
const aiResponse = await forwardToPythonService('/ai/query-builder', {
67+
prompt: safePrompt,
68+
schema_fields: schemaFields
69+
});
70+
71+
// 4. Return the structured JSON to the frontend
72+
// Ensure filters is always an array to prevent frontend crash
73+
const rawFilters = Array.isArray(aiResponse.filters) ? aiResponse.filters : [];
74+
const allowedOperators = new Set(['=', '_gt', '_lt', '_gte', '_lte', '_ne', '_regex']);
75+
const safeFilters = rawFilters.filter(f => {
76+
const isPrimitiveValue = ['string', 'number', 'boolean'].includes(typeof f?.value);
77+
return (
78+
f &&
79+
typeof f.field === 'string' &&
80+
typeof f.operator === 'string' &&
81+
allowedFields.has(f.field) &&
82+
allowedOperators.has(f.operator) &&
83+
isPrimitiveValue
84+
);
85+
});
86+
87+
res.status(200).json({
88+
success: true,
89+
data: {
90+
filters: safeFilters,
91+
sort: typeof aiResponse.sort === 'string' ? aiResponse.sort : '-createdAt'
92+
},
93+
message: "Query built successfully"
94+
});
95+
96+
} catch (error) {
97+
// Forward expected AppErrors
98+
if (error instanceof AppError) {
99+
return next(error);
100+
}
101+
102+
// Wrap Python/Axios errors
103+
if (error.response && error.response.data) {
104+
console.error("AI Service returned error:", error.response.status, error.response.data);
105+
106+
let errorMessage = "AI Service Error";
107+
if (typeof error.response.data === 'string') {
108+
errorMessage = error.response.data;
109+
} else if (error.response.data.detail) {
110+
errorMessage = typeof error.response.data.detail === 'string' ? error.response.data.detail : JSON.stringify(error.response.data.detail);
111+
} else {
112+
errorMessage = JSON.stringify(error.response.data);
113+
}
114+
115+
return next(new AppError(error.response.status || 500, errorMessage));
116+
}
117+
118+
next(new AppError(500, "Failed to build query via AI"));
119+
}
120+
};
121+
122+
module.exports = {
123+
queryBuilder
124+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const express = require('express');
2+
const router = express.Router({ mergeParams: true }); // mergeParams is crucial to access :projectId
3+
const aiController = require('../controllers/ai.controller');
4+
const authMiddleware = require('../middlewares/authMiddleware');
5+
6+
// All AI routes require the user to be authenticated
7+
router.use(authMiddleware);
8+
9+
/**
10+
* @route POST /api/projects/:projectId/ai/query-builder
11+
* @desc Generate MongoDB filters from natural language
12+
* @access Private
13+
*/
14+
router.post('/query-builder', aiController.queryBuilder);
15+
16+
module.exports = router;

0 commit comments

Comments
 (0)