From 5bd95d6427887e23daebaad09747edf0c1c29629 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 6 Jun 2024 23:30:00 +0100 Subject: [PATCH] chore: complete tutorial run-through --- app/create/VoteForm.tsx | 13 + app/edit/[id]/EditVoteForm.tsx | 13 + app/layout.tsx | 3 + app/vote/components/Info.tsx | 11 + app/webhooks/vote/route.ts | 225 ++++++++++ components/phone/phone-number-dropdown.tsx | 85 ++++ components/phone/register-form.tsx | 101 +++++ components/phone/register-phone.tsx | 106 +++++ components/phone/verify-form.tsx | 79 ++++ lib/actions/vote.ts | 5 + lib/hook/index.ts | 49 ++- lib/types/index.ts | 1 + lib/types/supabase.ts | 416 +++++++++--------- lib/utils.ts | 14 + package-lock.json | 291 +++++++++++- package.json | 3 + ...20240606191502_add_phone_number_voting.sql | 69 +++ 17 files changed, 1256 insertions(+), 228 deletions(-) create mode 100644 app/webhooks/vote/route.ts create mode 100644 components/phone/phone-number-dropdown.tsx create mode 100644 components/phone/register-form.tsx create mode 100644 components/phone/register-phone.tsx create mode 100644 components/phone/verify-form.tsx create mode 100644 supabase/migrations/20240606191502_add_phone_number_voting.sql diff --git a/app/create/VoteForm.tsx b/app/create/VoteForm.tsx index 231493c..5d51789 100644 --- a/app/create/VoteForm.tsx +++ b/app/create/VoteForm.tsx @@ -31,6 +31,9 @@ import { createVote } from "@/lib/actions/vote"; import { Textarea } from "../../components/ui/textarea"; import { IVoteOptions } from "@/lib/types"; +import { useAvailablePhoneNumbers } from "@/lib/hook"; +import PhoneNumberDropdown from "@/components/phone/phone-number-dropdown"; + const FormSchema = z .object({ vote_options: z @@ -43,6 +46,7 @@ const FormSchema = z .min(5, { message: "Title has a minimum characters of 5" }), description: z.string().optional(), end_date: z.date(), + phone_number: z.string(), }) .refine( (data) => { @@ -62,6 +66,7 @@ export default function VoteForm() { defaultValues: { title: "", vote_options: [], + phone_number: "", }, }); @@ -105,6 +110,8 @@ export default function VoteForm() { }); } + const { data: availablePhoneNumbers } = useAvailablePhoneNumbers(); + return (
@@ -256,6 +263,12 @@ export default function VoteForm() { )} /> + + + + + + + Not enabled + + {phoneNumbers && + phoneNumbers.map((number) => ( + + {number.displayNumber} + + ))} + + + + + )} + + + + )} + /> + ); +} diff --git a/components/phone/register-form.tsx b/components/phone/register-form.tsx new file mode 100644 index 0000000..d5dd645 --- /dev/null +++ b/components/phone/register-form.tsx @@ -0,0 +1,101 @@ +"use client"; + +import React from "react"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { parsePhoneNumber, toStoredPhoneNumberFormat } from "@/lib/utils"; + +const RegisterFormSchema = z + .object({ + phone_number: z.string(), + }) + .transform((val, ctx) => { + const { phone_number, ...rest } = val; + try { + const phoneNumber = parsePhoneNumber(phone_number); + + if (!phoneNumber?.isValid()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["phone_number"], + message: `is not a phone number`, + }); + return z.NEVER; + } + } catch (e) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["phone_number"], + message: `could not be parsed as a phone number`, + }); + return z.NEVER; + } + + return { ...rest, phone_number: toStoredPhoneNumberFormat(phone_number) }; + }); + +export type RegistrationSubmitHandler = ({ + phoneNumber, +}: { + phoneNumber: string; +}) => void; + +export type RegisterFormProps = { + onSubmit: RegistrationSubmitHandler; +}; + +export default function RegisterForm({ onSubmit }: RegisterFormProps) { + const registerForm = useForm>({ + mode: "onSubmit", + resolver: zodResolver(RegisterFormSchema), + defaultValues: { + phone_number: "", + }, + }); + + function onRegisterSubmit(data: z.infer) { + onSubmit({ phoneNumber: data.phone_number }); + } + + return ( + + + ( + + Phone Number + + + + + + + )} + /> + + + + + ); +} diff --git a/components/phone/register-phone.tsx b/components/phone/register-phone.tsx new file mode 100644 index 0000000..6b4cba3 --- /dev/null +++ b/components/phone/register-phone.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Alert, AlertTitle } from "../ui/alert"; +import { AlertCircle } from "lucide-react"; + +import { useState, useEffect } from "react"; + +import { createSupabaseBrowser } from "@/lib/supabase/client"; +import { User, VerifyMobileOtpParams } from "@supabase/supabase-js"; + +import RegisterForm, { RegistrationSubmitHandler } from "./register-form"; +import VerifyForm, { VerificationSubmitHandler } from "./verify-form"; + +import toast from "react-hot-toast"; + +enum VerifySteps { + REGISTER, + VERIFY, + SUCCESS, + ERROR, +} + +export default function RegisterPhone() { + const [step, setStep] = useState(VerifySteps.REGISTER); + const [user, setUser] = useState(null); + const [phoneNumber, setPhoneNumber] = useState(); + + const supabase = createSupabaseBrowser(); + + useEffect(() => { + const checkUser = async () => { + setUser((await supabase.auth.getUser()).data.user); + }; + checkUser(); + }, [supabase.auth]); + + const handleRegisterSubmit: RegistrationSubmitHandler = async ({ + phoneNumber, + }) => { + setPhoneNumber(phoneNumber); + + const { error } = await supabase.auth.updateUser({ + phone: phoneNumber, + }); + + if (error) { + console.error(error); + setStep(VerifySteps.ERROR); + } else { + setStep(VerifySteps.VERIFY); + } + }; + + const handleVerifySubmit: VerificationSubmitHandler = ({ + phoneNumber, + code, + }) => { + const otpParams: VerifyMobileOtpParams = { + phone: phoneNumber!, + token: code, + type: "phone_change", + }; + + const verify = async () => { + const { error } = await supabase.auth.verifyOtp(otpParams); + + if (error) { + console.error(error); + setStep(VerifySteps.ERROR); + } else { + setStep(VerifySteps.SUCCESS); + } + }; + + toast.promise(verify(), { + loading: "Verifying code...", + success: "Successfully verified", + error: "Fail to verify", + }); + }; + + const displayPrompt = user && !user.phone && step !== VerifySteps.SUCCESS; + + return ( + <> + {displayPrompt && ( + + + + Register your phone number to vote via SMS + + {step === VerifySteps.REGISTER && ( + + )} + {step === VerifySteps.VERIFY && ( + + )} + {step === VerifySteps.ERROR && Something went wrong} + + )} + + ); +} diff --git a/components/phone/verify-form.tsx b/components/phone/verify-form.tsx new file mode 100644 index 0000000..1290a65 --- /dev/null +++ b/components/phone/verify-form.tsx @@ -0,0 +1,79 @@ +"use client"; + +import React from "react"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; + +const VerifyFormSchema = z.object({ code: z.string() }); + +export type VerificationSubmitHandler = ({ + phoneNumber, + code, +}: { + phoneNumber: string; + code: string; +}) => void; + +export type VerifyFormProps = { + phoneNumber: string; + onSubmit: VerificationSubmitHandler; +}; + +export default function VerifyForm({ phoneNumber, onSubmit }: VerifyFormProps) { + const verifyForm = useForm>({ + mode: "onSubmit", + resolver: zodResolver(VerifyFormSchema), + defaultValues: { + code: "", + }, + }); + + async function onVerifySubmit(data: z.infer) { + onSubmit({ phoneNumber, code: data.code }); + } + + return ( +
+ + ( + + Verification Code + + + + + + + )} + /> + + + + + ); +} diff --git a/lib/actions/vote.ts b/lib/actions/vote.ts index 41abd5d..31221da 100644 --- a/lib/actions/vote.ts +++ b/lib/actions/vote.ts @@ -30,6 +30,7 @@ export async function createVote(data: { end_date: Date; title: string; description?: string; + phone_number?: string; }) { const supabase = await createSupabaseServer(); @@ -38,6 +39,7 @@ export async function createVote(data: { title: data.title, end_date: new Date(data.end_date).toISOString(), description: data.description || "", + phone_number: data.phone_number || "", }); if (error) { @@ -61,6 +63,7 @@ export async function updateVoteById( end_date: Date; description?: string; title: string; + phone_number: string; }, voteId: string, ) { @@ -71,11 +74,13 @@ export async function updateVoteById( title: data.title, end_date: data.end_date.toISOString(), description: data.description, + phone_number: data.phone_number, }) .eq("id", voteId); if (error) { throw error.message; } + revalidatePath("/vote/" + voteId); return redirect("/vote/" + voteId); } diff --git a/lib/hook/index.ts b/lib/hook/index.ts index 0d7a849..133c352 100644 --- a/lib/hook/index.ts +++ b/lib/hook/index.ts @@ -1,6 +1,10 @@ import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { createSupabaseBrowser } from "../supabase/client"; -import { sortVoteOptionsBy } from "../utils"; +import { + sortVoteOptionsBy, + toDisplayedPhoneNumberFormat, + toStoredPhoneNumberFormat, +} from "../utils"; import { IComment } from "../types"; export function useGetVote(id: string) { @@ -57,3 +61,46 @@ export function useComment(voteId: string) { staleTime: Infinity, }); } + +const configuredPhoneNumbers = process.env.NEXT_PUBLIC_PHONE_NUMBERS + ? process.env.NEXT_PUBLIC_PHONE_NUMBERS.split(",") + : []; + +export type FormattedNumber = { + e164: string; + displayNumber: string; +}; + +export function useAvailablePhoneNumbers(): UseQueryResult< + FormattedNumber[] +> { + const supabase = createSupabaseBrowser(); + + return useQuery({ + queryKey: ["available-phone-numbers"], + queryFn: async () => { + // Get phone numbers used in all active votes + const { error, data } = await supabase + .from("vote") + .select("phone_number") + .filter("end_date", "gte", new Date().toISOString()); + + if (error) { + console.error(error); + throw new Error("Failed to fetch phone numbers"); + } + const usedPhoneNumbers = data.map((row) => row.phone_number); + + const availableNumbers = configuredPhoneNumbers + .filter((tel) => !usedPhoneNumbers.includes(tel)) + .map((tel) => { + return { + e164: toStoredPhoneNumberFormat(tel), + displayNumber: toDisplayedPhoneNumberFormat(tel), + }; + }); + + return availableNumbers; + }, + }); +} diff --git a/lib/types/index.ts b/lib/types/index.ts index 0fbf7d7..9557423 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -13,6 +13,7 @@ export type IVote = { end_date: string; id: string; title: string; + phone_number: string | null; }; export type IComment = { diff --git a/lib/types/supabase.ts b/lib/types/supabase.ts index e3fc7cd..91bd5fa 100644 --- a/lib/types/supabase.ts +++ b/lib/types/supabase.ts @@ -4,264 +4,264 @@ export type Json = | boolean | null | { [key: string]: Json | undefined } - | Json[]; + | Json[] export type Database = { public: { Tables: { comments: { Row: { - created_at: string; - id: string; - is_edit: boolean; - send_by: string; - text: string; - vote_id: string; - }; + created_at: string + id: string + is_edit: boolean + send_by: string + text: string + vote_id: string + } Insert: { - created_at?: string; - id?: string; - is_edit?: boolean; - send_by?: string; - text: string; - vote_id: string; - }; + created_at?: string + id?: string + is_edit?: boolean + send_by?: string + text: string + vote_id: string + } Update: { - created_at?: string; - id?: string; - is_edit?: boolean; - send_by?: string; - text?: string; - vote_id?: string; - }; + created_at?: string + id?: string + is_edit?: boolean + send_by?: string + text?: string + vote_id?: string + } Relationships: [ { - foreignKeyName: "comments_vote_id_fkey"; - columns: ["vote_id"]; - isOneToOne: false; - referencedRelation: "vote"; - referencedColumns: ["id"]; + foreignKeyName: "comments_vote_id_fkey" + columns: ["vote_id"] + isOneToOne: false + referencedRelation: "vote" + referencedColumns: ["id"] }, { - foreignKeyName: "public_comments_send_by_fkey"; - columns: ["send_by"]; - isOneToOne: false; - referencedRelation: "users"; - referencedColumns: ["id"]; + foreignKeyName: "public_comments_send_by_fkey" + columns: ["send_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, - ]; - }; + ] + } profile: { Row: { - avatar_url: string | null; - created_at: string; - email: string | null; - full_name: string | null; - id: string; - phone: string | null; - updated_at: string; - }; + avatar_url: string | null + created_at: string + email: string | null + full_name: string | null + id: string + phone: string | null + updated_at: string + } Insert: { - avatar_url?: string | null; - created_at?: string; - email?: string | null; - full_name?: string | null; - id: string; - phone?: string | null; - updated_at?: string; - }; + avatar_url?: string | null + created_at?: string + email?: string | null + full_name?: string | null + id: string + phone?: string | null + updated_at?: string + } Update: { - avatar_url?: string | null; - created_at?: string; - email?: string | null; - full_name?: string | null; - id?: string; - phone?: string | null; - updated_at?: string; - }; + avatar_url?: string | null + created_at?: string + email?: string | null + full_name?: string | null + id?: string + phone?: string | null + updated_at?: string + } Relationships: [ { - foreignKeyName: "profile_id_fkey"; - columns: ["id"]; - isOneToOne: true; - referencedRelation: "users"; - referencedColumns: ["id"]; + foreignKeyName: "profile_id_fkey" + columns: ["id"] + isOneToOne: true + referencedRelation: "users" + referencedColumns: ["id"] }, - ]; - }; + ] + } vote: { Row: { - created_at: string; - created_by: string; - description: string | null; - end_date: string; - id: string; - title: string; - }; + created_at: string + created_by: string + description: string | null + end_date: string + id: string + phone_number: string | null + title: string + } Insert: { - created_at?: string; - created_by: string; - description?: string | null; - end_date: string; - id?: string; - title: string; - }; + created_at?: string + created_by: string + description?: string | null + end_date: string + id?: string + phone_number?: string | null + title: string + } Update: { - created_at?: string; - created_by?: string; - description?: string | null; - end_date?: string; - id?: string; - title?: string; - }; + created_at?: string + created_by?: string + description?: string | null + end_date?: string + id?: string + phone_number?: string | null + title?: string + } Relationships: [ { - foreignKeyName: "public_vote_created_by_fkey"; - columns: ["created_by"]; - isOneToOne: false; - referencedRelation: "users"; - referencedColumns: ["id"]; + foreignKeyName: "public_vote_created_by_fkey" + columns: ["created_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, - ]; - }; + ] + } vote_log: { Row: { - created_at: string; - id: string; - option: string; - user_id: string; - vote_id: string; - }; + created_at: string + id: string + option: string + user_id: string + vote_id: string + } Insert: { - created_at?: string; - id?: string; - option: string; - user_id: string; - vote_id: string; - }; + created_at?: string + id?: string + option: string + user_id: string + vote_id: string + } Update: { - created_at?: string; - id?: string; - option?: string; - user_id?: string; - vote_id?: string; - }; + created_at?: string + id?: string + option?: string + user_id?: string + vote_id?: string + } Relationships: [ { - foreignKeyName: "vote_log_vote_id_fkey"; - columns: ["vote_id"]; - isOneToOne: false; - referencedRelation: "vote"; - referencedColumns: ["id"]; + foreignKeyName: "vote_log_vote_id_fkey" + columns: ["vote_id"] + isOneToOne: false + referencedRelation: "vote" + referencedColumns: ["id"] }, - ]; - }; + ] + } vote_options: { Row: { - options: Json; - vote_id: string; - }; + options: Json + vote_id: string + } Insert: { - options: Json; - vote_id: string; - }; + options: Json + vote_id: string + } Update: { - options?: Json; - vote_id?: string; - }; + options?: Json + vote_id?: string + } Relationships: [ { - foreignKeyName: "vote_options_vote_id_fkey"; - columns: ["vote_id"]; - isOneToOne: true; - referencedRelation: "vote"; - referencedColumns: ["id"]; + foreignKeyName: "vote_options_vote_id_fkey" + columns: ["vote_id"] + isOneToOne: true + referencedRelation: "vote" + referencedColumns: ["id"] }, - ]; - }; - }; + ] + } + } Views: { - [_ in never]: never; - }; + [_ in never]: never + } Functions: { create_vote: { Args: { - options: Json; - title: string; - end_date: string; - description: string; - }; - Returns: string; - }; + options: Json + title: string + end_date: string + description: string + phone_number?: string + } + Returns: string + } get_vote: { Args: { - target_vote: string; - }; + target_vote: string + } Returns: { - vote_columns: unknown; - vote_options_columns: unknown; - vote_log_columns: unknown; - }[]; - }; + vote_columns: unknown + vote_options_columns: unknown + vote_log_columns: unknown + }[] + } is_expired: { Args: { - vote_id: string; - }; - Returns: boolean; - }; + vote_id: string + } + Returns: boolean + } is_voted: { Args: { - target_id: string; - }; - Returns: boolean; - }; + target_id: string + } + Returns: boolean + } update_vote: { Args: { - update_id: string; - option: string; - }; - Returns: undefined; - }; - }; + update_id: string + option: string + } + Returns: undefined + } + } Enums: { - [_ in never]: never; - }; + [_ in never]: never + } CompositeTypes: { - [_ in never]: never; - }; - }; -}; + [_ in never]: never + } + } +} -type PublicSchema = Database[Extract]; +type PublicSchema = Database[Extract] export type Tables< PublicTableNameOrOptions extends | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof ( - & Database[PublicTableNameOrOptions["schema"]]["Tables"] - & Database[PublicTableNameOrOptions["schema"]]["Views"] - ) + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } ? ( - & Database[PublicTableNameOrOptions["schema"]]["Tables"] - & Database[PublicTableNameOrOptions["schema"]]["Views"] - )[TableName] extends { - Row: infer R; - } ? R - : never - : PublicTableNameOrOptions extends keyof ( - & PublicSchema["Tables"] - & PublicSchema["Views"] - ) ? ( - & PublicSchema["Tables"] - & PublicSchema["Views"] - )[PublicTableNameOrOptions] extends { - Row: infer R; - } ? R +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & + PublicSchema["Views"]) + ? (PublicSchema["Tables"] & + PublicSchema["Views"])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never : never - : never; export type TablesInsert< PublicTableNameOrOptions extends @@ -272,15 +272,17 @@ export type TablesInsert< : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Insert: infer I; - } ? I - : never + Insert: infer I + } + ? I + : never : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { - Insert: infer I; - } ? I + Insert: infer I + } + ? I + : never : never - : never; export type TablesUpdate< PublicTableNameOrOptions extends @@ -291,15 +293,17 @@ export type TablesUpdate< : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Update: infer U; - } ? U - : never + Update: infer U + } + ? U + : never : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { - Update: infer U; - } ? U + Update: infer U + } + ? U + : never : never - : never; export type Enums< PublicEnumNameOrOptions extends @@ -312,4 +316,4 @@ export type Enums< ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] ? PublicSchema["Enums"][PublicEnumNameOrOptions] - : never; + : never diff --git a/lib/utils.ts b/lib/utils.ts index f523eed..1051ec6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,10 +1,24 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { IVoteOptions } from "./types"; +import { parsePhoneNumber } from "libphonenumber-js"; + +export { parsePhoneNumber } from "libphonenumber-js"; + +export function toStoredPhoneNumberFormat(phoneNumber: string) { + const parseNumber = parsePhoneNumber(phoneNumber); + return parseNumber.format("E.164"); +} + +export function toDisplayedPhoneNumberFormat(phoneNumber: string) { + const parseNumber = parsePhoneNumber(phoneNumber); + return parseNumber.formatInternational(); +} export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + export function nextWeek() { let currentDate = new Date(); let nextWeekDate = new Date(); diff --git a/package-lock.json b/package-lock.json index f16140c..453c468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,8 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^2.30.0", + "fast-jwt": "^4.0.1", + "libphonenumber-js": "^1.11.3", "lucide-react": "^0.294.0", "next": "^14.2.3", "next-themes": "^0.2.1", @@ -38,6 +40,7 @@ "sharp": "^0.33.0", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", + "twilio": "^5.1.1", "uuid": "^9.0.1", "zod": "^3.22.4" }, @@ -803,6 +806,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "engines": { + "node": ">=8" + } + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -2279,6 +2290,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2297,8 +2319,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { "version": "10.4.16", @@ -2358,6 +2379,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -2395,6 +2426,11 @@ "node": ">=8" } }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2447,6 +2483,11 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2462,7 +2503,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -2644,7 +2684,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2733,11 +2772,15 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2760,7 +2803,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -2791,7 +2833,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -2858,6 +2899,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.601", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.601.tgz", @@ -3467,6 +3516,20 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-4.0.1.tgz", + "integrity": "sha512-+mdSoH0QdOdFSbbGBctJu7L1yfXRtbmjbVJ4W/PEjyvivobDena0RKwihtBkOML1P+kUJ1QuewnH8u+mROsR1w==", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.39.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -3563,6 +3626,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3592,7 +3674,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3684,7 +3765,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", @@ -3824,7 +3904,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3865,7 +3944,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.2" }, @@ -3877,7 +3955,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3889,7 +3966,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4445,6 +4521,27 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4460,6 +4557,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4500,6 +4616,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.3.tgz", + "integrity": "sha512-RU0CTsLCu2v6VEzdP+W6UU2n5+jEpMDRkGxUeBgsAJgre3vKgm17eApISH9OQY4G0jZYJVIc8qXmz6CJFueAFg==" + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -4528,12 +4649,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4588,7 +4744,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -4597,7 +4752,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -4605,6 +4759,11 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4735,11 +4894,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4949,7 +5115,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5054,6 +5219,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5376,6 +5546,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5389,7 +5564,6 @@ "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "dev": true, "dependencies": { "side-channel": "^1.0.4" }, @@ -5743,6 +5917,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -5757,6 +5950,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -5765,6 +5963,11 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -5783,7 +5986,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, "dependencies": { "define-data-property": "^1.1.1", "get-intrinsic": "^1.2.1", @@ -5872,7 +6074,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -6373,6 +6574,46 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/twilio": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.1.1.tgz", + "integrity": "sha512-YpOvpQM17UW72QxK5ukMN0RCY0DdEzI+hTTXxHHhlOtuvpP50JMY0NtkvUViWzZVPJSegJrZPjX43GqmhL/7aw==", + "dependencies": { + "axios": "^1.6.8", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/twilio/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/twilio/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6844,6 +7085,14 @@ } } }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "engines": { + "node": ">=6.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 2f8aae8..a1c0cb8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^2.30.0", + "fast-jwt": "^4.0.1", + "libphonenumber-js": "^1.11.3", "lucide-react": "^0.294.0", "next": "^14.2.3", "next-themes": "^0.2.1", @@ -40,6 +42,7 @@ "sharp": "^0.33.0", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", + "twilio": "^5.1.1", "uuid": "^9.0.1", "zod": "^3.22.4" }, diff --git a/supabase/migrations/20240606191502_add_phone_number_voting.sql b/supabase/migrations/20240606191502_add_phone_number_voting.sql new file mode 100644 index 0000000..c7de7a1 --- /dev/null +++ b/supabase/migrations/20240606191502_add_phone_number_voting.sql @@ -0,0 +1,69 @@ +alter table "public"."vote" add column "phone_number" text; + +DROP FUNCTION IF EXISTS "public"."create_vote"(options jsonb, title text, end_date timestamp without time zone, description text); + + +CREATE OR REPLACE FUNCTION "public"."create_vote"("options" "jsonb", "title" "text", "end_date" timestamp without time zone, "description" "text", "phone_number" "text" DEFAULT NULL::"text") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +DECLARE + return_id uuid; + options_count INT; + key_value_type text; + position_value_type text; + vote_count_value_type text; +BEGIN + + + SELECT COUNT(*) INTO options_count + FROM jsonb_object_keys(options); + + + IF options_count <= 1 THEN + RAISE EXCEPTION 'Options must have more than one key.'; + END IF; + + + -- Check if all values associated with keys are objects + SELECT jsonb_typeof(value) INTO key_value_type + FROM jsonb_each(options) + WHERE NOT jsonb_typeof(value) IN ('object'); + + + IF key_value_type IS NOT NULL THEN + RAISE EXCEPTION 'All values in options must be objects.'; + END IF; + + + -- Check if all positions are numbers + SELECT jsonb_typeof(value) INTO position_value_type + FROM jsonb_each(options::jsonb -> 'position') + WHERE NOT jsonb_typeof(value) IN ('number'); + + + IF position_value_type IS NOT NULL THEN + RAISE EXCEPTION 'All positions in options must be numbers.'; + END IF; + + + -- Check if all vote_count are numbers + SELECT jsonb_typeof(value) INTO vote_count_value_type + FROM jsonb_each(options::jsonb -> 'vote_count') + WHERE NOT jsonb_typeof(value) IN ('number'); + + + IF vote_count_value_type IS NOT NULL THEN + RAISE EXCEPTION 'All vote_count in options must be numbers.'; + END IF; + + + INSERT INTO vote (created_by, title, end_date, description, phone_number) + VALUES (auth.uid(),title, end_date, description, phone_number) + RETURNING id INTO return_id; + + + INSERT INTO vote_options (vote_id,options) + VALUES (return_id, options); + return return_id; +END $$; \ No newline at end of file