Compare commits

..

16 Commits

Author SHA1 Message Date
Primakov Alexandr Alexandrovich
72a2667549 1.2.1 2025-02-08 10:39:49 +03:00
Primakov Alexandr Alexandrovich
39db7b4d26 fix 2025-02-08 10:39:45 +03:00
aa
ff25c0ecb9 Merge pull request 'fix path' (#93) from gamehub into master
Reviewed-on: #93
2025-02-08 10:34:05 +03:00
aaeii
f1a93bffb5 fix path 2025-02-08 10:33:24 +03:00
aa
aa231d4f43 Merge pull request 'upd json' (#92) from gamehub into master
Reviewed-on: #92
2025-02-08 09:59:04 +03:00
aaeii
f254d57db4 upd json 2025-02-08 09:57:34 +03:00
106f835934 Merge pull request 'dogsitters-finder' (#91) from dogsitters-finder into master
Reviewed-on: #91
2025-02-08 04:50:31 +03:00
f9b30a4cfd Merge branch 'master' into dogsitters-finder
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 04:45:56 +03:00
5e4a99529d Add backend and db settings 2025-02-08 04:44:17 +03:00
4d585002d7 Add backend and DB settings 2025-02-08 04:38:22 +03:00
b073fe3fdf Merge pull request 'Изменены запросы и добавлены новые' (#89) from dogsitters-finder-2 into master
Reviewed-on: #89
2025-02-08 02:39:15 +03:00
312cc229d8 Изменены запросы и добавлены новые
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 02:23:22 +03:00
11b1d670d0 Merge pull request 'small fixes in kfu-m-24-1/eng-it-lean' (#88) from kfu-m-24-1/eng-it-lean into master
Reviewed-on: #88
2025-02-07 12:26:56 +03:00
aa
771f75ef08 Merge pull request 'add new game, add link' (#87) from gamehub into master
Reviewed-on: #87
2025-02-05 22:26:15 +03:00
aaeii
edf9b2c82b add new game, add link 2025-02-05 22:24:05 +03:00
a88d3657bf Merge pull request 'kfu-m-24-1/eng-it-lean quick fix' (#86) from kfu-m-24-1/eng-it-lean into master
Reviewed-on: #86
2025-02-05 19:33:51 +03:00
28 changed files with 668 additions and 127 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "multi-stub", "name": "multi-stub",
"version": "1.2.0", "version": "1.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "multi-stub", "name": "multi-stub",
"version": "1.2.0", "version": "1.2.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ai": "^4.1.13", "ai": "^4.1.13",

View File

@@ -1,6 +1,6 @@
{ {
"name": "multi-stub", "name": "multi-stub",
"version": "1.2.0", "version": "1.2.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -81,7 +81,7 @@ app.use(require("./root"))
app.use("/kfu-m-24-1", require("./routers/kfu-m-24-1")) app.use("/kfu-m-24-1", require("./routers/kfu-m-24-1"))
app.use("/epja-2024-1", require("./routers/epja-2024-1")) app.use("/epja-2024-1", require("./routers/epja-2024-1"))
app.use("/v1/todo", require("./routers/todo")) app.use("/v1/todo", require("./routers/todo"))
app.use("/dogsitters-finder", require("./routers/dogsitters-finder")) // app.use("/dogsitters-finder", require("./routers/dogsitters-finder"))
app.use("/kazan-explore", require("./routers/kazan-explore")) app.use("/kazan-explore", require("./routers/kazan-explore"))
app.use("/edateam", require("./routers/edateam-legacy")) app.use("/edateam", require("./routers/edateam-legacy"))
app.use("/dry-wash", require("./routers/dry-wash")) app.use("/dry-wash", require("./routers/dry-wash"))

View File

@@ -0,0 +1,74 @@
const { Router } = require("express");
const hash = require("pbkdf2-password")();
const { promisify } = require("node:util");
const jwt = require('jsonwebtoken')
const { getAnswer } = require("../../utils/common");
const { AuthModel } = require("./model/todo/auth");
const { TOKEN_KEY } = require('./const')
const { UserModel } = require("./model/todo/user");
const { requiredValidate } = require('./utils')
const router = Router();
router.post(
"/signup",
requiredValidate("login", "password", "email"),
async (req, res, next) => {
const { login, password, email } = req.body
const user = await AuthModel.findOne({ login });
if (user) {
throw new Error("Пользователь с таким логином уже существует");
}
hash({ password }, async function (err, pass, salt, hash) {
if (err) return next(err);
const user = await UserModel.create({ login, email });
await AuthModel.create({ login, hash, salt, userId: user.id });
res.json(getAnswer(null, { ok: true }))
})
}
)
function authenticate(login, pass, cb) {
AuthModel.findOne({ login }).populate('userId').exec().then((user) => {
if (!user) return cb(null, null)
hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) {
if (err) return cb(err)
if (hash === user.hash) return cb(null, user)
cb(null, null)
})
})
}
const auth = promisify(authenticate)
router.post('/signin', requiredValidate('login', 'password'), async (req, res) => {
const { login, password } = req.body
const user = await auth(login, password)
if (!user) {
throw new Error("Неверный логин или пароль")
}
const accessToken = jwt.sign({
...JSON.parse(JSON.stringify(user.userId)),
}, TOKEN_KEY, {
expiresIn: '12h'
})
res.json(getAnswer(null, {
user: user.userId,
token: accessToken,
}))
})
module.exports = router

View File

@@ -0,0 +1,3 @@
exports.DSF_AUTH_PASSWD_MODEL_NAME = 'DSF_AUTH_PASSWD'
exports.DSF_AUTH_USER_MODEL_NAME = 'DSF_AUTH_USER'
exports.DSF_INTERACTION_MODEL_NAME = 'DSF_INTERACTION'

View File

@@ -7,34 +7,63 @@ router.get("/users", (request, response) => {
router.post("/auth", (request, response) => { router.post("/auth", (request, response) => {
const { phoneNumber, password } = request.body; const { phoneNumber, password } = request.body;
console.log(phoneNumber, password); console.log(phoneNumber, password);
if (phoneNumber === "89999999999") { if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
response.send(require("./json/auth/dogsitter.success.json")); response.send(require("../json/auth/success.json"));
} else if (phoneNumber === "89555555555") {
response.status(400).send(require("./json/auth/error.json"));
} else { } else {
response.send(require("./json/auth/owner.success.json")); response.status(401).send(require("../json/auth/error.json"));
} }
}); });
router.post("/auth/2fa", (request, response) => { router.post("/auth/2fa", (request, response) => {
const { code } = request.body; const { phoneNumber, code } = request.body;
if (code === "0000") { if (code === "0000" && phoneNumber === "89999999999") {
response.send(require("./json/2fa/success.json")); response.send(require("../json/2fa/dogsitter.success.json"));
} else if (code === "0000" && phoneNumber === "89559999999") {
response.send(require("../json/2fa/owner.success.json"));
} else { } else {
response.status(400).send(require("./json/2fa/error.json")); response.status(401).send(require("../json/2fa/error.json"));
} }
}); });
router.post("/register", (request, response) => { router.post("/register", (request, response) => {
const { firstName, secondName, phoneNumber, password, role } = request.body; const { firstName, secondName, phoneNumber, password, role } = request.body;
console.log(phoneNumber, password, role); console.log(phoneNumber, password, role);
if (phoneNumber === "89283244141" || phoneNumber === "89872855893") { if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
response.status(400).send(require("./json/register/error.json")); response.status(401).send(require("../json/register/error.json"));
} else if (role === "dogsitter") { } else if (role === "dogsitter") {
response.send(require("./json/register/dogsitter.success.json")); response.send(require("../json/register/dogsitter.success.json"));
} else { } else {
response.send(require("./json/register/owner.success.json")); response.send(require("../json/register/owner.success.json"));
} }
}); });
module.exports = router; router.get("/auth/session", (request, response) => {
const authHeader = request.headers.authorization;
if (!authHeader) {
return response.status(401).json({ error: "Authorization header missing" });
}
// Берём сам токен из заголовка
const token = authHeader.split(" ")[1];
if (!token) {
return response.status(401).json({ error: "Bearer token missing" });
}
const jwt = require("jsonwebtoken");
const secretKey = "secret";
try {
const decoded = jwt.verify(token, secretKey);
if (decoded.role === "dogsitter") {
response.send(require("../json/role/dogsitter.success.json"));
} else {
response.send(require("../json/role/owner.success.json"));
}
} catch (e) {
console.log("token e:", e);
return response.status(403).json({ error: "Invalid token" });
}
});

View File

@@ -0,0 +1,3 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.7q66wTNyLZp3TGFYF_JdU-yhlWViJulTxP_PCQzO4OI"
}

View File

@@ -1,4 +1,5 @@
{ {
"status": "error", "status": "error",
"message": "Invalid code." "message": "Invalid code",
"statusCode": 401
} }

View File

@@ -0,0 +1,3 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.sI9839YXveTpEWhdpr5QbCYllt6hHYO7NsrQDcrXZIQ"
}

View File

@@ -1,4 +0,0 @@
{
"status": "success",
"message": "Two-factor authentication passed."
}

View File

@@ -1,12 +0,0 @@
{
"data": {
"id": 1,
"phoneNumber": 89283244141,
"firstName": "Вася",
"secondName": "Пупкин",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
"price": 1500,
"aboutMe": "Я люблю собак"
}
}

View File

@@ -1,3 +1,5 @@
{ {
"error": "Пользователь не найден" "message": "Неверный логин или пароль",
} "error": "Unauthorized",
"statusCode": 401
}

View File

@@ -1,9 +0,0 @@
{
"data": {
"id": 3,
"phoneNumber": 89872855893,
"firstName": "Гадий",
"secondName": "Петрович",
"role": "owner"
}
}

View File

@@ -0,0 +1,5 @@
{
"status": "success",
"message": "Первый фактор аутентификации пройден",
"statusCode": 200
}

View File

@@ -1,12 +1,3 @@
{ {
"data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.T9V3-f3rD1deA5a2J-tYNw0cACEpzKHbhMPkc7gh8c0"
"id": 5,
"phoneNumber": 89555555555,
"firstName": "Масяня",
"secondName": "Карлова",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
"price": 100,
"aboutMe": "Все на свете - собаки"
}
} }

View File

@@ -1,3 +1,5 @@
{ {
"error": "Пользователь с таким номером телефона уже существует" "message": "Такой пользователь уже был зарегистрирован",
"error": "Unauthorized",
"statusCode": 401
} }

View File

@@ -1,9 +1,3 @@
{ {
"data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Niwicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.qgOhk9tNcaMRbarRWISTgvGx5Eq_X8fcA5lhdVs2tQI"
"id": 6,
"phoneNumber": 89888888888,
"firstName": "Генадий",
"secondName": "Паровозов",
"role": "owner"
}
} }

View File

@@ -0,0 +1,4 @@
{
"id": 1,
"role": "dogsitter"
}

View File

@@ -0,0 +1,5 @@
{
"message": "Неверный jwt token",
"error": "Forbidden",
"statusCode": 403
}

View File

@@ -0,0 +1,4 @@
{
"id": 3,
"role": "owner"
}

View File

@@ -0,0 +1,44 @@
const { Schema, model } = require("mongoose");
const {
DSF_AUTH_PASSWD_MODEL_NAME,
DSF_AUTH_USER_MODEL_NAME,
} = require("../../const");
const schema = new Schema({
login: {
type: String,
required: true,
unique: true
},
hash: {
type: String,
required: true
},
salt: {
type: String,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: DSF_AUTH_USER_MODEL_NAME
},
created: {
type: Date,
default: () => new Date().toISOString(),
},
});
schema.set("toJSON", {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id;
},
});
schema.virtual("id").get(function () {
return this._id.toHexString();
});
exports.AuthModel = model(DSF_AUTH_PASSWD_MODEL_NAME, schema);

View File

@@ -0,0 +1,24 @@
const { Schema, model } = require("mongoose");
const { DSF_AUTH_USER_MODEL_NAME, DSF_INTERACTION_MODEL_NAME } = require("../../const");
const interactionSchema = new Schema({
owner_id: {
type: Schema.Types.ObjectId,
ref: DSF_AUTH_USER_MODEL_NAME,
required: true
},
dogsitter_id: {
type: Schema.Types.ObjectId,
ref: DSF_AUTH_USER_MODEL_NAME,
required: true
},
timestamp: {
type: Date,
default: Date.now
}
});
interactionSchema.index({ owner_id: 1, dogsitter_id: 1 });
module.exports.Interaction = model(DSF_INTERACTION_MODEL_NAME, interactionSchema);

View File

@@ -0,0 +1,83 @@
const { Schema, model } = require("mongoose");
const { DSF_AUTH_USER_MODEL_NAME } = require("../../const");
const userSchema = new Schema({
phone_number: {
type: String,
required: true,
unique: true,
match: /^\+?\d{10,15}$/
},
first_name: {
type: String,
required: true,
trim: true
},
second_name: {
type: String,
required: true,
trim: true
},
role: {
type: String,
enum: ["dogsitter", "owner"],
required: true
},
location: {
type: String,
required: function() {
return this.role === "dogsitter";
}
},
price: {
type: Number,
min: 0,
required: function() {
return this.role === "dogsitter";
}
},
about_me: {
type: String,
maxlength: 500
},
rating: {
type: Number,
min: 0,
max: 5,
default: 0
},
ratings: {
type: [Number],
default: [],
validate: {
validator: function(arr) {
return arr.every(v => v >= 0 && v <= 5);
},
message: "Рейтинг должен быть в диапазоне от 0 до 5!"
}
},
tg: {
type: String,
match: /^[a-zA-Z0-9_]{5,32}$/
},
created: {
type: Date,
default: Date.now
}
});
userSchema.virtual("id").get(function() {
return this._id.toHexString();
});
userSchema.set("toJSON", {
virtuals: true,
versionKey: false,
transform: function(doc, ret) {
delete ret._id;
delete ret.__v;
}
});
module.exports.User = model(DSF_AUTH_USER_MODEL_NAME, userSchema);

View File

@@ -0,0 +1,149 @@
const { Router } = require('express')
const { expressjwt } = require('express-jwt')
const { getAnswer } = require('../../utils/common')
const { User, Interaction } = require('./model')
const { TOKEN_KEY } = require('./const')
const { requiredValidate } = require('./utils')
const router = Router()
// Получение списка пользователей
router.get('/users', async (req, res) => {
const users = await User.find()
.select('-__v -ratings -phone_number')
.lean()
console.log('get users successfull')
res.send(getAnswer(null, users))
})
// Получение конкретного пользователя
router.get('/dogsitter-viewing', async (req, res) => {
const { userId } = req.params
const user = await User.findById(userId)
.select('-__v -ratings')
.lean()
if (!user) {
return res.status(404).send(getAnswer(new Error('Пользователь не найден')))
}
res.send(getAnswer(null, user))
})
router.use(expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }))
// Добавление оценки пользователю
router.post('/dogsitter-viewing/rating', requiredValidate('value'), async (req, res) => {
const { userId } = req.params
const { value } = req.body
const authUserId = req.auth.id
try {
const user = await User.findById(userId)
if (!user) throw new Error('Пользователь не найден')
if (user.role !== 'dogsitter') throw new Error('Нельзя оценивать этого пользователя')
if (user.id === authUserId) throw new Error('Нельзя оценивать самого себя')
user.ratings.push(Number(value))
user.rating = user.ratings.reduce((a, b) => a + b, 0) / user.ratings.length
const updatedUser = await user.save()
res.send(getAnswer(null, {
id: updatedUser.id,
rating: updatedUser.rating.toFixed(1),
totalRatings: updatedUser.ratings.length
}))
} catch (error) {
res.status(400).send(getAnswer(error))
}
})
// Обновление информации пользователя
router.patch('/users', async (req, res) => {
const { userId } = req.params
const updates = req.body
try {
const user = await User.findByIdAndUpdate(userId, updates, { new: true })
.select('-__v -ratings')
if (!user) throw new Error('Пользователь не найден')
res.send(getAnswer(null, user))
} catch (error) {
res.status(400).send(getAnswer(error))
}
})
// Создание объекта взаимодействия
router.post('/interactions',
expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }),
requiredValidate('dogsitter_id'),
async (req, res) => {
try {
const { dogsitter_id } = req.body
const owner_id = req.auth.id // ID из JWT токена
// Проверка существования пользователей
const [owner, dogsitter] = await Promise.all([
User.findById(owner_id),
User.findById(dogsitter_id)
])
if (!owner || owner.role !== 'owner') {
throw new Error('Владелец не найден или имеет неверную роль')
}
if (!dogsitter || dogsitter.role !== 'dogsitter') {
throw new Error('Догситтер не найден или имеет неверную роль')
}
// Создание взаимодействия
const interaction = await Interaction.create({
owner_id,
dogsitter_id
})
res.send(getAnswer(null, {
id: interaction.id,
timestamp: interaction.timestamp
}))
} catch (error) {
res.status(400).send(getAnswer(error))
}
}
)
router.get('/interactions/check', async (req, res) => {
const { owner_id, dogsitter_id } = req.query;
if (!owner_id || !dogsitter_id) {
return res.status(400).send(getAnswer('Missing owner_id or dogsitter_id'));
}
try {
// Поиск взаимодействий по owner_id и dogsitter_id
const interactions = await Interaction.find({ owner_id, dogsitter_id })
.select('-__v') // Выбираем только нужные поля
.lean();
if (interactions.length === 0) {
return res.status(404).send(getAnswer('No interactions found'));
}
res.send(getAnswer(null, interactions));
} catch (error) {
console.error('Error checking interactions:', error);
res.status(500).send(getAnswer('Internal Server Error'));
}
});
module.exports = router

