Add developer tools page and secure guest scans

This commit is contained in:
2026-08-02 00:10:51 +02:00
parent c9e776ae98
commit c79e7b59ff
13 changed files with 328 additions and 33 deletions

View File

@@ -4,6 +4,7 @@ const crypto = require('crypto');
const dotenv = require('dotenv');
const express = require('express');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const loadEnvFiles = (filePaths) => {
const mergedFileEnv = {};
@@ -68,7 +69,7 @@ const {
identifyPlant,
isConfigured: isOpenAiConfigured,
} = require('./lib/openai');
const { applyCatalogGrounding, normalizeText } = require('./lib/scanGrounding');
const { applyCatalogGrounding, enforceEnglishName, normalizeText } = require('./lib/scanGrounding');
const { decideReviewOutcome, reviewAgreesWithPrimary } = require('./lib/scanReview');
const { ensureStorageBucketWithRetry, uploadImage, isStorageConfigured } = require('./lib/storage');
const { prepareTiktokPhotoUrls } = require('./lib/tiktok-assets');
@@ -82,6 +83,9 @@ const {
} = require('./lib/tiktok');
const app = express();
// Caddy is the only reverse proxy in front of this service (see
// greenlns-landing/Caddyfile), so trust exactly one hop for req.ip / X-Forwarded-For.
app.set('trust proxy', 1);
const port = Number(process.env.PORT || 3000);
const plantsPublicDir = path.join(__dirname, 'public', 'plants');
@@ -756,7 +760,25 @@ app.post('/v1/billing/sync-revenuecat', async (request, response) => {
}
});
app.post('/v1/scan', async (request, response) => {
// The 'Bearer guest' token (see resolveUserId above) grants a free,
// pre-auth demo scan with no credit charge. Without a limit here, that
// path is an unmetered, unauthenticated OpenAI proxy for anyone who
// knows the header — cap it per-IP so it stays a teaser, not a leak.
const guestScanLimiter = rateLimit({
windowMs: 24 * 60 * 60 * 1000,
limit: 3,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => request.header('authorization') !== 'Bearer guest',
message: {
error: {
code: 'GUEST_SCAN_LIMIT',
message: 'Daily free scan limit reached. Sign in or download the app for unlimited scans.',
},
},
});
app.post('/v1/scan', guestScanLimiter, async (request, response) => {
let userId = 'unknown';
let idempotencyKey = null;
let creditsCharged = 0;
@@ -924,6 +946,8 @@ app.post('/v1/scan', async (request, response) => {
modelPath.push('review-skipped-free-plan');
}
result = enforceEnglishName(result, language);
const payload = {
result,
lowConfidence: (result.confidence || 0) < LOW_CONFIDENCE_RESULT_THRESHOLD,