diff --git a/README.md b/README.md index 12d5046..ee5e140 100644 --- a/README.md +++ b/README.md @@ -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) - `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) - - `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. diff --git a/src/entities/Stores.ts b/src/entities/Stores.ts index 30f7fec..1c557e8 100644 --- a/src/entities/Stores.ts +++ b/src/entities/Stores.ts @@ -23,7 +23,7 @@ export class Store extends BaseEntity { @Column({ type: "enum", - enum: STORE_TYPE + enum: STORE_TYPE, }) type: StoreType; diff --git a/src/main.ts b/src/main.ts index f091c7f..18fd57b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -396,11 +396,9 @@ if (process.env.STORES_ENABLED) { } const user = await User.findOneBy( - c.req.param("user") - ? { username: c.req.param("user") } - : { id: uid } + c.req.param("user") ? { username: c.req.param("user") } : { id: uid }, ); - if(!user) { + if (!user) { return c.json({ error: "Invalid user ID" }, 400); } diff --git a/src/stores.ts b/src/stores.ts index 358ada2..513f98e 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -2,41 +2,49 @@ import z from "zod"; import { Store, type StoreType } from "./entities/Stores"; import type { User } from "./entities/User"; -export const locationStore = z.object({ - lat: z.number().min(-90).max(90), - lng: z.number().min(-180).max(180), - name: z.string().min(1).max(100) -}).strict(); +export const locationStore = z + .object({ + lat: z.number().min(-90).max(90), + lng: z.number().min(-180).max(180), + name: z.string().min(1).max(100), + }) + .strict(); -export const vehicleStore = z.object({ - name: z.string().min(1).max(100), - legalMaxSpeed: z.number().min(0).max(300), - actualMaxSpeed: z.number().min(0).max(300), - type: z.enum([ - "car", "truck", "motorcycle", "bicycle", "motor_scooter" - ]), - weight: z.number().min(0).max(100000).optional(), - width: z.number().min(0).max(10).optional(), - axisLoad: z.number().min(0).max(100000).optional(), - height: z.number().min(0).max(20).optional(), - length: z.number().min(0).max(100).optional(), - emissionClass: z.string().min(1).max(50), - fuelType: z.enum(["petrol", "diesel", "electric"]), - preferredFuel: z.string().min(1).max(50) -}).strict(); +export const vehicleStore = z + .object({ + name: z.string().min(1).max(100), + legalMaxSpeed: z.number().min(0).max(300), + actualMaxSpeed: z.number().min(0).max(300), + type: z.enum(["car", "truck", "motorcycle", "bicycle", "motor_scooter"]), + weight: z.number().min(0).max(100000).optional(), + width: z.number().min(0).max(10).optional(), + axisLoad: z.number().min(0).max(100000).optional(), + height: z.number().min(0).max(20).optional(), + length: z.number().min(0).max(100).optional(), + emissionClass: z.string().min(1).max(50), + fuelType: z.enum(["petrol", "diesel", "electric"]), + preferredFuel: z.string().min(1).max(50), + }) + .strict(); export const routeStore = z.object({ - locations: z.array(z.object({ - 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({ - type: z.number() - })) - })) -}) + locations: z.array( + z.object({ + 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({ + type: z.number(), + }), + ), + }), + ), +}); export const storeTypes: Record = { location: locationStore, @@ -45,25 +53,33 @@ export const storeTypes: Record = { }; export const SyncPayload = z.object({ - changes: z.array(z.object({ - id: z.uuid(), - operation: z.enum(["create", "update", "delete"]), - data: z.string(), - modified_at: z.string().refine(val => !isNaN(Date.parse(val)), { message: "Invalid date" }), - type: z.enum(["location", "vehicle", "route"]), - name: z.string().min(1).max(100) - })), - stores: z.array(z.object({ - id: z.uuid(), - modified_at: z.string().refine(val => !isNaN(Date.parse(val)), { message: "Invalid date" }) - })) -}) + changes: z.array( + z.object({ + id: z.uuid(), + operation: z.enum(["create", "update", "delete"]), + data: z.string(), + modified_at: z + .string() + .refine((val) => !isNaN(Date.parse(val)), { message: "Invalid date" }), + type: z.enum(["location", "vehicle", "route"]), + name: z.string().min(1).max(100), + }), + ), + 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; export function verifyStoreData(type: StoreType, data: string) { const schema = storeTypes[type]; - if(!schema) return false; + if (!schema) return false; try { const parsedData = JSON.parse(data); schema.parse(parsedData); @@ -76,14 +92,17 @@ export function verifyStoreData(type: StoreType, data: string) { export async function sync(payload: SyncPayload, user: User) { const changes: Store[] = []; // Apply changes from client - for(const change of payload.changes) { - const store = await Store.findOne({ where: { id: change.id }, relations: { user: true } }); - if(!verifyStoreData(change.type, change.data)) { + for (const change of payload.changes) { + const store = await Store.findOne({ + where: { id: change.id }, + relations: { user: true }, + }); + if (!verifyStoreData(change.type, change.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; } - if(!store) { + if (!store) { // Store doesn't exist, create it const newStore = new Store(); newStore.id = change.id; @@ -96,7 +115,7 @@ export async function sync(payload: SyncPayload, user: User) { await newStore.save(); continue; } - if(store.user.id !== user.id) { + if (store.user.id !== user.id) { // Not the owner of this store changes.push(store); // Send back the store to the client to overwrite their changes continue; @@ -108,12 +127,12 @@ export async function sync(payload: SyncPayload, user: User) { // 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 - for(const store of allStores) { - const clientStore = payload.stores.find(s => s.id === store.id); - if(!clientStore || new Date(clientStore.modified_at) < store.modified_at) { + for (const store of allStores) { + const clientStore = payload.stores.find((s) => s.id === store.id); + 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 } } - + return { changes }; }