diff --git a/app/routes/api.boxes.$deviceId.$sensorId.ts b/app/routes/api.boxes.$deviceId.$sensorId.ts index 88da5c54..5d1dc0ef 100644 --- a/app/routes/api.boxes.$deviceId.$sensorId.ts +++ b/app/routes/api.boxes.$deviceId.$sensorId.ts @@ -1,128 +1,53 @@ -import { type ActionFunction, type ActionFunctionArgs } from "react-router"; -import { postSingleMeasurement } from "~/lib/measurement-service.server"; +import { type ActionFunction, type ActionFunctionArgs } from 'react-router' +import { postSingleMeasurement } from '~/lib/measurement-service.server' +import { StandardResponse } from '~/utils/response-utils' export const action: ActionFunction = async ({ - request, - params, + request, + params, }: ActionFunctionArgs): Promise => { - try { - const { deviceId, sensorId } = params; - - if (!deviceId || !sensorId) { - return Response.json( - { - code: "Bad Request", - message: "Invalid device id or sensor id specified", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } - - const authorization = request.headers.get("authorization"); - const contentType = request.headers.get("content-type") || ""; - - if (!contentType.includes("application/json")) { - return Response.json( - { - code: "Unsupported Media Type", - message: "Content-Type must be application/json", - }, - { - status: 415, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } - - const body = await request.json(); - - await postSingleMeasurement(deviceId, sensorId, body, authorization); - - return new Response("Measurement saved in box", { - status: 201, - headers: { - "Content-Type": "text/plain; charset=utf-8", - }, - }); - } catch (err: any) { - if (err.name === "UnauthorizedError") { - return Response.json( - { - code: "Unauthorized", - message: err.message, - }, - { - status: 401, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } - - if (err.name === "NotFoundError") { - return Response.json( - { - code: "Not Found", - message: err.message, - }, - { - status: 404, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } - - if (err.name === "UnprocessableEntityError" || err.type === "UnprocessableEntityError") { - return Response.json( - { - code: "Unprocessable Entity", - message: err.message, - }, - { - status: 422, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } - - if (err.name === "ModelError" && err.type === "UnprocessableEntityError") { - return Response.json( - { - code: "Unprocessable Entity", - message: err.message, - }, - { - status: 422, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } - - return Response.json( - { - code: "Internal Server Error", - message: err.message || "An unexpected error occurred", - }, - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } -}; \ No newline at end of file + try { + const { deviceId, sensorId } = params + + if (!deviceId || !sensorId) + return StandardResponse.badRequest( + 'Invalid device id or sensor id specified', + ) + + const authorization = request.headers.get('authorization') + const contentType = request.headers.get('content-type') || '' + + if (!contentType.includes('application/json')) + return StandardResponse.unsupportedMediaType( + 'Content-Type must be application/json', + ) + + const body = await request.json() + + await postSingleMeasurement(deviceId, sensorId, body, authorization) + + return new Response('Measurement saved in box', { + status: 201, + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + }, + }) + } catch (err: any) { + if (err.name === 'UnauthorizedError') + return StandardResponse.unauthorized(err.message) + + if (err.name === 'NotFoundError') + return StandardResponse.notFound(err.message) + + if ( + err.name === 'UnprocessableEntityError' || + err.type === 'UnprocessableEntityError' || + (err.name === 'ModelError' && err.type === 'UnprocessableEntityError') + ) + return StandardResponse.unprocessableContent(err.message) + + return StandardResponse.internalServerError( + err.message || 'An unexpected error occurred', + ) + } +} diff --git a/app/routes/api.boxes.$deviceId.data.$sensorId.ts b/app/routes/api.boxes.$deviceId.data.$sensorId.ts index 3fd1b316..8a4bbd93 100644 --- a/app/routes/api.boxes.$deviceId.data.$sensorId.ts +++ b/app/routes/api.boxes.$deviceId.data.$sensorId.ts @@ -4,7 +4,7 @@ import { getMeasurements } from "~/models/sensor.server"; import { type Measurement } from "~/schema"; import { convertToCsv } from "~/utils/csv"; import { parseDateParam, parseEnumParam } from "~/utils/param-utils"; -import { badRequest, internalServerError, notFound } from "~/utils/response-utils"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -152,7 +152,7 @@ export const loader: LoaderFunction = async ({ let meas: Measurement[] | TransformedMeasurement[] = await getMeasurements(sensorId, fromDate.toISOString(), toDate.toISOString()); if (meas == null) - return notFound("Device not found."); + return StandardResponse.notFound("Device not found."); if (outliers) meas = transformOutliers(meas, outlierWindow, outliers == "replace"); @@ -177,7 +177,7 @@ export const loader: LoaderFunction = async ({ } catch (err) { console.warn(err); - return internalServerError(); + return StandardResponse.internalServerError(); } }; @@ -196,10 +196,10 @@ function collectParameters(request: Request, params: Params): // deviceId is there for legacy reasons const deviceId = params.deviceId; if (deviceId === undefined) - return badRequest("Invalid device id specified"); + return StandardResponse.badRequest("Invalid device id specified"); const sensorId = params.sensorId; if (sensorId === undefined) - return badRequest("Invalid sensor id specified"); + return StandardResponse.badRequest("Invalid sensor id specified"); const url = new URL(request.url); @@ -211,7 +211,7 @@ function collectParameters(request: Request, params: Params): let outlierWindow: number = 15; if (outlierWindowParam !== null) { if (Number.isNaN(outlierWindowParam) || Number(outlierWindowParam) < 1 || Number(outlierWindowParam) > 50) - return badRequest("Illegal value for parameter outlier-window. Allowed values: numbers between 1 and 50"); + return StandardResponse.badRequest("Illegal value for parameter outlier-window. Allowed values: numbers between 1 and 50"); outlierWindow = Number(outlierWindowParam); } diff --git a/app/routes/api.boxes.$deviceId.data.ts b/app/routes/api.boxes.$deviceId.data.ts index 6c29c686..05630ad8 100644 --- a/app/routes/api.boxes.$deviceId.data.ts +++ b/app/routes/api.boxes.$deviceId.data.ts @@ -1,5 +1,6 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { postNewMeasurements } from "~/lib/measurement-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const action: ActionFunction = async ({ request, @@ -7,20 +8,8 @@ export const action: ActionFunction = async ({ }: ActionFunctionArgs): Promise => { try { const deviceId = params.deviceId; - if (deviceId === undefined) { - return Response.json( - { - code: "Bad Request", - message: "Invalid device id specified", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } + if (deviceId === undefined) + return StandardResponse.badRequest("Invalid device id specified"); const searchParams = new URL(request.url).searchParams; const luftdaten = searchParams.get("luftdaten") !== null; @@ -55,57 +44,15 @@ export const action: ActionFunction = async ({ }); } catch (err: any) { // Handle different error types - if (err.name === "UnauthorizedError") { - return Response.json( - { - code: "Unauthorized", - message: err.message, - }, - { - status: 401, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } + if (err.name === "UnauthorizedError") + return StandardResponse.unauthorized(err.message); - if (err.name === "ModelError" && err.type === "UnprocessableEntityError") { - return Response.json( - { - code: "UnprocessableEntity", - message: err.message, - }, - { status: 422 } - ); - } + if (err.name === "ModelError" && err.type === "UnprocessableEntityError") + return StandardResponse.unprocessableContent(err.message); - if (err.name === "UnsupportedMediaTypeError") { - return Response.json( - { - code: "Unsupported Media Type", - message: err.message, - }, - { - status: 415, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); - } + if (err.name === "UnsupportedMediaTypeError") + return StandardResponse.unsupportedMediaType(err.message); - return Response.json( - { - code: "Internal Server Error", - message: err.message || "An unexpected error occurred", - }, - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.internalServerError(err.message || "An unexpected error occurred"); } }; \ No newline at end of file diff --git a/app/routes/api.boxes.$deviceId.locations.ts b/app/routes/api.boxes.$deviceId.locations.ts index 4a6f6c90..e03d5564 100644 --- a/app/routes/api.boxes.$deviceId.locations.ts +++ b/app/routes/api.boxes.$deviceId.locations.ts @@ -1,7 +1,7 @@ import { type Params, type LoaderFunction, type LoaderFunctionArgs } from "react-router"; import { getLocations } from "~/models/device.server"; import { parseDateParam, parseEnumParam } from "~/utils/param-utils"; -import { badRequest, internalServerError, notFound } from "~/utils/response-utils"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -104,7 +104,7 @@ export const loader: LoaderFunction = async ({ const locations = await getLocations({ id: deviceId}, fromDate, toDate); if (!locations) - return notFound("Device not found"); + return StandardResponse.notFound("Device not found"); const jsonLocations = locations.map((location) => { return { @@ -140,7 +140,7 @@ export const loader: LoaderFunction = async ({ } catch (err) { console.warn(err); - return internalServerError(); + return StandardResponse.internalServerError(); } }; @@ -153,7 +153,7 @@ function collectParameters(request: Request, params: Params): } { const deviceId = params.deviceId; if (deviceId === undefined) - return badRequest("Invalid device id specified"); + return StandardResponse.badRequest("Invalid device id specified"); const url = new URL(request.url); diff --git a/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts b/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts index 29573da7..89b38d09 100644 --- a/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts +++ b/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts @@ -1,5 +1,6 @@ import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; import { getLatestMeasurementsForSensor } from "~/lib/measurement-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const loader: LoaderFunction = async ({ request, @@ -8,88 +9,29 @@ export const loader: LoaderFunction = async ({ try { const deviceId = params.deviceId; if (deviceId === undefined) - return Response.json( - { - code: "Bad Request", - message: "Invalid device id specified", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("Invalid device id specified"); const sensorId = params.sensorId; if (sensorId === undefined) - return Response.json( - { - code: "Bad Request", - message: "Invalid sensor id specified", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("Invalid sensor id specified"); const searchParams = new URL(request.url).searchParams; const onlyValue = (searchParams.get("onlyValue")?.toLowerCase() ?? "") === "true"; if (sensorId === undefined && onlyValue) - return Response.json( - { - code: "Bad Request", - message: "onlyValue can only be used when a sensor id is specified", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("onlyValue can only be used when a sensor id is specified"); const meas = await getLatestMeasurementsForSensor(deviceId, sensorId, undefined); if (meas == null) - return new Response(JSON.stringify({ message: "Device not found." }), { - status: 404, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }); + return StandardResponse.notFound("Device not found."); if (onlyValue) - return Response.json(meas["lastMeasurement"]?.value ?? null, { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }); + return StandardResponse.ok(meas["lastMeasurement"]?.value ?? null); - return Response.json( - { ...meas, _id: meas.id }, // for legacy purposes - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.ok({ ...meas, _id: meas.id } /* for legacy purposes */); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.boxes.$deviceId.sensors.ts b/app/routes/api.boxes.$deviceId.sensors.ts index 8fd38c2a..6fd11237 100644 --- a/app/routes/api.boxes.$deviceId.sensors.ts +++ b/app/routes/api.boxes.$deviceId.sensors.ts @@ -1,5 +1,6 @@ import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; import { getLatestMeasurements } from "~/lib/measurement-service.server"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -36,58 +37,22 @@ export const loader: LoaderFunction = async ({ try { const deviceId = params.deviceId; if (deviceId === undefined) - return Response.json( - { - code: "Bad Request", - message: "Invalid device id specified", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("Invalid device id specified"); const url = new URL(request.url); const countParam = url.searchParams.get("count"); let count: undefined | number = undefined; if (countParam !== null && Number.isNaN(countParam)) - return Response.json( - { - error: "Bad Request", - message: "Illegal value for parameter count. allowed values: numbers", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("Illegal value for parameter count. allowed values: numbers"); + count = countParam === null ? undefined : Number(countParam); const meas = await getLatestMeasurements(deviceId, count); - return Response.json(meas, { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }); + return StandardResponse.ok(meas); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.boxes.ts b/app/routes/api.boxes.ts index 8aec5aaf..e867dbbc 100644 --- a/app/routes/api.boxes.ts +++ b/app/routes/api.boxes.ts @@ -4,6 +4,7 @@ import { CreateBoxSchema } from "~/lib/devices-service.server"; import { getUserFromJwt } from "~/lib/jwt"; import { createDevice } from "~/models/device.server"; import { type User } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -330,25 +331,18 @@ export const action: ActionFunction = async ({ // Check authentication const jwtResponse = await getUserFromJwt(request); - if (typeof jwtResponse === "string") { - return Response.json({ - code: "Forbidden", - message: "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, { status: 403 }); - } + if (typeof jwtResponse === "string") + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); switch (request.method) { case "POST": return await post(request, jwtResponse); default: - return Response.json({ message: "Method Not Allowed" }, { status: 405 }); + return StandardResponse.methodNotAllowed("Method Not Allowed"); } } catch (err) { console.error("Error in action:", err); - return Response.json({ - code: "Internal Server Error", - message: "The server was unable to complete your request. Please try again later.", - }, { status: 500 }); + return StandardResponse.internalServerError(); } }; @@ -359,10 +353,7 @@ async function post(request: Request, user: User) { try { requestData = await request.json(); } catch { - return Response.json({ - code: "Bad Request", - message: "Invalid JSON in request body", - }, { status: 400 }); + return StandardResponse.badRequest("Invalid JSON in request body"); } // Validate request data @@ -396,15 +387,9 @@ async function post(request: Request, user: User) { // Build response object using helper function const responseData = transformDeviceToApiFormat(newBox); - return Response.json(responseData, { - status: 201, - headers: { "Content-Type": "application/json" }, - }); + return StandardResponse.created(responseData); } catch (err) { console.error("Error creating box:", err); - return Response.json({ - code: "Internal Server Error", - message: "The server was unable to create the box. Please try again later.", - }, { status: 500 }); + return StandardResponse.internalServerError(); } } diff --git a/app/routes/api.claim.ts b/app/routes/api.claim.ts index 4810f75e..669f9293 100644 --- a/app/routes/api.claim.ts +++ b/app/routes/api.claim.ts @@ -1,58 +1,35 @@ import { type ActionFunctionArgs } from "react-router"; import { getUserFromJwt } from "~/lib/jwt"; import { claimBox } from "~/lib/transfer-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const action = async ({ request }: ActionFunctionArgs) => { const contentType = request.headers.get("content-type"); - if (!contentType || !contentType.includes("application/json")) { - return Response.json( - { - code: "UnsupportedMediaType", - message: "Unsupported content-type. Try application/json", - }, - { - status: 415, - headers: { - "content-type": "application/json; charset=utf-8", - }, - } - ); - } + if (!contentType || !contentType.includes("application/json")) + return StandardResponse.unsupportedMediaType("Unsupported content-type. Try application/json"); - if (request.method !== "POST") { - return new Response(null, { status: 405 }); - } + if (request.method !== "POST") + return StandardResponse.methodNotAllowed("Only POST allowed"); const jwtResponse = await getUserFromJwt(request); - if (typeof jwtResponse === "string") { - return Response.json( - { - code: "Forbidden", - message: "Invalid JWT. Please sign in", - }, - { status: 403 } - ); - } + if (typeof jwtResponse === "string") + return StandardResponse.forbidden("Invalid JWT. Please sign in"); try { const body = await request.json(); const { token } = body; - if (!token) { - return Response.json({ error: "token is required" }, { status: 400 }); - } + if (!token) + return StandardResponse.badRequest("token is required"); const result = await claimBox(jwtResponse.id, token); - return Response.json( - { + return StandardResponse.ok({ message: "Device successfully claimed!", data: result, - }, - { status: 200 } - ); + }); } catch (err) { console.error("Error claiming box:", err); return handleClaimError(err); @@ -66,25 +43,19 @@ const handleClaimError = (err: unknown) => { if ( message.includes("expired") || message.includes("Invalid or expired") - ) { - return Response.json({ error: message }, { status: 410 }); - } + ) + return StandardResponse.gone(message); - if (message.includes("not found")) { - return Response.json({ error: message }, { status: 404 }); - } + if (message.includes("not found")) + return StandardResponse.notFound(message); if ( message.includes("required") || message.includes("Invalid") || message.includes("already own") - ) { - return Response.json({ error: message }, { status: 400 }); - } + ) + return StandardResponse.badRequest(message); } - return Response.json( - { error: "Internal server error" }, - { status: 500 } - ); + return StandardResponse.internalServerError(); }; \ No newline at end of file diff --git a/app/routes/api.device.$deviceId.ts b/app/routes/api.device.$deviceId.ts index af9c896d..5d31aa3c 100644 --- a/app/routes/api.device.$deviceId.ts +++ b/app/routes/api.device.$deviceId.ts @@ -1,5 +1,6 @@ import { type LoaderFunctionArgs } from "react-router"; import { getDevice } from "~/models/device.server"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -64,47 +65,19 @@ import { getDevice } from "~/models/device.server"; export async function loader({ params }: LoaderFunctionArgs) { const { deviceId } = params; - if (!deviceId) { - return new Response(JSON.stringify({ message: "Device ID is required." }), { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }); - } + if (!deviceId) + return StandardResponse.badRequest("Device ID is required."); try { const device = await getDevice({ id: deviceId }); - if (!device) { - return new Response(JSON.stringify({ message: "Device not found." }), { - status: 404, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }); - } + if (!device) + return StandardResponse.notFound("Device not found."); - return new Response(JSON.stringify(device), { - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); + return StandardResponse.ok(device); } catch (error) { console.error("Error fetching box:", error); - if (error instanceof Response) { - throw error; - } - - return new Response( - JSON.stringify({ error: "Internal server error while fetching box" }), - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.internalServerError(); } } diff --git a/app/routes/api.devices.ts b/app/routes/api.devices.ts index de05cbc0..bba291a8 100644 --- a/app/routes/api.devices.ts +++ b/app/routes/api.devices.ts @@ -8,6 +8,7 @@ import { type FindDevicesOptions, } from "~/models/device.server"; import { type Device, type User } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -288,17 +289,10 @@ export async function loader({ request }: LoaderFunctionArgs) { if (!parseResult.success) { const { fieldErrors, formErrors } = parseResult.error.flatten(); - if (fieldErrors.format) { - throw Response.json( - { error: "Invalid format parameter" }, - { status: 422 }, - ); - } + if (fieldErrors.format) + throw StandardResponse.unprocessableContent("Invalid format parameter"); - throw Response.json( - { error: parseResult.error.flatten() }, - { status: 422 }, - ); + throw StandardResponse.unprocessableContent(`${parseResult.error.flatten()}`); } const params: FindDevicesOptions = parseResult.data; @@ -331,80 +325,47 @@ export async function action({ request, params }: ActionFunctionArgs) { const jwtResponse = await getUserFromJwt(request); if (typeof jwtResponse === "string") - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { - status: 403, - }, - ); + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); switch (request.method) { case "POST": return await post(request, jwtResponse); case "DELETE": return await del(request, jwtResponse, params); default: - return Response.json({ msg: "Method Not Allowed" }, { status: 405 }); + return StandardResponse.methodNotAllowed("Method Not Allowed") } } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } } async function del(request: Request, user: User, params: any) { const { deviceId } = params; - if (!deviceId) { - throw Response.json({ message: "Device ID is required" }, { status: 400 }); - } + if (!deviceId) + throw StandardResponse.badRequest("Device ID is required"); const device = (await getDevice({ id: deviceId })) as unknown as Device; - if (!device) { - throw Response.json({ message: "Device not found" }, { status: 404 }); - } + if (!device) + throw StandardResponse.notFound("Device not found"); const body = await request.json(); - if (!body.password) { - throw Response.json( - { message: "Password is required for device deletion" }, - { status: 400 }, - ); - } + if (!body.password) + throw StandardResponse.badRequest("Password is required for device deletion"); try { const deleted = await deleteDevice(user, device, body.password); if (deleted === "unauthorized") - return Response.json( - { message: "Password incorrect" }, - { - status: 401, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.unauthorized("Password incorrect"); - return Response.json(null, { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }); + return StandardResponse.ok(null); } catch (err) { console.warn(err); - return new Response("Internal Server Error", { status: 500 }); + return StandardResponse.internalServerError(); } } @@ -412,66 +373,39 @@ async function post(request: Request, user: User) { try { const body = await request.json(); - if (!body.location) { - throw Response.json( - { message: "missing required parameter location" }, - { status: 400 }, - ); - } + if (!body.location) + throw StandardResponse.badRequest("missing required parameter location"); let latitude: number, longitude: number, height: number | undefined; if (Array.isArray(body.location)) { // Handle array format [lat, lng, height?] - if (body.location.length < 2) { - throw Response.json( - { - message: `Illegal value for parameter location. missing latitude or longitude in location [${body.location.join(",")}]`, - }, - { status: 422 }, - ); - } + if (body.location.length < 2) + throw StandardResponse.unprocessableContent( + `Illegal value for parameter location. missing latitude or longitude in location [${body.location.join(',')}]`, + ) + latitude = Number(body.location[0]); longitude = Number(body.location[1]); height = body.location[2] ? Number(body.location[2]) : undefined; } else if (typeof body.location === "object" && body.location !== null) { // Handle object format { lat, lng, height? } - if (!("lat" in body.location) || !("lng" in body.location)) { - throw Response.json( - { - message: - "Illegal value for parameter location. missing latitude or longitude", - }, - { status: 422 }, - ); - } + if (!("lat" in body.location) || !("lng" in body.location)) + throw StandardResponse.unprocessableContent("Illegal value for parameter location. missing latitude or longitude"); + latitude = Number(body.location.lat); longitude = Number(body.location.lng); height = body.location.height ? Number(body.location.height) : undefined; - } else { - throw Response.json( - { - message: - "Illegal value for parameter location. Expected array or object", - }, - { status: 422 }, - ); - } + } else + throw StandardResponse.unprocessableContent("Illegal value for parameter location. Expected array or object"); - if (isNaN(latitude) || isNaN(longitude)) { - throw Response.json( - { message: "Invalid latitude or longitude values" }, - { status: 422 }, - ); - } + if (isNaN(latitude) || isNaN(longitude)) + throw StandardResponse.unprocessableContent("Invalid latitude or longitude values"); const rawAuthorizationHeader = request.headers.get("authorization"); - if (!rawAuthorizationHeader) { - throw Response.json( - { message: "Authorization header required" }, - { status: 401 }, - ); - } + if (!rawAuthorizationHeader) + throw StandardResponse.unauthorized("Authorization header required"); + const [, jwtString] = rawAuthorizationHeader.split(" "); const deviceData = { @@ -482,15 +416,12 @@ async function post(request: Request, user: User) { const newDevice = await createDevice(deviceData, user.id); - return Response.json( - { + return StandardResponse.created({ data: { ...newDevice, createdAt: newDevice.createdAt || new Date(), }, - }, - { status: 201 }, - ); + }); } catch (error) { console.error("Error creating device:", error); diff --git a/app/routes/api.getsensors.ts b/app/routes/api.getsensors.ts index faa333ef..a07266d0 100644 --- a/app/routes/api.getsensors.ts +++ b/app/routes/api.getsensors.ts @@ -1,5 +1,6 @@ import { type LoaderFunctionArgs } from "react-router"; import { getSensors } from "~/models/sensor.server"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -82,28 +83,18 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const deviceId = url.searchParams.get("deviceId"); - if (!deviceId) { - return new Response(JSON.stringify({ error: "deviceId is required" }), { - status: 400, - headers: { - "Content-Type": "application/json", - }, - }); - } + if (!deviceId) + return StandardResponse.badRequest("deviceId is required"); try{ const sensors = await getSensors(deviceId); return new Response(JSON.stringify(sensors), { + status: 200, headers: { - "Content-Type": "application/json", + "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache", }, }); }catch(error){ - return new Response(JSON.stringify({ error: "Failed to fetch sensors" }), { - status: 500, - headers: { - "Content-Type": "application/json", - }, - }); + return StandardResponse.internalServerError("Failed to fetch sensors"); } } diff --git a/app/routes/api.measurements.ts b/app/routes/api.measurements.ts index a8f9e825..8bc0fd1e 100644 --- a/app/routes/api.measurements.ts +++ b/app/routes/api.measurements.ts @@ -1,6 +1,7 @@ import { type ActionFunctionArgs } from "react-router"; import { drizzleClient } from "~/db.server"; import { measurement, type Measurement } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -82,9 +83,8 @@ import { measurement, type Measurement } from "~/schema"; * example: 25.4 */ export const action = async ({ request }: ActionFunctionArgs) => { - if (request.method !== "POST") { - return Response.json({ message: "Method not allowed" }, { status: 405 }); - } + if (request.method !== "POST") + return StandardResponse.methodNotAllowed("Method not allowed"); try { const payload: Measurement[] = await request.json(); @@ -97,9 +97,9 @@ export const action = async ({ request }: ActionFunctionArgs) => { await drizzleClient.insert(measurement).values(measurements); - return Response.json({ message: "Measurements successfully stored" }); + return StandardResponse.ok("Measurements successfully stored"); } catch (error) { - return Response.json({ message: error }, { status: 400 }); + return StandardResponse.badRequest(`${error}`); } }; \ No newline at end of file diff --git a/app/routes/api.sign-out.ts b/app/routes/api.sign-out.ts index 0f29ac61..a0a6ac21 100644 --- a/app/routes/api.sign-out.ts +++ b/app/routes/api.sign-out.ts @@ -1,6 +1,7 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { getUserFromJwt, revokeToken } from "~/lib/jwt"; import { type User } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; export const action: ActionFunction = async ({ request, @@ -16,17 +17,9 @@ export const action: ActionFunction = async ({ .toString(); const [, jwtString = ""] = rawAuthorizationHeader.split(" "); await revokeToken(user, jwtString); - return Response.json( - { code: "Ok", message: "Successfully signed out" }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.ok({ code: "Ok", message: "Successfully signed out" }); } catch (err) { console.warn(err); - return new Response("Internal Server Error", { - status: 500, - }); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.stats.ts b/app/routes/api.stats.ts index 74c6f6a3..5c50f3e8 100644 --- a/app/routes/api.stats.ts +++ b/app/routes/api.stats.ts @@ -1,5 +1,6 @@ import { type LoaderFunctionArgs } from "react-router"; import { getStatistics } from "~/lib/statistics-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export async function loader({ request }: LoaderFunctionArgs) { try { @@ -12,43 +13,14 @@ export async function loader({ request }: LoaderFunctionArgs) { humanParam.toLowerCase() !== "true" && humanParam.toLowerCase() !== "false" ) - return Response.json( - { - error: "Bad Request", - message: - "Illegal value for parameter human. allowed values: true, false", - }, - { - status: 400, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("Illegal value for parameter human. allowed values: true, false"); humanReadable = humanParam?.toLowerCase() === "true" || false; const stats = await getStatistics(humanReadable); - return Response.json(stats, { - status: 200, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); + return StandardResponse.ok(stats); } catch (e) { console.warn(e); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.internalServerError(); } } diff --git a/app/routes/api.tags.ts b/app/routes/api.tags.ts index a402c23b..2c8c48a4 100644 --- a/app/routes/api.tags.ts +++ b/app/routes/api.tags.ts @@ -1,32 +1,16 @@ import { type LoaderFunctionArgs } from "react-router"; import { getTags } from "~/lib/device-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export async function loader({}: LoaderFunctionArgs) { try { const tags = await getTags(); - return Response.json( - { + return StandardResponse.ok({ code: "Ok", data: tags, - }, - { - status: 200, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); + }); } catch (e) { console.warn(e); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } } diff --git a/app/routes/api.transfer.$deviceId.ts b/app/routes/api.transfer.$deviceId.ts index 7deccc22..c16e5b8b 100644 --- a/app/routes/api.transfer.$deviceId.ts +++ b/app/routes/api.transfer.$deviceId.ts @@ -4,33 +4,24 @@ import { getBoxTransfer, updateBoxTransferExpiration, } from "~/lib/transfer-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const loader = async ({ params, request }: LoaderFunctionArgs) => { const jwtResponse = await getUserFromJwt(request); - if (typeof jwtResponse === "string") { - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { status: 403 } - ); - } + if (typeof jwtResponse === "string") + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); const { deviceId } = params; - if (!deviceId) { - return Response.json({ error: "Device ID is required" }, { status: 400 }); - } + if (!deviceId) + return StandardResponse.badRequest("Device ID is required"); try { // Get transfer details - will throw if user doesn't own the device or transfer doesn't exist const transfer = await getBoxTransfer(jwtResponse.id, deviceId); - return Response.json( - { + return StandardResponse.ok({ data: { id: transfer.id, token: transfer.token, @@ -39,9 +30,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { createdAt: transfer.createdAt, updatedAt: transfer.updatedAt, }, - }, - { status: 200 } - ); + }); } catch (err) { console.error("Error fetching transfer:", err); return handleTransferError(err); @@ -51,26 +40,16 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { export const action = async ({ params, request }: ActionFunctionArgs) => { const jwtResponse = await getUserFromJwt(request); - if (typeof jwtResponse === "string") { - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { status: 403 } - ); - } + if (typeof jwtResponse === "string") + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); const { deviceId } = params; - if (!deviceId) { - return Response.json({ error: "Device ID is required" }, { status: 400 }); - } + if (!deviceId) + return StandardResponse.badRequest("Device ID is required"); - if (request.method !== "PUT") { - return new Response(null, { status: 405 }); - } + if (request.method !== "PUT") + return StandardResponse.methodNotAllowed(""); const contentType = request.headers.get("content-type"); const isJson = contentType?.includes("application/json"); @@ -98,13 +77,11 @@ const handleUpdateTransfer = async ( expiresAt = formData.get("expiresAt")?.toString(); } - if (!token) { - return Response.json({ error: "token is required" }, { status: 400 }); - } + if (!token) + return StandardResponse.badRequest("token is required"); - if (!expiresAt) { - return Response.json({ error: "expiresAt is required" }, { status: 400 }); - } + if (!expiresAt) + return StandardResponse.badRequest("expiresAt is required"); const updated = await updateBoxTransferExpiration( user.id, @@ -113,8 +90,7 @@ const handleUpdateTransfer = async ( expiresAt ); - return Response.json( - { + return StandardResponse.ok({ message: "Transfer successfully updated", data: { id: updated.id, @@ -124,9 +100,7 @@ const handleUpdateTransfer = async ( createdAt: updated.createdAt, updatedAt: updated.updatedAt, }, - }, - { status: 200 } - ); + }); } catch (err) { console.error("Error updating transfer:", err); return handleTransferError(err); @@ -137,17 +111,15 @@ const handleTransferError = (err: unknown) => { if (err instanceof Error) { const message = err.message; - if (message.includes("not found")) { - return Response.json({ error: message }, { status: 404 }); - } + if (message.includes("not found")) + return StandardResponse.notFound(message); if ( message.includes("permission") || message.includes("don't have") || message.includes("not the owner") - ) { - return Response.json({ error: message }, { status: 403 }); - } + ) + return StandardResponse.forbidden(message); if ( message.includes("expired") || @@ -155,13 +127,9 @@ const handleTransferError = (err: unknown) => { message.includes("required") || message.includes("format") || message.includes("future") - ) { - return Response.json({ error: message }, { status: 400 }); - } + ) + return StandardResponse.badRequest(message); } - return Response.json( - { error: "Internal server error" }, - { status: 500 } - ); + return StandardResponse.internalServerError(); }; \ No newline at end of file diff --git a/app/routes/api.transfer.ts b/app/routes/api.transfer.ts index 94991b47..13f61aca 100644 --- a/app/routes/api.transfer.ts +++ b/app/routes/api.transfer.ts @@ -5,24 +5,16 @@ import { removeBoxTransfer, validateTransferParams, } from "~/lib/transfer-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const action = async ({ request }: ActionFunctionArgs) => { const jwtResponse = await getUserFromJwt(request); - if (typeof jwtResponse === "string") { - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { status: 403 } - ); - } + if (typeof jwtResponse === "string") + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); - if (request.method !== "POST" && request.method !== "DELETE") { - return new Response(null, { status: 405 }); - } + if (request.method !== "POST" && request.method !== "DELETE") + return StandardResponse.methodNotAllowed(""); switch (request.method) { case "POST": { @@ -53,19 +45,15 @@ const handleCreateTransfer = async (request: Request, user: any) => { } const validation = validateTransferParams(boxId, expiresAt); - if (!validation.isValid) { - return Response.json({ error: validation.error }, { status: 400 }); - } + if (!validation.isValid) + return StandardResponse.badRequest(validation.error ?? ""); const transferCode = await createBoxTransfer(user.id, boxId!, expiresAt); - return Response.json( - { + return StandardResponse.created({ message: "Box successfully prepared for transfer", data: transferCode, - }, - { status: 201 } - ); + }); } catch (err) { console.error("Error creating transfer:", err); return handleTransferError(err); @@ -88,17 +76,15 @@ const handleRemoveTransfer = async (request: Request, user: any) => { token = formData.get("token")?.toString(); } - if (!boxId) { - return Response.json({ error: "boxId is required" }, { status: 400 }); - } + if (!boxId) + return StandardResponse.badRequest("boxId is required"); - if (!token) { - return Response.json({ error: "token is required" }, { status: 400 }); - } + if (!token) + return StandardResponse.badRequest("token is required"); await removeBoxTransfer(user.id, boxId, token); - return new Response(null, { status: 204 }); + return StandardResponse.noContent(); } catch (err) { console.error("Error removing transfer:", err); return handleTransferError(err); @@ -109,17 +95,15 @@ const handleTransferError = (err: unknown) => { if (err instanceof Error) { const message = err.message; - if (message.includes("not found")) { - return Response.json({ error: message }, { status: 404 }); - } + if (message.includes("not found")) + return StandardResponse.notFound(message); if ( message.includes("permission") || message.includes("don't have") || message.includes("not the owner") - ) { - return Response.json({ error: message }, { status: 403 }); - } + ) + return StandardResponse.forbidden(message); if ( message.includes("expired") || @@ -127,13 +111,9 @@ const handleTransferError = (err: unknown) => { message.includes("required") || message.includes("format") || message.includes("future") - ) { - return Response.json({ error: message }, { status: 400 }); - } + ) + return StandardResponse.badRequest(message); } - return Response.json( - { error: "Internal server error" }, - { status: 500 } - ); + return StandardResponse.internalServerError(); }; \ No newline at end of file diff --git a/app/routes/api.users.confirm-email.ts b/app/routes/api.users.confirm-email.ts index 2a3150b0..8bdef5db 100644 --- a/app/routes/api.users.confirm-email.ts +++ b/app/routes/api.users.confirm-email.ts @@ -1,80 +1,46 @@ -import { type ActionFunction, type ActionFunctionArgs } from "react-router"; -import { confirmEmail } from "~/lib/user-service.server"; +import { type ActionFunction, type ActionFunctionArgs } from 'react-router' +import { confirmEmail } from '~/lib/user-service.server' +import { StandardResponse } from '~/utils/response-utils' export const action: ActionFunction = async ({ - request, + request, }: ActionFunctionArgs) => { - let formData = new FormData(); - try { - formData = await request.formData(); - } catch { - // Just continue, it will fail in the next check - // The try catch block handles an exception that occurs if the - // request was sent without x-www-form-urlencoded content-type header - } + let formData = new FormData() + try { + formData = await request.formData() + } catch { + // Just continue, it will fail in the next check + // The try catch block handles an exception that occurs if the + // request was sent without x-www-form-urlencoded content-type header + } - if ( - !formData.has("token") || - formData.get("token")?.toString().trim().length === 0 - ) - return Response.json( - { message: "No email confirmation token specified." }, - { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + if ( + !formData.has('token') || + formData.get('token')?.toString().trim().length === 0 + ) + return StandardResponse.badRequest('No email confirmation token specified.'); - if ( - !formData.has("email") || - formData.get("email")?.toString().trim().length === 0 - ) - return Response.json( - { message: "No email address to confirm specified." }, - { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + if ( + !formData.has('email') || + formData.get('email')?.toString().trim().length === 0 + ) + return StandardResponse.badRequest('No email address to confirm specified.'); - try { - const updatedUser = await confirmEmail( - formData.get("token")!.toString(), - formData.get("email")!.toString(), - ); + try { + const updatedUser = await confirmEmail( + formData.get('token')!.toString(), + formData.get('email')!.toString(), + ) - if (updatedUser === null) - return Response.json( - { - code: "Forbidden", - message: "Invalid or expired confirmation token.", - }, - { - status: 403, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + if (updatedUser === null) + return StandardResponse.forbidden('Invalid or expired confirmation token.'); - return Response.json( - { - code: "Ok", - message: "E-Mail successfully confirmed. Thank you", - }, - { - status: 200, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); - } catch (err) { - console.warn(err); - return new Response("Internal Server Error", { status: 500 }); - } -}; + return StandardResponse.ok({ + code: 'Ok', + message: 'E-Mail successfully confirmed. Thank you', + }); + } catch (err) { + console.warn(err) + return StandardResponse.internalServerError(); + } +} diff --git a/app/routes/api.users.me.boxes.$deviceId.ts b/app/routes/api.users.me.boxes.$deviceId.ts index 77641efa..cd96f76e 100644 --- a/app/routes/api.users.me.boxes.$deviceId.ts +++ b/app/routes/api.users.me.boxes.$deviceId.ts @@ -2,6 +2,7 @@ import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; import { getUserFromJwt } from "~/lib/jwt"; import { getDevice } from "~/models/device.server"; import { user } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; export const loader: LoaderFunction = async ({ request, @@ -11,70 +12,23 @@ export const loader: LoaderFunction = async ({ const jwtResponse = await getUserFromJwt(request); if (typeof jwtResponse === "string") - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { - status: 403, - }, - ); + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); const user = jwtResponse; const deviceId = params.deviceId; if (deviceId === undefined) - return Response.json( - { - code: "Bad Request", - message: "Invalid device id specified", - }, - { - status: 400, - }, - ); + return StandardResponse.badRequest("Invalid device id specified"); const box = await getDevice({ id: deviceId }); if (box === undefined) - return Response.json( - { - code: "Bad Request", - message: "There is no such device with the given id", - }, - { - status: 400, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.badRequest("There is no such device with the given id"); if (box.user.id !== user.id) - return Response.json( - { code: "Forbidden", message: "User does not own this senseBox" }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.forbidden("User does not own this senseBox"); - return Response.json( - { code: "Ok", data: { box: box } }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.ok({ code: "Ok", data: { box: box } }); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.me.boxes.ts b/app/routes/api.users.me.boxes.ts index 30e5f009..3a39c5ab 100644 --- a/app/routes/api.users.me.boxes.ts +++ b/app/routes/api.users.me.boxes.ts @@ -2,6 +2,7 @@ import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; import { transformDeviceToApiFormat } from "~/lib/device-transform"; import { getUserFromJwt } from "~/lib/jwt"; import { getUserDevices } from "~/models/device.server"; +import { StandardResponse } from "~/utils/response-utils"; export const loader: LoaderFunction = async ({ request, @@ -10,45 +11,21 @@ export const loader: LoaderFunction = async ({ const jwtResponse = await getUserFromJwt(request); if (typeof jwtResponse === "string") - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { - status: 403, - }, - ); + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); const userBoxes = await getUserDevices(jwtResponse.id); const cleanedBoxes = userBoxes.map((box) => transformDeviceToApiFormat(box)); - return Response.json( - { + return StandardResponse.ok({ code: "Ok", data: { boxes: cleanedBoxes, boxes_count: cleanedBoxes.length, sharedBoxes: [], }, - }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.me.resend-email-confirmation.ts b/app/routes/api.users.me.resend-email-confirmation.ts index c6a5962d..d4adf4ab 100644 --- a/app/routes/api.users.me.resend-email-confirmation.ts +++ b/app/routes/api.users.me.resend-email-confirmation.ts @@ -1,6 +1,7 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { getUserFromJwt } from "~/lib/jwt"; import { resendEmailConfirmation } from "~/lib/user-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const action: ActionFunction = async ({ request, @@ -9,51 +10,18 @@ export const action: ActionFunction = async ({ const jwtResponse = await getUserFromJwt(request); if (typeof jwtResponse === "string") - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { - status: 403, - }, - ); + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); const result = await resendEmailConfirmation(jwtResponse); if (result === "already_confirmed") - return Response.json( - { - code: "Unprocessable Content", - message: `Email address ${jwtResponse.email} is already confirmed.`, - }, - { - status: 422, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.unprocessableContent(`Email address ${jwtResponse.email} is already confirmed.`); - return Response.json( - { + return StandardResponse.ok({ code: "Ok", message: `Email confirmation has been sent to ${result.unconfirmedEmail}`, - }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.me.ts b/app/routes/api.users.me.ts index 2a64dc78..25397d76 100644 --- a/app/routes/api.users.me.ts +++ b/app/routes/api.users.me.ts @@ -7,6 +7,7 @@ import { import { getUserFromJwt } from "~/lib/jwt"; import { deleteUser, updateUserDetails } from "~/lib/user-service.server"; import { type User } from "~/schema/user"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -279,36 +280,12 @@ export const loader: LoaderFunction = async ({ const jwtResponse = await getUserFromJwt(request); if (typeof jwtResponse === "string") - return Response.json( - { - code: "Forbidden", - message: - "Invalid JWT authorization. Please sign in to obtain new JWT.", - }, - { - status: 403, - }, - ); + return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); - return Response.json( - { code: "Ok", data: { me: jwtResponse } }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.ok({ code: "Ok", data: { me: jwtResponse } }); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } }; @@ -328,7 +305,7 @@ export const action: ActionFunction = async ({ case "DELETE": return await del(user, request); default: - return Response.json({ msg: "Method Not Allowed" }, { status: 405 }); + return StandardResponse.methodNotAllowed("Method Not Allowed"); } }; @@ -355,53 +332,23 @@ const put = async (user: User, request: Request): Promise => { if (updated === false) { if (messages.length > 0) { - return Response.json( - { - code: "Bad Request", - message: messageText, - }, - { - status: 400, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.badRequest(messageText); } - return Response.json( - { + return StandardResponse.ok({ code: "Ok", message: "No changed properties supplied. User remains unchanged.", - }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); } - return Response.json( - { + return StandardResponse.ok({ code: "Ok", message: `User successfully saved. ${messageText}`, data: { me: updatedUser }, - }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); } catch (err) { console.warn(err); - return Response.json( - { - error: "Internal Server Error", - message: - "The server was unable to complete your request. Please try again later.", - }, - { - status: 500, - }, - ); + return StandardResponse.internalServerError(); } }; @@ -418,7 +365,7 @@ const del = async (user: User, r: Request): Promise => { !formData.has("password") || formData.get("password")?.toString().length === 0 ) - return new Response("Bad Request", { status: 400 }); + return StandardResponse.badRequest("Bad Request"); const rawAuthorizationHeader = r.headers.get("authorization"); if (!rawAuthorizationHeader) throw new Error("no_token"); @@ -431,20 +378,11 @@ const del = async (user: User, r: Request): Promise => { ); if (deleted === "unauthorized") - return Response.json( - { message: "Password incorrect" }, - { - status: 401, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.unauthorized("Password incorrect"); - return Response.json(null, { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }); + return StandardResponse.ok(null); } catch (err) { console.warn(err); - return new Response("Internal Server Error", { status: 500 }); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.password-reset.ts b/app/routes/api.users.password-reset.ts index 97d89733..56d3a555 100644 --- a/app/routes/api.users.password-reset.ts +++ b/app/routes/api.users.password-reset.ts @@ -1,5 +1,6 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { resetPassword } from "~/lib/user-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const action: ActionFunction = async ({ request, @@ -17,29 +18,13 @@ export const action: ActionFunction = async ({ !formData.has("password") || formData.get("password")?.toString().trim().length === 0 ) - return Response.json( - { message: "No new password specified." }, - { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("No new password specified."); if ( !formData.has("token") || formData.get("token")?.toString().trim().length === 0 ) - return Response.json( - { message: "No password reset token specified." }, - { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("No password reset token specified."); try { const resetStatus = await resetPassword( @@ -50,49 +35,20 @@ export const action: ActionFunction = async ({ switch (resetStatus) { case "forbidden": case "expired": - return Response.json( - { - code: "Forbidden", - message: - resetStatus === "forbidden" + return StandardResponse.forbidden(resetStatus === "forbidden" ? "Password reset for this user not possible" - : "Password reset token expired", - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + : "Password reset token expired"); case "invalid_password_format": - return Response.json( - { - code: "Bad Request", - message: - "Password must be at least ${password_min_length} characters.", - }, - { - status: 400, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.badRequest("Password must be at least ${password_min_length} characters."); case "success": - return Response.json( - { + return StandardResponse.ok({ code: "Ok", message: "Password successfully changed. You can now login with your new password", - }, - { - status: 400, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); } } catch (err) { console.warn(err); - return Response.json("Internal Server Error", { - status: 500, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.refresh-auth.ts b/app/routes/api.users.refresh-auth.ts index f499f7b0..7d5f469b 100644 --- a/app/routes/api.users.refresh-auth.ts +++ b/app/routes/api.users.refresh-auth.ts @@ -2,6 +2,7 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { getUserFromJwt, hashJwt, refreshJwt } from "~/lib/jwt"; import { parseRefreshTokenData } from "~/lib/request-parsing"; import { type User } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -71,7 +72,7 @@ import { type User } from "~/schema"; * properties: * code: * type: string - * example: Unauthorized + * example: Forbidden * message: * type: string * enum: @@ -117,18 +118,8 @@ export const action: ActionFunction = async ({ // Parse request data - handles both JSON and form data automatically const data = await parseRefreshTokenData(request); - if (!data.token || data.token.trim().length === 0) { - return Response.json( - { - code: "Unauthorized", - message: "You must specify a token to refresh", - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); - } + if (!data.token || data.token.trim().length === 0) + return StandardResponse.forbidden("You must specify a token to refresh"); // We deliberately make casts and stuff like that, so everything // but the happy path will result in an internal server error. @@ -141,66 +132,28 @@ export const action: ActionFunction = async ({ const [, jwtString = ""] = rawAuthorizationHeader.split(" "); if (data.token !== hashJwt(jwtString)) - return Response.json( - { - code: "Unauthorized", - message: - "Refresh token invalid or too old. Please sign in with your username and password.", - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.forbidden("Refresh token invalid or too old. Please sign in with your username and password."); const { token, refreshToken } = (await refreshJwt(user, data.token)) || {}; if (token && refreshToken) - return Response.json( - { + return StandardResponse.ok({ code: "Authorized", message: "Successfully refreshed auth", data: { user }, token, refreshToken, - }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); else - return Response.json( - { - code: "Unauthorized", - message: - "Refresh token invalid or too old. Please sign in with your username and password.", - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.forbidden("Refresh token invalid or too old. Please sign in with your username and password."); } catch (error) { // Handle parsing errors - if (error instanceof Error && error.message.includes('Failed to parse')) { - return Response.json( - { - code: "Unauthorized", - message: `Invalid request format: ${error.message}`, - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - } - ); - } + if (error instanceof Error && error.message.includes('Failed to parse')) + return StandardResponse.forbidden(`Invalid request format: ${error.message}`); // Handle other errors console.warn(error); - return new Response("Internal Server Error", { - status: 500, - }); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.register.ts b/app/routes/api.users.register.ts index 52db6703..bb737c9e 100644 --- a/app/routes/api.users.register.ts +++ b/app/routes/api.users.register.ts @@ -8,6 +8,7 @@ import { } from "~/lib/user-service"; import { registerUser } from "~/lib/user-service.server"; import { type User } from "~/schema"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi @@ -180,7 +181,8 @@ export const action: ActionFunction = async ({ request, }: ActionFunctionArgs) => { const method = request.method; - if (method !== "POST") return new Response(null, { status: 405 }); + if (method !== "POST") + return StandardResponse.methodNotAllowed(""); try { // Parse request data - handles both JSON and form data automatically @@ -198,12 +200,7 @@ export const action: ActionFunction = async ({ ); if (!registration) // null is returned when no new user profile was created because it already exists - return new Response(JSON.stringify({ message: "User already exists." }), { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }); + return StandardResponse.badRequest("User already exists."); if ("validationKind" in registration) { // A validation was returned, therefore a bad request was sent in @@ -231,12 +228,7 @@ export const action: ActionFunction = async ({ msg = "Password must be at least 8 characters long."; break; } - return new Response(JSON.stringify({ message: msg }), { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }); + return StandardResponse.badRequest(msg); } const user = registration as User; @@ -244,50 +236,24 @@ export const action: ActionFunction = async ({ try { const { token, refreshToken } = await createToken(user); - return new Response( - JSON.stringify({ + return StandardResponse.created({ message: "Successfully registered new user", token: token, refreshToken: refreshToken, data: user, - }), - { - status: 201, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + }); } catch (err) { console.error("Unable to create JWT", err); - return new Response( - JSON.stringify({ - message: `Unable to create jwt for newly created user: ${(err as Error)?.message}`, - }), - { - status: 500, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.internalServerError(`Unable to create jwt for newly created user: ${(err as Error)?.message}`); } } catch (error) { // Handle parsing errors if (error instanceof Error && error.message.includes('Failed to parse')) { - return new Response( - JSON.stringify({ - message: `Invalid request format: ${error.message}` - }), - { - status: 400, - headers: { "content-type": "application/json; charset=utf-8" }, - } - ); + return StandardResponse.badRequest(`Invalid request format: ${error.message}`); } // Handle other errors console.error("Registration error:", error); - return new Response("Internal Server Error", { status: 500 }); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.request-password-reset.ts b/app/routes/api.users.request-password-reset.ts index 12f0debc..cf0b34c2 100644 --- a/app/routes/api.users.request-password-reset.ts +++ b/app/routes/api.users.request-password-reset.ts @@ -1,5 +1,6 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { requestPasswordReset } from "~/lib/user-service.server"; +import { StandardResponse } from "~/utils/response-utils"; export const action: ActionFunction = async ({ request, @@ -17,30 +18,16 @@ export const action: ActionFunction = async ({ !formData.has("email") || formData.get("email")?.toString().trim().length === 0 ) - return Response.json( - { message: "No email address specified." }, - { - status: 400, - headers: { - "content-type": "application/json; charset=utf-8", - }, - }, - ); + return StandardResponse.badRequest("No email address specified."); try { await requestPasswordReset(formData.get("email")!.toString()); // We don't want to leak valid/ invalid emails, so we confirm // the initiation no matter what the return value above is - return Response.json( - { code: "Ok", message: "Password reset initiated" }, - { status: 200 }, - ); + return StandardResponse.ok({ code: "Ok", message: "Password reset initiated" }); } catch (err) { console.warn(err); - return Response.json("Internal Server Error", { - status: 500, - headers: { "Content-Type": "application/json; charset: utf-8" }, - }); + return StandardResponse.internalServerError(); } }; diff --git a/app/routes/api.users.sign-in.ts b/app/routes/api.users.sign-in.ts index 11b6c5fb..07080619 100644 --- a/app/routes/api.users.sign-in.ts +++ b/app/routes/api.users.sign-in.ts @@ -1,6 +1,7 @@ import { type ActionFunction, type ActionFunctionArgs } from "react-router"; import { parseUserSignInData } from "~/lib/request-parsing"; import { signIn } from "~/lib/user-service.server"; +import { StandardResponse } from "~/utils/response-utils"; /** * @openapi * /api/users/sign-in: @@ -67,7 +68,7 @@ import { signIn } from "~/lib/user-service.server"; * properties: * code: * type: string - * example: Unauthorized + * example: Forbidden * message: * type: string * enum: @@ -116,75 +117,33 @@ export const action: ActionFunction = async ({ const email = data.email.trim(); const password = data.password.trim(); - if (!email || email.length === 0) { - return Response.json( - { - code: "Unauthorized", - message: "You must specify either your email or your username", - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); - } + if (!email || email.length === 0) + return StandardResponse.forbidden("You must specify either your email or your username"); if (!password || password.length === 0) { - return Response.json( - { - code: "Unauthorized", - message: "You must specify your password to sign in", - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.forbidden("You must specify your password to sign in"); } const { user, jwt, refreshToken } = (await signIn(email, password)) || {}; if (user && jwt && refreshToken) - return Response.json( - { + return StandardResponse.ok({ code: "Authorized", message: "Successfully signed in", data: { user }, token: jwt, refreshToken, - }, - { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + }); else - return Response.json( - { code: "Unauthorized", message: "User and or password not valid!" }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - }, - ); + return StandardResponse.forbidden("User and or password not valid!"); } catch (error) { // Handle parsing errors if (error instanceof Error && error.message.includes('Failed to parse')) { - return Response.json( - { - code: "Unauthorized", - message: `Invalid request format: ${error.message}`, - }, - { - status: 403, - headers: { "Content-Type": "application/json; charset=utf-8" }, - } - ); + return StandardResponse.forbidden(`Invalid request format: ${error.message}`); } // Handle other errors console.warn(error); - return new Response("Internal Server Error", { - status: 500, - }); + return StandardResponse.internalServerError(); } }; diff --git a/app/utils/param-utils.ts b/app/utils/param-utils.ts index 43a6366e..bed63add 100644 --- a/app/utils/param-utils.ts +++ b/app/utils/param-utils.ts @@ -1,4 +1,4 @@ -import { badRequest } from "./response-utils"; +import { StandardResponse } from "./response-utils"; /** * Parses a parameter from the url search paramaters into a date. @@ -14,7 +14,7 @@ export function parseDateParam(url: URL, paramName: string, defaultDate: Date): if (param) { const date = new Date(param) if (Number.isNaN(date.valueOf())) - return badRequest(`Illegal value for parameter ${paramName}. Allowed values: RFC3339Date`); + return StandardResponse.badRequest(`Illegal value for parameter ${paramName}. Allowed values: RFC3339Date`); return date } return defaultDate; @@ -34,7 +34,7 @@ export function parseEnumParam(url: URL, paramName: string, allowed const param = url.searchParams.get(paramName); if (param) { if (!allowedValues.includes(param)) - return badRequest(`Illegal value for parameter ${paramName}. Allowed values: ${allowedValues}`); + return StandardResponse.badRequest(`Illegal value for parameter ${paramName}. Allowed values: ${allowedValues}`); return param; } return defaultValue diff --git a/app/utils/response-utils.ts b/app/utils/response-utils.ts index 8aeb121e..595bf195 100644 --- a/app/utils/response-utils.ts +++ b/app/utils/response-utils.ts @@ -1,59 +1,134 @@ -/** - * Creates a response object for a bad request - * @param message The message for the response - * @returns The response - */ -export function badRequest(message: string): Response { - return Response.json( - { - error: 'Bad Request', - message: message, - }, - { - status: 400, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }, - ) -} +export class StandardResponse { + /** + * Creates a response object for a (json) success (ok) response + * @param data The data for the response + * @returns The response + */ + public static ok = (data: any): Response => this.successResponse(data, 200) -/** - * Creates a response object for an internal server error - * @param message The message for the response. Default: - * The server was unable to complete your request. Please try again later. - * @returns The response - */ -export function internalServerError(message = - "The server was unable to complete your request. Please try again later."): Response { - Response.error() - return Response.json( - { - error: "Internal Server Error", - message: message - }, - { - status: 500, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }, - ); -} + /** + * Creates a response object for a (json) "created" response + * @param data The data for the response + * @returns The response + */ + public static created = (data: any): Response => + this.successResponse(data, 201) + + /** + * Creates a response object for a (json) "no content" response + * @returns The response + */ + public static noContent = (): Response => new Response(null, { status: 204 }) + + /** + * Creates a response object for an arbitrary successful response + * @param data The data for the response + * @param status The status code + * @returns The response + */ + public static successResponse = (data: any, status: number): Response => + Response.json(data, { + status: status, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }) + + /** + * Creates a response object for a bad request + * @param message The message for the response + * @returns The response + */ + public static badRequest = (message: string): Response => + this.errorResponse('Bad Request', message, 400) + + /** + * Creates a response object for an unauthorized request + * @param message The message for the response + * @returns The response + */ + public static unauthorized = (message: string): Response => + this.errorResponse('Unauthorized', message, 401) + + /** + * Creates a response object for a forbidden request + * @param message The message for the response + * @returns The response + */ + // TODO: We often use this in cases where we previously returned the string + // "unuathorized" but with response code 403. For a future v2 API, reevaluate + // all places where this is returned and decide if 403 Forbidden or 401 Unauthorized should be returned. + public static forbidden = (message: string): Response => + this.errorResponse('Forbidden', message, 403) + + /** + * Creates a response object for a 404 + * @param message The message for the response + * @returns The response + */ + public static notFound = (message: string): Response => + this.errorResponse('Not Found', message, 404) + + /** + * Creates a response object for a request with a method that is not allowed + * @param message The message for the response + * @returns The response + */ + public static methodNotAllowed = (message: string): Response => + this.errorResponse('Method Not Allowed', message, 405) + + /** + * Creates a response object for a gone response + * @param message The message for the response + * @returns The response + */ + public static gone = (message: string): Response => + this.errorResponse('Gone', message, 410) + + /** + * Creates a response object for unsupported media type + * @param message The message for the response + * @returns The response + */ + public static unsupportedMediaType = (message: string): Response => + this.errorResponse('Unsupported Media Type', message, 415) + + /** + * Creates a response object for an unprocessable entity + * @param message The message for the response + * @returns The response + */ + public static unprocessableContent = (message: string): Response => + this.errorResponse('Unprocessable Content', message, 422) + + /** + * Creates a response object for an internal server error + * @param message The message for the response. Default: + * The server was unable to complete your request. Please try again later. + * @returns The response + */ + public static internalServerError = ( + message = 'The server was unable to complete your request. Please try again later.', + ): Response => this.errorResponse('Internal Server Error', message, 500) -/** - * Creates a response object for a 404 - * @param message The message for the response - * @returns The response - */ -export function notFound(message: string): Response { - return Response.json( + /** + * Creates a response object for an arbitrary error + * @param error The error string for the response + * @param message The message for the response. + * @param status The error code + * @returns The response + */ + public static errorResponse = ( + error: string, + message: string, + status: number, + ) => + Response.json( { - error: 'Not found', + code: error, message: message, + error: message, }, { - status: 404, + status: status, headers: { 'Content-Type': 'application/json; charset=utf-8', }, diff --git a/tests/routes/api.claim.spec.ts b/tests/routes/api.claim.spec.ts index abf90c98..3107d800 100644 --- a/tests/routes/api.claim.spec.ts +++ b/tests/routes/api.claim.spec.ts @@ -172,7 +172,7 @@ describe("openSenseMap API Routes: /boxes/claim", () => { expect(claimResponse.status).toBe(415); const body = await claimResponse.json(); - expect(body.code).toBe("UnsupportedMediaType"); + expect(body.code).toBe("Unsupported Media Type"); expect(body.message).toContain("application/json"); }); diff --git a/tests/routes/api.devices.spec.ts b/tests/routes/api.devices.spec.ts index 1189ebaf..4a3d5452 100644 --- a/tests/routes/api.devices.spec.ts +++ b/tests/routes/api.devices.spec.ts @@ -596,7 +596,7 @@ describe('openSenseMap API Routes: /boxes', () => { ) const badResult = await badDeleteResponse.json() - expect(badResult).toEqual({ message: 'Password incorrect' }) + expect(badResult.message).toBe('Password incorrect') }) it('should successfully delete the device with correct password', async () => { diff --git a/tests/routes/api.location.spec.ts b/tests/routes/api.location.spec.ts index b9550e52..8db61caf 100644 --- a/tests/routes/api.location.spec.ts +++ b/tests/routes/api.location.spec.ts @@ -559,7 +559,7 @@ describe("openSenseMap API Routes: Location Measurements", () => { expect(response.status).toBe(422); const errorData = await response.json(); - expect(errorData.code).toBe("Unprocessable Entity"); + expect(errorData.code).toBe("Unprocessable Content"); expect(errorData.message).toBe("Invalid location coordinates"); }); @@ -589,7 +589,7 @@ describe("openSenseMap API Routes: Location Measurements", () => { expect(response.status).toBe(422); const errorData = await response.json(); - expect(errorData.code).toBe("Unprocessable Entity"); + expect(errorData.code).toBe("Unprocessable Content"); expect(errorData.message).toBe("Invalid location coordinates"); }); }); diff --git a/tests/routes/api.users.me.boxes.$deviceId.spec.ts b/tests/routes/api.users.me.boxes.$deviceId.spec.ts index e8e12cd6..afee96f8 100644 --- a/tests/routes/api.users.me.boxes.$deviceId.spec.ts +++ b/tests/routes/api.users.me.boxes.$deviceId.spec.ts @@ -95,10 +95,8 @@ describe("openSenseMap API Routes: /users", () => { const forbiddenBody = await forbiddenResponse.json(); // Assert: Forbidden response expect(forbiddenResponse.status).toBe(403); - expect(forbiddenBody).toEqual({ - code: "Forbidden", - message: "User does not own this senseBox", - }); + expect(forbiddenBody.code).toBe("Forbidden"); + expect(forbiddenBody.message).toBe("User does not own this senseBox"); }); afterAll(async () => { diff --git a/vite.config.ts b/vite.config.ts index f57caf58..3d2f3d26 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -39,6 +39,7 @@ export default defineConfig(({ mode }) => { coverage: { reporter: ["text", "json-summary", "json"], }, + testTimeout: 10_000, }, }; });