177 lines
5.3 KiB
JavaScript
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}`);
|
|
});
|