ekyc

Electronic KYC process with uploading ID document using OAuth 2.1 (experimental)
Log | Files | Refs | README | LICENSE

form.ts (3441B)


      1 import { AuthSessionUseCase } from "#core/application/authn/session.ts";
      2 import { AEAD } from "#core/domain/crypto.ts";
      3 import { deleteCookie, getCookies, setCookie } from "$std/http/cookie.ts";
      4 import * as V from "$valita";
      5 
      6 const aead = new AEAD();
      7 
      8 export interface FormContexts {
      9   "/connect": { back: Link };
     10   "/register/email": { email: string; back: Link };
     11   "/verify/email": { uuid: string; back: Link };
     12   "/login": { uuid: string; back: Link };
     13   "/logout": { back: Link };
     14   "/register/phone": { back: Link; conflict?: true };
     15   "/verify/sms": { phoneNumber: string; back: Link };
     16   "/register/id-document": {
     17     side: "doc-front" | "doc-back" | "face-left" | "face-front" | "face-right";
     18     back: Link;
     19   };
     20   "/verify/id-document": { cursor: number };
     21   "/oauth2/callback": { flowId: string; clientId: string };
     22 }
     23 
     24 export type Link =
     25   | string
     26   | {
     27     [K in keyof FormContexts]: { form: K; context: FormContexts[K] };
     28   }[keyof FormContexts];
     29 
     30 export class Forms {
     31   constructor(
     32     readonly base: URL,
     33     readonly session: {
     34       token: string;
     35       uuid: string;
     36     } | null,
     37   ) {}
     38 
     39   static async parse(request: Request, authenticate: AuthSessionUseCase) {
     40     const url = new URL(request.url);
     41     const cookies = getCookies(request.headers);
     42     if (!("session" in cookies)) {
     43       return new this(url, null);
     44     }
     45 
     46     const sessionToken = cookies.session;
     47     const result = await authenticate.execute({ sessionToken });
     48 
     49     if (result.status === "expired") {
     50       return new this(url, null);
     51     }
     52 
     53     return new this(url, {
     54       token: sessionToken,
     55       uuid: result.uuid!,
     56     });
     57   }
     58 
     59   link(link: Link, session: string | boolean = true) {
     60     if (typeof link === "string") {
     61       return new URL(link, this.base);
     62     }
     63 
     64     const url = new URL(link.form, this.base);
     65     url.searchParams.set(
     66       "state",
     67       aead.encrypt(
     68         JSON.stringify(link.context),
     69         `${link.form}#${
     70           session
     71             ? (typeof session === "string"
     72               ? session
     73               : (this.session?.token ?? ""))
     74             : ""
     75         }`,
     76       ),
     77     );
     78     return url;
     79   }
     80 
     81   redirect(link: Link) {
     82     return Response.redirect(this.link(link), 303);
     83   }
     84 
     85   redirectWithSession(link: Link, sessionToken: string) {
     86     const url = this.link(link, sessionToken);
     87     const headers = new Headers();
     88     headers.set("location", url.href);
     89     setCookie(headers, {
     90       name: "session",
     91       value: sessionToken,
     92       path: "/",
     93       httpOnly: true,
     94       sameSite: "Lax",
     95     });
     96     return new Response(null, { headers, status: 303 });
     97   }
     98 
     99   redirectWithoutSession(link: Link) {
    100     const url = this.link(link);
    101     const headers = new Headers();
    102     headers.set("location", url.href);
    103     deleteCookie(headers, "session", { path: "/" });
    104     return new Response(null, { headers, status: 303 });
    105   }
    106 
    107   context<F extends keyof FormContexts>(form: F): FormContexts[F] | null {
    108     try {
    109       return JSON.parse(aead.decrypt(
    110         `${this.base.searchParams.get("state") ?? ""}`,
    111         `${form}#${this.session?.token ?? ""}`,
    112       )) as FormContexts[F];
    113     } catch {
    114       return null;
    115     }
    116   }
    117 
    118   async inputs<T>(request: Request, type: V.Type<T>) {
    119     const formData = await request.formData();
    120     return type.parse(
    121       Object.fromEntries(
    122         Array.from(formData.entries())
    123           .map(([k, v]) => [k, `${v}`]),
    124       ),
    125       { mode: "strip" },
    126     );
    127   }
    128 }