Files
camila/server.js
T

177 lines
5.3 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || 'puglindo26';
app.use(express.json());
app.use(cookieParser(process.env.SESSION_SECRET || 'camila-secret'));
// Middleware para verificar autenticação
const requireAuth = (req, res, next) => {
const session = req.signedCookies.camila_session || req.cookies.camila_session;
if (session === 'authenticated') {
next();
} else {
if (req.xhr || req.path.startsWith('/api/')) {
res.status(401).json({ error: 'Não autorizado' });
} else {
res.redirect('/login');
}
}
};
// Rota de login (página)
app.get('/login', (req, res) => {
const session = req.signedCookies.camila_session || req.cookies.camila_session;
if (session === 'authenticated') {
res.redirect('/');
} else {
res.sendFile(path.join(__dirname, 'public', 'login.html'));
}
});
// API para Login
app.post('/api/login', (req, res) => {
const { password } = req.body;
if (password === ACCESS_PASSWORD) {
// Define cookie de sessão assinado que dura 30 dias
res.cookie('camila_session', 'authenticated', {
signed: true,
httpOnly: true,
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 dias
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production'
});
res.json({ success: true });
} else {
res.status(401).json({ error: 'Senha incorreta' });
}
});
// API para Logout
app.post('/api/logout', (req, res) => {
res.clearCookie('camila_session');
res.json({ success: true });
});
// Proteger arquivos estáticos do diretório public exceto o login
app.use((req, res, next) => {
if (req.path === '/login' || req.path === '/login.html' || req.path === '/style.css' || req.path.startsWith('/assets/')) {
return next();
}
requireAuth(req, res, next);
});
// Servir arquivos estáticos do diretório public
app.use(express.static(path.join(__dirname, 'public')));
// Rota de Chat com OpenRouter (Streaming nativo)
app.post('/api/chat', requireAuth, async (req, res) => {
const { messages } = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Mensagens inválidas' });
}
try {
const openrouterUrl = 'https://openrouter.ai/api/v1/chat/completions';
const response = await fetch(openrouterUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'HTTP-Referer': 'https://camila.reifonas.cloud',
'X-Title': 'Camila ChatGPT replica',
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.OPENROUTER_MODEL,
messages: messages,
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Erro na API do OpenRouter:', errorText);
return res.status(response.status).json({ error: 'Erro ao processar requisição na API do OpenRouter' });
}
// Configurar headers para Server-Sent Events (SSE)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
// Manter a última linha inacabada no buffer
buffer = lines.pop();
for (const line of lines) {
const cleanLine = line.trim();
if (cleanLine.startsWith('data: ')) {
const dataContent = cleanLine.slice(6);
if (dataContent === '[DONE]') {
res.write('data: [DONE]\n\n');
continue;
}
try {
const parsed = JSON.parse(dataContent);
const content = parsed.choices?.[0]?.delta?.content || '';
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
} catch (e) {
// Ignorar linhas incompletas ou JSON inválido
}
}
}
}
// Enviar qualquer conteúdo restante no buffer
if (buffer) {
const cleanLine = buffer.trim();
if (cleanLine.startsWith('data: ')) {
const dataContent = cleanLine.slice(6);
if (dataContent !== '[DONE]') {
try {
const parsed = JSON.parse(dataContent);
const content = parsed.choices?.[0]?.delta?.content || '';
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
} catch (e) {}
}
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error) {
console.error('Erro na rota de chat:', error);
res.status(500).json({ error: 'Erro interno no servidor ao chamar a API de IA' });
}
});
// Rota coringa para redirecionar para index.html
app.get('*', requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`Servidor rodando na porta ${PORT}`);
});