diff --git a/app/features/img-upload/queries/oneUnvalidatedImage.ts b/app/features/img-upload/queries/oneUnvalidatedImage.ts new file mode 100644 index 000000000..f6d40aa2f --- /dev/null +++ b/app/features/img-upload/queries/oneUnvalidatedImage.ts @@ -0,0 +1,21 @@ +import { sql } from "~/db/sql"; +import type { UserSubmittedImage } from "~/db/types"; + +const stm = sql.prepare(/* sql */ ` + select + "id", + "url", + "submitterUserId" + from "UnvalidatedUserSubmittedImage" + where "validatedAt" is null + limit 1 +`); + +type UnvalidatedImage = Pick< + UserSubmittedImage, + "id" | "url" | "submitterUserId" +>; + +export function oneUnvalidatedImage(): UnvalidatedImage | null { + return stm.get(); +} diff --git a/app/features/img-upload/queries/validateImage.ts b/app/features/img-upload/queries/validateImage.ts new file mode 100644 index 000000000..a9f65baf2 --- /dev/null +++ b/app/features/img-upload/queries/validateImage.ts @@ -0,0 +1,12 @@ +import { sql } from "~/db/sql"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; + +const stm = sql.prepare(/* sql */ ` + update "UnvalidatedUserSubmittedImage" + set "validatedAt" = @validatedAt + where "id" = @id +`); + +export function validateImage(id: number) { + stm.run({ validatedAt: dateToDatabaseTimestamp(new Date()), id }); +} diff --git a/app/features/img-upload/routes/upload.admin.tsx b/app/features/img-upload/routes/upload.admin.tsx new file mode 100644 index 000000000..537f9589c --- /dev/null +++ b/app/features/img-upload/routes/upload.admin.tsx @@ -0,0 +1,62 @@ +import type { ActionFunction, LoaderArgs } from "@remix-run/node"; +import { Form, useLoaderData } from "@remix-run/react"; +import { Main } from "~/components/Main"; +import { SubmitButton } from "~/components/SubmitButton"; +import { requireUser } from "~/modules/auth"; +import { isAdmin } from "~/permissions"; +import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; +import { userSubmittedImage } from "~/utils/urls"; +import { oneUnvalidatedImage } from "../queries/oneUnvalidatedImage"; +import { validateImage } from "../queries/validateImage"; +import { validateImageSchema } from "../upload-schemas.server"; + +export const action: ActionFunction = async ({ request }) => { + const user = await requireUser(request); + const data = await parseRequestFormData({ + schema: validateImageSchema, + request, + }); + + validate(isAdmin(user)); + + validateImage(data.imageId); + + return null; +}; + +export const loader = async ({ request }: LoaderArgs) => { + const user = await requireUser(request); + + notFoundIfFalsy(isAdmin(user)); + + return { + image: oneUnvalidatedImage(), + }; +}; + +export default function ImageUploadAdminPage() { + return ( +
+ +
+ ); +} + +function ImageValidator() { + const data = useLoaderData(); + + if (!data.image) { + return <>All validated!; + } + + return ( + <> + +
+ + Ok +
+
From: {data.image.submitterUserId}
+ + ); +} diff --git a/app/features/img-upload/upload-schemas.server.ts b/app/features/img-upload/upload-schemas.server.ts new file mode 100644 index 000000000..63f60cfbd --- /dev/null +++ b/app/features/img-upload/upload-schemas.server.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; +import { id } from "~/utils/zod"; + +export const validateImageSchema = z.object({ + imageId: id, +}); diff --git a/remix.config.js b/remix.config.js index 09892a601..4aa2adb54 100644 --- a/remix.config.js +++ b/remix.config.js @@ -7,6 +7,7 @@ module.exports = { routes: async (defineRoutes) => { return defineRoutes((route) => { route("/upload", "features/img-upload/routes/upload.tsx"); + route("/upload/admin", "features/img-upload/routes/upload.admin.tsx"); route("/plans", "features/map-planner/routes/plans.tsx"); route("/analyzer", "features/build-analyzer/routes/analyzer.tsx"); route(