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;