From 57076ae4546937750fb00f337a3d650b62626322 Mon Sep 17 00:00:00 2001 From: Lukas <69743171+quick007@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:08:55 -0800 Subject: [PATCH] let users reset their auth token + settings page --- deno.jsonc | 8 +- fresh.gen.ts | 7 +- islands/components/pieces/deleteToken.tsx | 21 ++++ islands/events/components/delete.tsx | 15 ++- islands/events/viewing/register.tsx | 14 ++- routes/api/auth/regen.ts | 14 ++- routes/api/events/scan.ts | 4 +- routes/api/events/ticket/index.ts | 136 +++++++++++++++++++++- routes/user/settings/_layout.tsx | 75 ++++++++++-- routes/user/settings/auth.tsx | 14 +++ routes/user/settings/authentication.ts | 0 routes/user/settings/index.tsx | 2 +- utils/db/kv.ts | 1 - utils/db/kv.types.ts | 4 +- 14 files changed, 276 insertions(+), 39 deletions(-) create mode 100644 islands/components/pieces/deleteToken.tsx create mode 100644 routes/user/settings/auth.tsx delete mode 100644 routes/user/settings/authentication.ts diff --git a/deno.jsonc b/deno.jsonc index 5a259c5..f3a52e4 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -4,10 +4,10 @@ "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", // Do items like reordering imports "fmt": "deno fmt && npx prettier . --write", - "start": "deno run -A --unstable --watch=static/,routes/ dev.ts", - "build": "deno run -A --unstable dev.ts build", - "preview": "deno run --unstable -A main.ts", - "update": "deno run --unstable -A -r https://fresh.deno.dev/update ." + "start": "deno run -A --unstable-kv --watch=static/,routes/ dev.ts", + "build": "deno run -A --unstable-kv dev.ts build", + "preview": "deno run --unstable-kv -A main.ts", + "update": "deno run --unstable-kv -A -r https://fresh.deno.dev/update ." }, "lint": { "rules": { "tags": ["fresh", "recommended"], "include": ["no-unused-vars"] } diff --git a/fresh.gen.ts b/fresh.gen.ts index fadfb2b..dbcef2e 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -48,7 +48,7 @@ import * as $kv_insights_layout from "./routes/kv-insights/_layout.tsx"; import * as $kv_insights_middleware from "./routes/kv-insights/_middleware.ts"; import * as $login from "./routes/login.tsx"; import * as $user_settings_layout from "./routes/user/settings/_layout.tsx"; -import * as $user_settings_authentication from "./routes/user/settings/authentication.ts"; +import * as $user_settings_auth from "./routes/user/settings/auth.tsx"; import * as $user_settings_index from "./routes/user/settings/index.tsx"; import * as $components_dropinUI_trash from "./islands/components/dropinUI/trash.tsx"; import * as $components_pickers_calender from "./islands/components/pickers/calender.tsx"; @@ -56,6 +56,7 @@ import * as $components_pickers_dropdown from "./islands/components/pickers/drop import * as $components_pickers_image from "./islands/components/pickers/image.tsx"; import * as $components_pickers_select from "./islands/components/pickers/select.tsx"; import * as $components_pickers_time from "./islands/components/pickers/time.tsx"; +import * as $components_pieces_deleteToken from "./islands/components/pieces/deleteToken.tsx"; import * as $components_pieces_navDropDown from "./islands/components/pieces/navDropDown.tsx"; import * as $components_pieces_ticket from "./islands/components/pieces/ticket.tsx"; import * as $entriesManagement from "./islands/entriesManagement.tsx"; @@ -147,7 +148,7 @@ const manifest = { "./routes/kv-insights/_middleware.ts": $kv_insights_middleware, "./routes/login.tsx": $login, "./routes/user/settings/_layout.tsx": $user_settings_layout, - "./routes/user/settings/authentication.ts": $user_settings_authentication, + "./routes/user/settings/auth.tsx": $user_settings_auth, "./routes/user/settings/index.tsx": $user_settings_index, }, islands: { @@ -157,6 +158,8 @@ const manifest = { "./islands/components/pickers/image.tsx": $components_pickers_image, "./islands/components/pickers/select.tsx": $components_pickers_select, "./islands/components/pickers/time.tsx": $components_pickers_time, + "./islands/components/pieces/deleteToken.tsx": + $components_pieces_deleteToken, "./islands/components/pieces/navDropDown.tsx": $components_pieces_navDropDown, "./islands/components/pieces/ticket.tsx": $components_pieces_ticket, diff --git a/islands/components/pieces/deleteToken.tsx b/islands/components/pieces/deleteToken.tsx new file mode 100644 index 0000000..7a1980c --- /dev/null +++ b/islands/components/pieces/deleteToken.tsx @@ -0,0 +1,21 @@ +import CTA from "@/components/buttons/cta.tsx"; +import Deletion from "@/islands/events/components/delete.tsx"; +import { useSignal } from "@preact/signals"; + +const DeleteToken = () => { + const open = useSignal(false); + + return ( + fetch("/api/auth/regen")} + open={open} + routeTo="/" + customMsg="Resetting your authentication token will log you out of all devices. Please proceed with caution!" + > + open.value = true} btnSize="sm">Delete Token + + ); +}; + +export default DeleteToken; diff --git a/islands/events/components/delete.tsx b/islands/events/components/delete.tsx index 5e9f900..284bffb 100644 --- a/islands/events/components/delete.tsx +++ b/islands/events/components/delete.tsx @@ -12,18 +12,20 @@ const Deletion = ({ open, routeTo, children, + customMsg, }: { fetch: () => Promise; - routeTo: string; + routeTo?: string; name: string; open: Signal; children: ComponentChildren; + customMsg?: string; }) => { const [loading, setLoading] = useState(false); const deleteEvent = async () => { setLoading(true); - setLoading(false); + const res = await fetch(); const data = await res.json(); if (data.error) { @@ -31,8 +33,11 @@ const Deletion = ({ } else if (!res.ok) { setLoading("An unknown error occurred"); } else { - window.location.href = routeTo; + if (routeTo) { + location.href = routeTo; + } } + setLoading(false); }; const DeleteUI = () => { @@ -44,8 +49,8 @@ const Deletion = ({ Delete {name}

