first commit

This commit is contained in:
Alexej Wolff
2026-02-11 00:15:59 +01:00
commit cc003ffbd5
39 changed files with 12170 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# MongoDB
MONGODB_URI=mongodb://localhost:27017/resekai
# Discord OAuth2
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
DISCORD_REDIRECT_URI=http://localhost:3001/auth/discord/callback
# Session
SESSION_SECRET=your_super_secret_session_key
# Frontend URL
FRONTEND_URL=http://localhost:5174
# Server
PORT=3001
+28
View File
@@ -0,0 +1,28 @@
# Dependencies
node_modules
# Environment
.env
.env.local
.env.*.local
# Logs
logs
*.log
npm-debug.log*
# OS
.DS_Store
Thumbs.db
# Editor
.vscode
.idea
*.swp
*~
# Runtime
pids
*.pid
*.seed
*.pid.lock
+480
View File
@@ -0,0 +1,480 @@
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 session = await sessions.findOne({
storyId: req.params.storyId,
userId: req.session.userId,
});
res.json(session);
} 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");
// Преобразуем 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,
storyId: req.params.storyId,
userId: req.session.userId,
updatedAt: new Date(),
};
await sessions.updateOne(
{
storyId: req.params.storyId,
userId: req.session.userId,
},
{
$set: sessionData,
$setOnInsert: { createdAt: new Date() },
},
{ upsert: true },
);
res.json({ success: true });
} catch (error) {
console.error("Save session error:", error);
res.status(500).json({ error: "Failed to save 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" });
}
});
// Запуск сервера
connectDB().then(() => {
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
});
+1200
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "resekai-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --watch index.js",
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"mongodb": "^6.3.0",
"dotenv": "^16.3.1",
"express-session": "^1.17.3",
"connect-mongo": "^5.1.0"
}
}