Files
imgPub/server/index.js

327 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { tmpdir } from 'os';
import { join } from 'path';
import { promises as fs } from 'fs';
import { v4 as uuidV4 } from 'uuid';
import Epub from 'epub-gen';
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 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());
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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\n/g, '<br/>');
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.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', async (req, res) => {
const { text } = req.body || {};
if (!text || !text.trim()) {
return res.status(400).json({ message: 'Çevrilecek metin bulunamadı.' });
}
try {
const translated = await translateWithGlm(text);
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', 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 || 'imgPub OCR Export';
const author = meta?.author || 'imgPub';
const filename = meta?.filename || `imgpub${Date.now()}.epub`;
const content = [
{
title,
data: `<div>${sanitizeHtml(text)}</div>`,
},
];
const outputPath = join(tmpdir(), `imgpub-${uuidV4()}.epub`);
let coverPath;
try {
if (cover?.data) {
const coverBuffer = Buffer.from(cover.data, 'base64');
const coverExtension =
cover?.mimeType?.split('/').pop() || cover?.filename?.split('.').pop() || 'png';
coverPath = join(tmpdir(), `imgpub-cover-${uuidV4()}.${coverExtension}`);
await fs.writeFile(coverPath, coverBuffer);
}
const epubOptions = { title, author, content };
if (coverPath) {
epubOptions.cover = coverPath;
}
const epub = new Epub(epubOptions, outputPath);
await epub.promise;
const buffer = await fs.readFile(outputPath);
await fs.unlink(outputPath).catch(() => {});
if (coverPath) {
await fs.unlink(coverPath).catch(() => {});
}
res.json({ filename, data: buffer.toString('base64') });
} catch (error) {
console.error('EPUB generation failed:', error);
res.status(500).json({ message: 'EPUB generation failed' });
}
});
app.get('/', (_, res) => {
res.json({ status: 'ok' });
});
app.listen(PORT, () => {
console.log(`imgPub EPUB server listening on port ${PORT}`);
});