- Are you sure you want to delete this {name}? This action is - irrevocable and cannot be undone! + {customMsg || `Are you sure you want to delete this ${name}? This action is + irrevocable and cannot be undone!`}

{ + const formStates: { id: string, value: unknown }[] = []; + + for (const [key, value] of [...Object.entries(toggles.value), ...Object.entries(formState)]) { + if (isUUID(key)) { + formStates.push({ id: key, value }); + } + } + const fullTicket = { ...formState, - ...toggles.value, tickets: tickets.value, showtimeID: showTime.value, eventID, - fieldData: [], + fieldData: formStates, }; error.value = undefined; ticketID.value = "loading"; diff --git a/routes/api/auth/regen.ts b/routes/api/auth/regen.ts index 76be24e..6f30bf0 100644 --- a/routes/api/auth/regen.ts +++ b/routes/api/auth/regen.ts @@ -15,18 +15,22 @@ export const handler: Handlers = { const user = await getUser(req); if (user == undefined) { - deleteCookie(req.headers, "authToken"); - return new Response(JSON.stringify({ error: "User not found" }), { + const resp = new Response(JSON.stringify({ error: "User not found" }), { status: 400, }); + + deleteCookie(resp.headers, "authToken"); + return resp; } await generateAuthToken(user.email, true); - deleteCookie(req.headers, "authToken"); - - return new Response(JSON.stringify({ status: 200 }), { + const resp = new Response(JSON.stringify({ status: 200 }), { status: 200, }); + + deleteCookie(resp.headers, "authToken"); + + return resp; }, }; diff --git a/routes/api/events/scan.ts b/routes/api/events/scan.ts index 4b16c3f..7301e7c 100644 --- a/routes/api/events/scan.ts +++ b/routes/api/events/scan.ts @@ -100,8 +100,10 @@ export const handler: Handlers = { const isUsed = ticket.value.hasBeenUsed; - if (!isUsed) { + if (!isUsed || ticket.value.tickets > 0) { ticket.value.hasBeenUsed = true; + ticket.value.uses += 1; + await kv.set( [ "ticket", diff --git a/routes/api/events/ticket/index.ts b/routes/api/events/ticket/index.ts index 0495630..949d82a 100644 --- a/routes/api/events/ticket/index.ts +++ b/routes/api/events/ticket/index.ts @@ -13,8 +13,23 @@ import { EventRegisterError } from "@/utils/event/register.ts"; export const handler: Handlers = { async POST(req) { - const { eventID, email, showtimeID, fieldData, firstName, lastName } = - await req.json(); + const { + eventID, + email, + showtimeID, + fieldData, + firstName, + lastName, + tickets, + }: { + eventID: string; + email: string; + showtimeID: string; + fieldData: { id: string; value: string }[]; + firstName: string; + lastName: string; + tickets: number; + } = await req.json(); const basicParamValidation = Yup.object({ eventID: Yup.string().uuid().required(), @@ -23,6 +38,7 @@ export const handler: Handlers = { firstName: Yup.string().required().min(1), lastName: Yup.string().required().min(1), fieldData: Yup.array().required(), + tickets: Yup.number().required().min(1).max(10), }); try { @@ -34,6 +50,7 @@ export const handler: Handlers = { firstName, lastName, fieldData, + tickets, }, { strict: true, @@ -66,6 +83,114 @@ export const handler: Handlers = { ); } + if ( + fieldData.length != event.value.additionalFields.length || + fieldData + .map((f) => f.id) + .some( + (id) => !event.value.additionalFields.map((f) => f.id).includes(id), + ) + ) { + return new Response( + JSON.stringify({ + error: { + message: "Invalid field data", + code: EventRegisterError.OTHER, + }, + }), + { + status: 400, + }, + ); + } + + for (const field of event.value.additionalFields) { + if ( + fieldData.find((f) => f.id === field.id)?.value == undefined && + (field.required ?? true) == true + ) { + return new Response( + JSON.stringify({ + error: { + message: "Missing required field", + code: EventRegisterError.OTHER, + }, + }), + { + status: 400, + }, + ); + } + + if (fieldData.find((f) => f.id === field.id)?.value == undefined) { + continue; + } + + const value = fieldData.find((f) => f.id === field.id); + + const defaultYupSchema = { + id: Yup.string().uuid().required(), + }; + + let schema = Yup.object({ + ...defaultYupSchema, + }); + + switch (field.type) { + case "text": { + schema = Yup.object({ + ...defaultYupSchema, + value: Yup.string().required(), + }); + + break; + } + + case "toggle": { + schema = Yup.object({ + ...defaultYupSchema, + value: Yup.boolean().nonNullable(), + }); + + break; + } + + case "email": { + schema = Yup.object({ + ...defaultYupSchema, + value: Yup.string().email().required(), + }); + + break; + } + + case "number": { + schema = Yup.object({ + ...defaultYupSchema, + value: Yup.number().required(), + }); + + break; + } + } + + try { + schema.validateSync(value, { strict: true }); + } catch (e) { + return new Response( + JSON.stringify({ + error: { + message: e.message, + code: EventRegisterError.OTHER, + }, + }), + { + status: 400, + }, + ); + } + } + const showtime = event.value.showTimes.find((s) => s.id === showtimeID); if (showtime == undefined) { @@ -119,14 +244,15 @@ export const handler: Handlers = { const user = eventUser.value ?? ({ - onboarded: false, tickets: [], events: [], plan: Plan.BASIC, email, + joinedAt: Date.now().toString(), + authToken: "unregistered", } satisfies User); - if (user.onboarded) { + if (user.authToken != "unregistered") { const authToken = getUserAuthToken(req); if (authToken != user.authToken) { @@ -219,6 +345,8 @@ export const handler: Handlers = { firstName, lastName, fieldData: fieldData, + tickets, + uses: 0, }) .commit(); diff --git a/routes/user/settings/_layout.tsx b/routes/user/settings/_layout.tsx index 75f3120..fa4a7f1 100644 --- a/routes/user/settings/_layout.tsx +++ b/routes/user/settings/_layout.tsx @@ -1,14 +1,67 @@ import { defineLayout } from "$fresh/server.ts"; +import { ComponentChildren } from "preact"; +import LockAccess from "$tabler/lock-access.tsx"; +import Home from "$tabler/home.tsx"; +import { getUser } from "@/utils/db/kv.ts"; -export default defineLayout((req, ctx) => { - return ( -
-
- s -
-
- -
-
) - ; +export default defineLayout(async (req, ctx) => { + const user = await getUser(req); + const route = new URL(req.url).pathname; + + if (user == undefined) { + return new Response(undefined, { + headers: { + Location: "/login", + }, + status: 307, + }); + } + + const pages: { name: string; icon: ComponentChildren; route: string }[] = [ + { + name: "Home", + icon: , + route: "/user/settings", + }, + { + name: "Authentication", + icon: , + route: "/user/settings/auth", + }, + ]; + + + + return ( + <> +
+
+

Settings

+
+ {pages.map((page) => ( + + {page.icon} +

{page.name}

+
+ + ))} +
+
+
+ +
+
+ + ); }); diff --git a/routes/user/settings/auth.tsx b/routes/user/settings/auth.tsx new file mode 100644 index 0000000..f5284de --- /dev/null +++ b/routes/user/settings/auth.tsx @@ -0,0 +1,14 @@ +import DeleteToken from "@/islands/components/pieces/deleteToken.tsx"; + + +const Settings = () => { + return ( +
+

Delete Authentication Token

+

Resetting your authentication token can enhance your account security by invalidating any compromised tokens or connections, reducing the risk of unauthorized access to your sensitive information or resources. Additionally, it provides a fresh start, ensuring that only authorized devices or applications can access your account going forward.

+ +
+ ) +} + +export default Settings; \ No newline at end of file diff --git a/routes/user/settings/authentication.ts b/routes/user/settings/authentication.ts deleted file mode 100644 index e69de29..0000000 diff --git a/routes/user/settings/index.tsx b/routes/user/settings/index.tsx index c871266..23880b4 100644 --- a/routes/user/settings/index.tsx +++ b/routes/user/settings/index.tsx @@ -1,7 +1,7 @@ const Settings = () => { return (
- test + Coming Soon
) } diff --git a/utils/db/kv.ts b/utils/db/kv.ts index 7de1557..ee4d84f 100644 --- a/utils/db/kv.ts +++ b/utils/db/kv.ts @@ -60,7 +60,6 @@ export const createUser = async (email: string) => { authToken: userAuthToken, events: [], tickets: [], - onboarded: true, plan: Plan.BASIC, joinedAt: Date.now().toString(), }; diff --git a/utils/db/kv.types.ts b/utils/db/kv.types.ts index 809fcbc..a4441e6 100644 --- a/utils/db/kv.types.ts +++ b/utils/db/kv.types.ts @@ -2,6 +2,7 @@ export interface Field { id: string; name: string; description: string; + required?: boolean; type: "text" | "email" | "number" | "toggle"; } @@ -84,6 +85,8 @@ export interface Ticket { firstName: string; lastName: string; fieldData: FieldEntry[]; + tickets: number; + uses: number; } /** ["user", email] */ @@ -95,7 +98,6 @@ export interface User { authToken: string; plan: Plan; joinedAt: string; - onboarded: true; } export const enum Roles {