const express = require('express'); const bodyParser = require('body-parser'); const { GoogleGenerativeAI } = require('@google/generative-ai'); const { createClient } = require('redis'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 8080; const API_KEY = process.env.GEMINI_API_KEY; const AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN; // Rate Limiting Configuration const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10); const REDIS_HOST = process.env.REDIS_HOST || 'localhost'; const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379', 10); // Redis Client Setup const redisClient = createClient({ socket: { host: REDIS_HOST, port: REDIS_PORT } }); redisClient.on('error', (err) => { console.error('[Redis] Connection Error:', err); }); redisClient.on('connect', () => { console.log(`[Redis] Connected to ${REDIS_HOST}:${REDIS_PORT}`); }); // Initialize Redis connection (async () => { try { await redisClient.connect(); } catch (err) { console.error('[Redis] Failed to connect:', err); process.exit(1); // Exit if Redis is unavailable } })(); // Authentication Middleware (skip for /health) function authMiddleware(req, res, next) { if (!AUTH_TOKEN) { // If no token configured, skip auth (backward compatibility) console.warn('[Auth] WARNING: PROXY_AUTH_TOKEN is not set. Authentication disabled.'); return next(); } const authHeader = req.headers['authorization']; if (!authHeader || !authHeader.startsWith('Bearer ')) { console.log(`[Auth] Rejected: Missing or invalid Authorization header`); return res.status(401).json({ success: false, error: 'Authentication required' }); } const token = authHeader.substring(7); // Remove 'Bearer ' prefix if (token !== AUTH_TOKEN) { console.log(`[Auth] Rejected: Invalid token`); return res.status(403).json({ success: false, error: 'Invalid authentication token' }); } next(); } // Global middleware: Body parser first, then auth (skip /health) app.use(bodyParser.json({ limit: '10mb' })); app.use((req, res, next) => { if (req.path === '/health') return next(); authMiddleware(req, res, next); }); // Gemini Client with JSON response configuration const genAI = new GoogleGenerativeAI(API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash", // Flutter側(gemini_service.dart)と統一 generationConfig: { responseMimeType: "application/json", // Force JSON-only output temperature: 0.2, // チャート一貫性向上のため(Flutter側と統一) } }); // Helper: Get Today's Date String (YYYY-MM-DD) function getTodayString() { return new Date().toISOString().split('T')[0]; } // Helper: Check & Update Rate Limit (Redis-based) async function checkRateLimit(deviceId) { const today = getTodayString(); const redisKey = `usage:${deviceId}:${today}`; try { // Get current usage count const currentCount = await redisClient.get(redisKey); const count = currentCount ? parseInt(currentCount, 10) : 0; const remaining = DAILY_LIMIT - count; return { allowed: remaining > 0, current: count, limit: DAILY_LIMIT, remaining: remaining, redisKey: redisKey }; } catch (err) { console.error('[Redis] Error checking rate limit:', err); // Fallback: deny request if Redis is down return { allowed: false, current: 0, limit: DAILY_LIMIT, remaining: 0, error: 'Rate limit check failed' }; } } // Helper: Increment Usage Count (Redis-based) async function incrementUsage(deviceId) { const today = getTodayString(); const redisKey = `usage:${deviceId}:${today}`; try { // Increment count const newCount = await redisClient.incr(redisKey); // Set expiration to end of day (86400 seconds = 24 hours) const now = new Date(); const midnight = new Date(now); midnight.setHours(24, 0, 0, 0); const secondsUntilMidnight = Math.floor((midnight - now) / 1000); await redisClient.expire(redisKey, secondsUntilMidnight); return newCount; } catch (err) { console.error('[Redis] Error incrementing usage:', err); throw err; } } // API Endpoint (authentication enforced by global middleware) app.post('/analyze', async (req, res) => { const { device_id, images, prompt } = req.body; if (!device_id) { return res.status(400).json({ success: false, error: 'Device ID is required' }); } try { // 1. Check Rate Limit (Redis-based) const limitStatus = await checkRateLimit(device_id); if (!limitStatus.allowed) { console.log(`[Limit Reached] Device: ${device_id} (${limitStatus.current}/${limitStatus.limit})`); return res.status(429).json({ success: false, error: limitStatus.error || 'Daily limit reached', usage: { today: limitStatus.current, limit: limitStatus.limit } }); } console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0} | Current: ${limitStatus.current}/${limitStatus.limit}`); // 2. Prepare Gemini Request // Base64 images to GenerativeContentBlob const imageParts = (images || []).map(base64 => ({ inlineData: { data: base64, mimeType: "image/jpeg" } })); const result = await model.generateContent([prompt, ...imageParts]); const response = await result.response; const text = response.text(); // 3. Parse JSON from Markdown (e.g. ```json ... ```) console.log(`[Debug] Gemini raw response (first 200 chars): ${text.substring(0, 200)}`); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/); let jsonData; if (jsonMatch) { console.log('[Debug] Found JSON in code block'); jsonData = JSON.parse(jsonMatch[1]); } else { // Try parsing raw text if no code blocks console.log('[Debug] Attempting to parse raw text as JSON'); try { jsonData = JSON.parse(text); } catch (parseError) { console.error('[Error] JSON parse failed. Raw response:', text); throw new Error(`Failed to parse Gemini response as JSON. Response starts with: ${text.substring(0, 100)}`); } } // 4. Increment Usage (Redis-based) const newCount = await incrementUsage(device_id); console.log(`[Success] Device: ${device_id} | New Count: ${newCount}/${DAILY_LIMIT}`); // 5. Send Response res.json({ success: true, data: jsonData, usage: { today: newCount, limit: DAILY_LIMIT } }); } catch (error) { console.error('[Error] Gemini API or Redis Error:', error); res.status(500).json({ success: false, error: error.message || 'Internal Server Error' }); } }); // Health Check app.get('/health', (req, res) => { res.send('OK'); }); // Start Server app.listen(PORT, '0.0.0.0', () => { console.log(`Proxy Server running on port ${PORT}`); if (!API_KEY) console.error('WARNING: GEMINI_API_KEY is not set!'); if (!AUTH_TOKEN) console.error('WARNING: PROXY_AUTH_TOKEN is not set! Authentication is disabled.'); else console.log('Authentication: Bearer Token enabled'); });