admin page oluşturuldu.

This commit is contained in:
2025-11-21 01:21:37 +03:00
parent 26acaccacd
commit cf42825705
13 changed files with 915 additions and 60 deletions

View File

@@ -1,5 +1,6 @@
import 'dotenv/config';
import express from 'express';
import { createServer } from 'http';
import cors from 'cors';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
@@ -9,6 +10,7 @@ import { promises as fs } from 'fs';
import { v4 as uuidV4 } from 'uuid';
import Epub from 'epub-gen';
import { fileURLToPath } from 'url';
import { Server as SocketIOServer } from 'socket.io';
const requiredEnv = [
'SUPABASE_URL',
@@ -24,10 +26,62 @@ requiredEnv.forEach((key) => {
});
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 ADMIN_EMAILS = (process.env.ADMIN_ALLOWED_EMAILS || '')
.split(',')
.map((item) => item.trim().toLowerCase())
.filter(Boolean);
const activeUserConnections = new Map();
const presenceTimers = new Map();
const incrementActiveUser = (userId) => {
if (!userId) return;
const current = activeUserConnections.get(userId) || 0;
activeUserConnections.set(userId, current + 1);
};
const decrementActiveUser = (userId) => {
if (!userId) return;
const current = activeUserConnections.get(userId) || 0;
if (current <= 1) {
activeUserConnections.delete(userId);
} else {
activeUserConnections.set(userId, current - 1);
}
};
const app = express();
const server = createServer(app);
const io = new SocketIOServer(server, {
cors: {
origin: allowedOrigins,
credentials: true,
},
});
const emitAdminEvent = (event, payload = {}) => {
io.to('admins').emit(event, { ...payload, ts: Date.now() });
};
const emitStats = async () => {
let totalUsers = 0;
try {
const { count, error } = await supabase
.from(USERS_TABLE)
.select('id', { count: 'exact', head: true });
if (!error && typeof count === 'number') {
totalUsers = count;
}
} catch (error) {
console.warn('Kullanıcı sayısı alınamadı', error.message);
}
emitAdminEvent('stats:update', {
activeUsers: activeUserConnections.size,
totalUsers,
});
};
const normalizeOrigin = (value = '') => {
try {
@@ -57,7 +111,12 @@ const enforceClientOrigin = (req, res, 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';
import {
getActiveTranslationModel,
listTranslationModels,
setActiveTranslationModel,
translateWithActiveModel,
} from './src/services/translationService.js';
app.use(
cors({
@@ -82,6 +141,8 @@ const createToken = (user) =>
sub: user.id,
email: user.email,
username: user.username,
name: user.name,
role: user.role || 'user',
},
JWT_SECRET,
{ expiresIn: '7d' },
@@ -92,8 +153,16 @@ const mapUserRecord = (record) => ({
email: record.email,
name: record.name,
username: record.username,
role: record.role || 'user',
});
const isAdminAllowed = (user) => {
if (!user) return false;
const role = user.role || 'user';
const email = (user.email || '').toLowerCase();
return role === 'admin' && ADMIN_EMAILS.includes(email);
};
const authMiddleware = (req, res, next) => {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
@@ -109,6 +178,85 @@ const authMiddleware = (req, res, next) => {
}
};
const adminMiddleware = (req, res, next) => {
if (!req.user || !isAdminAllowed(req.user)) {
return res.status(403).json({ message: 'Bu işlem için yetkiniz yok.' });
}
return next();
};
io.use((socket, next) => {
const token =
socket.handshake.auth?.token ||
(socket.handshake.headers?.authorization || '').replace(/^Bearer\s+/i, '').trim();
if (!token) {
return next(new Error('Yetkisiz erişim'));
}
try {
const payload = jwt.verify(token, JWT_SECRET);
socket.user = payload;
return next();
} catch (error) {
return next(new Error('Oturum doğrulanamadı'));
}
});
io.on('connection', (socket) => {
const email = socket.user?.email || 'bilinmiyor';
const displayName =
socket.user?.name || socket.user?.username || (socket.user?.email || '').split('@')[0] || 'kullanıcı';
console.log(`[Socket] Bağlandı: ${socket.id} (${email})`);
const adminUser = isAdminAllowed(socket.user);
if (adminUser) {
socket.join('admins');
}
socket.emit('connection:ready', { message: 'Socket bağlantısı kuruldu.', admin: adminUser });
if (socket.user?.sub) {
incrementActiveUser(socket.user.sub);
const existingTimer = presenceTimers.get(socket.user.sub);
if (existingTimer) {
clearTimeout(existingTimer);
presenceTimers.delete(socket.user.sub);
}
emitAdminEvent('presence:update', {
userId: socket.user.sub,
email: socket.user.email,
name: displayName,
active: true,
});
emitStats();
}
socket.on('ping', () => {
socket.emit('pong');
});
socket.on('disconnect', (reason) => {
console.log(`[Socket] Koptu: ${socket.id} sebep=${reason}`);
if (socket.user?.sub) {
decrementActiveUser(socket.user.sub);
if (activeUserConnections.has(socket.user.sub)) {
emitStats();
return;
}
// Reconnect sırasında çift log oluşmasını engellemek için kısa gecikme
const timeoutId = setTimeout(() => {
presenceTimers.delete(socket.user.sub);
emitAdminEvent('presence:update', {
userId: socket.user.sub,
email: socket.user.email,
name: displayName,
active: false,
});
emitStats();
}, 1500);
presenceTimers.set(socket.user.sub, timeoutId);
}
});
});
const authRouter = express.Router();
authRouter.use(enforceClientOrigin);
@@ -142,8 +290,9 @@ authRouter.post('/register', async (req, res) => {
email: normalizedEmail,
username,
password_hash: passwordHash,
role: 'user',
})
.select('id,email,name,username')
.select('id,email,name,username,role')
.single();
if (insertError || !createdUser) {
@@ -151,6 +300,12 @@ authRouter.post('/register', async (req, res) => {
}
const token = createToken(createdUser);
emitAdminEvent('user:registered', {
email: createdUser.email,
name: createdUser.name,
id: createdUser.id,
});
emitStats();
return res.status(201).json({ token, user: mapUserRecord(createdUser) });
});
@@ -163,7 +318,7 @@ authRouter.post('/login', async (req, res) => {
const normalizedEmail = email.trim().toLowerCase();
const { data: userRecord, error: fetchError } = await supabase
.from(USERS_TABLE)
.select('id,email,name,username,password_hash')
.select('id,email,name,username,password_hash,role')
.eq('email', normalizedEmail)
.maybeSingle();
@@ -214,7 +369,7 @@ authRouter.post('/oauth', async (req, res) => {
const { data: existingUser, error: fetchError } = await supabase
.from(USERS_TABLE)
.select('id,email,name,username')
.select('id,email,name,username,role')
.eq('email', email)
.maybeSingle();
@@ -226,13 +381,17 @@ authRouter.post('/oauth', async (req, res) => {
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')
.upsert(
{
name,
email,
username,
password_hash: 'GOOGLE_OAUTH',
role: 'user',
},
{ onConflict: 'email' },
)
.select('id,email,name,username,role')
.single();
if (insertError || !insertedUser) {
@@ -242,6 +401,7 @@ authRouter.post('/oauth', async (req, res) => {
}
const token = createToken(userRecord);
emitStats();
return res.json({ token, user: mapUserRecord(userRecord) });
} catch (error) {
console.error('Google OAuth hatası:', error);
@@ -252,7 +412,7 @@ authRouter.post('/oauth', async (req, res) => {
authRouter.get('/me', authMiddleware, async (req, res) => {
const { data: userRecord, error } = await supabase
.from(USERS_TABLE)
.select('id,email,name,username')
.select('id,email,name,username,role')
.eq('id', req.user.sub)
.single();
@@ -282,6 +442,24 @@ authRouter.post('/forgot-password', async (req, res) => {
app.use('/auth', authRouter);
app.get('/admin/translation-models', authMiddleware, adminMiddleware, (_req, res) => {
return res.json({
active: getActiveTranslationModel(),
models: listTranslationModels(),
});
});
app.post('/admin/translation-models', authMiddleware, adminMiddleware, (req, res) => {
const { model } = req.body || {};
try {
const newModel = setActiveTranslationModel(model);
emitAdminEvent('translate:model-changed', { active: newModel });
return res.json({ active: newModel, models: listTranslationModels() });
} catch (error) {
return res.status(400).json({ message: error.message || 'Model değiştirilemedi.' });
}
});
app.post('/translate', authMiddleware, async (req, res) => {
const { text } = req.body || {};
if (!text || !text.trim()) {
@@ -290,11 +468,23 @@ app.post('/translate', authMiddleware, async (req, res) => {
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 });
const translated = await translateWithActiveModel(text);
const model = getActiveTranslationModel();
console.log('[Translate] Çeviri başarıyla döndü', { model });
emitAdminEvent('translate:completed', {
userId: req.user?.sub,
email: req.user?.email,
length: text.length,
model,
});
return res.json({ text: translated, model });
} catch (error) {
console.error('GLM çeviri hatası:', error);
emitAdminEvent('translate:failed', {
userId: req.user?.sub,
email: req.user?.email,
message: error?.message,
});
return res.status(500).json({ message: error.message || 'Çeviri tamamlanamadı.' });
}
});
@@ -369,9 +559,21 @@ app.post('/generate-epub', authMiddleware, async (req, res) => {
if (coverPath) {
await fs.unlink(coverPath).catch(() => {});
}
emitAdminEvent('epub:generated', {
userId: req.user?.sub,
email: req.user?.email,
filename,
byteLength: buffer.length,
language,
});
res.json({ filename, data: buffer.toString('base64') });
} catch (error) {
console.error('EPUB generation failed:', error);
emitAdminEvent('epub:failed', {
userId: req.user?.sub,
email: req.user?.email,
message: error?.message,
});
res.status(500).json({ message: 'EPUB generation failed' });
}
});
@@ -380,6 +582,6 @@ app.get('/', (_, res) => {
res.json({ status: 'ok' });
});
app.listen(PORT, () => {
server.listen(PORT, () => {
console.log(`imgPub EPUB server listening on port ${PORT}`);
});

View File

@@ -15,6 +15,7 @@
"epub-gen": "^0.1.0",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"socket.io": "^4.8.1",
"nodemon": "^3.1.4",
"uuid": "^9.0.1"
}

View File

@@ -78,8 +78,10 @@ const extractContent = (payload) => {
return '';
};
export const isGlmConfigured = () => Boolean(GLM_API_KEY);
export const translateWithGlm = async (text) => {
if (!GLM_API_KEY) {
if (!isGlmConfigured()) {
throw new Error('ZAI_GLM_API_KEY veya ANTHROPIC_API_KEY tanımlı değil.');
}

View File

@@ -0,0 +1,77 @@
import { translateWithGlm, isGlmConfigured } from './glmClient.js';
const AVAILABLE_PROVIDERS = {
glm: {
id: 'glm',
label: 'GLM / Anthropic',
isConfigured: () => isGlmConfigured(),
translate: translateWithGlm,
envKeys: ['ZAI_GLM_API_KEY', 'ANTHROPIC_API_KEY'],
},
openai: {
id: 'openai',
label: 'OpenAI ChatGPT',
isConfigured: () => Boolean(process.env.OPENAI_API_KEY),
translate: async () => {
throw new Error('OpenAI çeviri modeli henüz yapılandırılmadı.');
},
envKeys: ['OPENAI_API_KEY'],
},
deepseek: {
id: 'deepseek',
label: 'DeepSeek',
isConfigured: () => Boolean(process.env.DEEPSEEK_API_KEY),
translate: async () => {
throw new Error('DeepSeek çeviri modeli henüz yapılandırılmadı.');
},
envKeys: ['DEEPSEEK_API_KEY'],
},
};
const normalizedModel = (value) => (value || '').toString().trim().toLowerCase();
const resolveInitialModel = () => {
const preferred = normalizedModel(process.env.TRANSLATION_ACTIVE_MODEL || 'glm');
const preferredProvider = AVAILABLE_PROVIDERS[preferred];
if (preferredProvider && preferredProvider.isConfigured()) {
return preferredProvider.id;
}
const firstConfigured = Object.values(AVAILABLE_PROVIDERS).find((provider) =>
provider.isConfigured(),
);
return firstConfigured ? firstConfigured.id : 'glm';
};
let activeModel = resolveInitialModel();
export const getActiveTranslationModel = () => activeModel;
export const setActiveTranslationModel = (modelId) => {
const provider = AVAILABLE_PROVIDERS[normalizedModel(modelId)];
if (!provider) {
throw new Error('Geçersiz çeviri modeli.');
}
if (!provider.isConfigured()) {
throw new Error(`${provider.label} için API anahtarı tanımlı değil.`);
}
activeModel = provider.id;
return activeModel;
};
export const listTranslationModels = () =>
Object.values(AVAILABLE_PROVIDERS).map((provider) => ({
id: provider.id,
label: provider.label,
configured: provider.isConfigured(),
active: provider.id === activeModel,
requiredEnv: provider.envKeys,
}));
export const translateWithActiveModel = async (text) => {
const provider = AVAILABLE_PROVIDERS[activeModel];
if (!provider || !provider.isConfigured()) {
throw new Error('Aktif çeviri modeli yapılandırılmadı.');
}
return provider.translate(text);
};