Files
ReSekai/server/index.js
T

644 lines
18 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 express from "express";
import cors from "cors";
import session from "express-session";
import MongoStore from "connect-mongo";
import { MongoClient, ObjectId } from "mongodb";
import dotenv from "dotenv";
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001;
// MongoDB подключение
const mongoClient = new MongoClient(process.env.MONGODB_URI);
let db;
async function connectDB() {
await mongoClient.connect();
db = mongoClient.db("resekai");
console.log("✅ Connected to MongoDB");
}
// Middleware
app.use(express.json());
app.use(
cors({
origin: process.env.FRONTEND_URL,
credentials: true,
}),
);
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
dbName: "resekai",
collectionName: "sessions",
}),
cookie: {
secure: false, // true в production с HTTPS
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней
},
}),
);
// Discord OAuth2 конфиг
const DISCORD_API = "https://discord.com/api/v10";
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
const DISCORD_REDIRECT_URI = process.env.DISCORD_REDIRECT_URI;
// ============ AUTH ROUTES ============
// Начало авторизации Discord
app.get("/auth/discord", (req, res) => {
const params = new URLSearchParams({
client_id: DISCORD_CLIENT_ID,
redirect_uri: DISCORD_REDIRECT_URI,
response_type: "code",
scope: "identify email",
});
res.redirect(`https://discord.com/api/oauth2/authorize?${params}`);
});
// Callback от Discord
app.get("/auth/discord/callback", async (req, res) => {
const { code } = req.query;
if (!code) {
return res.redirect(`${process.env.FRONTEND_URL}?error=no_code`);
}
try {
// Получаем токен
const tokenResponse = await fetch(`${DISCORD_API}/oauth2/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: DISCORD_CLIENT_ID,
client_secret: DISCORD_CLIENT_SECRET,
grant_type: "authorization_code",
code,
redirect_uri: DISCORD_REDIRECT_URI,
}),
});
const tokenData = await tokenResponse.json();
if (!tokenData.access_token) {
console.error("Token error:", tokenData);
return res.redirect(`${process.env.FRONTEND_URL}?error=token_failed`);
}
// Получаем информацию о пользователе
const userResponse = await fetch(`${DISCORD_API}/users/@me`, {
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const discordUser = await userResponse.json();
// Сохраняем или обновляем пользователя в БД
const users = db.collection("users");
const existingUser = await users.findOne({ discordId: discordUser.id });
let user;
if (existingUser) {
// Обновляем существующего пользователя
await users.updateOne(
{ discordId: discordUser.id },
{
$set: {
username: discordUser.username,
email: discordUser.email,
avatar: discordUser.avatar,
updatedAt: new Date(),
},
},
);
user = await users.findOne({ discordId: discordUser.id });
} else {
// Создаём нового пользователя
const newUser = {
discordId: discordUser.id,
username: discordUser.username,
email: discordUser.email,
avatar: discordUser.avatar,
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await users.insertOne(newUser);
user = { ...newUser, _id: result.insertedId };
}
// Сохраняем в сессию
req.session.userId = user._id.toString();
req.session.discordId = discordUser.id;
res.redirect(`${process.env.FRONTEND_URL}?auth=success`);
} catch (error) {
console.error("Auth error:", error);
res.redirect(`${process.env.FRONTEND_URL}?error=auth_failed`);
}
});
// Получить текущего пользователя
app.get("/auth/me", async (req, res) => {
if (!req.session.userId) {
return res.json({ user: null });
}
try {
const users = db.collection("users");
const user = await users.findOne({ _id: new ObjectId(req.session.userId) });
if (!user) {
return res.json({ user: null });
}
res.json({
user: {
id: user._id,
discordId: user.discordId,
username: user.username,
email: user.email,
avatar: user.avatar,
},
});
} catch (error) {
console.error("Get user error:", error);
res.json({ user: null });
}
});
// Выход
app.post("/auth/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Logout failed" });
}
res.clearCookie("connect.sid");
res.json({ success: true });
});
});
// ============ STORIES ROUTES ============
// Middleware для проверки авторизации
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
};
// Получить все истории пользователя
app.get("/api/stories", requireAuth, async (req, res) => {
try {
const stories = db.collection("stories");
const userStories = await stories
.find({ userId: req.session.userId })
.sort({ updatedAt: -1 })
.toArray();
res.json(userStories);
} catch (error) {
console.error("Get stories error:", error);
res.status(500).json({ error: "Failed to get stories" });
}
});
// Получить одну историю
app.get("/api/stories/:id", requireAuth, async (req, res) => {
try {
const stories = db.collection("stories");
const story = await stories.findOne({
_id: new ObjectId(req.params.id),
userId: req.session.userId,
});
if (!story) {
return res.status(404).json({ error: "Story not found" });
}
res.json(story);
} catch (error) {
console.error("Get story error:", error);
res.status(500).json({ error: "Failed to get story" });
}
});
// Создать историю
app.post("/api/stories", requireAuth, async (req, res) => {
try {
const stories = db.collection("stories");
const newStory = {
...req.body,
userId: req.session.userId,
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await stories.insertOne(newStory);
res.json({ ...newStory, _id: result.insertedId });
} catch (error) {
console.error("Create story error:", error);
res.status(500).json({ error: "Failed to create story" });
}
});
// Обновить историю
app.put("/api/stories/:id", requireAuth, async (req, res) => {
try {
const stories = db.collection("stories");
const result = await stories.updateOne(
{
_id: new ObjectId(req.params.id),
userId: req.session.userId,
},
{
$set: {
...req.body,
updatedAt: new Date(),
},
},
);
if (result.matchedCount === 0) {
return res.status(404).json({ error: "Story not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Update story error:", error);
res.status(500).json({ error: "Failed to update story" });
}
});
// Удалить историю
app.delete("/api/stories/:id", requireAuth, async (req, res) => {
try {
const stories = db.collection("stories");
const result = await stories.deleteOne({
_id: new ObjectId(req.params.id),
userId: req.session.userId,
});
if (result.deletedCount === 0) {
return res.status(404).json({ error: "Story not found" });
}
// Также удаляем связанные сессии игры
const sessions = db.collection("game_sessions");
await sessions.deleteMany({ storyId: req.params.id });
res.json({ success: true });
} catch (error) {
console.error("Delete story error:", error);
res.status(500).json({ error: "Failed to delete story" });
}
});
// ============ GAME SESSIONS ROUTES ============
// Получить список сессий для истории
app.get("/api/sessions/:storyId", requireAuth, async (req, res) => {
try {
const sessions = db.collection("game_sessions");
const userSessions = await sessions
.find({
storyId: req.params.storyId,
userId: req.session.userId,
})
.sort({ updatedAt: -1 })
.toArray();
// Возвращаем список с базовой инфой (без полных сообщений)
const sessionsList = userSessions.map((s) => ({
id: s._id.toString(),
name: s.name || "Сессия",
messagesCount: s.messages?.length || 0,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
}));
res.json(sessionsList);
} catch (error) {
console.error("Get sessions list error:", error);
res.status(500).json({ error: "Failed to get sessions" });
}
});
// Получить конкретную сессию
app.get("/api/sessions/:storyId/:sessionId", requireAuth, async (req, res) => {
try {
const sessions = db.collection("game_sessions");
const session = await sessions.findOne({
_id: new ObjectId(req.params.sessionId),
storyId: req.params.storyId,
userId: req.session.userId,
});
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ ...session, id: session._id.toString() });
} catch (error) {
console.error("Get session error:", error);
res.status(500).json({ error: "Failed to get session" });
}
});
// Создать новую сессию
app.post("/api/sessions/:storyId", requireAuth, async (req, res) => {
try {
const sessions = db.collection("game_sessions");
// Считаем существующие сессии для нумерации
const existingCount = await sessions.countDocuments({
storyId: req.params.storyId,
userId: req.session.userId,
});
const sessionData = {
storyId: req.params.storyId,
userId: req.session.userId,
name: req.body.name || `Сессия ${existingCount + 1}`,
playerId: req.body.playerId || null,
messages: [],
currentState: {
location: "start",
health: 100,
inventory: [],
questProgress: {},
},
storySummary: "",
keyEvents: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await sessions.insertOne(sessionData);
res.json({
id: result.insertedId.toString(),
name: sessionData.name,
messagesCount: 0,
createdAt: sessionData.createdAt,
updatedAt: sessionData.updatedAt,
});
} catch (error) {
console.error("Create session error:", error);
res.status(500).json({ error: "Failed to create session" });
}
});
// Обновить сессию
app.put("/api/sessions/:storyId/:sessionId", requireAuth, async (req, res) => {
try {
const sessions = db.collection("game_sessions");
// Преобразуем timestamp строки обратно в Date
const messages = (req.body.messages || []).map((msg) => ({
...msg,
timestamp: new Date(msg.timestamp),
}));
// Убираем _id и createdAt из body чтобы не было конфликтов
const { createdAt, _id, id, ...bodyWithoutMeta } = req.body;
const sessionData = {
...bodyWithoutMeta,
messages,
updatedAt: new Date(),
};
const result = await sessions.updateOne(
{
_id: new ObjectId(req.params.sessionId),
storyId: req.params.storyId,
userId: req.session.userId,
},
{ $set: sessionData },
);
if (result.matchedCount === 0) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Update session error:", error);
res.status(500).json({ error: "Failed to update session" });
}
});
// Удалить сессию
app.delete(
"/api/sessions/:storyId/:sessionId",
requireAuth,
async (req, res) => {
try {
const sessions = db.collection("game_sessions");
const result = await sessions.deleteOne({
_id: new ObjectId(req.params.sessionId),
storyId: req.params.storyId,
userId: req.session.userId,
});
if (result.deletedCount === 0) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Delete session error:", error);
res.status(500).json({ error: "Failed to delete session" });
}
},
);
// ============ PLAYER CHARACTERS ROUTES ============
// Получить всех персонажей пользователя
app.get("/api/characters", requireAuth, async (req, res) => {
try {
const characters = db.collection("player_characters");
const userCharacters = await characters
.find({ userId: req.session.userId })
.sort({ updatedAt: -1 })
.toArray();
res.json(userCharacters);
} catch (error) {
console.error("Get characters error:", error);
res.status(500).json({ error: "Failed to get characters" });
}
});
// Получить одного персонажа
app.get("/api/characters/:id", requireAuth, async (req, res) => {
try {
const characters = db.collection("player_characters");
const character = await characters.findOne({
_id: new ObjectId(req.params.id),
userId: req.session.userId,
});
if (!character) {
return res.status(404).json({ error: "Character not found" });
}
res.json(character);
} catch (error) {
console.error("Get character error:", error);
res.status(500).json({ error: "Failed to get character" });
}
});
// Создать персонажа
app.post("/api/characters", requireAuth, async (req, res) => {
try {
const characters = db.collection("player_characters");
const newCharacter = {
...req.body,
userId: req.session.userId,
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await characters.insertOne(newCharacter);
res.json({ ...newCharacter, _id: result.insertedId });
} catch (error) {
console.error("Create character error:", error);
res.status(500).json({ error: "Failed to create character" });
}
});
// Обновить персонажа
app.put("/api/characters/:id", requireAuth, async (req, res) => {
try {
const characters = db.collection("player_characters");
const result = await characters.updateOne(
{
_id: new ObjectId(req.params.id),
userId: req.session.userId,
},
{
$set: {
...req.body,
updatedAt: new Date(),
},
},
);
if (result.matchedCount === 0) {
return res.status(404).json({ error: "Character not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Update character error:", error);
res.status(500).json({ error: "Failed to update character" });
}
});
// Удалить персонажа
app.delete("/api/characters/:id", requireAuth, async (req, res) => {
try {
const characters = db.collection("player_characters");
const result = await characters.deleteOne({
_id: new ObjectId(req.params.id),
userId: req.session.userId,
});
if (result.deletedCount === 0) {
return res.status(404).json({ error: "Character not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Delete character error:", error);
res.status(500).json({ error: "Failed to delete character" });
}
});
// ============ ADMIN STATS ============
// Получить статистику по всем историям и токенам
app.get("/api/admin/stats", requireAuth, async (req, res) => {
try {
const stories = db.collection("stories");
const gameSessions = db.collection("game_sessions");
// Получаем все истории пользователя
const userStories = await stories
.find({ userId: req.session.userId })
.toArray();
// Получаем все сессии пользователя
const userSessions = await gameSessions
.find({ storyId: { $in: userStories.map((s) => s._id.toString()) } })
.toArray();
// Считаем статистику для каждой истории
const storyStats = userStories.map((story) => {
const session = userSessions.find(
(s) => s.storyId === story._id.toString(),
);
const messages = session?.messages || [];
const messageCount = messages.length;
// Примерный подсчёт токенов (1 токен ≈ 3 символа для русского)
const totalChars = messages.reduce(
(sum, msg) => sum + (msg.content?.length || 0),
0,
);
const tokens = Math.round(totalChars / 3);
return {
id: story._id.toString(),
title: story.title,
messageCount,
tokens,
lastPlayed: session?.updatedAt || null,
};
});
// Сортируем по токенам (больше сверху)
storyStats.sort((a, b) => b.tokens - a.tokens);
// Общая статистика
const totalTokens = storyStats.reduce((sum, s) => sum + s.tokens, 0);
res.json({
totalStories: userStories.length,
totalSessions: userSessions.length,
totalTokens,
stories: storyStats,
});
} catch (error) {
console.error("Get admin stats error:", error);
res.status(500).json({ error: "Failed to get stats" });
}
});
// Запуск сервера
connectDB().then(() => {
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
});