const express = require('express'); const bodyParser = require('body-parser'); const { GoogleGenerativeAI } = require('@google/generative-ai'); 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 (Simple In-Memory) const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10); const usageStore = {}; // { deviceId: { date: 'YYYY-MM-DD', count: 0 } } // 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: Auth first (skip /health), then body parser app.use((req, res, next) => { if (req.path === '/health') return next(); authMiddleware(req, res, next); }); app.use(bodyParser.json({ limit: '10mb' })); // Gemini Client const genAI = new GoogleGenerativeAI(API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); // Flutter側(gemini_service.dart)と統一 // Helper: Get Today's Date String (YYYY-MM-DD) function getTodayString() { return new Date().toISOString().split('T')[0]; } // Helper: Check & Update Rate Limit function checkRateLimit(deviceId) { const today = getTodayString(); if (!usageStore[deviceId]) { usageStore[deviceId] = { date: today, count: 0 }; } // Reset if new day if (usageStore[deviceId].date !== today) { usageStore[deviceId] = { date: today, count: 0 }; } const currentUsage = usageStore[deviceId]; const remaining = DAILY_LIMIT - currentUsage.count; return { allowed: remaining > 0, current: currentUsage.count, limit: DAILY_LIMIT, remaining: remaining }; } // 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' }); } // 1. Check Rate Limit const limitStatus = 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: 'Daily limit reached', usage: { today: limitStatus.current, limit: limitStatus.limit } }); } console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0}`); try { // 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 ... ```) const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/); let jsonData; if (jsonMatch) { jsonData = JSON.parse(jsonMatch[1]); } else { // Try parsing raw text if no code blocks jsonData = JSON.parse(text); } // 4. Update Usage usageStore[device_id].count++; console.log(`[Success] Device: ${device_id} | Count: ${usageStore[device_id].count}/${DAILY_LIMIT}`); // 5. Send Response res.json({ success: true, data: jsonData, usage: { today: usageStore[device_id].count, limit: DAILY_LIMIT } }); } catch (error) { console.error('Gemini API 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, () => { 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'); });