Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ──────────────────────────────────────────────
# BlackRoad Production Environment Variables
# Copy to .env and fill in real values
# ──────────────────────────────────────────────

# ── Server ────────────────────────────────────
NODE_ENV=production
PORT=4000

# ── Stripe ────────────────────────────────────
STRIPE_SECRET_KEY=sk_live_REPLACE_ME
STRIPE_PUBLISHABLE_KEY=pk_live_REPLACE_ME
STRIPE_WEBHOOK_SECRET=whsec_REPLACE_ME

# ── Google Drive ──────────────────────────────
GOOGLE_DRIVE_CLIENT_ID=REPLACE_ME
GOOGLE_DRIVE_CLIENT_SECRET=REPLACE_ME
GOOGLE_DRIVE_REDIRECT_URI=https://blackroad.io/api/drive/callback
GOOGLE_DRIVE_FOLDER_ID=REPLACE_ME

# ── Database ──────────────────────────────────
DATABASE_URL=postgresql://user:password@localhost:5432/blackroad

# ── LLM / Lucidia ────────────────────────────
LLM_BASE_URL=http://127.0.0.1:8083

# ── S3 / Object Storage ──────────────────────
S3_BUCKET=blackroad-uploads
S3_REGION=us-east-1
S3_ACCESS_KEY=REPLACE_ME
S3_SECRET_KEY=REPLACE_ME

# ── Auth / JWT ────────────────────────────────
JWT_SECRET=REPLACE_ME
SESSION_SECRET=REPLACE_ME
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
package-lock.json
.env
*.log
*.sqlite
Expand Down
69 changes: 69 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict';

/**
* Centralised configuration loader.
*
* Reads from process.env (populate via .env in development).
* All Stripe and Drive keys are required in production; missing keys are
* surfaced through the /api/config/status health endpoint.
*/

const REQUIRED_KEYS = [
'STRIPE_SECRET_KEY',
'STRIPE_PUBLISHABLE_KEY',
'STRIPE_WEBHOOK_SECRET',
'GOOGLE_DRIVE_CLIENT_ID',
'GOOGLE_DRIVE_CLIENT_SECRET',
'GOOGLE_DRIVE_FOLDER_ID',
'DATABASE_URL',
'JWT_SECRET'
];

function loadConfig() {
return {
port: process.env.PORT || 4000,
nodeEnv: process.env.NODE_ENV || 'development',

stripe: {
secretKey: process.env.STRIPE_SECRET_KEY || '',
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || '',
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || ''
},

drive: {
clientId: process.env.GOOGLE_DRIVE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_DRIVE_CLIENT_SECRET || '',
redirectUri: process.env.GOOGLE_DRIVE_REDIRECT_URI || 'https://blackroad.io/api/drive/callback',
folderId: process.env.GOOGLE_DRIVE_FOLDER_ID || ''
},

database: {
url: process.env.DATABASE_URL || ''
},

llm: {
baseUrl: (process.env.LLM_BASE_URL || 'http://127.0.0.1:8083').replace(/\/$/, '')
},

s3: {
bucket: process.env.S3_BUCKET || 'blackroad-uploads',
region: process.env.S3_REGION || 'us-east-1',
accessKey: process.env.S3_ACCESS_KEY || '',
secretKey: process.env.S3_SECRET_KEY || ''
},

auth: {
jwtSecret: process.env.JWT_SECRET || '',
sessionSecret: process.env.SESSION_SECRET || ''
}
};
}

/**
* Returns a list of required environment variables that are missing or empty.
*/
function getMissingKeys() {
return REQUIRED_KEYS.filter(k => !process.env[k]);
}

module.exports = { loadConfig, getMissingKeys, REQUIRED_KEYS };
134 changes: 134 additions & 0 deletions config/products.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use strict';

/**
* BlackRoad production product catalog.
*
* Each entry maps to a portal described in PORTALS.md and is ready to be
* synced with Stripe Products via the API.
*/

const products = [
{
id: 'roadbook',
name: 'Roadbook',
description: 'Trip journals with photos, itineraries and map routes.',
active: true,
tier: 'free',
stripe_price_id: null, // populated after Stripe sync
metadata: { portal: 'roadbook', stack: 'node-prisma-postgres-s3' }
},
{
id: 'roadview',
name: 'Roadview',
description: 'Stream user journeys on an interactive map with clustering and filters.',
active: true,
tier: 'pro',
stripe_price_id: null,
metadata: { portal: 'roadview', stack: 'websocket-postgis' }
},
{
id: 'lucidia',
name: 'Lucidia',
description: 'Knowledge base of travel guides and user tips using MDX articles.',
active: true,
tier: 'free',
stripe_price_id: null,
metadata: { portal: 'lucidia', stack: 'nestjs-search' }
},
{
id: 'roadcode',
name: 'Roadcode',
description: 'Collaborative coding playground for trip-related tools.',
active: true,
tier: 'pro',
stripe_price_id: null,
metadata: { portal: 'roadcode', stack: 'containers-git' }
},
{
id: 'radius',
name: 'Radius',
description: 'Local meetup planner highlighting events near a user\'s current position.',
active: true,
tier: 'free',
stripe_price_id: null,
metadata: { portal: 'radius', stack: 'redis-geospatial' }
},
{
id: 'roadworld',
name: 'Roadworld',
description: 'Social feed aggregating activity from all portals.',
active: true,
tier: 'pro',
stripe_price_id: null,
metadata: { portal: 'roadworld', stack: 'kafka-graphql' }
},
{
id: 'roadcoin',
name: 'Roadcoin',
description: 'Wallet dashboard showing token balances and staking actions.',
active: true,
tier: 'enterprise',
stripe_price_id: null,
metadata: { portal: 'roadcoin', stack: 'rust-ledger' }
},
{
id: 'roadchain',
name: 'Roadchain',
description: 'Explorer for on-chain travel proofs and smart-contract interactions.',
active: true,
tier: 'enterprise',
stripe_price_id: null,
metadata: { portal: 'roadchain', stack: 'indexer-chain' }
},
{
id: 'roadie',
name: 'Roadie',
description: 'Mobile assistant guiding users through trips with voice commands.',
active: true,
tier: 'pro',
stripe_price_id: null,
metadata: { portal: 'roadie', stack: 'stt-pipeline' }
},
{
id: 'prism',
name: 'Prism Console',
description: 'Master console unifying tools from all portals through customizable tiles and global search.',
active: true,
tier: 'enterprise',
stripe_price_id: null,
metadata: { portal: 'prism', stack: 'orchestrator-sso' }
},
{
id: 'blackroad-drive',
name: 'BlackRoad Drive',
description: 'Cloud storage for trip documents, photos and itineraries backed by Google Drive and S3.',
active: true,
tier: 'pro',
stripe_price_id: null,
metadata: { portal: 'drive', stack: 'google-drive-s3' }
}
];

