diff --git a/README.md b/README.md index dde3485..60582c1 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,12 @@ To update database types run: ```bash npm run supabase:typegen:app ``` + +## Super admin + +To access admin pages follow these steps: + +- Register new user +- Go to Profile and add Multi-Factor Authentication +- Sign out and Sign in +- Authenticate with mfa (at current time profile page prompts it again) diff --git a/app/auth/membership-confirmation/layout.tsx b/app/auth/membership-confirmation/layout.tsx new file mode 100644 index 0000000..8212f3c --- /dev/null +++ b/app/auth/membership-confirmation/layout.tsx @@ -0,0 +1,11 @@ +import { withI18n } from '~/lib/i18n/with-i18n'; + +async function SiteLayout(props: React.PropsWithChildren) { + return ( +
+ {props.children} +
+ ); +} + +export default withI18n(SiteLayout); diff --git a/app/auth/membership-confirmation/page.tsx b/app/auth/membership-confirmation/page.tsx new file mode 100644 index 0000000..8154abb --- /dev/null +++ b/app/auth/membership-confirmation/page.tsx @@ -0,0 +1,46 @@ +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 '@kit/notifications/components'; +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +import { withI18n } from '~/lib/i18n/with-i18n'; + +async function UpdateAccountSuccess() { + const { t } = useTranslation('account'); + const client = getSupabaseServerClient(); + + const { + data: { user }, + } = await client.auth.getUser(); + + if (!user?.id) { + redirect(pathsConfig.app.home); + } + + const { data: accountData } = usePersonalAccountData(user.id); + + if (!accountData?.id) { + redirect(pathsConfig.app.home); + } + + return ( + + ); +} + +export default withI18n(UpdateAccountSuccess); diff --git a/app/auth/update-account/success/page.tsx b/app/auth/update-account/success/page.tsx deleted file mode 100644 index a31b5d2..0000000 --- a/app/auth/update-account/success/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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 ; -} - -export default withI18n(UpdateAccountSuccess); diff --git a/config/paths.config.ts b/config/paths.config.ts index fdb6c6d..ebfcbea 100644 --- a/config/paths.config.ts +++ b/config/paths.config.ts @@ -10,6 +10,7 @@ const PathsSchema = z.object({ passwordUpdate: z.string().min(1), updateAccount: z.string().min(1), updateAccountSuccess: z.string().min(1), + membershipConfirmation: z.string().min(1), }), app: z.object({ home: z.string().min(1), @@ -42,6 +43,7 @@ const pathsConfig = PathsSchema.parse({ passwordUpdate: '/update-password', updateAccount: '/auth/update-account', updateAccountSuccess: '/auth/update-account/success', + membershipConfirmation: '/auth/membership-confirmation', }, app: { home: '/home', diff --git a/package.json b/package.json index d8242ea..a4307b3 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "pino-pretty": "^13.0.0", "prettier": "^3.5.3", "react-hook-form": "^7.57.0", - "supabase": "^2.26.9", + "supabase": "^2.30.4", "tailwindcss": "4.1.7", "tailwindcss-animate": "^1.0.7", "typescript": "^5.8.3", diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index 35a4465..2ab78f5 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -47,16 +47,13 @@ class AccountsApi { } /** - * @name loadUserAccounts - * Load only user-owned accounts (not just memberships). - */ + * @name loadUserAccounts + * Load only user-owned accounts (not just memberships). + */ async loadUserAccounts() { const authUser = await this.client.auth.getUser(); - const { - data, - error: userError, - } = authUser + const { data, error: userError } = authUser; if (userError) { throw userError; @@ -66,14 +63,16 @@ class AccountsApi { const { data: accounts, error } = await this.client .from('accounts_memberships') - .select(` + .select( + ` account_id, user_accounts ( name, slug, - picture_url + picture_url, + ) + `, ) - `) .eq('user_id', user.id) .eq('account_role', 'owner'); @@ -88,7 +87,6 @@ class AccountsApi { })); } - async loadTempUserAccounts() { const { data: accounts, error } = await this.client .from('user_accounts') diff --git a/packages/features/auth/package.json b/packages/features/auth/package.json index 66cc1b3..d9a8ef0 100644 --- a/packages/features/auth/package.json +++ b/packages/features/auth/package.json @@ -16,7 +16,8 @@ "./mfa": "./src/mfa.ts", "./captcha/client": "./src/captcha/client/index.ts", "./captcha/server": "./src/captcha/server/index.ts", - "./resend-email-link": "./src/components/resend-auth-link-form.tsx" + "./resend-email-link": "./src/components/resend-auth-link-form.tsx", + "./lib/utils/*": "./src/lib/utils/*.ts" }, "devDependencies": { "@hookform/resolvers": "^5.0.1", diff --git a/packages/features/auth/src/components/sign-in-methods-container.tsx b/packages/features/auth/src/components/sign-in-methods-container.tsx index c6d243e..fd2d51d 100644 --- a/packages/features/auth/src/components/sign-in-methods-container.tsx +++ b/packages/features/auth/src/components/sign-in-methods-container.tsx @@ -55,14 +55,14 @@ export function SignInMethodsContainer(props: { } try { - const { data: hasPersonalCode } = await client.rpc( - 'has_personal_code', + const { data: hasConsentPersonalData } = await client.rpc( + 'has_consent_personal_data', { account_id: userId, }, ); - if (hasPersonalCode) { + if (hasConsentPersonalData) { router.replace(props.paths.returnPath); } else { router.replace(props.paths.updateAccount); diff --git a/packages/features/auth/src/server/actions/update-account-actions.ts b/packages/features/auth/src/server/actions/update-account-actions.ts index eb24be0..0efdab8 100644 --- a/packages/features/auth/src/server/actions/update-account-actions.ts +++ b/packages/features/auth/src/server/actions/update-account-actions.ts @@ -23,7 +23,7 @@ export interface AccountSubmitData { } export const onUpdateAccount = enhanceAction( - async (params) => { + async (params: AccountSubmitData) => { const client = getSupabaseServerClient(); const api = createAuthApi(client); @@ -36,7 +36,14 @@ export const onUpdateAccount = enhanceAction( } console.warn('On update account error: ', err); } - redirect(pathsConfig.auth.updateAccountSuccess); + const hasUnseenMembershipConfirmation = + await api.hasUnseenMembershipConfirmation(); + + if (hasUnseenMembershipConfirmation) { + redirect(pathsConfig.auth.membershipConfirmation); + } else { + redirect(pathsConfig.app.selectPackage); + } }, { schema: UpdateAccountSchema, diff --git a/packages/features/auth/src/server/api.ts b/packages/features/auth/src/server/api.ts index 6c10ca1..0e6f9ce 100644 --- a/packages/features/auth/src/server/api.ts +++ b/packages/features/auth/src/server/api.ts @@ -13,14 +13,24 @@ class AuthApi { constructor(private readonly client: SupabaseClient) {} /** - * @name hasPersonalCode - * @description Check if given account ID has added personal code. - * @param id + * @name hasUnseenMembershipConfirmation + * @description Check if given user ID has any unseen membership confirmation. */ - async hasPersonalCode(id: string) { - const { data, error } = await this.client.rpc('has_personal_code', { - account_id: id, - }); + async hasUnseenMembershipConfirmation() { + const { + data: { user }, + } = await this.client.auth.getUser(); + + if (!user) { + throw new Error('User not authenticated'); + } + + const { data, error } = await this.client.rpc( + 'has_unseen_membership_confirmation', + { + p_user_id: user.id, + }, + ); if (error) { throw error; diff --git a/packages/features/notifications/src/components/index.ts b/packages/features/notifications/src/components/index.ts index 08883b6..ba10a7f 100644 --- a/packages/features/notifications/src/components/index.ts +++ b/packages/features/notifications/src/components/index.ts @@ -1,3 +1,2 @@ export * from './notifications-popover'; export * from './success-notification'; -export * from './update-account-success-notification'; diff --git a/packages/features/notifications/src/components/update-account-success-notification.tsx b/packages/features/notifications/src/components/update-account-success-notification.tsx deleted file mode 100644 index b36c79b..0000000 --- a/packages/features/notifications/src/components/update-account-success-notification.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'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 ( - - ); -}; diff --git a/packages/features/team-accounts/src/server/services/account-invitations.service.ts b/packages/features/team-accounts/src/server/services/account-invitations.service.ts index 6fd7160..6d6615f 100644 --- a/packages/features/team-accounts/src/server/services/account-invitations.service.ts +++ b/packages/features/team-accounts/src/server/services/account-invitations.service.ts @@ -117,7 +117,10 @@ class AccountInvitationsService { } const isUserAlreadyMember = members.find((member) => { - return member.email === invitation.email || member.personal_code === invitation.personal_code; + return ( + member.email === invitation.email || + member.personal_code === invitation.personal_code + ); }); if (isUserAlreadyMember) { diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 11527f5..7278dc8 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -17,7 +17,7 @@ export type Database = { changed_data: Json | null id: number operation: string - record_key: number | null + record_key: string | null row_data: Json | null schema_name: string table_name: string @@ -29,7 +29,7 @@ export type Database = { changed_data?: Json | null id?: number operation: string - record_key?: number | null + record_key?: string | null row_data?: Json | null schema_name: string table_name: string @@ -41,7 +41,7 @@ export type Database = { changed_data?: Json | null id?: number operation?: string - record_key?: number | null + record_key?: string | null row_data?: Json | null schema_name?: string table_name?: string @@ -252,6 +252,7 @@ export type Database = { account_role: string created_at: string created_by: string | null + has_seen_confirmation: boolean updated_at: string updated_by: string | null user_id: string @@ -261,6 +262,7 @@ export type Database = { account_role: string created_at?: string created_by?: string | null + has_seen_confirmation?: boolean updated_at?: string updated_by?: string | null user_id: string @@ -270,6 +272,7 @@ export type Database = { account_role?: string created_at?: string created_by?: string | null + has_seen_confirmation?: boolean updated_at?: string updated_by?: string | null user_id?: string @@ -901,74 +904,21 @@ export type Database = { }, ] } - medreport_product_groups: { - Row: { - created_at: string - id: number - name: string - updated_at: string | null - } - Insert: { - created_at?: string - id?: number - name: string - updated_at?: string | null - } - Update: { - created_at?: string - id?: number - name?: string - updated_at?: string | null - } - Relationships: [] - } - medreport_products: { - Row: { - created_at: string - id: number - name: string - product_group_id: number | null - updated_at: string | null - } - Insert: { - created_at?: string - id?: number - name: string - product_group_id?: number | null - updated_at?: string | null - } - Update: { - created_at?: string - id?: number - name?: string - product_group_id?: number | null - updated_at?: string | null - } - Relationships: [ - { - foreignKeyName: "medreport_products_product_groups_id_fkey" - columns: ["product_group_id"] - isOneToOne: false - referencedRelation: "medreport_product_groups" - referencedColumns: ["id"] - }, - ] - } - medreport_products_analyses_relations: { + medusa_products_analyses_relations: { Row: { analysis_element_id: number | null analysis_id: number | null - product_id: number + medusa_product_id: number } Insert: { analysis_element_id?: number | null analysis_id?: number | null - product_id: number + medusa_product_id: number } Update: { analysis_element_id?: number | null analysis_id?: number | null - product_id?: number + medusa_product_id?: number } Relationships: [ { @@ -985,27 +935,20 @@ export type Database = { referencedRelation: "analyses" referencedColumns: ["id"] }, - { - foreignKeyName: "medreport_products_analyses_product_id_fkey" - columns: ["product_id"] - isOneToOne: true - referencedRelation: "medreport_products" - referencedColumns: ["id"] - }, ] } - medreport_products_external_services_relations: { + medusa_products_external_services_relations: { Row: { connected_online_service_id: number - product_id: number + medusa_product_id: number } Insert: { connected_online_service_id: number - product_id: number + medusa_product_id: number } Update: { connected_online_service_id?: number - product_id?: number + medusa_product_id?: number } Relationships: [ { @@ -1015,13 +958,6 @@ export type Database = { referencedRelation: "connected_online_services" referencedColumns: ["id"] }, - { - foreignKeyName: "medreport_products_connected_online_services_product_id_fkey" - columns: ["product_id"] - isOneToOne: false - referencedRelation: "medreport_products" - referencedColumns: ["id"] - }, ] } nonces: { @@ -1464,10 +1400,6 @@ export type Database = { Args: { target_team_account_id: string; target_user_id: string } Returns: boolean } - check_personal_code_exists: { - Args: { code: string } - Returns: boolean - } create_invitation: { Args: { account_id: string; email: string; role: string } Returns: { @@ -1574,6 +1506,10 @@ export type Database = { Args: { target_account_id: string } Returns: boolean } + has_consent_personal_data: { + Args: { account_id: string } + Returns: boolean + } has_more_elevated_role: { Args: { target_user_id: string @@ -1590,10 +1526,6 @@ export type Database = { } Returns: boolean } - has_personal_code: { - Args: { account_id: string } - Returns: boolean - } has_role_on_account: { Args: { account_id: string; account_role?: string } Returns: boolean @@ -1606,6 +1538,10 @@ export type Database = { } Returns: boolean } + has_unseen_membership_confirmation: { + Args: { p_user_id?: string } + Returns: boolean + } is_aal2: { Args: Record Returns: boolean diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80b71bd..2232b40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,8 +196,8 @@ importers: specifier: ^3.5.3 version: 3.5.3 supabase: - specifier: ^2.26.9 - version: 2.26.9 + specifier: ^2.30.4 + version: 2.30.4 tailwindcss: specifier: 4.1.7 version: 4.1.7 @@ -7296,8 +7296,8 @@ packages: stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - supabase@2.26.9: - resolution: {integrity: sha512-wHl7HtAD2iHMVXL8JZyfSjdI0WYM7EF0ydThp1tSvDANaD2JHCZc8GH1NdzglbwGqdHmjCYeSZ+H28fmucYl7Q==} + supabase@2.30.4: + resolution: {integrity: sha512-AOCyd2vmBBMTXbnahiCU0reRNxKS4n5CrPciUF2tcTrQ8dLzl1HwcLfe5DrG8E0QRcKHPDdzprmh/2+y4Ta5MA==} engines: {npm: '>=8'} hasBin: true @@ -14753,7 +14753,7 @@ snapshots: stylis@4.2.0: {} - supabase@2.26.9: + supabase@2.30.4: dependencies: bin-links: 5.0.0 https-proxy-agent: 7.0.6 diff --git a/public/locales/et/account.json b/public/locales/et/account.json index f943293..ade6520 100644 --- a/public/locales/et/account.json +++ b/public/locales/et/account.json @@ -126,10 +126,7 @@ "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" + "userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid" }, "consentModal": { "title": "Enne toimetama hakkamist", @@ -143,5 +140,10 @@ "consentToAnonymizedCompanyData": { "label": "Nõustun osalema tööandja statistikas", "description": "Nõustun anonümiseeritud kujul terviseandmete kasutamisega tööandja statistikas" + }, + "membershipConfirmation": { + "successTitle": "Tere, {{firstName}} {{lastName}}", + "successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!", + "successButton": "Jätka" } -} \ No newline at end of file +} diff --git a/supabase/migrations/20250707150416_membership_confirmation.sql b/supabase/migrations/20250707150416_membership_confirmation.sql new file mode 100644 index 0000000..b530cd0 --- /dev/null +++ b/supabase/migrations/20250707150416_membership_confirmation.sql @@ -0,0 +1,41 @@ +alter table "public"."accounts_memberships" add column "has_seen_confirmation" boolean not null default false; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.has_unseen_membership_confirmation(p_user_id uuid DEFAULT auth.uid()) + RETURNS boolean + LANGUAGE sql + SECURITY DEFINER + SET search_path TO 'public', 'extensions' +AS $function$ + select exists ( + select 1 + from public.accounts_memberships am + where am.user_id = p_user_id + and am.has_seen_confirmation = false + ); +$function$ +; + +grant execute on function public.has_unseen_membership_confirmation(uuid) + to authenticated, anon; + +drop function if exists "public"."has_personal_code"(account_id uuid); + +CREATE OR REPLACE FUNCTION public.has_consent_personal_data(account_id uuid) + RETURNS boolean + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = '' +AS $function$BEGIN + RETURN EXISTS ( + SELECT 1 + FROM public.accounts + WHERE id = account_id + AND has_consent_personal_data IS TRUE + ); +END;$function$ +; + +grant execute on function public.has_consent_personal_data(uuid) + to authenticated, anon;