Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
FROM node:20-alpine AS build
|
FROM node:20-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["node", "server.js"]
|
||||||
@@ -3,7 +3,11 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:80" # Maps port 8080 on the host to port 80 in the container
|
- "8080:80"
|
||||||
|
environment:
|
||||||
|
- VITE_GEMINI_API_KEY=${VITE_GEMINI_API_KEY}
|
||||||
|
- APP_USERNAME=${APP_USERNAME}
|
||||||
|
- APP_PASSWORD=${APP_PASSWORD}
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
87
index.html
87
index.html
@@ -32,7 +32,11 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app { width: 100%; max-width: 680px; padding: 2rem 0; }
|
.app { display: none; width: 100%; max-width: 680px; padding: 2rem 0; }
|
||||||
|
|
||||||
|
#login-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: var(--color-background-secondary); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
|
.login-card { background: var(--color-background-primary); padding: 2rem; border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-lg); width: 100%; max-width: 320px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
|
||||||
|
.login-card h2 { font-family: 'Lora', serif; font-size: 24px; margin-bottom: 1.25rem; text-align: center; }
|
||||||
|
|
||||||
.header { margin-bottom: 2rem; border-bottom: 0.5px solid var(--color-border-tertiary); padding-bottom: 1.25rem; }
|
.header { margin-bottom: 2rem; border-bottom: 0.5px solid var(--color-border-tertiary); padding-bottom: 1.25rem; }
|
||||||
.header h1 { font-family: 'Lora', serif; font-size: 26px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 4px; }
|
.header h1 { font-family: 'Lora', serif; font-size: 26px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 4px; }
|
||||||
@@ -111,7 +115,23 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div id="login-overlay">
|
||||||
|
<div class="login-card">
|
||||||
|
<h2 style="font-family: 'Lora', serif; font-size: 26px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 20px;">Welcome</h2>
|
||||||
|
<div class="form-field" style="margin-bottom: 12px;">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" id="login-username" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field" style="margin-bottom: 12px;">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" id="login-password" />
|
||||||
|
</div>
|
||||||
|
<button class="generate-btn" id="login-btn" style="margin-top: 12px;">Sign in</button>
|
||||||
|
<div id="login-error" style="color: var(--color-text-danger); font-size: 13px; margin-top: 10px; text-align: center; display: none;">Invalid credentials</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app" id="app-content">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Recipe Generator</h1>
|
<h1>Recipe Generator</h1>
|
||||||
<p>Tell the AI what you have and what you want — it handles the rest.</p>
|
<p>Tell the AI what you have and what you want — it handles the rest.</p>
|
||||||
@@ -187,6 +207,45 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
let sessionUser = '';
|
||||||
|
let sessionPass = '';
|
||||||
|
|
||||||
|
const loginBtn = document.getElementById('login-btn');
|
||||||
|
const loginOverlay = document.getElementById('login-overlay');
|
||||||
|
const appContent = document.getElementById('app-content');
|
||||||
|
|
||||||
|
loginBtn.addEventListener('click', async () => {
|
||||||
|
const user = document.getElementById('login-username').value;
|
||||||
|
const pass = document.getElementById('login-password').value;
|
||||||
|
const btn = document.getElementById('login-btn');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerText = 'Verifying...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: user, password: pass })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
sessionUser = user;
|
||||||
|
sessionPass = pass;
|
||||||
|
loginOverlay.style.display = 'none';
|
||||||
|
appContent.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('login-error').style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('login-error').innerText = 'Network error. Please try again.';
|
||||||
|
document.getElementById('login-error').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = 'Sign in';
|
||||||
|
});
|
||||||
|
|
||||||
const ingredients = [];
|
const ingredients = [];
|
||||||
const ingWrapper = document.getElementById('ing-wrapper');
|
const ingWrapper = document.getElementById('ing-wrapper');
|
||||||
const ingInput = document.getElementById('ing-input');
|
const ingInput = document.getElementById('ing-input');
|
||||||
@@ -277,31 +336,21 @@ Respond with ONLY a valid JSON object. Use this exact structure:
|
|||||||
Rules: use provided ingredients as the base, add pantry staples as needed, 5–10 steps, each instruction 1–3 richly detailed sentences, respect all dietary restrictions strictly.`;
|
Rules: use provided ingredients as the base, add pantry staples as needed, 5–10 steps, each instruction 1–3 richly detailed sentences, respect all dietary restrictions strictly.`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Vite will inject this automatically from the .env file
|
const resp = await fetch('/api/generate', {
|
||||||
const API_KEY = import.meta.env.VITE_GEMINI_API_KEY;
|
|
||||||
|
|
||||||
const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${API_KEY}`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'content-type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
systemInstruction: {
|
username: sessionUser,
|
||||||
parts: [{ text: systemPrompt }]
|
password: sessionPass,
|
||||||
},
|
systemPrompt: systemPrompt,
|
||||||
contents: [{
|
userPrompt: userPrompt
|
||||||
parts: [{ text: userPrompt }]
|
|
||||||
}],
|
|
||||||
generationConfig: {
|
|
||||||
responseMimeType: "application/json"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error(data.error?.message || `API error ${resp.status}`);
|
throw new Error(data.error || `Server error ${resp.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = data.candidates[0].content.parts[0].text;
|
const raw = data.candidates[0].content.parts[0].text;
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
"name": "recipe-generator",
|
"name": "recipe-generator",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Recipe generator using Gemini",
|
"description": "Recipe generator using Gemini",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
|
|||||||
73
server.js
Normal file
73
server.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Serve static frontend files from Vite build
|
||||||
|
app.use(express.static(path.join(__dirname, 'dist')));
|
||||||
|
|
||||||
|
// Secure credentials from environment variables, or fallback to defaults
|
||||||
|
const VALID_USER = process.env.APP_USERNAME || 'admin';
|
||||||
|
const VALID_PASS = process.env.APP_PASSWORD || 'souschef';
|
||||||
|
const GEMINI_API_KEY = process.env.VITE_GEMINI_API_KEY || process.env.GEMINI_API_KEY;
|
||||||
|
|
||||||
|
app.post('/api/login', (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
if (username === VALID_USER && password === VALID_PASS) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/generate', async (req, res) => {
|
||||||
|
const { username, password, userPrompt, systemPrompt } = req.body;
|
||||||
|
|
||||||
|
if (username !== VALID_USER || password !== VALID_PASS) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GEMINI_API_KEY) {
|
||||||
|
return res.status(500).json({ error: 'API key not configured on server' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
systemInstruction: { parts: [{ text: systemPrompt }] },
|
||||||
|
contents: [{ parts: [{ text: userPrompt }] }],
|
||||||
|
generationConfig: { responseMimeType: "application/json" }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(data.error?.message || `API error ${resp.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For any other route, send the frontend
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 80;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Secure proxy server running on port ${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user