function getAllProducts() {
return products;
}

function getActiveProducts() {
return products.filter(p => p.active);
}

function getProductById(id) {
return products.find(p => p.id === id) || null;
}

function getProductsByTier(tier) {
return products.filter(p => p.tier === tier);
}

module.exports = {
products,
getAllProducts,
getActiveProducts,
getProductById,
getProductsByTier
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"start": "node server_full.js"
},
"dependencies": {
"express": "^4.18.2"
"express": "^4.18.2",
"stripe": "^14.11.0"
}
}
97 changes: 95 additions & 2 deletions server_full.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

const express = require('express');
const http = require('http');
const { loadConfig, getMissingKeys } = require('./config');
const { getAllProducts, getActiveProducts, getProductById, getProductsByTier } = require('./config/products');

const app = express();
const server = http.createServer(app);
const config = loadConfig();

app.use(express.json());

Expand All @@ -18,14 +21,100 @@ app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'blackroad-api' });
});

// ── Config status (production readiness check) ──────────────
app.get('/api/config/status', (req, res) => {
const missing = getMissingKeys();
res.json({
ready: missing.length === 0,
environment: config.nodeEnv,
missing_keys: missing
});
});

// ── Products ─────────────────────────────────────────────────
app.get('/api/products', (req, res) => {
const { tier, active } = req.query;
let result;
if (tier) {
result = getProductsByTier(tier);
} else if (active === 'true') {
result = getActiveProducts();
} else {
result = getAllProducts();
}
res.json({ products: result, count: result.length });
});

app.get('/api/products/:id', (req, res) => {
const product = getProductById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
});

// ── Stripe products sync endpoint ────────────────────────────
app.post('/api/stripe/products/sync', async (req, res) => {
if (!config.stripe.secretKey) {
return res.status(503).json({ error: 'STRIPE_SECRET_KEY not configured' });
}
try {
const stripe = require('stripe')(config.stripe.secretKey);
const catalog = getActiveProducts();
const synced = [];

for (const product of catalog) {
// Check if product already exists in Stripe by blackroad_id metadata
const existing = await stripe.products.search({
query: `metadata["blackroad_id"]:"${product.id}"`
});

let stripeProduct;
if (existing.data.length > 0) {
stripeProduct = await stripe.products.update(existing.data[0].id, {
name: product.name,
description: product.description,
active: product.active,
metadata: { blackroad_id: product.id, tier: product.tier, ...product.metadata }
});
synced.push({ id: product.id, stripe_id: stripeProduct.id, action: 'updated' });
} else {
stripeProduct = await stripe.products.create({
name: product.name,
description: product.description,
active: product.active,
metadata: { blackroad_id: product.id, tier: product.tier, ...product.metadata }
});
synced.push({ id: product.id, stripe_id: stripeProduct.id, action: 'created' });
}
}

res.json({ synced, count: synced.length });
} catch (err) {
console.error('Stripe sync error:', err);
res.status(500).json({ error: 'Stripe sync failed' });
}
});

// ── Drive status endpoint ────────────────────────────────────
app.get('/api/drive/status', (req, res) => {
const configured = !!(config.drive.clientId && config.drive.clientSecret);
res.json({
configured,
folder_id: config.drive.folderId || null,
redirect_uri: config.drive.redirectUri
});
});

// Chat bridge
app.post('/api/llm/chat', async (req, res) => {
try {
const { message } = req.body ?? {};
if (typeof message !== 'string' || !message.trim()) {
return res.status(400).json({ error: 'message (string) required' });
}
const r = await fetch('http://127.0.0.1:8000/chat', {
const llmUrl = config.llm.baseUrl;
const r = await fetch(`${llmUrl}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
Expand All @@ -38,7 +127,11 @@ app.post('/api/llm/chat', async (req, res) => {
}
});

const PORT = process.env.PORT || 4000;
const PORT = config.port;
server.listen(PORT, () => {
const missing = getMissingKeys();
console.log(`BlackRoad API listening on port ${PORT}`);
if (missing.length > 0) {
console.warn(`⚠ Missing env keys: ${missing.join(', ')}`);
}
});