import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { tmpdir } from 'os';
import { dirname, join } from 'path';
import { promises as fs } from 'fs';
import { v4 as uuidV4 } from 'uuid';
import Epub from 'epub-gen';
import { fileURLToPath } from 'url';
const requiredEnv = [
'SUPABASE_URL',
'SUPABASE_SERVICE_ROLE_KEY',
'JWT_SECRET',
'ZAI_GLM_API_KEY',
];
requiredEnv.forEach((key) => {
if (!process.env[key]) {
console.error(`Missing required environment variable: ${key}`);
process.exit(1);
}
});
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 4000;
const ORIGIN = process.env.CLIENT_ORIGIN || 'http://localhost:5173';
const allowedOrigins = ORIGIN.split(',').map((origin) => origin.trim()).filter(Boolean);
const normalizeOrigin = (value = '') => {
try {
return new URL(value).origin;
} catch (error) {
return value.replace(/\/$/, '');
}
};
const trustedOrigins = allowedOrigins.map(normalizeOrigin).filter(Boolean);
const enforceClientOrigin = (req, res, next) => {
if (!trustedOrigins.length) {
return next();
}
const header = req.get('origin') || req.get('referer');
if (!header) {
return res.status(403).json({ message: 'Bu istemciye izin verilmiyor.' });
}
const requestOrigin = normalizeOrigin(header);
if (!requestOrigin || !trustedOrigins.includes(requestOrigin)) {
return res.status(403).json({ message: 'Bu istemciye izin verilmiyor.' });
}
return next();
};
const USERS_TABLE = process.env.SUPABASE_USERS_TABLE || 'users';
const JWT_SECRET = process.env.JWT_SECRET;
import { supabase } from './src/services/supabaseClient.js';
import { translateWithGlm } from './src/services/glmClient.js';
app.use(
cors({
origin: allowedOrigins,
credentials: true,
}),
);
app.use(express.json({ limit: '10mb' }));
const sanitizeHtml = (text = '') =>
text
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\n/g, '
');
const createToken = (user) =>
jwt.sign(
{
sub: user.id,
email: user.email,
username: user.username,
},
JWT_SECRET,
{ expiresIn: '7d' },
);
const mapUserRecord = (record) => ({
id: record.id,
email: record.email,
name: record.name,
username: record.username,
});
const authMiddleware = (req, res, next) => {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) {
return res.status(401).json({ message: 'Yetkisiz erişim' });
}
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload;
return next();
} catch (error) {
return res.status(401).json({ message: 'Oturum süresi doldu, lütfen tekrar giriş yap.' });
}
};
const authRouter = express.Router();
authRouter.use(enforceClientOrigin);
authRouter.post('/register', async (req, res) => {
const { name, email, password } = req.body || {};
if (!name || !email || !password) {
return res.status(400).json({ message: 'Ad, email ve şifre gereklidir.' });
}
const normalizedEmail = email.trim().toLowerCase();
const username = normalizedEmail.split('@')[0];
const { data: existingUser, error: existingError } = await supabase
.from(USERS_TABLE)
.select('id')
.eq('email', normalizedEmail)
.maybeSingle();
if (existingError && existingError.code !== 'PGRST116') {
return res.status(500).json({ message: 'Kullanıcı kontrolü başarısız.' });
}
if (existingUser) {
return res.status(409).json({ message: 'Bu email adresi ile zaten bir hesap mevcut.' });
}
const passwordHash = await bcrypt.hash(password, 10);
const { data: createdUser, error: insertError } = await supabase
.from(USERS_TABLE)
.insert({
name: name.trim(),
email: normalizedEmail,
username,
password_hash: passwordHash,
})
.select('id,email,name,username')
.single();
if (insertError || !createdUser) {
return res.status(500).json({ message: 'Kullanıcı oluşturulamadı.' });
}
const token = createToken(createdUser);
return res.status(201).json({ token, user: mapUserRecord(createdUser) });
});
authRouter.post('/login', async (req, res) => {
const { email, password } = req.body || {};
if (!email || !password) {
return res.status(400).json({ message: 'Email ve şifre gereklidir.' });
}
const normalizedEmail = email.trim().toLowerCase();
const { data: userRecord, error: fetchError } = await supabase
.from(USERS_TABLE)
.select('id,email,name,username,password_hash')
.eq('email', normalizedEmail)
.maybeSingle();
if (fetchError && fetchError.code !== 'PGRST116') {
return res.status(500).json({ message: 'Giriş işlemi başarısız.' });
}
if (!userRecord) {
return res.status(401).json({ message: 'Email veya şifre hatalı.' });
}
const validPassword = await bcrypt.compare(password, userRecord.password_hash);
if (!validPassword) {
return res.status(401).json({ message: 'Email veya şifre hatalı.' });
}
const token = createToken(userRecord);
return res.json({ token, user: mapUserRecord(userRecord) });
});
authRouter.post('/oauth', async (req, res) => {
const { accessToken } = req.body || {};
if (!accessToken) {
return res.status(400).json({ message: 'Geçerli bir Google oturumu bulunamadı.' });
}
try {
const response = await fetch(`${process.env.SUPABASE_URL}/auth/v1/user`, {
headers: {
Authorization: `Bearer ${accessToken}`,
apikey: process.env.SUPABASE_SERVICE_ROLE_KEY,
},
});
if (!response.ok) {
return res.status(401).json({ message: 'Google oturumu doğrulanamadı.' });
}
const supabaseUser = await response.json();
const email = supabaseUser?.email?.toLowerCase();
if (!email) {
return res.status(400).json({ message: 'Google hesabında email bilgisi bulunamadı.' });
}
const name =
supabaseUser?.user_metadata?.full_name ||
supabaseUser?.user_metadata?.name ||
email.split('@')[0];
const username = (supabaseUser?.user_metadata?.user_name || email.split('@')[0]).replace(/[^a-zA-Z0-9-_]/g, '');
const { data: existingUser, error: fetchError } = await supabase
.from(USERS_TABLE)
.select('id,email,name,username')
.eq('email', email)
.maybeSingle();
if (fetchError && fetchError.code !== 'PGRST116') {
return res.status(500).json({ message: 'Kullanıcı sorgulanamadı.' });
}
let userRecord = existingUser;
if (!userRecord) {
const { data: insertedUser, error: insertError } = await supabase
.from(USERS_TABLE)
.insert({
name,
email,
username,
password_hash: 'GOOGLE_OAUTH',
})
.select('id,email,name,username')
.single();
if (insertError || !insertedUser) {
return res.status(500).json({ message: 'Google hesabı oluşturulamadı.' });
}
userRecord = insertedUser;
}
const token = createToken(userRecord);
return res.json({ token, user: mapUserRecord(userRecord) });
} catch (error) {
console.error('Google OAuth hatası:', error);
return res.status(500).json({ message: 'Google girişi tamamlanamadı.' });
}
});
authRouter.get('/me', authMiddleware, async (req, res) => {
const { data: userRecord, error } = await supabase
.from(USERS_TABLE)
.select('id,email,name,username')
.eq('id', req.user.sub)
.single();
if (error || !userRecord) {
return res.status(404).json({ message: 'Kullanıcı bulunamadı.' });
}
return res.json({ user: mapUserRecord(userRecord) });
});
authRouter.post('/logout', (_req, res) => {
return res.json({ message: 'Çıkış yapıldı.' });
});
authRouter.post('/forgot-password', async (req, res) => {
const { email } = req.body || {};
if (!email) {
return res.status(400).json({ message: 'Email gereklidir.' });
}
// Supabase Auth kullanılmadığı için burada sadece bilgilendirici bir cevap döndürüyoruz.
return res.json({
message:
'Şifre sıfırlama talebin alındı. Bu demo ortamında e-posta gönderimi aktif değildir.',
});
});
app.use('/auth', authRouter);
app.post('/translate', authMiddleware, async (req, res) => {
const { text } = req.body || {};
if (!text || !text.trim()) {
return res.status(400).json({ message: 'Çevrilecek metin bulunamadı.' });
}
console.log('[Translate] İstek alındı', { length: text.length, snippet: text.slice(0, 60) });
try {
const translated = await translateWithGlm(text);
console.log('[Translate] Çeviri başarıyla döndü');
return res.json({ text: translated });
} catch (error) {
console.error('GLM çeviri hatası:', error);
return res.status(500).json({ message: error.message || 'Çeviri tamamlanamadı.' });
}
});
app.post('/generate-epub', authMiddleware, async (req, res) => {
const { text, meta, cover } = req.body || {};
if (!text || !text.trim()) {
return res.status(400).json({ message: 'text is required' });
}
const title = meta?.title?.trim() || 'imgPub OCR Export';
const filename = meta?.filename || `imgpub${Date.now()}.epub`;
const authors =
Array.isArray(meta?.authors) && meta.authors.length
? meta.authors.filter(Boolean)
: meta?.author
? [meta.author]
: ['imgPub'];
const publisher = meta?.publisher || 'imgPub';
const language = meta?.language || 'tr';
const description = meta?.description || title;
const content = [
{
title,
data: `