View File

@@ -8,22 +8,44 @@ router.get("/update-like", (request, response) => {
response.send(require("./json/gamepage/success.json")); response.send(require("./json/gamepage/success.json"));
}); });
router.get("/add-to-cart", (request, response) => {
response.send(require("./json/home-page-data/games-in-cart.json"));
});
router.get("/categories", (request, response) => { router.get("/categories", (request, response) => {
response.send(require("./json/home-page-data/all-games.json")); response.send(require("./json/home-page-data/all-games.json"));
}); });
router.get("/shopping-cart", (request, response) => { router.get("/favourites", (request, response) => {
response.send(require("./json/shopping-cart/success.json")); response.send(require("./json/home-page-data/all-games.json"));
}); });
router.get("/home", (request, response) => { // router.get("/shopping-cart", (request, response) => {
response.send(require("./json/home-page-data/success.json")); // response.send(require("../json/shopping-cart/success.json"));
// });
router.get("/shopping-cart", (request, response) => {
response.send(require("./json/home-page-data/games-in-cart.json"));
});
// Добавляем поддержку разных ответов для /home
router.get("/home", (req, res) => {
if (stubs.home === "success") {
res.send(require("./json/home-page-data/success.json"));
} else if (stubs.home === "empty") {
res.send({ data: [] }); // Отправляем пустой массив
} else {
res.status(500).json({ success: false, message: "Server error" });
}
}); });
router.get("/all-games", (request, response) => { router.get("/all-games", (request, response) => {
response.send(require("./json/home-page-data/all-games.json")); response.send(require("./json/home-page-data/all-games.json"));
}); });
const stubs = {
home: "success",
};
// // Маршрут для обновления лайков // // Маршрут для обновления лайков
// router.post("/update-like", (request, response) => { // router.post("/update-like", (request, response) => {
@@ -38,7 +60,6 @@ router.get("/all-games", (request, response) => {
// }); // });
// }); // });
const fs = require("fs").promises; const fs = require("fs").promises;
const path = require("path"); const path = require("path");
@@ -49,7 +70,7 @@ const commentsFilePath = path.join(__dirname, "./json/gamepage/success.json");
async function readComments() { async function readComments() {
const data = await fs.readFile(commentsFilePath, "utf-8"); const data = await fs.readFile(commentsFilePath, "utf-8");
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
return parsedData; return parsedData;
} }
// Write to JSON file // Write to JSON file
@@ -88,5 +109,137 @@ router.post("/update-like", async (req, res) => {
} }
}); });
// Путь к JSON-файлу с корзиной
const cartFilePath = path.join(
__dirname,
"./json/home-page-data/games-in-cart.json"
);
// Функция для чтения JSON-файла
async function readCart() {
const data = await fs.readFile(cartFilePath, "utf-8");
return JSON.parse(data);
}
// Функция для записи в JSON-файл
async function writeCart(data) {
await fs.writeFile(cartFilePath, JSON.stringify(data, null, 2), "utf-8");
}
// Маршрут для добавления/удаления товара в корзине
router.post("/add-to-cart", async (req, res) => {
const { id, action } = req.body;
// Проверка наличия id и action
if (id === undefined || action === undefined) {
return res
.status(400)
.json({ success: false, message: "Invalid id or action" });
}
try {
const cartData = await readCart();
let ids = cartData.data.ids;
if (action === "add") {
// Если action "add", добавляем товар, если его нет в корзине
if (!ids?.includes(id)) {
ids.push(id);
}
} else if (action === "remove") {
// Если action "remove", удаляем товар, если он есть в корзине
if (ids?.includes(id)) {
ids = ids.filter((item) => item !== id);
}
} else {
// Если action невалиден
return res
.status(400)
.json({ success: false, message: "Invalid action" });
}
// Записываем обновленные данные обратно в файл
cartData.data.ids = ids;
await writeCart(cartData);
res.status(200).json({
success: true,
message: "Cart updated successfully",
data: cartData.data, // Возвращаем обновленные данные
});
} catch (error) {
console.error("Error updating cart:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
module.exports = router; module.exports = router;
const createElement = (key, value, buttonTitle) => `
<label>
<input name="${key}" type="radio" ${
stubs[key] === value ? "checked" : ""
} onclick="fetch('/api/admin/set/${key}/${value}')"/>
${buttonTitle || value}
</label>
`;
router.get("/admin/home", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /home</legend>
${createElement("home", "success", "Отдать успешный ответ")}
${createElement("home", "empty", "Отдать пустой массив")}
${createElement("home", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/game-page", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /game-page</legend>
${createElement("game-page", "success", "Отдать успешный ответ")}
${createElement("game-page", "empty", "Отдать пустой массив")}
${createElement("game-page", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/categories", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /categories</legend>
${createElement("categories", "success", "Отдать успешный ответ")}
${createElement("categories", "empty", "Отдать пустой массив")}
${createElement("categories", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/favourites", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /favourites</legend>
${createElement("favourites", "success", "Отдать успешный ответ")}
${createElement("favourites", "empty", "Отдать пустой массив")}
${createElement("favourites", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/set/:key/:value", (request, response) => {
const { key, value } = request.params;
stubs[key] = value;
response.send("Настройки обновлены!");
});

View File

@@ -5,28 +5,28 @@
{ {
"username": ользователь1", "username": ользователь1",
"text": "Текст комментария 1", "text": "Текст комментария 1",
"likes": 11, "likes": 13,
"rating": 8, "rating": 8,
"date": "2025-03-01T10:00:00Z" "date": "2025-03-01T10:00:00Z"
}, },
{ {
"username": ользователь2", "username": ользователь2",
"text": "Текст комментария 2", "text": "Текст комментария 2",
"likes": 7, "likes": 10,
"rating": 7, "rating": 7,
"date": "2025-01-01T10:00:00Z" "date": "2025-01-01T10:00:00Z"
}, },
{ {
"username": ользователь3", "username": ользователь3",
"text": "Текст комментария 3", "text": "Текст комментария 3",
"likes": 2, "likes": 4,
"rating": 3, "rating": 3,
"date": "2025-02-01T10:00:00Z" "date": "2025-02-01T10:00:00Z"
}, },
{ {
"username": ользователь4", "username": ользователь4",
"text": "Текст комментария 4", "text": "Текст комментария 4",
"likes": 15, "likes": 18,
"rating": 2, "rating": 2,
"date": "2025-12-01T10:00:00Z" "date": "2025-12-01T10:00:00Z"
} }

View File

@@ -3,41 +3,43 @@
"data": [ "data": [
{ {
"id": 1, "id": 1,
"title": "Elden Ring",
"image": "game17",
"price": 3295,
"old_price": 3599,
"imgPath": "img_top_17",
"description": "Крупномасштабная RPG, действие которой происходит в обширном открытом мире c богатой мифологией и множеством опасных врагов.",
"category": "RPG"
},
{
"id": 2,
"title": "The Witcher 3: Wild Hunt", "title": "The Witcher 3: Wild Hunt",
"image": "game1", "image": "game1",
"price": 990, "price": 990,
"old_price": 1200, "old_price": 1200,
"os": "windows",
"imgPath": "img_top_1", "imgPath": "img_top_1",
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.", "description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
"category": "RPG" "category": "RPG"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 2, "id": 17,
"title": "Red Dead Redemption 2", "title": "Red Dead Redemption 2",
"image": "game2", "image": "game2",
"price": 980, "price": 980,
"old_price": 3800, "old_price": 3800,
"os": "windows",
"imgPath": "img_top_2", "imgPath": "img_top_2",
"description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.", "description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.",
"category": "Adventures" "category": "Adventures"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 3, "id": 3,
"title": "Forza Horizon 5", "title": "Forza Horizon 5",
"image": "game3", "image": "game3",
"price": 1900, "price": 1900,
"os": "windows",
"imgPath": "img_top_3", "imgPath": "img_top_3",
"description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.", "description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.",
"category": "Race" "category": "Race"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 4, "id": 4,
@@ -45,72 +47,66 @@
"image": "game4", "image": "game4",
"price": 1200, "price": 1200,
"old_price": 2500, "old_price": 2500,
"os": "windows",
"imgPath": "img_top_4", "imgPath": "img_top_4",
"description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.", "description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.",
"category": "Shooters" "category": "Shooters"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 5, "id": 5,
"title": "Counter-Strike 2", "title": "Counter-Strike 2",
"image": "game5", "image": "game5",
"price": 479, "price": 479,
"os": "windows",
"imgPath": "img_top_5", "imgPath": "img_top_5",
"description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.", "description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.",
"category": "Shooters" "category": "Shooters"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 6, "id": 6,
"title": "Grand Theft Auto V", "title": "Grand Theft Auto V",
"image": "game6", "image": "game6",
"price": 700, "price": 700,
"os": "windows",
"imgPath": "img_top_6", "imgPath": "img_top_6",
"description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.", "description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.",
"category": "Adventures" "category": "Adventures"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 7, "id": 7,
"title": "Assassins Creed IV: Black Flag", "title": "Assassins Creed IV: Black Flag",
"image": "game7", "image": "game7",
"price": 1100, "price": 1100,
"os": "windows",
"imgPath": "img_top_7", "imgPath": "img_top_7",
"description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.", "description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.",
"category": "Adventures" "category": "Adventures"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 8, "id": 8,
"title": "Spider-Man", "title": "Spider-Man",
"image": "game8", "image": "game8",
"price": 3800, "price": 3800,
"os": "windows",
"imgPath": "img_top_8", "imgPath": "img_top_8",
"description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.", "description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.",
"category": "Action" "category": "Action"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 9, "id": 9,
"title": "Assassins Creed Mirage", "title": "Assassins Creed Mirage",
"image": "game9", "image": "game9",
"price": 1600, "price": 1600,
"os": "windows",
"imgPath": "img_top_9", "imgPath": "img_top_9",
"description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.", "description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.",
"category": "Action" "category": "Action"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 10, "id": 10,
@@ -118,79 +114,72 @@
"image": "game10", "image": "game10",
"price": 800, "price": 800,
"old_price": 2200, "old_price": 2200,
"os": "windows",
"imgPath": "img_top_10", "imgPath": "img_top_10",
"description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.", "description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.",
"category": "RPG" "category": "RPG"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 11, "id": 11,
"title": "ARK: Survival Evolved", "title": "ARK: Survival Evolved",
"image": "game11", "image": "game11",
"price": 790, "price": 790,
"os": "windows",
"imgPath": "img_top_11", "imgPath": "img_top_11",
"description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.", "description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.",
"category": "Simulators" "category": "Simulators"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 12, "id": 12,
"title": "FIFA 23", "title": "FIFA 23",
"image": "game12", "image": "game12",
"price": 3900, "price": 3900,
"os": "windows",
"imgPath": "img_top_12", "imgPath": "img_top_12",
"description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.", "description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.",
"category": "Sports" "category": "Sports"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 13, "id": 13,
"title": "Dirt 5", "title": "Dirt 5",
"image": "game13", "image": "game13",
"price": 2300, "price": 2300,
"os": "windows",
"imgPath": "img_top_13", "imgPath": "img_top_13",
"description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.", "description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.",
"category": "Race" "category": "Race"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 14, "id": 14,
"title": "Cyberpunk 2077", "title": "Cyberpunk 2077",
"image": "game14", "image": "game14",
"price": 3400, "price": 3400,
"os": "windows",
"imgPath": "img_top_14", "imgPath": "img_top_14",
"description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.", "description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.",
"category": "RPG" "category": "RPG"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 15, "id": 15,
"title": "Age of Empires IV", "title": "Age of Empires IV",
"image": "game15", "image": "game15",
"price": 3200, "price": 3200,
"os": "windows",
"imgPath": "img_top_15", "imgPath": "img_top_15",
"description": "Классическая стратегия в реальном времени с историческими кампаниями.", "description": "Классическая стратегия в реальном времени с историческими кампаниями.",
"category": "Strategies" "category": "Strategies"
,"fav1": "star1",
"fav2": "star2"
}, },
{ {
"id": 16, "id": 16,
"title": "Civilization VI", "title": "Civilization VI",
"image": "game16", "image": "game16",
"price": 4200, "price": 4200,
"os": "windows",
"imgPath": "img_top_16", "imgPath": "img_top_16",
"description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.", "description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.",
"category": "Strategies" "category": "Strategies"

View File

@@ -105,23 +105,27 @@
{ {
"image": "news1", "image": "news1",
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare", "text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
"imgPath": "img_news_1" "imgPath": "img_news_1",
"link": "https://gamemag.ru/news/185583/delta-force-hawk-ops-gameplay-showcase-havoc-warfare"
}, },
{ {
"image": "news2", "image": "news2",
"text": "Первый трейлер Assassins Creed Shadows — с темнокожим самураем в феодальной Японии", "text": "Первый трейлер Assassins Creed Shadows — с темнокожим самураем в феодальной Японии",
"imgPath": "img_news_2" "imgPath": "img_news_2",
"link": "https://stopgame.ru/newsdata/62686/pervyy_trailer_assassin_s_creed_shadows_s_temnokozhim_samuraem_v_feodalnoy_yaponii"
}, },
{ {
"image": "news3", "image": "news3",
"text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony", "text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony",
"imgPath": "img_news_3" "imgPath": "img_news_3",
"link": "https://stopgame.ru/newsdata/62706/prizrak_cusimy_vyshel_na_pk_i_uzhe_stavit_rekordy_dlya_sony"
}, },
{ {
"image": "news4", "image": "news4",
"text": "Авторы Skull and Bones расширяют планы на второй сезо", "text": "Авторы Skull and Bones расширяют планы на второй сезон",
"imgPath": "img_news_4" "imgPath": "img_news_4",
"link": "https://stopgame.ru/newsdata/62711/avtory_skull_and_bones_rasshiryayut_plany_na_vtoroy_sezon"
} }
] ]
} }
} }