feat: add registration and authentication

This commit is contained in:
Vaka.pro
2026-05-21 00:01:35 +03:00
parent 13dd8fa426
commit 35c3554742
37 changed files with 2162 additions and 81 deletions

View File

@@ -2,8 +2,10 @@ import { Router, Request, Response } from "express";
import { pool } from "../db";
import { rowToDto, bodyToColumns, RaceRow } from "../mappers/race";
import { extractRaceCoverImage } from "../raceCoverImage";
import { requireAuth } from "../authMiddleware";
const router = Router();
router.use(requireAuth);
type ValidationErrorBody = {
error: "validation_error";
@@ -69,6 +71,9 @@ router.get("/races", async (req: Request, res: Response) => {
const params: unknown[] = [];
let idx = 1;
conditions.push(`owner_user_id = $${idx++}`);
params.push(req.auth!.user.id);
if (yearResult.value != null) {
conditions.push(`EXTRACT(YEAR FROM race_date) = $${idx++}`);
params.push(yearResult.value);
@@ -94,8 +99,8 @@ router.get("/races", async (req: Request, res: Response) => {
router.get("/races/:id", async (req: Request, res: Response) => {
try {
const { rows } = await pool.query<RaceRow>(
"SELECT * FROM races WHERE id = $1",
[req.params.id],
"SELECT * FROM races WHERE id = $1 AND owner_user_id = $2",
[req.params.id, req.auth!.user.id],
);
if (rows.length === 0) {
res.status(404).json({ error: "not_found", details: ["Race not found"] });
@@ -113,12 +118,14 @@ router.get("/races/:id", async (req: Request, res: Response) => {
router.post("/races", async (req: Request, res: Response) => {
const body = req.body;
if (!body.id || !body.date || !body.title || body.distanceKm == null) {
validationError(res, ["Fields id, date, title, distanceKm are required"]);
const slug = typeof body.slug === "string" && body.slug.trim() ? body.slug.trim() : body.id;
if (!slug || !body.date || !body.title || body.distanceKm == null) {
validationError(res, ["Fields slug, date, title, distanceKm are required"]);
return;
}
const payload = { ...body };
const payload = { ...body, slug };
const hasManualCover = typeof payload.coverImageUrl === "string" && payload.coverImageUrl.trim() !== "";
const hasOfficialUrl = typeof payload.officialUrl === "string" && payload.officialUrl.trim() !== "";
@@ -127,8 +134,12 @@ router.post("/races", async (req: Request, res: Response) => {
}
const { columns, values } = bodyToColumns(payload);
columns.unshift("id");
values.unshift(body.id);
columns.unshift("owner_user_id");
values.unshift(req.auth!.user.id);
if (!columns.includes("slug")) {
columns.push("slug");
values.push(slug);
}
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
const sql = `INSERT INTO races (${columns.join(", ")}) VALUES (${placeholders}) RETURNING *`;
@@ -140,7 +151,7 @@ router.post("/races", async (req: Request, res: Response) => {
if (err.code === "23505") {
res.status(409).json({
error: "conflict",
details: ["Race with this id already exists"],
details: ["Race with this slug already exists"],
});
return;
}
@@ -162,7 +173,8 @@ router.patch("/races/:id", async (req: Request, res: Response) => {
const sets = columns.map((col, i) => `${col} = $${i + 1}`);
sets.push(`updated_at = NOW()`);
values.push(req.params.id);
const sql = `UPDATE races SET ${sets.join(", ")} WHERE id = $${values.length} RETURNING *`;
values.push(req.auth!.user.id);
const sql = `UPDATE races SET ${sets.join(", ")} WHERE id = $${values.length - 1} AND owner_user_id = $${values.length} RETURNING *`;
try {
const { rows } = await pool.query<RaceRow>(sql, values);
@@ -182,8 +194,8 @@ router.patch("/races/:id", async (req: Request, res: Response) => {
router.delete("/races/:id", async (req: Request, res: Response) => {
try {
const { rowCount } = await pool.query(
"DELETE FROM races WHERE id = $1",
[req.params.id],
"DELETE FROM races WHERE id = $1 AND owner_user_id = $2",
[req.params.id, req.auth!.user.id],
);
if (rowCount === 0) {
res.status(404).json({ error: "not_found", details: ["Race not found"] });