feat: add hazards
Some checks failed
TrafficCue Server CI / check (push) Failing after 27s
TrafficCue Server CD / build (push) Successful in 1m48s

This commit is contained in:
2025-09-18 11:48:37 +02:00
parent 7b8210ab7e
commit fb8e3ae8d5
4 changed files with 119 additions and 0 deletions

32
src/entities/Hazard.ts Normal file
View File

@ -0,0 +1,32 @@
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import type { User } from "./User";
@Entity()
export class Hazard extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({
type: "float"
})
latitude: number;
@Column({
type: "float"
})
longitude: number;
@Column()
type: string;
@Column()
votes: number;
@Column({
default: () => "NOW()",
})
created_at: Date;
@ManyToOne("User", (u: User) => u.hazards)
user: User;
}

View File

@ -7,6 +7,7 @@ import {
} from "typeorm";
import { type Review } from "./Review";
import { type Saved } from "./Saved";
import type { Hazard } from "./Hazard";
@Entity()
export class User extends BaseEntity {
@ -22,6 +23,9 @@ export class User extends BaseEntity {
@OneToMany("Saved", (s: Saved) => s.user)
saved: Saved[];
@OneToMany("Hazards", (h: Hazard) => h.user)
hazards: Hazard[];
@Column({
default: () => "NOW()",
})

View File

@ -10,6 +10,7 @@ import { Review } from "./entities/Review";
import { User } from "./entities/User";
import { Saved } from "./entities/Saved";
import { ILike } from "typeorm";
import { Hazard } from "./entities/Hazard";
const app = new Hono();
const { upgradeWebSocket, websocket } = createBunWebSocket<ServerWebSocket>();
@ -165,6 +166,87 @@ if (process.env.REVIEWS_ENABLED) {
});
}
const VALID_HAZARD_TYPES = [
"bumpy-road"
];
if (process.env.HAZARDS_ENABLED) {
app.get("/api/hazards", async (c) => {
const { lat, lon, radius } = c.req.query();
if (!lat || !lon || !radius) {
return c.json({ error: "Latitude, longitude, and radius are required" }, 400);
}
// Remove unnecessary precision from lat/lon
const nlat = Number(parseFloat(lat).toFixed(4));
const nlon = Number(parseFloat(lon).toFixed(4));
const nradius = Number(radius);
if(isNaN(nradius) || nradius <= 0) {
return c.json({ error: "Invalid radius" }, 400);
}
if(nradius > 1000) {
return c.json({ error: "Radius too large, max is 1000 km" }, 400);
}
console.log(`Fetching hazards for lat: ${lat}, lon: ${lon}, radius: ${radius} km`);
const hazards = await Hazard.createQueryBuilder("hazard") // Crazy math to get a rough bounding box for the radius in km
.where("hazard.latitude BETWEEN :minLat AND :maxLat", { minLat: nlat - nradius / 110.574, maxLat: nlat + nradius / 110.574 })
.andWhere("hazard.longitude BETWEEN :minLon AND :maxLon", { minLon: nlon - nradius / (111.320 * Math.cos(nlat * (Math.PI / 180))), maxLon: nlon + nradius / (111.320 * Math.cos(nlat * (Math.PI / 180))) })
.getMany();
return c.json(hazards);
});
app.post("/api/hazards", async (c) => {
const { type, lat, lon } = await c.req.json();
if (!type || !lat || !lon) {
return c.json(
{ error: "Type, latitude, and longitude are required" },
400,
);
}
if(!VALID_HAZARD_TYPES.includes(type)) {
return c.json(
{ error: "Invalid hazard type" },
400,
);
}
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json({ error: "Unauthorized" }, 401);
}
const token = authHeader.split(" ")[1];
if (!token) {
return c.json({ error: "Unauthorized" }, 401);
}
const isValid = await verifyToken(token);
if (!isValid) {
return c.json({ error: "Unauthorized" }, 401);
}
const uid = await getTokenUID(token);
if (!uid) {
return c.json({ error: "Unauthorized" }, 401);
}
const user = await User.findOneBy({ id: uid });
if (!user) {
return c.json({ error: "Invalid user ID" }, 400);
}
// Remove unnecessary precision from lat/lon
const nlat = Number(parseFloat(lat).toFixed(4));
const nlon = Number(parseFloat(lon).toFixed(4));
const hazard = new Hazard();
hazard.latitude = nlat;
hazard.longitude = nlon;
hazard.type = type;
hazard.user = user;
await hazard.save();
return c.json({ success: true });
});
}
app.get("/api/saved", async (c) => {
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {