style: run prettier
All checks were successful
TrafficCue Server CI / check (push) Successful in 54s
TrafficCue Server CD / build (push) Successful in 2m0s

This commit is contained in:
2025-09-27 19:47:41 +02:00
parent 4d8c3aef68
commit 30334d13d1
4 changed files with 79 additions and 62 deletions

View File

@ -65,6 +65,6 @@ You can run this yourself to host your own instance, or contribute to the offici
- `OIDC_JWKS_URL` (the JWKS/Certificate URL of your OIDC server) - `OIDC_JWKS_URL` (the JWKS/Certificate URL of your OIDC server)
- `REVIEWS_ENABLED` (optional, set to `true` to enable POI reviews by users, requires OIDC) - `REVIEWS_ENABLED` (optional, set to `true` to enable POI reviews by users, requires OIDC)
- `HAZARDS_ENABLED` (optional, set to `true` to enable hazard reporting by users, requires OIDC) - `HAZARDS_ENABLED` (optional, set to `true` to enable hazard reporting by users, requires OIDC)
- `STORES_ENALED` (optional, set to `true` to enable user stores, requires OIDC) - `STORES_ENALED` (optional, set to `true` to enable user stores, requires OIDC)
When configuring your OIDC server, make sure to enable Public Client and PCKE support. When configuring your OIDC server, make sure to enable Public Client and PCKE support.

View File

@ -23,7 +23,7 @@ export class Store extends BaseEntity {
@Column({ @Column({
type: "enum", type: "enum",
enum: STORE_TYPE enum: STORE_TYPE,
}) })
type: StoreType; type: StoreType;

View File

@ -396,11 +396,9 @@ if (process.env.STORES_ENABLED) {
} }
const user = await User.findOneBy( const user = await User.findOneBy(
c.req.param("user") c.req.param("user") ? { username: c.req.param("user") } : { id: uid },
? { username: c.req.param("user") }
: { id: uid }
); );
if(!user) { if (!user) {
return c.json({ error: "Invalid user ID" }, 400); return c.json({ error: "Invalid user ID" }, 400);
} }

View File

@ -2,41 +2,49 @@ import z from "zod";
import { Store, type StoreType } from "./entities/Stores"; import { Store, type StoreType } from "./entities/Stores";
import type { User } from "./entities/User"; import type { User } from "./entities/User";
export const locationStore = z.object({ export const locationStore = z
lat: z.number().min(-90).max(90), .object({
lng: z.number().min(-180).max(180), lat: z.number().min(-90).max(90),
name: z.string().min(1).max(100) lng: z.number().min(-180).max(180),
}).strict(); name: z.string().min(1).max(100),
})
.strict();
export const vehicleStore = z.object({ export const vehicleStore = z
name: z.string().min(1).max(100), .object({
legalMaxSpeed: z.number().min(0).max(300), name: z.string().min(1).max(100),
actualMaxSpeed: z.number().min(0).max(300), legalMaxSpeed: z.number().min(0).max(300),
type: z.enum([ actualMaxSpeed: z.number().min(0).max(300),
"car", "truck", "motorcycle", "bicycle", "motor_scooter" type: z.enum(["car", "truck", "motorcycle", "bicycle", "motor_scooter"]),
]), weight: z.number().min(0).max(100000).optional(),
weight: z.number().min(0).max(100000).optional(), width: z.number().min(0).max(10).optional(),
width: z.number().min(0).max(10).optional(), axisLoad: z.number().min(0).max(100000).optional(),
axisLoad: z.number().min(0).max(100000).optional(), height: z.number().min(0).max(20).optional(),
height: z.number().min(0).max(20).optional(), length: z.number().min(0).max(100).optional(),
length: z.number().min(0).max(100).optional(), emissionClass: z.string().min(1).max(50),
emissionClass: z.string().min(1).max(50), fuelType: z.enum(["petrol", "diesel", "electric"]),
fuelType: z.enum(["petrol", "diesel", "electric"]), preferredFuel: z.string().min(1).max(50),
preferredFuel: z.string().min(1).max(50) })
}).strict(); .strict();
export const routeStore = z.object({ export const routeStore = z.object({
locations: z.array(z.object({ locations: z.array(
lat: z.number().min(-90).max(90), z.object({
lon: z.number().min(-180).max(180) lat: z.number().min(-90).max(90),
})), lon: z.number().min(-180).max(180),
legs: z.array(z.object({ }),
shape: z.string(), ),
maneuvers: z.array(z.object({ legs: z.array(
type: z.number() z.object({
})) shape: z.string(),
})) maneuvers: z.array(
}) z.object({
type: z.number(),
}),
),
}),
),
});
export const storeTypes: Record<StoreType, z.ZodSchema> = { export const storeTypes: Record<StoreType, z.ZodSchema> = {
location: locationStore, location: locationStore,
@ -45,25 +53,33 @@ export const storeTypes: Record<StoreType, z.ZodSchema> = {
}; };
export const SyncPayload = z.object({ export const SyncPayload = z.object({
changes: z.array(z.object({ changes: z.array(
id: z.uuid(), z.object({
operation: z.enum(["create", "update", "delete"]), id: z.uuid(),
data: z.string(), operation: z.enum(["create", "update", "delete"]),
modified_at: z.string().refine(val => !isNaN(Date.parse(val)), { message: "Invalid date" }), data: z.string(),
type: z.enum(["location", "vehicle", "route"]), modified_at: z
name: z.string().min(1).max(100) .string()
})), .refine((val) => !isNaN(Date.parse(val)), { message: "Invalid date" }),
stores: z.array(z.object({ type: z.enum(["location", "vehicle", "route"]),
id: z.uuid(), name: z.string().min(1).max(100),
modified_at: z.string().refine(val => !isNaN(Date.parse(val)), { message: "Invalid date" }) }),
})) ),
}) stores: z.array(
z.object({
id: z.uuid(),
modified_at: z
.string()
.refine((val) => !isNaN(Date.parse(val)), { message: "Invalid date" }),
}),
),
});
export type SyncPayload = z.infer<typeof SyncPayload>; export type SyncPayload = z.infer<typeof SyncPayload>;
export function verifyStoreData(type: StoreType, data: string) { export function verifyStoreData(type: StoreType, data: string) {
const schema = storeTypes[type]; const schema = storeTypes[type];
if(!schema) return false; if (!schema) return false;
try { try {
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
schema.parse(parsedData); schema.parse(parsedData);
@ -76,14 +92,17 @@ export function verifyStoreData(type: StoreType, data: string) {
export async function sync(payload: SyncPayload, user: User) { export async function sync(payload: SyncPayload, user: User) {
const changes: Store[] = []; const changes: Store[] = [];
// Apply changes from client // Apply changes from client
for(const change of payload.changes) { for (const change of payload.changes) {
const store = await Store.findOne({ where: { id: change.id }, relations: { user: true } }); const store = await Store.findOne({
if(!verifyStoreData(change.type, change.data)) { where: { id: change.id },
relations: { user: true },
});
if (!verifyStoreData(change.type, change.data)) {
// Invalid data // Invalid data
if(store) changes.push(store); // Send back the store to the client to overwrite their changes if (store) changes.push(store); // Send back the store to the client to overwrite their changes
continue; continue;
} }
if(!store) { if (!store) {
// Store doesn't exist, create it // Store doesn't exist, create it
const newStore = new Store(); const newStore = new Store();
newStore.id = change.id; newStore.id = change.id;
@ -96,7 +115,7 @@ export async function sync(payload: SyncPayload, user: User) {
await newStore.save(); await newStore.save();
continue; continue;
} }
if(store.user.id !== user.id) { if (store.user.id !== user.id) {
// Not the owner of this store // Not the owner of this store
changes.push(store); // Send back the store to the client to overwrite their changes changes.push(store); // Send back the store to the client to overwrite their changes
continue; continue;
@ -108,12 +127,12 @@ export async function sync(payload: SyncPayload, user: User) {
// Find stores that are out of date on the client // Find stores that are out of date on the client
const allStores = await Store.findBy({ user: { id: user.id } }); // TODO: include friends' public stores, TODO: use SQL query to only get modified stores const allStores = await Store.findBy({ user: { id: user.id } }); // TODO: include friends' public stores, TODO: use SQL query to only get modified stores
for(const store of allStores) { for (const store of allStores) {
const clientStore = payload.stores.find(s => s.id === store.id); const clientStore = payload.stores.find((s) => s.id === store.id);
if(!clientStore || new Date(clientStore.modified_at) < store.modified_at) { if (!clientStore || new Date(clientStore.modified_at) < store.modified_at) {
changes.push(store); // Client doesn't have this store or it's out of date changes.push(store); // Client doesn't have this store or it's out of date
} }
} }
return { changes }; return { changes };
} }