Merge pull request #22 from MR-medreport/B2B-34

B2B-34: update account form
This commit is contained in:
danelkungla
2025-06-30 15:46:18 +03:00
committed by GitHub
54 changed files with 2347 additions and 3081 deletions

View File

@@ -1,10 +1,15 @@
# MedReport README
## Prerequisites ## Prerequisites
"node": ">=20.0.0",
"pnpm": ">=9.0.0" ```json
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
```
## Project structure ## Project structure
```
```text
/ app - pages / app - pages
/ components - custom components, helper components that not provided by any package. Place to extend an redefine components from packages / components - custom components, helper components that not provided by any package. Place to extend an redefine components from packages
/ config - bunch of configs, that are provided by starter kit. / config - bunch of configs, that are provided by starter kit.
@@ -25,12 +30,10 @@
/ supabase - primary supabase / supabase - primary supabase
/ tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config / tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config
/ utils / utils
``` ```
## Migration from old structure ## Migration from old structure
```bash ```bash
pnpm clean pnpm clean
pnpm i pnpm i
@@ -43,4 +46,27 @@ pnpm add <pacakge-name> -w
``` ```
## Supabase ## Supabase
TODO
Start supabase in docker
```bash
npm run supabase:start
```
Link your local supabase with a supabase project
```bash
npm run supabase:deploy
```
After editing supabase tables/functions etc update migration files
```bash
npm run supabase:db:diff
```
To update database types run:
```bash
npm run supabase:typegen:app
```

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'; import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-title'; import { MedReportLogo } from '@/components/med-report-logo';
import { ArrowRightIcon } from 'lucide-react'; import { ArrowRightIcon } from 'lucide-react';
import { CtaButton, Hero } from '@kit/ui/marketing'; import { CtaButton, Hero } from '@kit/ui/marketing';
@@ -49,7 +49,7 @@ function MainCallToActionButton() {
</CtaButton> </CtaButton>
<CtaButton variant={'link'}> <CtaButton variant={'link'}>
<Link href={'/register-company'}> <Link href={'/company-offer'}>
<Trans i18nKey={'account:createCompanyAccount'} /> <Trans i18nKey={'account:createCompanyAccount'} />
</Link> </Link>
</CtaButton> </CtaButton>

View File

@@ -4,12 +4,11 @@ import React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { MedReportLogo } from '@/components/med-report-title'; import { MedReportLogo } from '@/components/med-report-logo';
import { SubmitButton } from '@/components/ui/submit-button'; import { SubmitButton } from '@/components/ui/submit-button';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service'; import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { submitCompanyRegistration } from '@/lib/services/register-company.service';
import { CompanySubmitData } from '@/lib/types/company'; import { CompanySubmitData } from '@/lib/types/company';
import { companySchema } from '@/lib/validations/companySchema'; import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -19,36 +18,35 @@ import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label'; import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
export default function RegisterCompany() { export default function CompanyOffer() {
const router = useRouter(); const router = useRouter();
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isValid, isSubmitting }, formState: { isValid, isSubmitting },
} = useForm({ } = useForm({
resolver: zodResolver(companySchema), resolver: zodResolver(companyOfferSchema),
mode: 'onChange', mode: 'onChange',
}); });
const language = useTranslation().i18n.language; const language = useTranslation().i18n.language;
async function onSubmit(data: CompanySubmitData) { const onSubmit = async (data: CompanySubmitData) => {
const formData = new FormData(); const formData = new FormData();
Object.entries(data).forEach(([key, value]) => { Object.entries(data).forEach(([key, value]) => {
if (value !== undefined) formData.append(key, value); if (value !== undefined) formData.append(key, value);
}); });
try { try {
await submitCompanyRegistration(formData);
sendCompanyOfferEmail(data, language) sendCompanyOfferEmail(data, language)
.then(() => router.push('/register-company/success')) .then(() => router.push('/company-offer/success'))
.catch((error) => alert('error: ' + error)); .catch((error) => alert('error: ' + error));
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
alert('Server validation error: ' + err.message); console.warn('Server validation error: ' + err.message);
} }
alert('Server validation error'); console.warn('Server validation error: ', err);
} }
} };
return ( return (
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border"> <div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">

View File

@@ -0,0 +1,16 @@
'use client';
import { SuccessNotification } from '@/packages/features/notifications/src/components';
export default function CompanyRegistrationSuccess() {
return (
<SuccessNotification
titleKey="account:requestCompanyAccount:successTitle"
descriptionKey="account:requestCompanyAccount:successDescription"
buttonProps={{
buttonTitleKey: 'account:requestCompanyAccount:successButton',
href: '/',
}}
/>
);
}

View File

@@ -1,37 +0,0 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-title';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
export default function CompanyRegistrationSuccess() {
return (
<div className="border-border rounded-3xl border px-16 pt-4 pb-12">
<MedReportLogo />
<div className="flex flex-col items-center px-4">
<Image
src="/assets/success.png"
alt="Success"
className="pt-6 pb-8"
width={326}
height={195}
/>
<h1 className="pb-2">
<Trans i18nKey="account:requestCompanyAccount:successTitle" />
</h1>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="account:requestCompanyAccount:successDescription" />
</p>
</div>
<Button className="mt-8 w-full">
<Link href="/">
<Trans i18nKey="account:requestCompanyAccount:successButton" />
</Link>
</Button>
</div>
);
}

View File

@@ -1,6 +1,8 @@
import React from 'react';
import Link from 'next/link';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import Link from "next/link";
import React from "react";
export default async function SignIn() { export default async function SignIn() {
return ( return (
@@ -15,7 +17,7 @@ export default async function SignIn() {
<Link href="/">ID-Kaart</Link> <Link href="/">ID-Kaart</Link>
</Button> </Button>
<Button variant="outline"> <Button variant="outline">
<Link href="/register-company">Loo ettevõtte konto</Link> <Link href="/company-offer">Loo ettevõtte konto</Link>
</Button> </Button>
</div> </div>
); );

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -1,5 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { register } from 'module';
import { SignInMethodsContainer } from '@kit/auth/sign-in'; import { SignInMethodsContainer } from '@kit/auth/sign-in';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading'; import { Heading } from '@kit/ui/heading';
@@ -26,7 +28,8 @@ export const generateMetadata = async () => {
}; };
async function SignInPage({ searchParams }: SignInPageProps) { async function SignInPage({ searchParams }: SignInPageProps) {
const { invite_token: inviteToken, next = '' } = await searchParams; const { invite_token: inviteToken, next = pathsConfig.app.home } =
await searchParams;
const signUpPath = const signUpPath =
pathsConfig.auth.signUp + pathsConfig.auth.signUp +
@@ -36,6 +39,7 @@ async function SignInPage({ searchParams }: SignInPageProps) {
callback: pathsConfig.auth.callback, callback: pathsConfig.auth.callback,
returnPath: next ?? pathsConfig.app.home, returnPath: next ?? pathsConfig.app.home,
joinTeam: pathsConfig.app.joinTeam, joinTeam: pathsConfig.app.joinTeam,
updateAccount: pathsConfig.auth.updateAccount,
}; };
return ( return (

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -27,6 +27,7 @@ interface Props {
const paths = { const paths = {
callback: pathsConfig.auth.callback, callback: pathsConfig.auth.callback,
appHome: pathsConfig.app.home, appHome: pathsConfig.app.home,
updateAccount: pathsConfig.auth.updateAccount,
}; };
async function SignUpPage({ searchParams }: Props) { async function SignUpPage({ searchParams }: Props) {

View File

@@ -0,0 +1,11 @@
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) {
return (
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
{props.children}
</div>
);
}
export default withI18n(SiteLayout);

View File

@@ -0,0 +1,43 @@
import { redirect } from 'next/navigation';
import { BackButton } from '@/components/back-button';
import { MedReportLogo } from '@/components/med-report-logo';
import pathsConfig from '@/config/paths.config';
import { signOutAction } from '@/lib/actions/sign-out';
import { UpdateAccountForm } from '@/packages/features/auth/src/components/update-account-form';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
async function UpdateAccount() {
const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
if (!user) {
redirect(pathsConfig.auth.signIn);
}
return (
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
<div className="relative flex w-1/2 min-w-md flex-col px-12 pt-7 pb-22 text-center">
<BackButton onBack={signOutAction} />
<MedReportLogo />
<h1 className="pt-8">
<Trans i18nKey={'account:updateAccount:title'} />
</h1>
<p className="text-muted-foreground pt-1 text-sm">
<Trans i18nKey={'account:updateAccount:description'} />
</p>
<UpdateAccountForm user={user} />
</div>
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
</div>
);
}
export default withI18n(UpdateAccount);

View File

@@ -0,0 +1,17 @@
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { UpdateAccountSuccessNotification } from '@kit/notifications/components';
import { withI18n } from '~/lib/i18n/with-i18n';
async function UpdateAccountSuccess() {
const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
return <UpdateAccountSuccessNotification userId={user?.id} />;
}
export default withI18n(UpdateAccountSuccess);

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -2,6 +2,7 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@/packages/supabase/src/database.types';
import { z } from 'zod'; import { z } from 'zod';
import { createAccountsApi } from '@kit/accounts/api'; import { createAccountsApi } from '@kit/accounts/api';
@@ -13,7 +14,6 @@ import { requireUser } from '@kit/supabase/require-user';
import appConfig from '~/config/app.config'; import appConfig from '~/config/app.config';
import billingConfig from '~/config/billing.config'; import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config'; import pathsConfig from '~/config/paths.config';
import { Database } from '~/lib/database.types';
import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema'; import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema';

View File

@@ -2,6 +2,7 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@/packages/supabase/src/database.types';
import { z } from 'zod'; import { z } from 'zod';
import { LineItemSchema } from '@kit/billing'; import { LineItemSchema } from '@kit/billing';
@@ -14,7 +15,6 @@ import { createTeamAccountsApi } from '@kit/team-accounts/api';
import appConfig from '~/config/app.config'; import appConfig from '~/config/app.config';
import billingConfig from '~/config/billing.config'; import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config'; import pathsConfig from '~/config/paths.config';
import { Database } from '~/lib/database.types';
import { TeamCheckoutSchema } from '../schema/team-billing.schema'; import { TeamCheckoutSchema } from '../schema/team-billing.schema';

View File

@@ -2,8 +2,9 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@/packages/supabase/src/database.types';
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader'; import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
import { Database } from '~/lib/database.types';
/** /**
* Load data for the members page * Load data for the members page

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'; import Link from 'next/link';
import { MedReportLogo } from './med-report-title'; import { MedReportLogo } from './med-report-logo';
function LogoImage({ function LogoImage({
className, className,

View File

@@ -0,0 +1,31 @@
'use client';
import { useRouter } from 'next/navigation';
import { ArrowLeft } from '@/public/assets/arrow-left';
import { Trans } from '@kit/ui/trans';
export function BackButton({ onBack }: { onBack?: () => void }) {
const router = useRouter();
return (
<form
action={() => {
if (onBack) {
onBack();
} else {
router.back();
}
}}
>
<button className="absolute top-4 left-4 flex cursor-pointer flex-row items-center gap-3">
<div className="flex items-center justify-center rounded-sm border p-3">
<ArrowLeft />
</div>
<span className="text-sm">
<Trans i18nKey="common:goBack" />
</span>
</button>
</form>
);
}

View File

@@ -1,9 +1,11 @@
import { signOutAction } from "@/lib/actions/sign-out"; import Link from 'next/link';
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import Link from "next/link"; import { signOutAction } from '@/lib/actions/sign-out';
import { Badge } from "@/components/ui/badge"; import { hasEnvVars } from '@/utils/supabase/check-env-vars';
import { Button } from "@/components/ui/button"; import { createClient } from '@/utils/supabase/server';
import { createClient } from "@/utils/supabase/server";
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
export default async function AuthButton() { export default async function AuthButton() {
const supabase = await createClient(); const supabase = await createClient();
@@ -15,11 +17,11 @@ export default async function AuthButton() {
if (!hasEnvVars) { if (!hasEnvVars) {
return ( return (
<> <>
<div className="flex gap-4 items-center"> <div className="flex items-center gap-4">
<div> <div>
<Badge <Badge
variant={"default"} variant={'default'}
className="font-normal pointer-events-none" className="pointer-events-none font-normal"
> >
Please update .env.local file with anon key and url Please update .env.local file with anon key and url
</Badge> </Badge>
@@ -28,18 +30,18 @@ export default async function AuthButton() {
<Button <Button
asChild asChild
size="sm" size="sm"
variant={"outline"} variant={'outline'}
disabled disabled
className="opacity-75 cursor-none pointer-events-none" className="pointer-events-none cursor-none opacity-75"
> >
<Link href="/sign-in">Sign in</Link> <Link href="/sign-in">Sign in</Link>
</Button> </Button>
<Button <Button
asChild asChild
size="sm" size="sm"
variant={"default"} variant={'default'}
disabled disabled
className="opacity-75 cursor-none pointer-events-none" className="pointer-events-none cursor-none opacity-75"
> >
<Link href="example/sign-up">Sign up</Link> <Link href="example/sign-up">Sign up</Link>
</Button> </Button>
@@ -52,14 +54,14 @@ export default async function AuthButton() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
Hey, {user.email}! Hey, {user.email}!
<form action={signOutAction}> <form action={signOutAction}>
<Button type="submit" variant={"outline"}> <Button type="submit" variant={'outline'}>
Sign out Sign out
</Button> </Button>
</form> </form>
</div> </div>
) : ( ) : (
<div className="flex gap-2"> <div className="flex gap-2">
<Button asChild size="sm" variant={"outline"}> <Button asChild size="sm" variant={'outline'}>
<Link href="/sign-in">Sign in</Link> <Link href="/sign-in">Sign in</Link>
</Button> </Button>
</div> </div>

View File

@@ -8,6 +8,8 @@ const PathsSchema = z.object({
callback: z.string().min(1), callback: z.string().min(1),
passwordReset: z.string().min(1), passwordReset: z.string().min(1),
passwordUpdate: z.string().min(1), passwordUpdate: z.string().min(1),
updateAccount: z.string().min(1),
updateAccountSuccess: z.string().min(1),
}), }),
app: z.object({ app: z.object({
home: z.string().min(1), home: z.string().min(1),
@@ -31,6 +33,8 @@ const pathsConfig = PathsSchema.parse({
callback: '/auth/callback', callback: '/auth/callback',
passwordReset: '/auth/password-reset', passwordReset: '/auth/password-reset',
passwordUpdate: '/update-password', passwordUpdate: '/update-password',
updateAccount: '/auth/update-account',
updateAccountSuccess: '/auth/update-account/success',
}, },
app: { app: {
home: '/home', home: '/home',

View File

@@ -1,10 +1,11 @@
"use server"; 'use server';
import { createClient } from "@/utils/supabase/server"; import { redirect } from 'next/navigation';
import { redirect } from "next/navigation";
import { createClient } from '@/utils/supabase/server';
export const signOutAction = async () => { export const signOutAction = async () => {
const supabase = await createClient(); const supabase = await createClient();
await supabase.auth.signOut(); await supabase.auth.signOut();
return redirect("/sign-in"); return redirect('/');
}; };

View File

@@ -1,26 +0,0 @@
'use server';
import { companySchema } from '@/lib/validations/companySchema';
export async function submitCompanyRegistration(formData: FormData) {
const data = {
companyName: formData.get('companyName')?.toString() || '',
contactPerson: formData.get('contactPerson')?.toString() || '',
email: formData.get('email')?.toString() || '',
phone: formData.get('phone')?.toString() || '',
};
const result = companySchema.safeParse(data);
if (!result.success) {
const errors = result.error.errors.map((err) => ({
path: err.path.join('.'),
message: err.message,
}));
throw new Error(
'Validation failed: ' +
errors.map((e) => `${e.path}: ${e.message}`).join(', '),
);
}
}

View File

@@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
export const companySchema = z.object({ export const companyOfferSchema = z.object({
companyName: z.string({ companyName: z.string({
required_error: 'Company name is required', required_error: 'Company name is required',
}), }),

View File

@@ -139,7 +139,7 @@ function getPatterns() {
handler: adminMiddleware, handler: adminMiddleware,
}, },
{ {
pattern: new URLPattern({ pathname: '/auth/*?' }), pattern: new URLPattern({ pathname: '/auth/update-account' }),
handler: async (req: NextRequest, res: NextResponse) => { handler: async (req: NextRequest, res: NextResponse) => {
const { const {
data: { user }, data: { user },
@@ -147,21 +147,27 @@ function getPatterns() {
// the user is logged out, so we don't need to do anything // the user is logged out, so we don't need to do anything
if (!user) { if (!user) {
return; return NextResponse.redirect(new URL('/', req.nextUrl.origin).href);
} }
// check if we need to verify MFA (user is authenticated but needs to verify MFA) const client = createMiddlewareClient(req, res);
const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa; const userIsSuperAdmin = await isSuperAdmin(client);
// If user is logged in and does not need to verify MFA, if (userIsSuperAdmin) {
// redirect to home page. // check if we need to verify MFA (user is authenticated but needs to verify MFA)
if (!isVerifyMfa) { const isVerifyMfa =
const nextPath = req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home;
return NextResponse.redirect( // If user is logged in and does not need to verify MFA,
new URL(nextPath, req.nextUrl.origin).href, // redirect to home page.
); if (!isVerifyMfa) {
const nextPath =
req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home;
return NextResponse.redirect(
new URL(nextPath, req.nextUrl.origin).href,
);
}
} }
}, },
}, },

View File

@@ -25,9 +25,7 @@
"supabase:db:lint": "supabase db lint", "supabase:db:lint": "supabase db lint",
"supabase:db:diff": "supabase db diff", "supabase:db:diff": "supabase db diff",
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push", "supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
"supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app", "supabase:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts",
"supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts",
"supabase:typegen:app": "supabase gen types typescript --local > ./supabase/database.types.ts",
"supabase:db:dump:local": "supabase db dump --local --data-only", "supabase:db:dump:local": "supabase db dump --local --data-only",
"sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts", "sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts",
"sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts" "sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts"

View File

@@ -26,7 +26,8 @@ export function usePersonalAccountData(
` `
id, id,
name, name,
picture_url picture_url,
last_name
`, `,
) )
.eq('primary_owner_user_id', userId) .eq('primary_owner_user_id', userId)

View File

@@ -211,7 +211,6 @@ async function TeamAccountPage(props: {
<div> <div>
<div className={'flex flex-col gap-y-8'}> <div className={'flex flex-col gap-y-8'}>
<div className={'flex flex-col gap-y-2.5'}> <div className={'flex flex-col gap-y-2.5'}>
<Heading level={6}>Company Employees</Heading> <Heading level={6}>Company Employees</Heading>

View File

@@ -4,6 +4,8 @@ import { useRouter } from 'next/navigation';
import type { Provider } from '@supabase/supabase-js'; import type { Provider } from '@supabase/supabase-js';
import { useSupabase } from '@/packages/supabase/src/hooks/use-supabase';
import { isBrowser } from '@kit/shared/utils'; import { isBrowser } from '@kit/shared/utils';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Separator } from '@kit/ui/separator'; import { Separator } from '@kit/ui/separator';
@@ -20,6 +22,7 @@ export function SignInMethodsContainer(props: {
callback: string; callback: string;
joinTeam: string; joinTeam: string;
returnPath: string; returnPath: string;
updateAccount: string;
}; };
providers: { providers: {
@@ -28,13 +31,14 @@ export function SignInMethodsContainer(props: {
oAuth: Provider[]; oAuth: Provider[];
}; };
}) { }) {
const client = useSupabase();
const router = useRouter(); const router = useRouter();
const redirectUrl = isBrowser() const redirectUrl = isBrowser()
? new URL(props.paths.callback, window?.location.origin).toString() ? 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 the user has an invite token, we should join the team
if (props.inviteToken) { if (props.inviteToken) {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
@@ -45,8 +49,28 @@ export function SignInMethodsContainer(props: {
router.replace(joinTeamPath); router.replace(joinTeamPath);
} else { } else {
// otherwise, we should redirect to the return path if (!userId) {
router.replace(props.paths.returnPath); 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;
}
} }
}; };

View File

@@ -16,6 +16,7 @@ export function SignUpMethodsContainer(props: {
paths: { paths: {
callback: string; callback: string;
appHome: string; appHome: string;
updateAccount: string;
}; };
providers: { providers: {

View 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>
);
}

View 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',
}),
});

View File

@@ -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,
},
);

View 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);
}

View File

@@ -1,2 +1,3 @@
export * from './components/sign-up-methods-container'; export * from './components/sign-up-methods-container';
export * from './schemas/password-sign-up.schema'; export * from './schemas/password-sign-up.schema';
export * from './components/update-account-form';

View File

@@ -1,5 +1,5 @@
{ {
"extends": "@kit/tsconfig/base.json", "extends": "../../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}, },

View File

@@ -1 +1,3 @@
export * from './notifications-popover'; export * from './notifications-popover';
export * from './success-notification';
export * from './update-account-success-notification';

View File

@@ -0,0 +1,50 @@
import Image from 'next/image';
import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-logo';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
export const SuccessNotification = ({
showLogo = true,
title,
titleKey,
descriptionKey,
buttonProps,
}: {
showLogo?: boolean;
title?: string;
titleKey?: string;
descriptionKey?: string;
buttonProps?: {
buttonTitleKey: string;
href: string;
};
}) => {
return (
<div className="border-border rounded-3xl border px-16 pt-4 pb-12">
{showLogo && <MedReportLogo />}
<div className="flex flex-col items-center px-4">
<Image
src="/assets/success.png"
alt="Success"
className="pt-6 pb-8"
width={326}
height={195}
/>
<h1 className="pb-2">{title || <Trans i18nKey={titleKey} />}</h1>
<p className="text-muted-foreground text-sm">
<Trans i18nKey={descriptionKey} />
</p>
</div>
{buttonProps && (
<Button className="mt-8 w-full">
<Link href={buttonProps.href}>
<Trans i18nKey={buttonProps.buttonTitleKey} />
</Link>
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,39 @@
'use client';
import { redirect } from 'next/navigation';
import pathsConfig from '@/config/paths.config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from './success-notification';
export const UpdateAccountSuccessNotification = ({
userId,
}: {
userId?: string;
}) => {
const { t } = useTranslation('account');
if (!userId) {
redirect(pathsConfig.app.home);
}
const { data: accountData } = usePersonalAccountData(userId);
return (
<SuccessNotification
showLogo={false}
title={t('account:updateAccount:successTitle', {
firstName: accountData?.name,
lastName: accountData?.last_name,
})}
descriptionKey="account:updateAccount:successDescription"
buttonProps={{
buttonTitleKey: 'account:updateAccount:successButton',
href: pathsConfig.app.home,
}}
/>
);
};

View File

@@ -1,5 +1,5 @@
{ {
"extends": "@kit/tsconfig/base.json", "extends": "../../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -70,7 +70,7 @@ export function VersionUpdater(props: { intervalTimeInSecond?: number }) {
setDismissed(true); setDismissed(true);
}} }}
> >
<Trans i18nKey="common:back" /> <Trans i18nKey="common:goBack" />
</Button> </Button>
<Button onClick={() => window.location.reload()}> <Button onClick={() => window.location.reload()}>

View File

@@ -12,7 +12,7 @@ const Checkbox: React.FC<
> = ({ className, ...props }) => ( > = ({ className, ...props }) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={cn( className={cn(
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-xs border shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50', 'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-sm border shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className, className,
)} )}
{...props} {...props}

View File

@@ -1,5 +1,5 @@
{ {
"extends": "@kit/tsconfig/base.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"paths": { "paths": {

View File

@@ -0,0 +1,28 @@
export const ArrowLeft = () => (
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<mask id="path-1-inside-1_583_1979" fill="white">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.40588 4.96054C9.59333 5.14799 9.59333 5.45191 9.40588 5.63937L6.54529 8.49995L9.40588 11.3605C9.59333 11.548 9.59333 11.8519 9.40588 12.0394C9.21843 12.2268 8.91451 12.2268 8.72706 12.0394L5.52706 8.83937C5.43705 8.74934 5.38647 8.62725 5.38647 8.49995C5.38647 8.37265 5.43705 8.25055 5.52706 8.16054L8.72706 4.96054C8.91451 4.77308 9.21843 4.77308 9.40588 4.96054Z"
/>
</mask>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.40588 4.96054C9.59333 5.14799 9.59333 5.45191 9.40588 5.63937L6.54529 8.49995L9.40588 11.3605C9.59333 11.548 9.59333 11.8519 9.40588 12.0394C9.21843 12.2268 8.91451 12.2268 8.72706 12.0394L5.52706 8.83937C5.43705 8.74934 5.38647 8.62725 5.38647 8.49995C5.38647 8.37265 5.43705 8.25055 5.52706 8.16054L8.72706 4.96054C8.91451 4.77308 9.21843 4.77308 9.40588 4.96054Z"
fill="#09090B"
/>
<path
d="M9.40588 4.96054L8.69875 5.66762L8.69877 5.66765L9.40588 4.96054ZM9.40588 5.63937L10.113 6.34647V6.34647L9.40588 5.63937ZM6.54529 8.49995L5.83818 7.79285L5.13108 8.49995L5.83818 9.20706L6.54529 8.49995ZM9.40588 11.3605L8.69877 12.0677L8.69885 12.0677L9.40588 11.3605ZM9.40588 12.0394L10.1129 12.7465L10.1131 12.7464L9.40588 12.0394ZM8.72706 12.0394L8.01995 12.7465L8.02001 12.7465L8.72706 12.0394ZM5.52706 8.83937L4.81991 9.54643L4.81995 9.54647L5.52706 8.83937ZM5.52706 8.16054L6.23417 8.86765H6.23417L5.52706 8.16054ZM8.72706 4.96054L9.43417 5.66765L9.43419 5.66762L8.72706 4.96054ZM9.40588 4.96054L8.69877 5.66765C8.4957 5.46458 8.4957 5.13533 8.69877 4.93226L9.40588 5.63937L10.113 6.34647C10.691 5.76849 10.691 4.83141 10.113 4.25343L9.40588 4.96054ZM9.40588 5.63937L8.69877 4.93226L5.83818 7.79285L6.54529 8.49995L7.2524 9.20706L10.113 6.34647L9.40588 5.63937ZM6.54529 8.49995L5.83818 9.20706L8.69877 12.0677L9.40588 11.3605L10.113 10.6534L7.2524 7.79285L6.54529 8.49995ZM9.40588 11.3605L8.69885 12.0677C8.49556 11.8645 8.49585 11.5353 8.69865 11.3324L9.40588 12.0394L10.1131 12.7464C10.6908 12.1685 10.6911 11.2314 10.1129 10.6534L9.40588 11.3605ZM9.40588 12.0394L8.69883 11.3322C8.9019 11.1292 9.23104 11.1292 9.43411 11.3322L8.72706 12.0394L8.02001 12.7465C8.59797 13.3244 9.53497 13.3244 10.1129 12.7465L9.40588 12.0394ZM8.72706 12.0394L9.43417 11.3323L6.23417 8.13226L5.52706 8.83937L4.81995 9.54647L8.01995 12.7465L8.72706 12.0394ZM5.52706 8.83937L6.23421 8.1323C6.33168 8.22978 6.38647 8.36202 6.38647 8.49995H5.38647H4.38647C4.38647 8.89248 4.54241 9.26889 4.81991 9.54643L5.52706 8.83937ZM5.38647 8.49995H6.38647C6.38647 8.63785 6.3317 8.77012 6.23417 8.86765L5.52706 8.16054L4.81995 7.45343C4.5424 7.73099 4.38647 8.10744 4.38647 8.49995H5.38647ZM5.52706 8.16054L6.23417 8.86765L9.43417 5.66765L8.72706 4.96054L8.01995 4.25343L4.81995 7.45343L5.52706 8.16054ZM8.72706 4.96054L9.43419 5.66762C9.2311 5.87072 8.90183 5.87072 8.69875 5.66762L9.40588 4.96054L10.113 4.25345C9.53503 3.67544 8.59791 3.67544 8.01993 4.25345L8.72706 4.96054Z"
fill="#18181B"
mask="url(#path-1-inside-1_583_1979)"
/>
</svg>
);

View File

@@ -0,0 +1,16 @@
export const ExternalLink = () => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7.66667V11.6667C11 12.0203 10.8595 12.3594 10.6095 12.6095C10.3594 12.8595 10.0203 13 9.66667 13H2.33333C1.97971 13 1.64057 12.8595 1.39052 12.6095C1.14048 12.3594 1 12.0203 1 11.6667V4.33333C1 3.97971 1.14048 3.64057 1.39052 3.39052C1.64057 3.14048 1.97971 3 2.33333 3H6.33333M9 1H13M13 1V5M13 1L5.66667 8.33333"
stroke="#09090B"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

View File

@@ -120,5 +120,15 @@
"successTitle": "Päring edukalt saadetud!", "successTitle": "Päring edukalt saadetud!",
"successDescription": "Saadame teile esimesel võimalusel vastuse", "successDescription": "Saadame teile esimesel võimalusel vastuse",
"successButton": "Tagasi kodulehele" "successButton": "Tagasi kodulehele"
},
"updateAccount": {
"title": "Isikuandmed",
"description": "Jätkamiseks palun sisestage enda isikuandmed",
"button": "Jätka",
"userConsentLabel": "Nõustun isikuandmete kasutamisega platvormil",
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid",
"successTitle": "Tere, {{firstName}} {{lastName}}",
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
"successButton": "Jätka"
} }
} }

View File

@@ -19,7 +19,7 @@
"clear": "Clear", "clear": "Clear",
"notFound": "Not Found", "notFound": "Not Found",
"backToHomePage": "Back to Home Page", "backToHomePage": "Back to Home Page",
"goBack": "Go Back", "goBack": "Tagasi",
"genericServerError": "Sorry, something went wrong.", "genericServerError": "Sorry, something went wrong.",
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.", "genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
"pageNotFound": "Sorry, this page does not exist.", "pageNotFound": "Sorry, this page does not exist.",
@@ -54,7 +54,6 @@
"newVersionAvailable": "New version available", "newVersionAvailable": "New version available",
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.", "newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
"newVersionSubmitButton": "Reload and Update", "newVersionSubmitButton": "Reload and Update",
"back": "Back",
"routes": { "routes": {
"home": "Home", "home": "Home",
"account": "Account", "account": "Account",
@@ -97,6 +96,12 @@
"companyName": "Ettevõtte nimi", "companyName": "Ettevõtte nimi",
"contactPerson": "Kontaktisik", "contactPerson": "Kontaktisik",
"email": "E-mail", "email": "E-mail",
"phone": "Telefon" "phone": "Telefon",
"firstName": "Eesnimi",
"lastName": "Perenimi",
"personalCode": "Isikukood",
"city": "Linn",
"weight": "Kaal",
"height": "Pikkus"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
alter table "public"."accounts" add column "last_name" text;
alter table "public"."accounts" add column "personal_code" text;
alter table "public"."accounts" add column "city" text;
alter table "public"."accounts" add column "has_consent_personal_data" boolean;
alter table "public"."accounts" add column "phone" text;
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.update_account(p_name character varying, p_last_name text, p_personal_code text, p_phone text, p_city text, p_has_consent_personal_data boolean, p_uid uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$begin
update public.accounts
set name = coalesce(p_name, name),
last_name = coalesce(p_last_name, last_name),
personal_code = coalesce(p_personal_code, personal_code),
phone = coalesce(p_phone, phone),
city = coalesce(p_city, city),
has_consent_personal_data = coalesce(p_has_consent_personal_data,
has_consent_personal_data)
where id = p_uid;
end;$function$
;
grant
execute on function public.update_account(p_name character varying, p_last_name text, p_personal_code text, p_phone text, p_city text, p_has_consent_personal_data boolean, p_uid uuid) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION public.has_personal_code(account_id uuid)
RETURNS boolean
LANGUAGE plpgsql
AS $function$BEGIN
RETURN EXISTS (
SELECT 1
FROM public.accounts
WHERE id = account_id
AND personal_code IS NOT NULL
AND personal_code <> ''
);
END;$function$
;
grant
execute on function public.has_personal_code(account_id uuid) to authenticated,
service_role;
create table "public"."account_params" (
"recorded_at" timestamp with time zone not null default now(),
"id" uuid not null default gen_random_uuid(),
"account_id" uuid not null default auth.uid(),
"weight" integer,
"height" integer
);
alter table "public"."account_params" enable row level security;
CREATE UNIQUE INDEX account_params_pkey ON public.account_params USING btree (id);
alter table "public"."account_params" add constraint "account_params_pkey" PRIMARY KEY using index "account_params_pkey";
grant delete on table "public"."account_params" to "anon";
grant insert on table "public"."account_params" to "anon";
grant references on table "public"."account_params" to "anon";
grant select on table "public"."account_params" to "anon";
grant trigger on table "public"."account_params" to "anon";
grant truncate on table "public"."account_params" to "anon";
grant update on table "public"."account_params" to "anon";
grant delete on table "public"."account_params" to "authenticated";
grant insert on table "public"."account_params" to "authenticated";
grant references on table "public"."account_params" to "authenticated";
grant select on table "public"."account_params" to "authenticated";
grant trigger on table "public"."account_params" to "authenticated";
grant truncate on table "public"."account_params" to "authenticated";
grant update on table "public"."account_params" to "authenticated";
grant delete on table "public"."account_params" to "service_role";
grant insert on table "public"."account_params" to "service_role";
grant references on table "public"."account_params" to "service_role";
grant select on table "public"."account_params" to "service_role";
grant trigger on table "public"."account_params" to "service_role";
grant truncate on table "public"."account_params" to "service_role";
grant update on table "public"."account_params" to "service_role";
-- Everyone can read their own rows
create policy "users can read their params"
on public.account_params
for select
to authenticated
using ( account_id = auth.uid() );
-- Everyone can insert rows for themselves
create policy "users can insert their params"
on public.account_params
for insert
to authenticated
with check ( account_id = auth.uid() );