Merge branch 'main' into B2B-30
This commit is contained in:
@@ -4,6 +4,8 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { useSupabase } from '@/packages/supabase/src/hooks/use-supabase';
|
||||
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
@@ -20,6 +22,7 @@ export function SignInMethodsContainer(props: {
|
||||
callback: string;
|
||||
joinTeam: string;
|
||||
returnPath: string;
|
||||
updateAccount: string;
|
||||
};
|
||||
|
||||
providers: {
|
||||
@@ -28,13 +31,14 @@ export function SignInMethodsContainer(props: {
|
||||
oAuth: Provider[];
|
||||
};
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const router = useRouter();
|
||||
|
||||
const redirectUrl = isBrowser()
|
||||
? new URL(props.paths.callback, window?.location.origin).toString()
|
||||
: '';
|
||||
|
||||
const onSignIn = () => {
|
||||
const onSignIn = async (userId?: string) => {
|
||||
// if the user has an invite token, we should join the team
|
||||
if (props.inviteToken) {
|
||||
const searchParams = new URLSearchParams({
|
||||
@@ -45,8 +49,28 @@ export function SignInMethodsContainer(props: {
|
||||
|
||||
router.replace(joinTeamPath);
|
||||
} else {
|
||||
// otherwise, we should redirect to the return path
|
||||
router.replace(props.paths.returnPath);
|
||||
if (!userId) {
|
||||
router.replace(props.paths.callback);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: hasPersonalCode } = await client.rpc(
|
||||
'has_personal_code',
|
||||
{
|
||||
account_id: userId,
|
||||
},
|
||||
);
|
||||
|
||||
if (hasPersonalCode) {
|
||||
router.replace(props.paths.returnPath);
|
||||
} else {
|
||||
router.replace(props.paths.updateAccount);
|
||||
}
|
||||
} catch {
|
||||
router.replace(props.paths.callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export function SignUpMethodsContainer(props: {
|
||||
paths: {
|
||||
callback: string;
|
||||
appHome: string;
|
||||
updateAccount: string;
|
||||
};
|
||||
|
||||
providers: {
|
||||
|
||||
225
packages/features/auth/src/components/update-account-form.tsx
Normal file
225
packages/features/auth/src/components/update-account-form.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { User } from '@supabase/supabase-js';
|
||||
|
||||
import { ExternalLink } from '@/public/assets/external-link';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Checkbox } from '@kit/ui/checkbox';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { UpdateAccountSchema } from '../schemas/update-account.schema';
|
||||
import { onUpdateAccount } from '../server/actions/update-account-actions';
|
||||
|
||||
export function UpdateAccountForm({ user }: { user: User }) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(UpdateAccountSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
personalCode: '',
|
||||
email: user.email,
|
||||
phone: '',
|
||||
city: '',
|
||||
weight: 0,
|
||||
height: 0,
|
||||
userConsent: false,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-6 px-6 pt-10 text-left"
|
||||
onSubmit={form.handleSubmit(onUpdateAccount)}
|
||||
>
|
||||
<FormField
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:firstName'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:lastName'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="personalCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:personalCode'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:email'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:phone'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:city'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-between gap-4">
|
||||
<FormField
|
||||
name="weight"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:weight'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="kg"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="height"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:height'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="cm"
|
||||
type="number"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
name="userConsent"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-row items-center gap-2 pb-1">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'account:updateAccount:userConsentLabel'} />
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={''}
|
||||
className="flex flex-row items-center gap-2 text-sm hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink />
|
||||
<Trans i18nKey={'account:updateAccount:userConsentUrlTitle'} />
|
||||
</Link>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button disabled={form.formState.isSubmitting} type="submit">
|
||||
<Trans i18nKey={'account:updateAccount:button'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
44
packages/features/auth/src/schemas/update-account.schema.ts
Normal file
44
packages/features/auth/src/schemas/update-account.schema.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateAccountSchema = z.object({
|
||||
firstName: z
|
||||
.string({
|
||||
required_error: 'First name is required',
|
||||
})
|
||||
.nonempty(),
|
||||
lastName: z
|
||||
.string({
|
||||
required_error: 'Last name is required',
|
||||
})
|
||||
.nonempty(),
|
||||
personalCode: z
|
||||
.string({
|
||||
required_error: 'Personal code is required',
|
||||
})
|
||||
.nonempty(),
|
||||
email: z.string().email({
|
||||
message: 'Email is required',
|
||||
}),
|
||||
phone: z
|
||||
.string({
|
||||
required_error: 'Phone number is required',
|
||||
})
|
||||
.nonempty(),
|
||||
city: z.string().optional(),
|
||||
weight: z
|
||||
.number({
|
||||
required_error: 'Weight is required',
|
||||
invalid_type_error: 'Weight must be a number',
|
||||
})
|
||||
.gt(0, { message: 'Weight must be greater than 0' }),
|
||||
|
||||
height: z
|
||||
.number({
|
||||
required_error: 'Height is required',
|
||||
invalid_type_error: 'Height must be a number',
|
||||
})
|
||||
.gt(0, { message: 'Height must be greater than 0' }),
|
||||
userConsent: z.boolean().refine((val) => val === true, {
|
||||
message: 'Must be true',
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
'use server';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
import { UpdateAccountSchema } from '../../schemas/update-account.schema';
|
||||
import { createAuthApi } from '../api';
|
||||
|
||||
export interface AccountSubmitData {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
personalCode: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
city?: string;
|
||||
weight: number | null;
|
||||
height: number | null;
|
||||
userConsent: boolean;
|
||||
}
|
||||
|
||||
export const onUpdateAccount = enhanceAction(
|
||||
async (params) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createAuthApi(client);
|
||||
|
||||
try {
|
||||
await api.updateAccount(params);
|
||||
console.log('SUCCESS', pathsConfig.auth.updateAccountSuccess);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.warn('On update account error: ' + err.message);
|
||||
}
|
||||
console.warn('On update account error: ', err);
|
||||
}
|
||||
redirect(pathsConfig.auth.updateAccountSuccess);
|
||||
},
|
||||
{
|
||||
schema: UpdateAccountSchema,
|
||||
},
|
||||
);
|
||||
93
packages/features/auth/src/server/api.ts
Normal file
93
packages/features/auth/src/server/api.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import { AccountSubmitData } from './actions/update-account-actions';
|
||||
|
||||
/**
|
||||
* Class representing an API for interacting with user accounts.
|
||||
* @constructor
|
||||
* @param {SupabaseClient<Database>} client - The Supabase client instance.
|
||||
*/
|
||||
class AuthApi {
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
/**
|
||||
* @name hasPersonalCode
|
||||
* @description Check if given account ID has added personal code.
|
||||
* @param id
|
||||
*/
|
||||
async hasPersonalCode(id: string) {
|
||||
const { data, error } = await this.client.rpc('has_personal_code', {
|
||||
account_id: id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name updateAccount
|
||||
* @description Update required fields for the account.
|
||||
* @param data
|
||||
*/
|
||||
async updateAccount(data: AccountSubmitData) {
|
||||
const {
|
||||
data: { user },
|
||||
} = await this.client.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const { error } = await this.client.rpc('update_account', {
|
||||
p_name: data.firstName,
|
||||
p_last_name: data.lastName,
|
||||
p_personal_code: data.personalCode,
|
||||
p_phone: data.phone || '',
|
||||
p_city: data.city || '',
|
||||
p_has_consent_personal_data: data.userConsent,
|
||||
p_uid: user.id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (data.height || data.weight) {
|
||||
await this.updateAccountParams(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name updateAccountParams
|
||||
* @description Update account parameters.
|
||||
* @param data
|
||||
*/
|
||||
async updateAccountParams(data: AccountSubmitData) {
|
||||
const {
|
||||
data: { user },
|
||||
} = await this.client.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
console.log('test', user, data);
|
||||
const response = await this.client.from('account_params').insert({
|
||||
account_id: user.id,
|
||||
height: data.height,
|
||||
weight: data.weight,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw response.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createAuthApi(client: SupabaseClient<Database>) {
|
||||
return new AuthApi(client);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './components/sign-up-methods-container';
|
||||
export * from './schemas/password-sign-up.schema';
|
||||
export * from './components/update-account-form';
|
||||
|
||||
Reference in New Issue
Block a user