Initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Paket dosyalarını kopyala ve bağımlılıkları yükle
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Kaynak kodları kopyala ve derle
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Sadece production bağımlılıklarını yükle (daha küçük imaj boyutu)
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
# Derlenmiş kodları builder aşamasından al
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
# Uygulama portunu belirt
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
# Gerekli ortam değişkenleri (Coolify üzerinden de ezilebilir)
|
||||||
|
ENV PORT=3001
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Uygulamayı başlat
|
||||||
|
CMD ["npm", "start"]
|
||||||
Generated
+3302
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "laawos-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.112.2",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.6.2",
|
||||||
|
"heic-convert": "^2.1.0",
|
||||||
|
"mammoth": "^1.12.0",
|
||||||
|
"multer": "^2.2.0",
|
||||||
|
"officeparser": "^7.5.1",
|
||||||
|
"openai": "^7.4.0",
|
||||||
|
"pdf-parse": "^2.4.5",
|
||||||
|
"tesseract.js": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/multer": "^2.2.0",
|
||||||
|
"@types/node": "^26.2.0",
|
||||||
|
"@types/pdf-parse": "^1.1.5",
|
||||||
|
"tsx": "^4.23.11",
|
||||||
|
"typescript": "^7.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
const supabase = createClient('http://supabasekong-rhi9nc2gvlhrqnm734tiqdl6.167.233.145.149.sslip.io', 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc4NjEyNDEwMCwiZXhwIjo0OTQxNzk3NzAwLCJyb2xlIjoic2VydmljZV9yb2xlIn0.4wS7vys6ZoCxAyUSSwFN0_BHvm_LRWPDIlJ1dHPifx4');
|
||||||
|
async function test() {
|
||||||
|
const { data, error } = await supabase.from('cases').select('*, documents(id)');
|
||||||
|
console.log('Error:', error);
|
||||||
|
console.log('Data:', JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
test();
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
|
||||||
|
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { caseId } = req.params;
|
||||||
|
const { content } = req.body;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!content || !caseId) {
|
||||||
|
return res.status(400).json({ error: 'Missing caseId or content' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: userMessage, error: userMsgError } = await supabase
|
||||||
|
.from('chat_messages')
|
||||||
|
.insert([{ case_id: caseId, user_id: userId, role: 'user', content }])
|
||||||
|
.select().single();
|
||||||
|
if (userMsgError) throw userMsgError;
|
||||||
|
|
||||||
|
// 1. Dosyaya ait belge analizlerini çek (EKSİK OLAN KISIM)
|
||||||
|
const { data: analyses } = await supabase
|
||||||
|
.from('analyses')
|
||||||
|
.select('summary_json')
|
||||||
|
.eq('case_id', caseId);
|
||||||
|
|
||||||
|
const { data: documents } = await supabase
|
||||||
|
.from('documents')
|
||||||
|
.select('filename, extracted_text')
|
||||||
|
.eq('case_id', caseId);
|
||||||
|
|
||||||
|
// 2. Geçmiş sohbet
|
||||||
|
const { data: pastMessages } = await supabase
|
||||||
|
.from('chat_messages')
|
||||||
|
.select('role, content')
|
||||||
|
.eq('case_id', caseId)
|
||||||
|
.order('created_at', { ascending: true })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
// 3. Belge bağlamını sistem promptuna göm
|
||||||
|
let documentContext = '';
|
||||||
|
if (documents && documents.length > 0) {
|
||||||
|
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
|
||||||
|
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
|
||||||
|
} else {
|
||||||
|
documentContext = '\n\nBu dosyaya henüz hiçbir belge yüklenmemiştir. Genel hukuki bilgiyle cevap ver, ama bunu kullanıcıya açıkça belirt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analyses && analyses.length > 0) {
|
||||||
|
documentContext += '\n\nÖNCEKİ ANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPrompt = `Sen bir hukuk asistanısın. Bu dava dosyasıyla ilgili kullanıcının sorularını, aşağıda verilen belge içeriğine dayanarak cevaplıyorsun. Eğer belge yoksa veya soruyu cevaplamaya yetmiyorsa, bunu açıkça belirt, uydurma bilgi verme.${documentContext}`;
|
||||||
|
|
||||||
|
const messages: any[] = [{ role: 'system', content: systemPrompt }];
|
||||||
|
if (pastMessages) {
|
||||||
|
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||||||
|
} else {
|
||||||
|
messages.push({ role: 'user', content });
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
|
||||||
|
// Native endpoint'e geçiş
|
||||||
|
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
|
||||||
|
|
||||||
|
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: aiModel,
|
||||||
|
messages: messages,
|
||||||
|
think: false,
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const aiContent = data.message?.content || 'Yanıt alınamadı.';
|
||||||
|
|
||||||
|
const { data: aiMessage, error: aiMsgError } = await supabase
|
||||||
|
.from('chat_messages')
|
||||||
|
.insert([{ case_id: caseId, user_id: userId, role: 'assistant', content: aiContent }])
|
||||||
|
.select().single();
|
||||||
|
if (aiMsgError) throw aiMsgError;
|
||||||
|
|
||||||
|
res.json({ message: aiMessage });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Chat Error:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import * as officeParser from 'officeparser';
|
||||||
|
import Tesseract from 'tesseract.js';
|
||||||
|
const heicConvert = require('heic-convert');
|
||||||
|
|
||||||
|
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const file = req.file;
|
||||||
|
const { case_id } = req.body;
|
||||||
|
const user_id = req.user?.id;
|
||||||
|
|
||||||
|
if (!file || !case_id || !user_id) {
|
||||||
|
return res.status(400).json({ error: 'Missing file or case_id' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let extractedText = '';
|
||||||
|
const mimeType = file.mimetype.toLowerCase();
|
||||||
|
|
||||||
|
// IMAGE & OCR LOGIC
|
||||||
|
if (mimeType.includes('image/')) {
|
||||||
|
let imageBuffer = file.buffer;
|
||||||
|
|
||||||
|
// Convert HEIC to JPEG
|
||||||
|
if (mimeType === 'image/heic' || file.originalname.toLowerCase().endsWith('.heic')) {
|
||||||
|
imageBuffer = await heicConvert({
|
||||||
|
buffer: file.buffer,
|
||||||
|
format: 'JPEG',
|
||||||
|
quality: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Tesseract OCR (Turkish language)
|
||||||
|
const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur');
|
||||||
|
extractedText = text;
|
||||||
|
|
||||||
|
// DOCUMENT LOGIC
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// officeParser natively supports pdf, docx, doc, rtf, txt, etc.
|
||||||
|
extractedText = await officeParser.parseOffice(file.buffer);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('officeParser Error:', err);
|
||||||
|
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Upload File to Supabase Storage
|
||||||
|
const filePath = `${user_id}/${Date.now()}_${file.originalname}`;
|
||||||
|
const { error: uploadError } = await supabase.storage
|
||||||
|
.from('case-documents')
|
||||||
|
.upload(filePath, file.buffer, {
|
||||||
|
contentType: file.mimetype,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploadError) {
|
||||||
|
console.error('Storage Upload Error:', uploadError);
|
||||||
|
return res.status(500).json({ error: 'Failed to upload document to storage' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Veritabanına Dosya Bilgisini Kaydet (documents tablosu)
|
||||||
|
const { data: documentRecord, error: docError } = await supabase
|
||||||
|
.from('documents')
|
||||||
|
.insert([{
|
||||||
|
case_id,
|
||||||
|
user_id,
|
||||||
|
storage_path: filePath,
|
||||||
|
filename: file.originalname,
|
||||||
|
mime_type: file.mimetype,
|
||||||
|
file_size: file.size,
|
||||||
|
ocr_status: 'done',
|
||||||
|
extracted_text: extractedText
|
||||||
|
}])
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (docError) throw docError;
|
||||||
|
|
||||||
|
// 3. DeepSeek (OpenAI) ile Hukuki Analiz Çıkarımı
|
||||||
|
// Promptu tasarlıyoruz. JSON dönmesini istiyoruz.
|
||||||
|
const systemPrompt = `
|
||||||
|
Sen uzman bir Türk avukatısın. Sana verilen dava dosyasını/belgeyi okuyup analiz edeceksin.
|
||||||
|
Çıktıyı KESİNLİKLE geçerli bir JSON formatında vermelisin. JSON şu anahtarları içermelidir:
|
||||||
|
- "taraflar": (string) Davacı ve Davalı bilgileri
|
||||||
|
- "tarihler": (string) Belgedeki kritik tarihler (Dava tarihi, tebliğ vb.)
|
||||||
|
- "hukukiDayanak": (string) Belgedeki ilgili kanun maddeleri ve hukuki temeller
|
||||||
|
- "ilkDegerlendirme": (string) Dosyaya dair ilk stratejik ve hukuki değerlendirmen
|
||||||
|
|
||||||
|
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
|
||||||
|
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
|
||||||
|
|
||||||
|
const response = await fetch(`${runpodUrl}/v1/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: aiModel,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: extractedText.substring(0, 15000) }
|
||||||
|
],
|
||||||
|
think: false,
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
// Support both custom (data.message.content) and OpenAI standard (data.choices[0].message.content) formats just in case
|
||||||
|
const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content);
|
||||||
|
let summaryJson = {};
|
||||||
|
try {
|
||||||
|
summaryJson = JSON.parse(aiContent || '{}');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse AI response as JSON:', aiContent);
|
||||||
|
summaryJson = { error: 'AI did not return valid JSON', raw: aiContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Analiz sonucunu veritabanına kaydet (analyses tablosu)
|
||||||
|
const { data: analysisRecord, error: analysisError } = await supabase
|
||||||
|
.from('analyses')
|
||||||
|
.insert([{
|
||||||
|
case_id,
|
||||||
|
document_id: documentRecord.id,
|
||||||
|
summary_json: summaryJson,
|
||||||
|
model_used: aiModel
|
||||||
|
}])
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (analysisError) throw analysisError;
|
||||||
|
|
||||||
|
// Başarıyla frontend'e döndür
|
||||||
|
res.json({
|
||||||
|
message: 'Document processed successfully',
|
||||||
|
document: documentRecord,
|
||||||
|
analysis: analysisRecord
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Upload Error:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
export const searchPrecedents = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { phrase, court_types, page_number } = req.body;
|
||||||
|
|
||||||
|
if (!phrase) {
|
||||||
|
return res.status(400).json({ error: 'Arama metni (phrase) zorunludur.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let optimizedPhrase = phrase;
|
||||||
|
|
||||||
|
// Eğer sorgu 3 kelimeden uzunsa (doğal dil sorusu ise), AI ile anahtar kelimelere dönüştür
|
||||||
|
if (phrase.split(' ').length > 3) {
|
||||||
|
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
|
||||||
|
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
|
||||||
|
|
||||||
|
const systemPrompt = `Sen uzman bir Türk avukatısın. Kullanıcının uzun ve doğal dille yazdığı hukuki soruyu, Yargıtay içtihat arama motorunda en iyi sonucu verecek şekilde 2 ila en fazla 4 temel hukuki terime/anahtar kelimeye çevir.
|
||||||
|
SADECE anahtar kelimeleri aralarına boşluk koyarak yaz. Noktalama işareti, açıklama veya ek kelimeler (ve, ile, vb.) kullanma.
|
||||||
|
Örnek Kullanıcı: İşveren tarafından performans düşüklüğü gerekçesiyle iş sözleşmesi feshedilen işçinin işe iade davası açması halinde ispat yükü kime aittir
|
||||||
|
Örnek Cevap: performans düşüklüğü ispat yükü işe iade`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const aiResponse = await fetch(`${runpodUrl}/v1/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: aiModel,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: phrase }
|
||||||
|
],
|
||||||
|
think: false,
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (aiResponse.ok) {
|
||||||
|
const data = await aiResponse.json();
|
||||||
|
const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content);
|
||||||
|
if (aiContent) {
|
||||||
|
optimizedPhrase = aiContent.trim().replace(/["'\n]/g, '');
|
||||||
|
console.log(`[AI Search] Orijinal: "${phrase}" -> Optimize: "${optimizedPhrase}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('AI Query Optimization API returned status:', aiResponse.status);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('AI Query Optimization Fetch Error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yargı MCP REST API'sine (8001 portu veya YARGI_MCP_URL) optimize edilmiş sorgu ile istek at
|
||||||
|
const mcpBaseUrl = process.env.YARGI_MCP_URL || 'http://localhost:8001';
|
||||||
|
const mcpResponse = await fetch(`${mcpBaseUrl}/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
phrase: optimizedPhrase,
|
||||||
|
court_types: court_types || ["YARGITAYKARARI", "DANISTAYKARAR"],
|
||||||
|
page_number: page_number || 1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mcpResponse.ok) {
|
||||||
|
const errorData = await mcpResponse.text();
|
||||||
|
console.error('Yargı MCP API Error:', errorData);
|
||||||
|
return res.status(mcpResponse.status).json({ error: 'Arama servisi hata döndürdü', details: errorData });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await mcpResponse.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Search Precedents Error:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPrecedentDocument = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const documentId = String(req.params.documentId || '');
|
||||||
|
|
||||||
|
if (!documentId) {
|
||||||
|
return res.status(400).json({ error: 'documentId zorunludur.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcpBaseUrl = process.env.YARGI_MCP_URL || 'http://localhost:8001';
|
||||||
|
const mcpResponse = await fetch(`${mcpBaseUrl}/document/${encodeURIComponent(documentId)}`);
|
||||||
|
|
||||||
|
if (!mcpResponse.ok) {
|
||||||
|
const errorData = await mcpResponse.text();
|
||||||
|
console.error('Yargı MCP API Error:', errorData);
|
||||||
|
return res.status(mcpResponse.status).json({ error: 'Belge servisi hata döndürdü', details: errorData });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await mcpResponse.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Get Precedent Document Error:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
|
import routes from './routes';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// P0-6: Rate Limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
|
max: 200, // limit each IP to 200 requests per windowMs
|
||||||
|
message: { error: 'Günlük kullanım limitine ulaştınız. (Too many requests)' }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(limiter); // Apply rate limiting to all requests
|
||||||
|
|
||||||
|
// Mount all API routes
|
||||||
|
app.use('/api', routes);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server is running on http://localhost:${port}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// --- DEEPSEEK CONFIGURATION (PASIFİZE EDİLDİ) ---
|
||||||
|
// const apiKey = process.env.DEEPSEEK_API_KEY;
|
||||||
|
// if (!apiKey) {
|
||||||
|
// console.warn('WARNING: DEEPSEEK_API_KEY is missing from environment variables.');
|
||||||
|
// }
|
||||||
|
// export const openai = new OpenAI({
|
||||||
|
// baseURL: 'https://api.deepseek.com/v1',
|
||||||
|
// apiKey: apiKey || 'dummy-key',
|
||||||
|
// });
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
// --- MİZAN (RUNPOD) CONFIGURATION ---
|
||||||
|
const runpodBaseUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net/v1';
|
||||||
|
const runpodApiKey = process.env.RUNPOD_API_KEY || 'dummy-key';
|
||||||
|
|
||||||
|
export const openai = new OpenAI({
|
||||||
|
baseURL: runpodBaseUrl,
|
||||||
|
apiKey: runpodApiKey,
|
||||||
|
});
|
||||||
|
// ------------------------------------
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.SUPABASE_URL;
|
||||||
|
// We use the service role key in the backend to bypass RLS when necessary (e.g. inserting system analyses),
|
||||||
|
// or we can use anon key if RLS allows it. Let's assume we use the anon key or service role key provided in ENV.
|
||||||
|
// However, the frontend passes the user_id, so the backend can act on behalf of the user.
|
||||||
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseKey) {
|
||||||
|
throw new Error('Missing Supabase URL or Key in environment variables');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
|
||||||
|
export interface AuthenticatedRequest extends Request {
|
||||||
|
user?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requireAuth = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized: Missing or invalid token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
const { data: { user }, error } = await supabase.auth.getUser(token);
|
||||||
|
|
||||||
|
if (error || !user) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized: Invalid token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// P0-2: Lisans Kontrolü
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('license_status')
|
||||||
|
.eq('id', user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!profile || (profile.license_status !== 'active' && profile.license_status !== 'trial')) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden: License is not active or in trial' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
|
||||||
|
// P0-3: Kaynak Sahipliği Doğrulama (Eğer istekte caseId veya case_id varsa)
|
||||||
|
const caseId = req.params.caseId || req.body?.case_id;
|
||||||
|
if (caseId) {
|
||||||
|
const { data: caseRecord, error: caseError } = await supabase
|
||||||
|
.from('cases')
|
||||||
|
.select('id')
|
||||||
|
.eq('id', caseId)
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (caseError || !caseRecord) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden: You do not own this case' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth middleware error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error during authentication' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { requireAuth } from '../middleware/auth';
|
||||||
|
import { chatMessage } from '../controllers/chat.controller';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/:caseId/message', requireAuth, chatMessage);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { requireAuth } from '../middleware/auth';
|
||||||
|
import { uploadDocument } from '../controllers/document.controller';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
|
router.post('/upload', upload.single('file'), requireAuth, uploadDocument);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import documentRoutes from './document.routes';
|
||||||
|
import chatRoutes from './chat.routes';
|
||||||
|
import precedentRoutes from './precedent.routes';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', message: 'LegalOS Backend is running' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/documents', documentRoutes);
|
||||||
|
router.use('/chat', chatRoutes);
|
||||||
|
router.use('/', precedentRoutes); // Keeps the same paths like /api/search-precedents
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { searchPrecedents, getPrecedentDocument } from '../controllers/precedent.controller';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/search-precedents', searchPrecedents);
|
||||||
|
router.get('/precedent-document/:documentId', getPrecedentDocument);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user