diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx index 7b833565..5ed36fee 100644 --- a/app/(auth)/sign-up/page.tsx +++ b/app/(auth)/sign-up/page.tsx @@ -1,21 +1,30 @@ 'use client'; -import { useForm, useWatch, SubmitHandler } from 'react-hook-form'; +import { useForm, useWatch, SubmitHandler, Controller } from 'react-hook-form'; import { createClient } from '@/app/lib/supabase/browser-client'; -import { useState } from 'react'; +import { forwardRef, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; import { Form } from 'react-bootstrap'; +import type { FormControlProps } from 'react-bootstrap'; +import { PatternFormat } from 'react-number-format'; + type Inputs = { firstName: string; lastName: string; email: string; + phone: string; password: string; passwordConfirmation: string; }; import pathLogo from '@/public/path.png'; +const BootstrapInput = forwardRef( + (props, ref) => , +); +BootstrapInput.displayName = 'BootstrapInput'; + export default function SignUpPage() { const { register, @@ -40,6 +49,7 @@ export default function SignUpPage() { data: { first_name: formData.firstName, last_name: formData.lastName, + phone: formData.phone, }, }, }); @@ -119,6 +129,40 @@ export default function SignUpPage() { {errors.email?.message} + + { + const digits = value?.replace(/\D/g, ''); + return ( + !digits || + digits.length === 10 || + 'Phone number must be 10 digits' + ); + }, + }} + render={({ field }) => ( + { + field.onChange(values.value); + }} + customInput={BootstrapInput} + isInvalid={!!errors.phone} + className="auth-field" + /> + )} + /> + + {errors.phone?.message} + + =0.10.0" } @@ -6365,7 +6354,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7205,7 +7193,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7368,7 +7355,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7712,7 +7698,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/supabase/migrations/20260515042016_add_phone_to_users.sql b/supabase/migrations/20260515042016_add_phone_to_users.sql new file mode 100644 index 00000000..d0ad5676 --- /dev/null +++ b/supabase/migrations/20260515042016_add_phone_to_users.sql @@ -0,0 +1,223 @@ +alter table "public"."users" add column "phone" text; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION private.can_assign_role(new_role_id integer) + RETURNS boolean + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +declare + is_owner boolean; + is_superadmin boolean; + is_admin boolean; + new_role_name text; +begin + -- Retrieve the name of the role being assigned + select name + from public.roles + where role_id = new_role_id + into new_role_name; + + -- Determine if the current user has the 'owner' role + select (auth.jwt() ->> 'user_role') = 'owner' into is_owner; + + -- If the current user is an owner and the new role is one of the allowed roles, return true + if is_owner and new_role_name in ('superadmin', 'admin', 'requestor', 'default') then + return true; + end if; + + -- Determine if the current user has the 'superadmin' role + select (auth.jwt() ->> 'user_role') = 'superadmin' into is_superadmin; + + -- If the current user is a superadmin and the new role is one of the allowed roles, return true + if is_superadmin and new_role_name in ('admin', 'requestor', 'default') then + return true; + end if; + + -- Determine if the current user has the 'admin' role + select (auth.jwt() ->> 'user_role') = 'admin' into is_admin; + + -- If the current user is an admin and the new role is one of the allowed roles, return true + if is_admin and new_role_name in ('requestor', 'default') then + return true; + end if; + + return false; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION private.can_manage_store(store_to_manage_id uuid) + RETURNS boolean + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +declare + -- Declare variables + is_owner boolean; + is_superadmin boolean; + +begin + -- Determine if current user is 'superadmin' or 'owner', return true + select (auth.jwt() ->> 'user_role') = 'owner' into is_owner; + select (auth.jwt() ->> 'user_role') = 'superadmin' into is_superadmin; + + if is_owner or is_superadmin then + return true; + end if; + + -- If current user is a store admin, return true + if exists ( + select 1 + from public.store_admins + where store_to_manage_id = store_id and auth.uid() = user_id + ) then + return true; + end if; + return false; + +end; +$function$ +; + +CREATE OR REPLACE FUNCTION private.handle_new_store_admin() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +declare + current_role_name text; +begin + select r.name into current_role_name + from public.user_roles ur + join public.roles r on ur.role_id = r.role_id + where ur.user_id = new.user_id; + + if current_role_name in ('default', 'requestor') then + update public.user_roles + set role_id = 3 + where user_id = new.user_id; + end if; + return new; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION private.handle_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +begin + + insert into public.users (user_id, first_name, last_name, email, phone) + values ( + new.id, + new.raw_user_meta_data ->> 'first_name', + new.raw_user_meta_data ->> 'last_name', + new.email, + new.raw_user_meta_data ->> 'phone' + ); + + insert into public.user_roles(user_id, role_id) + values ( + new.id, + case + when right(new.email, 9) = 'epath.org' then 2 + else 1 + end + ); + + return new; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION private.handle_user_email_update() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +declare +begin + update public.users + set email = new.email + where user_id = new.id; + return new; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION private.handle_user_role_update() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +declare + updated_role_name text; +begin + select r.name into updated_role_name + from public.roles r + where r.role_id = new.role_id; + + if updated_role_name in ('default', 'requestor') then + delete from public.store_admins + where user_id = new.user_id; + end if; + return new; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION public.can_manage_store(store_to_manage_id uuid) + RETURNS boolean + LANGUAGE sql + SECURITY DEFINER + SET search_path TO '' +AS $function$ + select private.can_manage_store(store_to_manage_id); +$function$ +; + +CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb) + RETURNS jsonb + LANGUAGE plpgsql + STABLE + SET search_path TO 'public' +AS $function$ + declare + claims jsonb; + user_role text; + begin + select r.name into user_role + from public.user_roles ur + join public.roles r on ur.role_id = r.role_id + where ur.user_id = (event ->> 'user_id')::uuid + limit 1; + + claims := event -> 'claims'; + + if user_role is not null then + -- Set the claim + claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role)); + else + claims := jsonb_set(claims, '{user_role}', 'null'::jsonb); + end if; + + -- Update the claims object in the original event + event := jsonb_set(event, '{claims}', claims); + + -- Return the modified or original event + return event; + end; +$function$ +; + + diff --git a/supabase/schemas/functions/handle_new_user.sql b/supabase/schemas/functions/handle_new_user.sql index 0ad836d4..072fe008 100644 --- a/supabase/schemas/functions/handle_new_user.sql +++ b/supabase/schemas/functions/handle_new_user.sql @@ -5,12 +5,13 @@ set search_path = '' as $$ begin - insert into public.users (user_id, first_name, last_name, email) + insert into public.users (user_id, first_name, last_name, email, phone) values ( new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name', - new.email + new.email, + new.raw_user_meta_data ->> 'phone' ); insert into public.user_roles(user_id, role_id) diff --git a/supabase/schemas/users.sql b/supabase/schemas/users.sql index 77675b69..b345dd13 100644 --- a/supabase/schemas/users.sql +++ b/supabase/schemas/users.sql @@ -9,6 +9,7 @@ create table users ( ) stored, email text, profile_photo_url text, + phone text, constraint fk_auth_users foreign key (user_id) references auth.users (id) on delete cascade ); diff --git a/supabase/seeds/auth.sql b/supabase/seeds/auth.sql index ed6701b7..72f954ce 100644 --- a/supabase/seeds/auth.sql +++ b/supabase/seeds/auth.sql @@ -30,7 +30,7 @@ insert into current_timestamp, '{"provider":"email","providers":["email"]}', ( - '{"first_name": "First' || i || '", "last_name": "Last' || i || '"}' + '{"first_name": "First' || i || '", "last_name": "Last' || i || '", "phone": "(000) 000-000' || i || '"}' )::jsonb, current_timestamp, current_timestamp, @@ -74,7 +74,9 @@ values current_timestamp, current_timestamp, '{"provider":"email","providers":["email"]}', - ('{"first_name": "First9", "last_name": "Last9"}')::jsonb, + ( + '{"first_name": "First9", "last_name": "Last9", "phone": "(000) 000-0009"}' + )::jsonb, current_timestamp, current_timestamp, '', @@ -94,7 +96,7 @@ values current_timestamp, '{"provider":"email","providers":["email"]}', ( - '{"first_name": "First10", "last_name": "Last10"}' + '{"first_name": "First10", "last_name": "Last10", "phone": "(000) 000-0010"}' )::jsonb, current_timestamp, current_timestamp,