Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
.playwright-cli/
.cache/
backend/node_modules/
backend/.cache/
backend/.env
backend/package-lock.json
client/node_modules/
client/dist/
client/.env
client/package-lock.json
8 changes: 8 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100
}
8 changes: 8 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PORT=3001
CLIENT_ORIGIN=http://localhost:5173
CLIENT_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173,http://localhost:4174,http://127.0.0.1:4174
Comment on lines +2 to +3
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А может просто ['*']?

YANDEX_RASP_API_KEY=
YANDEX_RASP_API_BASE=https://api.rasp.yandex-net.ru/v3.0
STATION_CACHE_MAX_AGE_HOURS=24
STATION_SEARCH_LIMIT=12
NEAREST_STATION_LIMIT=8
26 changes: 26 additions & 0 deletions backend/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import "dotenv/config";

export const PORT = Number(process.env.PORT || 3001);
export const CLIENT_ORIGIN = process.env.CLIENT_ORIGIN || "http://localhost:5173";
export const CLIENT_ORIGINS = String(process.env.CLIENT_ORIGINS || CLIENT_ORIGIN)
.split(",")
.map((origin) => origin.trim())
.filter(Boolean);
export const YANDEX_RASP_API_KEY = process.env.YANDEX_RASP_API_KEY || "";
export const YANDEX_RASP_API_BASE =
process.env.YANDEX_RASP_API_BASE || "https://api.rasp.yandex-net.ru/v3.0";
export const STATION_CACHE_MAX_AGE_HOURS = Number(process.env.STATION_CACHE_MAX_AGE_HOURS || 24);
export const STATION_SEARCH_LIMIT = Number(process.env.STATION_SEARCH_LIMIT || 12);
export const NEAREST_STATION_LIMIT = Number(process.env.NEAREST_STATION_LIMIT || 8);

export const RAIL_STATION_TYPES = [
"station",
"platform",
"stop",
"checkpoint",
"post",
"crossing",
"overtaking_point",
"train_station",
"unknown",
];
14 changes: 14 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "suburban-backend",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2"
}
}
128 changes: 128 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import cors from "cors";
import express from "express";
import { CLIENT_ORIGINS, NEAREST_STATION_LIMIT, PORT, STATION_SEARCH_LIMIT } from "./config.js";
import { searchNearbyStationsFromIndex, searchStationsByName } from "./services/stationIndex.js";
import { getStationSchedule, searchRoutesBetweenStations } from "./services/yandexRasp.js";

const app = express();

function isAllowedOrigin(origin) {
if (!origin) {
return true;
}

if (CLIENT_ORIGINS.includes(origin)) {
return true;
}

return /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
}

app.use(
cors({
origin(origin, callback) {
if (isAllowedOrigin(origin)) {
callback(null, true);
return;
}

callback(new Error(`CORS blocked for origin: ${origin}`));
},
}),
);
app.use(express.json());

function sendError(response, error, fallbackMessage) {
console.error("[api]", error);
response.status(error.statusCode || 500).json({
error: error.message || fallbackMessage,
});
}

app.get("/api/ping", (_request, response) => {
response.json({
ok: true,
now: new Date().toISOString(),
});
});

app.get("/api/stations/suggest", async (request, response) => {
try {
const searchText = String(request.query.searchText || "");
const limit = Number(request.query.limit || STATION_SEARCH_LIMIT);
const stations = await searchStationsByName(searchText, limit);

response.json({
stations,
});
} catch (error) {
sendError(response, error, "Не удалось выполнить поиск станций.");
}
});

app.get("/api/stations/nearby", async (request, response) => {
try {
const latitude = Number(request.query.lat);
const longitude = Number(request.query.lng);
const distance = Number(request.query.distance || 15);
const limit = Number(request.query.limit || NEAREST_STATION_LIMIT);

if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
response.status(400).json({
error: "Для поиска по карте нужны координаты lat и lng.",
});
return;
}

const stations = await searchNearbyStationsFromIndex(latitude, longitude, distance, limit);

response.json({
stations,
});
} catch (error) {
sendError(response, error, "Не удалось получить станции рядом с выбранной точкой.");
}
});

app.get("/api/stations/:stationCode/schedule", async (request, response) => {
try {
const stationCode = String(request.params.stationCode || "");
const date = String(request.query.date || "");

if (!stationCode) {
response.status(400).json({
error: "Не передан код станции.",
});
return;
}

const schedule = await getStationSchedule(stationCode, date);
response.json(schedule);
} catch (error) {
sendError(response, error, "Не удалось загрузить расписание по станции.");
}
});

app.get("/api/routes/search", async (request, response) => {
try {
const fromStationCode = String(request.query.from || "");
const toStationCode = String(request.query.to || "");
const date = String(request.query.date || "");

if (!fromStationCode || !toStationCode) {
response.status(400).json({
error: "Для поиска маршрута нужно выбрать станции отправления и прибытия.",
});
return;
}

const routes = await searchRoutesBetweenStations(fromStationCode, toStationCode, date);
response.json(routes);
} catch (error) {
sendError(response, error, "Не удалось загрузить маршруты между станциями.");
}
});

app.listen(PORT, () => {
console.log(`Backend started on http://localhost:${PORT}`);
});
Loading