diff --git a/.env b/.env index 09a319e..164e333 100644 --- a/.env +++ b/.env @@ -33,9 +33,9 @@ NEXT_PUBLIC_LOCALES_PATH=apps/web/public/locales # FEATURE FLAGS NEXT_PUBLIC_ENABLE_THEME_TOGGLE=true NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION=true -NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=true -NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true -NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true +NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=false +NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=false +NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=false NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true NEXT_PUBLIC_LANGUAGE_PRIORITY=application @@ -46,4 +46,7 @@ NEXT_TELEMETRY_DISABLED=1 LOGGER=pino -NEXT_PUBLIC_DEFAULT_LOCALE=et \ No newline at end of file +NEXT_PUBLIC_DEFAULT_LOCALE=et + +NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom +NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom \ No newline at end of file diff --git a/.env.development b/.env.development index 82a3969..c22cdb6 100644 --- a/.env.development +++ b/.env.development @@ -10,12 +10,6 @@ SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhY SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET # EMAILS -EMAIL_SENDER="Makerkit " -EMAIL_PORT=54325 -EMAIL_HOST=localhost -EMAIL_TLS=false -EMAIL_USER=user -EMAIL_PASSWORD=password # CONTACT FORM CONTACT_EMAIL=test@makerkit.dev @@ -24,4 +18,10 @@ CONTACT_EMAIL=test@makerkit.dev NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= # MAILER -MAILER_PROVIDER=nodemailer \ No newline at end of file +MAILER_PROVIDER=nodemailer +# EMAIL_SENDER= +# EMAIL_USER= # refer to your email provider's documentation +# EMAIL_PASSWORD= # refer to your email provider's documentation +# EMAIL_HOST= # refer to your email provider's documentation +# EMAIL_PORT= # or 465 for SSL +# EMAIL_TLS= # or false for SSL (see provider documentation) \ No newline at end of file diff --git a/.env.example b/.env.example index 3686882..46d3948 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,11 @@ NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key MEDIPOST_URL=your-medipost-url MEDIPOST_USER=your-medipost-user MEDIPOST_PASSWORD=your-medipost-password -MEDIPOST_RECIPIENT=your-medipost-recipient \ No newline at end of file +MEDIPOST_RECIPIENT=your-medipost-recipient + +EMAIL_SENDER= +EMAIL_USER= # refer to your email provider's documentation +EMAIL_PASSWORD= # refer to your email provider's documentation +EMAIL_HOST= # refer to your email provider's documentation +EMAIL_PORT= # or 465 for SSL +EMAIL_TLS= # or false for SSL (see provider documentation) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..be22dd3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine as builder + +WORKDIR /app + +RUN npm install -g pnpm@9 + +# Copy necessary files for workspace resolution +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY packages packages +COPY tooling tooling + +# Install all dependencies +RUN pnpm install --frozen-lockfile + +COPY . . + +RUN pnpm build \ No newline at end of file diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index d1c8b2d..09dd6bc 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { MedReportTitle } from '@/components/med-report-title'; +import { MedReportLogo } from '@/components/med-report-title'; import { ArrowRightIcon } from 'lucide-react'; import { CtaButton, Hero } from '@kit/ui/marketing'; @@ -13,7 +13,7 @@ function Home() {
} + title={} subtitle={ diff --git a/app/(public)/register-company/page.tsx b/app/(public)/register-company/page.tsx index faad2fc..7e3875a 100644 --- a/app/(public)/register-company/page.tsx +++ b/app/(public)/register-company/page.tsx @@ -4,20 +4,19 @@ import React from 'react'; import { useRouter } from 'next/navigation'; -import { MedReportTitle } from '@/components/med-report-title'; -import { SubmitButton } from '@/components/ui/submit-button'; -import { sendCompanyOfferEmail } from '@/lib/services/mailer.service'; -import { submitCompanyRegistration } from '@/lib/services/register-company.service'; -import { CompanySubmitData } from '@/lib/types/company'; -import { companySchema } from '@/lib/validations/companySchema'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import { FormItem } from '@kit/ui/form'; -import { Input } from '@kit/ui/input'; -import { Label } from '@kit/ui/label'; -import { Trans } from '@kit/ui/trans'; +import { MedReportLogo } from "@/components/med-report-title"; +import React from "react"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useForm } from "react-hook-form"; +import { companySchema } from "@/lib/validations/companySchema"; +import { CompanySubmitData } from "@/lib/types/company"; +import { submitCompanyRegistration } from "@/lib/services/register-company.service"; +import { useRouter } from "next/navigation"; +import { Label } from "@kit/ui/label"; +import { Input } from "@kit/ui/input"; +import { SubmitButton } from "@/components/ui/submit-button"; +import { FormItem } from "@kit/ui/form"; +import { Trans } from "@kit/ui/trans"; export default function RegisterCompany() { const router = useRouter(); diff --git a/app/(public)/register-company/success/page.tsx b/app/(public)/register-company/success/page.tsx index def24a5..35f19e5 100644 --- a/app/(public)/register-company/success/page.tsx +++ b/app/(public)/register-company/success/page.tsx @@ -1,10 +1,9 @@ import Image from 'next/image'; import Link from 'next/link'; -import { MedReportTitle } from '@/components/med-report-title'; -import { Button } from '@/packages/ui/src/shadcn/button'; +import { MedReportLogo } from '@/components/med-report-title'; -import { Trans } from '@kit/ui/trans'; +import { Button } from '@kit/ui/button'; export default function CompanyRegistrationSuccess() { return ( diff --git a/app/admin/_components/admin-sidebar.tsx b/app/admin/_components/admin-sidebar.tsx index d7d655c..5b6c230 100644 --- a/app/admin/_components/admin-sidebar.tsx +++ b/app/admin/_components/admin-sidebar.tsx @@ -15,6 +15,7 @@ import { SidebarHeader, SidebarMenu, SidebarMenuButton, + useSidebar, } from '@kit/ui/shadcn-sidebar'; import { AppLogo } from '~/components/app-logo'; @@ -22,11 +23,12 @@ import { ProfileAccountDropdownContainer } from '~/components/personal-account-d export function AdminSidebar() { const path = usePathname(); + const { open } = useSidebar(); return ( - + @@ -64,4 +66,4 @@ export function AdminSidebar() { ); -} +} \ No newline at end of file diff --git a/app/admin/accounts/page.tsx b/app/admin/accounts/page.tsx index d83191b..21c1104 100644 --- a/app/admin/accounts/page.tsx +++ b/app/admin/accounts/page.tsx @@ -2,6 +2,7 @@ import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs'; import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table'; import { AdminCreateUserDialog } from '@kit/admin/components/admin-create-user-dialog'; +import { AdminCreateCompanyDialog } from '@kit/admin/components/admin-create-company-dialog'; import { AdminGuard } from '@kit/admin/components/admin-guard'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; @@ -30,10 +31,14 @@ async function AccountsPage(props: AdminAccountsPageProps) { return ( <> }> -
+
- + + + + +
diff --git a/components.json b/components.json index ec9676b..edf8b70 100644 --- a/components.json +++ b/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "tailwind.config.ts", - "css": "app/globals.css", + "css": "styles/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" diff --git a/components/app-logo.tsx b/components/app-logo.tsx index f5b354a..d11f922 100644 --- a/components/app-logo.tsx +++ b/components/app-logo.tsx @@ -1,47 +1,36 @@ import Link from 'next/link'; -import { cn } from '@kit/ui/utils'; +import { MedReportLogo } from './med-report-title'; function LogoImage({ className, - width = 105, + compact = false, }: { className?: string; width?: number; + compact?: boolean; }) { - return ( - - - - ); + return ; } export function AppLogo({ href, label, className, + compact = false, }: { href?: string | null; className?: string; label?: string; + compact?: boolean; }) { if (href === null) { - return ; + return ; } return ( - + ); } diff --git a/components/med-report-title.tsx b/components/med-report-title.tsx index e8398cb..efbea64 100644 --- a/components/med-report-title.tsx +++ b/components/med-report-title.tsx @@ -1,10 +1,11 @@ +import { cn } from "@/lib/utils"; import { MedReportSmallLogo } from "@/public/assets/med-report-small-logo"; -export const MedReportTitle = () => ( -
+export const MedReportLogo = ({ className, compact = false }: { className?: string, compact?: boolean }) => ( +
- + {!compact && MedReport - + }
); diff --git a/jobs/sync-analysis-groups.ts b/jobs/sync-analysis-groups.ts index a0d977e..e31c76b 100644 --- a/jobs/sync-analysis-groups.ts +++ b/jobs/sync-analysis-groups.ts @@ -1,4 +1,5 @@ import { createClient as createCustomClient } from '@supabase/supabase-js'; + import axios from 'axios'; import { format } from 'date-fns'; import { config } from 'dotenv'; @@ -126,6 +127,7 @@ async function syncData() { }); } + const codes: any[] = []; for (const analysisGroup of analysisGroups) { // SAVE ANALYSIS GROUP const { data: insertedAnalysisGroup, error } = await supabase @@ -148,15 +150,17 @@ async function syncData() { const analysisGroupId = insertedAnalysisGroup[0].id; const analysisGroupCodes = toArray(analysisGroup.Kood); - const codes = analysisGroupCodes.map((kood) => ({ - hk_code: kood.HkKood, - hk_code_multiplier: kood.HkKoodiKordaja, - coefficient: kood.Koefitsient, - price: kood.Hind, - analysis_group_id: analysisGroupId, - analysis_element_id: null, - analysis_id: null, - })); + codes.push( + ...analysisGroupCodes.map((kood) => ({ + hk_code: kood.HkKood, + hk_code_multiplier: kood.HkKoodiKordaja, + coefficient: kood.Koefitsient, + price: kood.Hind, + analysis_group_id: analysisGroupId, + analysis_element_id: null, + analysis_id: null, + })), + ); const analysisGroupItems = toArray(analysisGroup.Uuring); @@ -229,7 +233,7 @@ async function syncData() { } const insertedAnalysisId = insertedAnalysis[0].id; - if (analysisElement.Kood) { + if (analysis.Kood) { const analysisCodes = toArray(analysis.Kood); codes.push( @@ -249,6 +253,8 @@ async function syncData() { } } + await supabase.from('codes').upsert(codes); + await supabase.schema('audit').from('sync_entries').insert({ operation: 'ANALYSES_SYNC', status: 'SUCCESS', diff --git a/package.json b/package.json index e79bb89..be13199 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@types/react-dom": "19.1.5", "babel-plugin-react-compiler": "19.1.0-rc.2", "cssnano": "^7.0.7", + "dotenv": "^16.5.0", "pino-pretty": "^13.0.0", "prettier": "^3.5.3", "react-hook-form": "^7.57.0", @@ -100,8 +101,7 @@ "tailwindcss": "4.1.7", "tailwindcss-animate": "^1.0.7", "typescript": "^5.8.3", - "yup": "^1.6.1", - "dotenv": "^16.5.0" + "yup": "^1.6.1" }, "prettier": "@kit/prettier-config", "browserslist": [ diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index acf096d..909a929 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -24,6 +24,7 @@ import { cn } from '@kit/ui/utils'; import { CreateTeamAccountDialog } from '../../../team-accounts/src/components/create-team-account-dialog'; import { usePersonalAccountData } from '../hooks/use-personal-account-data'; +import { useUserWorkspace } from '../hooks/use-user-workspace'; interface AccountSelectorProps { accounts: Array<{ @@ -63,6 +64,7 @@ export function AccountSelector({ const [isCreatingAccount, setIsCreatingAccount] = useState(false); const { t } = useTranslation('teams'); const personalData = usePersonalAccountData(userId); + const { user } = useUserWorkspace(); const value = useMemo(() => { return selectedAccount ?? PERSONAL_ACCOUNT_SLUG; @@ -89,6 +91,16 @@ export function AccountSelector({ ); + const isSuperAdmin = useMemo(() => { + const factors = user?.factors ?? []; + const hasAdminRole = user?.app_metadata.role === 'super-admin'; + const hasTotpFactor = factors.some( + (factor) => factor.factor_type === 'totp' && factor.status === 'verified', + ); + + return hasAdminRole && hasTotpFactor; + }, [user]); + return ( <> @@ -172,7 +184,6 @@ export function AccountSelector({ > - - +
-
- Team Members + Company Employees
diff --git a/packages/features/admin/src/components/admin-accounts-table.tsx b/packages/features/admin/src/components/admin-accounts-table.tsx index abaee2d..06a14c7 100644 --- a/packages/features/admin/src/components/admin-accounts-table.tsx +++ b/packages/features/admin/src/components/admin-accounts-table.tsx @@ -132,7 +132,7 @@ function AccountsTableFilters(props: { Account Type All accounts - Team + Company Personal @@ -183,7 +183,7 @@ function getColumns(): ColumnDef[] { id: 'type', header: 'Type', cell: ({ row }) => { - return row.original.is_personal_account ? 'Personal' : 'Team'; + return row.original.is_personal_account ? 'Personal' : 'Company'; }, }, { @@ -248,7 +248,7 @@ function getColumns(): ColumnDef[] { e.preventDefault()}> - Delete Team Account + Delete Company Account diff --git a/packages/features/admin/src/components/admin-create-company-dialog.tsx b/packages/features/admin/src/components/admin-create-company-dialog.tsx new file mode 100644 index 0000000..54d14e9 --- /dev/null +++ b/packages/features/admin/src/components/admin-create-company-dialog.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useState, useTransition } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@kit/ui/form'; +import { If } from '@kit/ui/if'; +import { Input } from '@kit/ui/input'; +import { toast } from '@kit/ui/sonner'; + +import { createCompanyAccountAction } from '../lib/server/admin-server-actions'; +import { CreateCompanySchema, CreateCompanySchemaType } from '../lib/server/schema/create-company.schema'; +import { Trans } from '@kit/ui/trans'; + +export function AdminCreateCompanyDialog(props: React.PropsWithChildren) { + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [open, setOpen] = useState(false); + + const form = useForm({ + resolver: zodResolver(CreateCompanySchema), + defaultValues: { + name: '', + }, + mode: 'onChange', + }); + + const onSubmit = (data: CreateCompanySchemaType) => { + startTransition(async () => { + try { + const error = await createCompanyAccountAction(data); + + if (!error) { + toast.success('Company created successfully'); + form.reset(); + + setOpen(false); + setError(null); + + + } else { + setError('Something went wrong with company creation'); + + } + + + } catch (e) { + setError(e instanceof Error ? e.message : 'Error'); + } + }); + }; + + return ( + + {props.children} + + + + Create New Company Account + + + Complete the form below to create a new company account. + + + +
+ + + + Error + + {error} + + + + { + return ( + + + + + + + + + + + + + + + + ); + }} + /> + + + Cancel + + + + + +
+
+ ); +} diff --git a/packages/features/admin/src/components/admin-create-user-dialog.tsx b/packages/features/admin/src/components/admin-create-user-dialog.tsx index cb5105e..bc21c4a 100644 --- a/packages/features/admin/src/components/admin-create-user-dialog.tsx +++ b/packages/features/admin/src/components/admin-create-user-dialog.tsx @@ -58,7 +58,7 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) { const result = await createUserAction(data); if (result.success) { - toast.success('User creates successfully'); + toast.success('User created successfully'); form.reset(); setOpen(false); diff --git a/packages/features/admin/src/components/admin-dashboard.tsx b/packages/features/admin/src/components/admin-dashboard.tsx index 997d5a5..0492441 100644 --- a/packages/features/admin/src/components/admin-dashboard.tsx +++ b/packages/features/admin/src/components/admin-dashboard.tsx @@ -36,10 +36,10 @@ export async function AdminDashboard() { - Team Accounts + Company Accounts - The number of team accounts that have been created. + The number of company accounts that have been created. @@ -49,43 +49,6 @@ export async function AdminDashboard() {
- - - - Paying Customers - - The number of paying customers with active subscriptions. - - - - -
-
{data.subscriptions}
-
-
-
- - - - Trials - - - The number of trial subscriptions currently active. - - - - -
-
{data.trials}
-
-
-
- -
-

- The above data is estimated and may not be 100% accurate. -

-
); } diff --git a/packages/features/admin/src/components/admin-members-table.tsx b/packages/features/admin/src/components/admin-members-table.tsx index d00e51c..8d256be 100644 --- a/packages/features/admin/src/components/admin-members-table.tsx +++ b/packages/features/admin/src/components/admin-members-table.tsx @@ -52,7 +52,7 @@ function getColumns(): ColumnDef[] { { header: 'Role', cell: ({ row }) => { - return row.original.role; + return row.original.role === 'owner' ? 'HR' : 'Employee'; }, }, { diff --git a/packages/features/admin/src/lib/server/admin-server-actions.ts b/packages/features/admin/src/lib/server/admin-server-actions.ts index 260724f..cac9ae9 100644 --- a/packages/features/admin/src/lib/server/admin-server-actions.ts +++ b/packages/features/admin/src/lib/server/admin-server-actions.ts @@ -20,6 +20,8 @@ import { ResetPasswordSchema } from './schema/reset-password.schema'; import { createAdminAccountsService } from './services/admin-accounts.service'; import { createAdminAuthUserService } from './services/admin-auth-user.service'; import { adminAction } from './utils/admin-action'; +import { CreateCompanySchema } from './schema/create-company.schema'; +import { createCreateCompanyAccountService } from './services/admin-create-company-account.service'; /** * @name banUserAction @@ -222,6 +224,42 @@ export const resetPasswordAction = adminAction( ), ); +export const createCompanyAccountAction = enhanceAction( + async ({ name }, user) => { + const logger = await getLogger(); + const client = getSupabaseServerClient(); + const service = createCreateCompanyAccountService(client); + + const ctx = { + name: 'team-accounts.create', + userId: user.id, + accountName: name, + }; + + logger.info(ctx, `Creating company account...`); + + const { data, error } = await service.createNewOrganizationAccount({ + name, + userId: user.id, + }); + + if (error) { + logger.error({ ...ctx, error }, `Failed to create company account`); + + return { + error: true, + }; + } + + logger.info(ctx, `Company account created`); + + redirect(`/home/${data.slug}/settings`); + }, + { + schema: CreateCompanySchema, + }, +); + function revalidateAdmin() { revalidatePath('/admin', 'layout'); } diff --git a/packages/features/admin/src/lib/server/schema/create-company.schema.ts b/packages/features/admin/src/lib/server/schema/create-company.schema.ts new file mode 100644 index 0000000..4fc0a04 --- /dev/null +++ b/packages/features/admin/src/lib/server/schema/create-company.schema.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; + +/** + * @name RESERVED_NAMES_ARRAY + * @description Array of reserved names for team accounts + * This is a list of names that cannot be used for team accounts as they are reserved for other purposes. + * Please include any new reserved names here. + */ +const RESERVED_NAMES_ARRAY = [ + 'settings', + 'billing', + // please add more reserved names here +]; + +const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/; + +/** + * @name CompanyNameSchema + */ +export const CompanyNameSchema = z + .string({ + description: 'The name of the company account', + }) + .min(2) + .max(50) + .refine( + (name) => { + return !SPECIAL_CHARACTERS_REGEX.test(name); + }, + { + message: 'teams:specialCharactersError', + }, + ) + .refine( + (name) => { + return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase()); + }, + { + message: 'teams:reservedNameError', + }, + ); + +/** + * @name CreateCompanySchema + * @description Schema for creating a team account + */ +export const CreateCompanySchema = z.object({ + name: CompanyNameSchema, +}); + +export type CreateCompanySchemaType = z.infer; + diff --git a/packages/features/admin/src/lib/server/services/admin-create-company-account.service.ts b/packages/features/admin/src/lib/server/services/admin-create-company-account.service.ts new file mode 100644 index 0000000..db59f34 --- /dev/null +++ b/packages/features/admin/src/lib/server/services/admin-create-company-account.service.ts @@ -0,0 +1,45 @@ +import 'server-only'; + +import { SupabaseClient } from '@supabase/supabase-js'; + +import { getLogger } from '@kit/shared/logger'; +import { Database } from '@kit/supabase/database'; + +export function createCreateCompanyAccountService( + client: SupabaseClient, +) { + return new CreateTeamAccountService(client); +} + +class CreateTeamAccountService { + private readonly namespace = 'accounts.create-team-account'; + + constructor(private readonly client: SupabaseClient) {} + + async createNewOrganizationAccount(params: { name: string; userId: string }) { + const logger = await getLogger(); + const ctx = { ...params, namespace: this.namespace }; + + logger.info(ctx, `Creating new company account...`); + + const { error, data } = await this.client.rpc('create_team_account', { + account_name: params.name, + }); + + if (error) { + logger.error( + { + error, + ...ctx, + }, + `Error creating company account`, + ); + + throw new Error('Error creating company account'); + } + + logger.info(ctx, `Company account created successfully`); + + return { data, error }; + } +} diff --git a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts index a741c20..1783688 100644 --- a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts @@ -38,9 +38,7 @@ export const createTeamAccountAction = enhanceAction( logger.info(ctx, `Team account created`); - const accountHomePath = '/home/' + data.slug; - - redirect(accountHomePath); + redirect(`/home/${data.slug}/settings`); }, { schema: CreateTeamSchema, diff --git a/packages/ui/components.json b/packages/ui/components.json index 32cc1dd..826d097 100644 --- a/packages/ui/components.json +++ b/packages/ui/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "./tailwind.config.ts", - "css": "../../apps/web/styles/globals.css", + "css": "../../styles/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aeee463..09bd6ee 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,11 @@ packages: - packages/** - - tooling/* \ No newline at end of file + - tooling/* + +onlyBuiltDependencies: + - '@sentry/cli' + - '@tailwindcss/oxide' + - protobufjs + - sharp + - supabase + - unrs-resolver diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json index 3bb81d4..7db0925 100644 --- a/public/locales/en/auth.json +++ b/public/locales/en/auth.json @@ -63,8 +63,8 @@ "sendingEmailCode": "Sending code...", "resetPasswordError": "Sorry, we could not reset your password. Please try again.", "emailPlaceholder": "your@email.com", - "inviteAlertHeading": "You have been invited to join a team", - "inviteAlertBody": "Please sign in or sign up to accept the invite and join the team.", + "inviteAlertHeading": "You have been invited to join a company", + "inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.", "acceptTermsAndConditions": "I accept the and ", "termsOfService": "Terms of Service", "privacyPolicy": "Privacy Policy", diff --git a/public/locales/en/billing.json b/public/locales/en/billing.json index 9efef35..84c849e 100644 --- a/public/locales/en/billing.json +++ b/public/locales/en/billing.json @@ -18,8 +18,8 @@ "checkoutSuccessBackButton": "Proceed to App", "cannotManageBillingAlertTitle": "You cannot manage billing", "cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.", - "manageTeamPlan": "Manage your Team Plan", - "manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.", + "manageTeamPlan": "Manage your Company Plan", + "manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.", "basePlan": "Base Plan", "billingInterval": { "label": "Choose your billing interval", @@ -34,9 +34,9 @@ "redirectingToPayment": "Redirecting to checkout. Please wait...", "proceedToPayment": "Proceed to Payment", "startTrial": "Start Trial", - "perTeamMember": "Per team member", + "perTeamMember": "Per company employee", "perUnit": "Per {{unit}} usage", - "teamMembers": "Team Members", + "teamMembers": "Company Employees", "includedUpTo": "Up to {{upTo}} {{unit}} included in the plan", "fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}", "andAbove": "above {{ previousTier }} {{ unit }}", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c194ce3..8aade69 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,8 +1,8 @@ { "homeTabLabel": "Home", "homeTabDescription": "Welcome to your home page", - "accountMembers": "Team Members", - "membersTabDescription": "Here you can manage the members of your team.", + "accountMembers": "Company Employees", + "membersTabDescription": "Here you can manage the employees of your company.", "billingTabLabel": "Billing", "billingTabDescription": "Manage your billing and subscription", "dashboardTabLabel": "Dashboard", @@ -58,7 +58,7 @@ "routes": { "home": "Home", "account": "Account", - "members": "Members", + "members": "Employees", "billing": "Billing", "dashboard": "Dashboard", "settings": "Settings", @@ -70,7 +70,7 @@ "label": "Owner" }, "member": { - "label": "Member" + "label": "Employee" } }, "otp": { diff --git a/public/locales/en/teams.json b/public/locales/en/teams.json index 2a0b614..d09d78d 100644 --- a/public/locales/en/teams.json +++ b/public/locales/en/teams.json @@ -4,26 +4,26 @@ }, "settings": { "pageTitle": "Settings", - "pageDescription": "Manage your Team details", - "teamLogo": "Team Logo", - "teamLogoDescription": "Update your team's logo to make it easier to identify", - "teamName": "Team Name", - "teamNameDescription": "Update your team's name", + "pageDescription": "Manage your Company details", + "teamLogo": "Company Logo", + "teamLogoDescription": "Update your company's logo to make it easier to identify", + "teamName": "Company Name", + "teamNameDescription": "Update your company's name", "dangerZone": "Danger Zone", "dangerZoneDescription": "This section contains actions that are irreversible" }, "members": { - "pageTitle": "Members" + "pageTitle": "Employees" }, "billing": { "pageTitle": "Billing" }, - "yourTeams": "Your Teams ({{teamsCount}})", - "createTeam": "Create a Team", - "creatingTeam": "Creating Team...", + "yourTeams": "Your Companies ({{teamsCount}})", + "createTeam": "Create a Company", + "creatingTeam": "Creating Company...", "personalAccount": "Personal Account", "searchAccount": "Search Account...", - "membersTabLabel": "Members", + "membersTabLabel": "Employees", "memberName": "Name", "youLabel": "You", "emailLabel": "Email", @@ -31,108 +31,108 @@ "primaryOwnerLabel": "Primary Owner", "joinedAtLabel": "Joined at", "invitedAtLabel": "Invited at", - "inviteMembersPageSubheading": "Invite members to your Team", - "createTeamModalHeading": "Create Team", - "createTeamModalDescription": "Create a new Team to manage your projects and members.", - "teamNameLabel": "Team Name", - "teamNameDescription": "Your team name should be unique and descriptive", - "createTeamSubmitLabel": "Create Team", - "createTeamSuccess": "Team created successfully", - "createTeamError": "Team not created. Please try again.", - "createTeamLoading": "Creating team...", + "inviteMembersPageSubheading": "Invite employees to your Company", + "createTeamModalHeading": "Create Company", + "createTeamModalDescription": "Create a new Company to manage your projects and employees.", + "teamNameLabel": "Company Name", + "teamNameDescription": "Your company name should be unique and descriptive", + "createTeamSubmitLabel": "Create Company", + "createTeamSuccess": "Company created successfully", + "createTeamError": "Company not created. Please try again.", + "createTeamLoading": "Creating company...", "settingsPageLabel": "General", - "createTeamDropdownLabel": "New team", + "createTeamDropdownLabel": "New company", "changeRole": "Change Role", "removeMember": "Remove from Account", - "inviteMembersSuccess": "Members invited successfully!", + "inviteMembersSuccess": "Employees invited successfully!", "inviteMembersError": "Sorry, we encountered an error! Please try again", - "inviteMembersLoading": "Inviting members...", + "inviteMembersLoading": "Inviting employees...", "removeInviteButtonLabel": "Remove invite", "addAnotherMemberButtonLabel": "Add another one", "inviteMembersButtonLabel": "Send Invites", "removeMemberModalHeading": "You are removing this user", - "removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.", - "removeMemberSuccessMessage": "Member removed successfully", + "removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.", + "removeMemberSuccessMessage": "Employee removed successfully", "removeMemberErrorMessage": "Sorry, we encountered an error. Please try again", - "removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.", - "removeMemberLoadingMessage": "Removing member...", - "removeMemberSubmitLabel": "Remove User from Team", + "removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.", + "removeMemberLoadingMessage": "Removing employee...", + "removeMemberSubmitLabel": "Remove User from Company", "chooseDifferentRoleError": "Role is the same as the current one", "updateRole": "Update Role", "updateRoleLoadingMessage": "Updating role...", "updateRoleSuccessMessage": "Role updated successfully", "updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.", - "updateMemberRoleModalHeading": "Update Member's Role", + "updateMemberRoleModalHeading": "Update Employee's Role", "updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.", "roleMustBeDifferent": "Role must be different from the current one", "memberRoleInputLabel": "Member role", "updateRoleDescription": "Pick a role for this member.", "updateRoleSubmitLabel": "Update Role", "transferOwnership": "Transfer Ownership", - "transferOwnershipDescription": "Transfer ownership of the team to another member.", + "transferOwnershipDescription": "Transfer ownership of the company account to another employee.", "transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.", - "transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the team.", + "transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the company account.", "deleteInvitation": "Delete Invitation", - "deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the team.", + "deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.", "deleteInviteSuccessMessage": "Invite deleted successfully", "deleteInviteErrorMessage": "Invite not deleted. Please try again.", "deleteInviteLoadingMessage": "Deleting invite. Please wait...", "confirmDeletingMemberInvite": "You are deleting the invite to {{ email }}", - "transferOwnershipDisclaimer": "You are transferring ownership of the selected team to {{ member }}.", + "transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to {{ member }}.", "transferringOwnership": "Transferring ownership...", "transferOwnershipSuccess": "Ownership successfully transferred", "transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.", "deleteInviteSubmitLabel": "Delete Invite", "youBadgeLabel": "You", - "updateTeamLoadingMessage": "Updating Team...", - "updateTeamSuccessMessage": "Team successfully updated", - "updateTeamErrorMessage": "Could not update Team. Please try again.", + "updateTeamLoadingMessage": "Updating Company...", + "updateTeamSuccessMessage": "Company successfully updated", + "updateTeamErrorMessage": "Could not update Company. Please try again.", "updateLogoErrorMessage": "Could not update Logo. Please try again.", - "teamNameInputLabel": "Team Name", - "teamLogoInputHeading": "Upload your team's Logo", - "teamLogoInputSubheading": "Please choose a photo to upload as your team logo.", - "updateTeamSubmitLabel": "Update Team", - "inviteMembersHeading": "Invite Members to your Team", - "inviteMembersDescription": "Invite members to your team by entering their email and role.", - "emailPlaceholder": "member@email.com", - "membersPageHeading": "Members", - "inviteMembersButton": "Invite Members", - "invitingMembers": "Inviting members...", - "inviteMembersSuccessMessage": "Members invited successfully", - "inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.", + "teamNameInputLabel": "Company Name", + "teamLogoInputHeading": "Upload your company's Logo", + "teamLogoInputSubheading": "Please choose a photo to upload as your company logo.", + "updateTeamSubmitLabel": "Update Company", + "inviteMembersHeading": "Invite Employees to your Company", + "inviteMembersDescription": "Invite employees to your company by entering their email and role.", + "emailPlaceholder": "employee@email.com", + "membersPageHeading": "Employees", + "inviteMembersButton": "Invite Employees", + "invitingMembers": "Inviting employees...", + "inviteMembersSuccessMessage": "Employees invited successfully", + "inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.", "pendingInvitesHeading": "Pending Invites", - "pendingInvitesDescription": " Here you can manage the pending invitations to your team.", + "pendingInvitesDescription": " Here you can manage the pending invitations to your company.", "noPendingInvites": "No pending invites found", - "loadingMembers": "Loading members...", - "loadMembersError": "Sorry, we couldn't fetch your team's members.", - "loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.", - "loadingInvitedMembers": "Loading invited members...", + "loadingMembers": "Loading employees...", + "loadMembersError": "Sorry, we couldn't fetch your company's employees.", + "loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.", + "loadingInvitedMembers": "Loading invited employees...", "invitedBadge": "Invited", "duplicateInviteEmailError": "You have already entered this email address", "invitingOwnAccountError": "Hey, that's your email!", "dangerZone": "Danger Zone", - "dangerZoneSubheading": "Delete or leave your team", - "deleteTeam": "Delete Team", - "deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.", - "deletingTeam": "Deleting team", - "deleteTeamModalHeading": "Deleting Team", - "deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.", - "deleteTeamInputField": "Type the name of the team to confirm", - "leaveTeam": "Leave Team", - "leavingTeamModalHeading": "Leaving Team", - "leavingTeamModalDescription": "You are about to leave this team. You will no longer have access to it.", - "leaveTeamDescription": "Click the button below to leave the team. Remember, you will no longer have access to it and will need to be re-invited to join.", - "deleteTeamDisclaimer": "You are deleting the team {{ teamName }}. This action cannot be undone.", - "leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.", - "deleteTeamErrorHeading": "Sorry, we couldn't delete your team.", - "leaveTeamErrorHeading": "Sorry, we couldn't leave your team.", - "searchMembersPlaceholder": "Search members", - "createTeamErrorHeading": "Sorry, we couldn't create your team.", - "createTeamErrorMessage": "We encountered an error creating your team. Please try again.", - "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.", - "transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.", - "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.", - "updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.", + "dangerZoneSubheading": "Delete or leave your company", + "deleteTeam": "Delete Company", + "deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.", + "deletingTeam": "Deleting company", + "deleteTeamModalHeading": "Deleting Company", + "deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.", + "deleteTeamInputField": "Type the name of the company to confirm", + "leaveTeam": "Leave Company", + "leavingTeamModalHeading": "Leaving Company", + "leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.", + "leaveTeamDescription": "Click the button below to leave the company. Remember, you will no longer have access to it and will need to be re-invited to join.", + "deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.", + "leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.", + "deleteTeamErrorHeading": "Sorry, we couldn't delete your company.", + "leaveTeamErrorHeading": "Sorry, we couldn't leave your company.", + "searchMembersPlaceholder": "Search employees", + "createTeamErrorHeading": "Sorry, we couldn't create your company.", + "createTeamErrorMessage": "We encountered an error creating your company. Please try again.", + "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.", + "transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.", + "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.", + "updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.", "searchInvitations": "Search Invitations", "updateInvitation": "Update Invitation", "removeInvitation": "Remove Invitation", @@ -144,20 +144,20 @@ "active": "Active", "inviteStatus": "Status", "inviteNotFoundOrExpired": "Invite not found or expired", - "inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the team owner to renew the invite.", + "inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company HR to renew the invite.", "backToHome": "Back to Home", - "renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the team.", + "renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.", "renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.", "renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.", "signInWithDifferentAccount": "Sign in with a different account", "signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.", "acceptInvitationHeading": "Accept Invitation to join {{accountName}}", - "acceptInvitationDescription": "You have been invited to join the team {{accountName}}. If you wish to accept the invitation, please click the button below.", + "acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.", "continueAs": "Continue as {{email}}", - "joinTeamAccount": "Join Team", - "joiningTeam": "Joining team...", - "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.", - "leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.", + "joinTeamAccount": "Join Company", + "joiningTeam": "Joining company...", + "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.", + "leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.", "reservedNameError": "This name is reserved. Please choose a different one.", "specialCharactersError": "This name cannot contain special characters. Please choose a different one." } diff --git a/public/locales/et/auth.json b/public/locales/et/auth.json index 3bb81d4..7db0925 100644 --- a/public/locales/et/auth.json +++ b/public/locales/et/auth.json @@ -63,8 +63,8 @@ "sendingEmailCode": "Sending code...", "resetPasswordError": "Sorry, we could not reset your password. Please try again.", "emailPlaceholder": "your@email.com", - "inviteAlertHeading": "You have been invited to join a team", - "inviteAlertBody": "Please sign in or sign up to accept the invite and join the team.", + "inviteAlertHeading": "You have been invited to join a company", + "inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.", "acceptTermsAndConditions": "I accept the and ", "termsOfService": "Terms of Service", "privacyPolicy": "Privacy Policy", diff --git a/public/locales/et/billing.json b/public/locales/et/billing.json index 9efef35..84c849e 100644 --- a/public/locales/et/billing.json +++ b/public/locales/et/billing.json @@ -18,8 +18,8 @@ "checkoutSuccessBackButton": "Proceed to App", "cannotManageBillingAlertTitle": "You cannot manage billing", "cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.", - "manageTeamPlan": "Manage your Team Plan", - "manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.", + "manageTeamPlan": "Manage your Company Plan", + "manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.", "basePlan": "Base Plan", "billingInterval": { "label": "Choose your billing interval", @@ -34,9 +34,9 @@ "redirectingToPayment": "Redirecting to checkout. Please wait...", "proceedToPayment": "Proceed to Payment", "startTrial": "Start Trial", - "perTeamMember": "Per team member", + "perTeamMember": "Per company employee", "perUnit": "Per {{unit}} usage", - "teamMembers": "Team Members", + "teamMembers": "Company Employees", "includedUpTo": "Up to {{upTo}} {{unit}} included in the plan", "fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}", "andAbove": "above {{ previousTier }} {{ unit }}", diff --git a/public/locales/et/common.json b/public/locales/et/common.json index 076489f..6242a03 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -1,8 +1,8 @@ { "homeTabLabel": "Home", "homeTabDescription": "Welcome to your home page", - "accountMembers": "Team Members", - "membersTabDescription": "Here you can manage the members of your team.", + "accountMembers": "Company Employees", + "membersTabDescription": "Here you can manage the employees of your company.", "billingTabLabel": "Billing", "billingTabDescription": "Manage your billing and subscription", "dashboardTabLabel": "Dashboard", @@ -58,7 +58,7 @@ "routes": { "home": "Home", "account": "Account", - "members": "Members", + "members": "Employees", "billing": "Billing", "dashboard": "Dashboard", "settings": "Settings", @@ -70,7 +70,7 @@ "label": "Owner" }, "member": { - "label": "Member" + "label": "Employee" } }, "otp": { diff --git a/public/locales/et/marketing.json b/public/locales/et/marketing.json index 96420bd..3deb11a 100644 --- a/public/locales/et/marketing.json +++ b/public/locales/et/marketing.json @@ -36,5 +36,5 @@ "contactErrorDescription": "An error occurred while sending your message. Please try again later", "footerDescription": "Here you can add a description about your company or product", "copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.", - "heroSubtitle": "Lihtne, mugav ja kiire ülevaade Sinu tervise seisundist" + "heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest" } diff --git a/public/locales/et/teams.json b/public/locales/et/teams.json index 2a0b614..d09d78d 100644 --- a/public/locales/et/teams.json +++ b/public/locales/et/teams.json @@ -4,26 +4,26 @@ }, "settings": { "pageTitle": "Settings", - "pageDescription": "Manage your Team details", - "teamLogo": "Team Logo", - "teamLogoDescription": "Update your team's logo to make it easier to identify", - "teamName": "Team Name", - "teamNameDescription": "Update your team's name", + "pageDescription": "Manage your Company details", + "teamLogo": "Company Logo", + "teamLogoDescription": "Update your company's logo to make it easier to identify", + "teamName": "Company Name", + "teamNameDescription": "Update your company's name", "dangerZone": "Danger Zone", "dangerZoneDescription": "This section contains actions that are irreversible" }, "members": { - "pageTitle": "Members" + "pageTitle": "Employees" }, "billing": { "pageTitle": "Billing" }, - "yourTeams": "Your Teams ({{teamsCount}})", - "createTeam": "Create a Team", - "creatingTeam": "Creating Team...", + "yourTeams": "Your Companies ({{teamsCount}})", + "createTeam": "Create a Company", + "creatingTeam": "Creating Company...", "personalAccount": "Personal Account", "searchAccount": "Search Account...", - "membersTabLabel": "Members", + "membersTabLabel": "Employees", "memberName": "Name", "youLabel": "You", "emailLabel": "Email", @@ -31,108 +31,108 @@ "primaryOwnerLabel": "Primary Owner", "joinedAtLabel": "Joined at", "invitedAtLabel": "Invited at", - "inviteMembersPageSubheading": "Invite members to your Team", - "createTeamModalHeading": "Create Team", - "createTeamModalDescription": "Create a new Team to manage your projects and members.", - "teamNameLabel": "Team Name", - "teamNameDescription": "Your team name should be unique and descriptive", - "createTeamSubmitLabel": "Create Team", - "createTeamSuccess": "Team created successfully", - "createTeamError": "Team not created. Please try again.", - "createTeamLoading": "Creating team...", + "inviteMembersPageSubheading": "Invite employees to your Company", + "createTeamModalHeading": "Create Company", + "createTeamModalDescription": "Create a new Company to manage your projects and employees.", + "teamNameLabel": "Company Name", + "teamNameDescription": "Your company name should be unique and descriptive", + "createTeamSubmitLabel": "Create Company", + "createTeamSuccess": "Company created successfully", + "createTeamError": "Company not created. Please try again.", + "createTeamLoading": "Creating company...", "settingsPageLabel": "General", - "createTeamDropdownLabel": "New team", + "createTeamDropdownLabel": "New company", "changeRole": "Change Role", "removeMember": "Remove from Account", - "inviteMembersSuccess": "Members invited successfully!", + "inviteMembersSuccess": "Employees invited successfully!", "inviteMembersError": "Sorry, we encountered an error! Please try again", - "inviteMembersLoading": "Inviting members...", + "inviteMembersLoading": "Inviting employees...", "removeInviteButtonLabel": "Remove invite", "addAnotherMemberButtonLabel": "Add another one", "inviteMembersButtonLabel": "Send Invites", "removeMemberModalHeading": "You are removing this user", - "removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.", - "removeMemberSuccessMessage": "Member removed successfully", + "removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.", + "removeMemberSuccessMessage": "Employee removed successfully", "removeMemberErrorMessage": "Sorry, we encountered an error. Please try again", - "removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.", - "removeMemberLoadingMessage": "Removing member...", - "removeMemberSubmitLabel": "Remove User from Team", + "removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.", + "removeMemberLoadingMessage": "Removing employee...", + "removeMemberSubmitLabel": "Remove User from Company", "chooseDifferentRoleError": "Role is the same as the current one", "updateRole": "Update Role", "updateRoleLoadingMessage": "Updating role...", "updateRoleSuccessMessage": "Role updated successfully", "updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.", - "updateMemberRoleModalHeading": "Update Member's Role", + "updateMemberRoleModalHeading": "Update Employee's Role", "updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.", "roleMustBeDifferent": "Role must be different from the current one", "memberRoleInputLabel": "Member role", "updateRoleDescription": "Pick a role for this member.", "updateRoleSubmitLabel": "Update Role", "transferOwnership": "Transfer Ownership", - "transferOwnershipDescription": "Transfer ownership of the team to another member.", + "transferOwnershipDescription": "Transfer ownership of the company account to another employee.", "transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.", - "transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the team.", + "transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the company account.", "deleteInvitation": "Delete Invitation", - "deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the team.", + "deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.", "deleteInviteSuccessMessage": "Invite deleted successfully", "deleteInviteErrorMessage": "Invite not deleted. Please try again.", "deleteInviteLoadingMessage": "Deleting invite. Please wait...", "confirmDeletingMemberInvite": "You are deleting the invite to {{ email }}", - "transferOwnershipDisclaimer": "You are transferring ownership of the selected team to {{ member }}.", + "transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to {{ member }}.", "transferringOwnership": "Transferring ownership...", "transferOwnershipSuccess": "Ownership successfully transferred", "transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.", "deleteInviteSubmitLabel": "Delete Invite", "youBadgeLabel": "You", - "updateTeamLoadingMessage": "Updating Team...", - "updateTeamSuccessMessage": "Team successfully updated", - "updateTeamErrorMessage": "Could not update Team. Please try again.", + "updateTeamLoadingMessage": "Updating Company...", + "updateTeamSuccessMessage": "Company successfully updated", + "updateTeamErrorMessage": "Could not update Company. Please try again.", "updateLogoErrorMessage": "Could not update Logo. Please try again.", - "teamNameInputLabel": "Team Name", - "teamLogoInputHeading": "Upload your team's Logo", - "teamLogoInputSubheading": "Please choose a photo to upload as your team logo.", - "updateTeamSubmitLabel": "Update Team", - "inviteMembersHeading": "Invite Members to your Team", - "inviteMembersDescription": "Invite members to your team by entering their email and role.", - "emailPlaceholder": "member@email.com", - "membersPageHeading": "Members", - "inviteMembersButton": "Invite Members", - "invitingMembers": "Inviting members...", - "inviteMembersSuccessMessage": "Members invited successfully", - "inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.", + "teamNameInputLabel": "Company Name", + "teamLogoInputHeading": "Upload your company's Logo", + "teamLogoInputSubheading": "Please choose a photo to upload as your company logo.", + "updateTeamSubmitLabel": "Update Company", + "inviteMembersHeading": "Invite Employees to your Company", + "inviteMembersDescription": "Invite employees to your company by entering their email and role.", + "emailPlaceholder": "employee@email.com", + "membersPageHeading": "Employees", + "inviteMembersButton": "Invite Employees", + "invitingMembers": "Inviting employees...", + "inviteMembersSuccessMessage": "Employees invited successfully", + "inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.", "pendingInvitesHeading": "Pending Invites", - "pendingInvitesDescription": " Here you can manage the pending invitations to your team.", + "pendingInvitesDescription": " Here you can manage the pending invitations to your company.", "noPendingInvites": "No pending invites found", - "loadingMembers": "Loading members...", - "loadMembersError": "Sorry, we couldn't fetch your team's members.", - "loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.", - "loadingInvitedMembers": "Loading invited members...", + "loadingMembers": "Loading employees...", + "loadMembersError": "Sorry, we couldn't fetch your company's employees.", + "loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.", + "loadingInvitedMembers": "Loading invited employees...", "invitedBadge": "Invited", "duplicateInviteEmailError": "You have already entered this email address", "invitingOwnAccountError": "Hey, that's your email!", "dangerZone": "Danger Zone", - "dangerZoneSubheading": "Delete or leave your team", - "deleteTeam": "Delete Team", - "deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.", - "deletingTeam": "Deleting team", - "deleteTeamModalHeading": "Deleting Team", - "deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.", - "deleteTeamInputField": "Type the name of the team to confirm", - "leaveTeam": "Leave Team", - "leavingTeamModalHeading": "Leaving Team", - "leavingTeamModalDescription": "You are about to leave this team. You will no longer have access to it.", - "leaveTeamDescription": "Click the button below to leave the team. Remember, you will no longer have access to it and will need to be re-invited to join.", - "deleteTeamDisclaimer": "You are deleting the team {{ teamName }}. This action cannot be undone.", - "leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.", - "deleteTeamErrorHeading": "Sorry, we couldn't delete your team.", - "leaveTeamErrorHeading": "Sorry, we couldn't leave your team.", - "searchMembersPlaceholder": "Search members", - "createTeamErrorHeading": "Sorry, we couldn't create your team.", - "createTeamErrorMessage": "We encountered an error creating your team. Please try again.", - "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.", - "transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.", - "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.", - "updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.", + "dangerZoneSubheading": "Delete or leave your company", + "deleteTeam": "Delete Company", + "deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.", + "deletingTeam": "Deleting company", + "deleteTeamModalHeading": "Deleting Company", + "deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.", + "deleteTeamInputField": "Type the name of the company to confirm", + "leaveTeam": "Leave Company", + "leavingTeamModalHeading": "Leaving Company", + "leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.", + "leaveTeamDescription": "Click the button below to leave the company. Remember, you will no longer have access to it and will need to be re-invited to join.", + "deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.", + "leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.", + "deleteTeamErrorHeading": "Sorry, we couldn't delete your company.", + "leaveTeamErrorHeading": "Sorry, we couldn't leave your company.", + "searchMembersPlaceholder": "Search employees", + "createTeamErrorHeading": "Sorry, we couldn't create your company.", + "createTeamErrorMessage": "We encountered an error creating your company. Please try again.", + "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.", + "transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.", + "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.", + "updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.", "searchInvitations": "Search Invitations", "updateInvitation": "Update Invitation", "removeInvitation": "Remove Invitation", @@ -144,20 +144,20 @@ "active": "Active", "inviteStatus": "Status", "inviteNotFoundOrExpired": "Invite not found or expired", - "inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the team owner to renew the invite.", + "inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company HR to renew the invite.", "backToHome": "Back to Home", - "renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the team.", + "renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.", "renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.", "renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.", "signInWithDifferentAccount": "Sign in with a different account", "signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.", "acceptInvitationHeading": "Accept Invitation to join {{accountName}}", - "acceptInvitationDescription": "You have been invited to join the team {{accountName}}. If you wish to accept the invitation, please click the button below.", + "acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.", "continueAs": "Continue as {{email}}", - "joinTeamAccount": "Join Team", - "joiningTeam": "Joining team...", - "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.", - "leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.", + "joinTeamAccount": "Join Company", + "joiningTeam": "Joining company...", + "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.", + "leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.", "reservedNameError": "This name is reserved. Please choose a different one.", "specialCharactersError": "This name cannot contain special characters. Please choose a different one." } diff --git a/public/locales/ru/auth.json b/public/locales/ru/auth.json index 3bb81d4..7db0925 100644 --- a/public/locales/ru/auth.json +++ b/public/locales/ru/auth.json @@ -63,8 +63,8 @@ "sendingEmailCode": "Sending code...", "resetPasswordError": "Sorry, we could not reset your password. Please try again.", "emailPlaceholder": "your@email.com", - "inviteAlertHeading": "You have been invited to join a team", - "inviteAlertBody": "Please sign in or sign up to accept the invite and join the team.", + "inviteAlertHeading": "You have been invited to join a company", + "inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.", "acceptTermsAndConditions": "I accept the and ", "termsOfService": "Terms of Service", "privacyPolicy": "Privacy Policy", diff --git a/public/locales/ru/billing.json b/public/locales/ru/billing.json index 9efef35..84c849e 100644 --- a/public/locales/ru/billing.json +++ b/public/locales/ru/billing.json @@ -18,8 +18,8 @@ "checkoutSuccessBackButton": "Proceed to App", "cannotManageBillingAlertTitle": "You cannot manage billing", "cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.", - "manageTeamPlan": "Manage your Team Plan", - "manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.", + "manageTeamPlan": "Manage your Company Plan", + "manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.", "basePlan": "Base Plan", "billingInterval": { "label": "Choose your billing interval", @@ -34,9 +34,9 @@ "redirectingToPayment": "Redirecting to checkout. Please wait...", "proceedToPayment": "Proceed to Payment", "startTrial": "Start Trial", - "perTeamMember": "Per team member", + "perTeamMember": "Per company employee", "perUnit": "Per {{unit}} usage", - "teamMembers": "Team Members", + "teamMembers": "Company Employees", "includedUpTo": "Up to {{upTo}} {{unit}} included in the plan", "fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}", "andAbove": "above {{ previousTier }} {{ unit }}", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index c194ce3..8aade69 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -1,8 +1,8 @@ { "homeTabLabel": "Home", "homeTabDescription": "Welcome to your home page", - "accountMembers": "Team Members", - "membersTabDescription": "Here you can manage the members of your team.", + "accountMembers": "Company Employees", + "membersTabDescription": "Here you can manage the employees of your company.", "billingTabLabel": "Billing", "billingTabDescription": "Manage your billing and subscription", "dashboardTabLabel": "Dashboard", @@ -58,7 +58,7 @@ "routes": { "home": "Home", "account": "Account", - "members": "Members", + "members": "Employees", "billing": "Billing", "dashboard": "Dashboard", "settings": "Settings", @@ -70,7 +70,7 @@ "label": "Owner" }, "member": { - "label": "Member" + "label": "Employee" } }, "otp": { diff --git a/public/locales/ru/marketing.json b/public/locales/ru/marketing.json index 50076c7..a214fc2 100644 --- a/public/locales/ru/marketing.json +++ b/public/locales/ru/marketing.json @@ -36,5 +36,5 @@ "contactErrorDescription": "An error occurred while sending your message. Please try again later", "footerDescription": "Here you can add a description about your company or product", "copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.", - "heroSubtitle": "Простой, удобный и быстрый обзор вашего состояния здоровья" + "heroSubtitle": "A simple, convenient, and quick overview of your health condition" } diff --git a/public/locales/ru/teams.json b/public/locales/ru/teams.json index 2a0b614..d09d78d 100644 --- a/public/locales/ru/teams.json +++ b/public/locales/ru/teams.json @@ -4,26 +4,26 @@ }, "settings": { "pageTitle": "Settings", - "pageDescription": "Manage your Team details", - "teamLogo": "Team Logo", - "teamLogoDescription": "Update your team's logo to make it easier to identify", - "teamName": "Team Name", - "teamNameDescription": "Update your team's name", + "pageDescription": "Manage your Company details", + "teamLogo": "Company Logo", + "teamLogoDescription": "Update your company's logo to make it easier to identify", + "teamName": "Company Name", + "teamNameDescription": "Update your company's name", "dangerZone": "Danger Zone", "dangerZoneDescription": "This section contains actions that are irreversible" }, "members": { - "pageTitle": "Members" + "pageTitle": "Employees" }, "billing": { "pageTitle": "Billing" }, - "yourTeams": "Your Teams ({{teamsCount}})", - "createTeam": "Create a Team", - "creatingTeam": "Creating Team...", + "yourTeams": "Your Companies ({{teamsCount}})", + "createTeam": "Create a Company", + "creatingTeam": "Creating Company...", "personalAccount": "Personal Account", "searchAccount": "Search Account...", - "membersTabLabel": "Members", + "membersTabLabel": "Employees", "memberName": "Name", "youLabel": "You", "emailLabel": "Email", @@ -31,108 +31,108 @@ "primaryOwnerLabel": "Primary Owner", "joinedAtLabel": "Joined at", "invitedAtLabel": "Invited at", - "inviteMembersPageSubheading": "Invite members to your Team", - "createTeamModalHeading": "Create Team", - "createTeamModalDescription": "Create a new Team to manage your projects and members.", - "teamNameLabel": "Team Name", - "teamNameDescription": "Your team name should be unique and descriptive", - "createTeamSubmitLabel": "Create Team", - "createTeamSuccess": "Team created successfully", - "createTeamError": "Team not created. Please try again.", - "createTeamLoading": "Creating team...", + "inviteMembersPageSubheading": "Invite employees to your Company", + "createTeamModalHeading": "Create Company", + "createTeamModalDescription": "Create a new Company to manage your projects and employees.", + "teamNameLabel": "Company Name", + "teamNameDescription": "Your company name should be unique and descriptive", + "createTeamSubmitLabel": "Create Company", + "createTeamSuccess": "Company created successfully", + "createTeamError": "Company not created. Please try again.", + "createTeamLoading": "Creating company...", "settingsPageLabel": "General", - "createTeamDropdownLabel": "New team", + "createTeamDropdownLabel": "New company", "changeRole": "Change Role", "removeMember": "Remove from Account", - "inviteMembersSuccess": "Members invited successfully!", + "inviteMembersSuccess": "Employees invited successfully!", "inviteMembersError": "Sorry, we encountered an error! Please try again", - "inviteMembersLoading": "Inviting members...", + "inviteMembersLoading": "Inviting employees...", "removeInviteButtonLabel": "Remove invite", "addAnotherMemberButtonLabel": "Add another one", "inviteMembersButtonLabel": "Send Invites", "removeMemberModalHeading": "You are removing this user", - "removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.", - "removeMemberSuccessMessage": "Member removed successfully", + "removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.", + "removeMemberSuccessMessage": "Employee removed successfully", "removeMemberErrorMessage": "Sorry, we encountered an error. Please try again", - "removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.", - "removeMemberLoadingMessage": "Removing member...", - "removeMemberSubmitLabel": "Remove User from Team", + "removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.", + "removeMemberLoadingMessage": "Removing employee...", + "removeMemberSubmitLabel": "Remove User from Company", "chooseDifferentRoleError": "Role is the same as the current one", "updateRole": "Update Role", "updateRoleLoadingMessage": "Updating role...", "updateRoleSuccessMessage": "Role updated successfully", "updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.", - "updateMemberRoleModalHeading": "Update Member's Role", + "updateMemberRoleModalHeading": "Update Employee's Role", "updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.", "roleMustBeDifferent": "Role must be different from the current one", "memberRoleInputLabel": "Member role", "updateRoleDescription": "Pick a role for this member.", "updateRoleSubmitLabel": "Update Role", "transferOwnership": "Transfer Ownership", - "transferOwnershipDescription": "Transfer ownership of the team to another member.", + "transferOwnershipDescription": "Transfer ownership of the company account to another employee.", "transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.", - "transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the team.", + "transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the company account.", "deleteInvitation": "Delete Invitation", - "deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the team.", + "deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.", "deleteInviteSuccessMessage": "Invite deleted successfully", "deleteInviteErrorMessage": "Invite not deleted. Please try again.", "deleteInviteLoadingMessage": "Deleting invite. Please wait...", "confirmDeletingMemberInvite": "You are deleting the invite to {{ email }}", - "transferOwnershipDisclaimer": "You are transferring ownership of the selected team to {{ member }}.", + "transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to {{ member }}.", "transferringOwnership": "Transferring ownership...", "transferOwnershipSuccess": "Ownership successfully transferred", "transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.", "deleteInviteSubmitLabel": "Delete Invite", "youBadgeLabel": "You", - "updateTeamLoadingMessage": "Updating Team...", - "updateTeamSuccessMessage": "Team successfully updated", - "updateTeamErrorMessage": "Could not update Team. Please try again.", + "updateTeamLoadingMessage": "Updating Company...", + "updateTeamSuccessMessage": "Company successfully updated", + "updateTeamErrorMessage": "Could not update Company. Please try again.", "updateLogoErrorMessage": "Could not update Logo. Please try again.", - "teamNameInputLabel": "Team Name", - "teamLogoInputHeading": "Upload your team's Logo", - "teamLogoInputSubheading": "Please choose a photo to upload as your team logo.", - "updateTeamSubmitLabel": "Update Team", - "inviteMembersHeading": "Invite Members to your Team", - "inviteMembersDescription": "Invite members to your team by entering their email and role.", - "emailPlaceholder": "member@email.com", - "membersPageHeading": "Members", - "inviteMembersButton": "Invite Members", - "invitingMembers": "Inviting members...", - "inviteMembersSuccessMessage": "Members invited successfully", - "inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.", + "teamNameInputLabel": "Company Name", + "teamLogoInputHeading": "Upload your company's Logo", + "teamLogoInputSubheading": "Please choose a photo to upload as your company logo.", + "updateTeamSubmitLabel": "Update Company", + "inviteMembersHeading": "Invite Employees to your Company", + "inviteMembersDescription": "Invite employees to your company by entering their email and role.", + "emailPlaceholder": "employee@email.com", + "membersPageHeading": "Employees", + "inviteMembersButton": "Invite Employees", + "invitingMembers": "Inviting employees...", + "inviteMembersSuccessMessage": "Employees invited successfully", + "inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.", "pendingInvitesHeading": "Pending Invites", - "pendingInvitesDescription": " Here you can manage the pending invitations to your team.", + "pendingInvitesDescription": " Here you can manage the pending invitations to your company.", "noPendingInvites": "No pending invites found", - "loadingMembers": "Loading members...", - "loadMembersError": "Sorry, we couldn't fetch your team's members.", - "loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.", - "loadingInvitedMembers": "Loading invited members...", + "loadingMembers": "Loading employees...", + "loadMembersError": "Sorry, we couldn't fetch your company's employees.", + "loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.", + "loadingInvitedMembers": "Loading invited employees...", "invitedBadge": "Invited", "duplicateInviteEmailError": "You have already entered this email address", "invitingOwnAccountError": "Hey, that's your email!", "dangerZone": "Danger Zone", - "dangerZoneSubheading": "Delete or leave your team", - "deleteTeam": "Delete Team", - "deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.", - "deletingTeam": "Deleting team", - "deleteTeamModalHeading": "Deleting Team", - "deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.", - "deleteTeamInputField": "Type the name of the team to confirm", - "leaveTeam": "Leave Team", - "leavingTeamModalHeading": "Leaving Team", - "leavingTeamModalDescription": "You are about to leave this team. You will no longer have access to it.", - "leaveTeamDescription": "Click the button below to leave the team. Remember, you will no longer have access to it and will need to be re-invited to join.", - "deleteTeamDisclaimer": "You are deleting the team {{ teamName }}. This action cannot be undone.", - "leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.", - "deleteTeamErrorHeading": "Sorry, we couldn't delete your team.", - "leaveTeamErrorHeading": "Sorry, we couldn't leave your team.", - "searchMembersPlaceholder": "Search members", - "createTeamErrorHeading": "Sorry, we couldn't create your team.", - "createTeamErrorMessage": "We encountered an error creating your team. Please try again.", - "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.", - "transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.", - "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.", - "updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.", + "dangerZoneSubheading": "Delete or leave your company", + "deleteTeam": "Delete Company", + "deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.", + "deletingTeam": "Deleting company", + "deleteTeamModalHeading": "Deleting Company", + "deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.", + "deleteTeamInputField": "Type the name of the company to confirm", + "leaveTeam": "Leave Company", + "leavingTeamModalHeading": "Leaving Company", + "leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.", + "leaveTeamDescription": "Click the button below to leave the company. Remember, you will no longer have access to it and will need to be re-invited to join.", + "deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.", + "leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.", + "deleteTeamErrorHeading": "Sorry, we couldn't delete your company.", + "leaveTeamErrorHeading": "Sorry, we couldn't leave your company.", + "searchMembersPlaceholder": "Search employees", + "createTeamErrorHeading": "Sorry, we couldn't create your company.", + "createTeamErrorMessage": "We encountered an error creating your company. Please try again.", + "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.", + "transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.", + "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.", + "updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.", "searchInvitations": "Search Invitations", "updateInvitation": "Update Invitation", "removeInvitation": "Remove Invitation", @@ -144,20 +144,20 @@ "active": "Active", "inviteStatus": "Status", "inviteNotFoundOrExpired": "Invite not found or expired", - "inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the team owner to renew the invite.", + "inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company HR to renew the invite.", "backToHome": "Back to Home", - "renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the team.", + "renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.", "renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.", "renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.", "signInWithDifferentAccount": "Sign in with a different account", "signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.", "acceptInvitationHeading": "Accept Invitation to join {{accountName}}", - "acceptInvitationDescription": "You have been invited to join the team {{accountName}}. If you wish to accept the invitation, please click the button below.", + "acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.", "continueAs": "Continue as {{email}}", - "joinTeamAccount": "Join Team", - "joiningTeam": "Joining team...", - "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.", - "leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.", + "joinTeamAccount": "Join Company", + "joiningTeam": "Joining company...", + "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.", + "leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.", "reservedNameError": "This name is reserved. Please choose a different one.", "specialCharactersError": "This name cannot contain special characters. Please choose a different one." } diff --git a/styles/STYLE_GUIDE.md b/styles/STYLE_GUIDE.md index 87d577f..b9cb522 100644 --- a/styles/STYLE_GUIDE.md +++ b/styles/STYLE_GUIDE.md @@ -62,6 +62,13 @@ Example of usage --color-text-foreground -> className="[property name]-text-foreground" -> className="text-text-foreground", border-text-foreground ``` +## Components +- Add the component to the `packages/ui/src/shadcn` directory. +- Replace the imports with the relative imports. +- Export the component by adding a new export to the package.json file. +- Import the component directly from the package. +read more on [makerkit doc](https://makerkit.dev/docs/next-supabase-turbo/customization/adding-shadcn-ui-components) - +## Fonts +https://makerkit.dev/docs/next-supabase-turbo/customization/fonts \ No newline at end of file diff --git a/supabase/config.toml b/supabase/config.toml index 4483207..0e17285 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -69,7 +69,7 @@ enabled = true # Port to use for Supabase Studio. port = 54323 # External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" +api_url = "env(SUPABASE_API_URL)" # OpenAI API Key to use for Supabase AI in the Supabase Studio. openai_api_key = "env(OPENAI_API_KEY)" @@ -82,7 +82,7 @@ port = 54324 # Uncomment to expose additional ports for testing user applications that send emails. # smtp_port = 54325 # pop3_port = 54326 -# admin_email = "admin@email.com" +# admin_email = "" # sender_name = "Admin" [storage] @@ -107,7 +107,7 @@ enabled = true # in emails. site_url = "http://127.0.0.1:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] +additional_redirect_urls = ["https://127.0.0.1:3000","http://localhost:3000/auth/callback", "http://localhost:3000/update-password"] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). jwt_expiry = 3600 # If disabled, the refresh token will never expire. @@ -129,7 +129,7 @@ password_requirements = "" [auth.rate_limit] # Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. -email_sent = 2 +email_sent = 1000 # Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. sms_sent = 30 # Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. @@ -152,7 +152,7 @@ token_verifications = 30 enable_signup = true # If enabled, a user will be required to confirm any email change on both the old, and new email # addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true +double_confirm_changes = false # If enabled, users need to confirm their email address before signing in. enable_confirmations = false # If enabled, users will need to reauthenticate or have logged in recently to change their password. @@ -175,9 +175,26 @@ otp_expiry = 3600 # sender_name = "Admin" # Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" +[auth.email.template.invite] +subject = "You have been invited" +content_path = "./supabase/templates/invite-user.html" + + +[auth.email.template.confirmation] +subject = "Confirm your email" +content_path = "./supabase/templates/confirm-email.html" + +[auth.email.template.recovery] +subject = "Reset your password" +content_path = "./supabase/templates/reset-password.html" + +[auth.email.template.email_change] +subject = "Confirm your email change" +content_path = "./supabase/templates/change-email-address.html" + +[auth.email.template.magic_link] +subject = "Sign in to MedReport" +content_path = "./supabase/templates/magic-link.html" [auth.sms] # Allow/disallow new user signups via SMS to your project. @@ -220,13 +237,13 @@ max_enrolled_factors = 10 # Control MFA via App Authenticator (TOTP) [auth.mfa.totp] -enroll_enabled = false -verify_enabled = false +enroll_enabled = true +verify_enabled = true # Configure MFA via Phone Messaging [auth.mfa.phone] -enroll_enabled = false -verify_enabled = false +enroll_enabled = true +verify_enabled = true otp_length = 6 template = "Your code is {{ .Code }}" max_frequency = "5s" diff --git a/supabase/migrations/20250612193715_nonces.sql b/supabase/migrations/20250612193715_nonces.sql new file mode 100644 index 0000000..22f3714 --- /dev/null +++ b/supabase/migrations/20250612193715_nonces.sql @@ -0,0 +1,349 @@ +/* + * ------------------------------------------------------- + * Section: Nonces + * We create the schema for the nonces. Nonces are used to create one-time tokens for authentication purposes. + * ------------------------------------------------------- + */ + +create extension if not exists pg_cron; + +-- Create a table to store one-time tokens (nonces) +CREATE TABLE IF NOT EXISTS public.nonces ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + client_token TEXT NOT NULL, -- token sent to client (hashed) + nonce TEXT NOT NULL, -- token stored in DB (hashed) + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NULL, -- Optional to support anonymous tokens + purpose TEXT NOT NULL, -- e.g., 'password-reset', 'email-verification', etc. + + -- Status fields + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + used_at TIMESTAMPTZ, + revoked BOOLEAN NOT NULL DEFAULT FALSE, -- For administrative revocation + revoked_reason TEXT, -- Reason for revocation if applicable + + -- Audit fields + verification_attempts INTEGER NOT NULL DEFAULT 0, -- Track attempted uses + last_verification_at TIMESTAMPTZ, -- Timestamp of last verification attempt + last_verification_ip INET, -- For tracking verification source + last_verification_user_agent TEXT, -- For tracking client information + + -- Extensibility fields + metadata JSONB DEFAULT '{}'::JSONB, -- optional metadata + scopes TEXT[] DEFAULT '{}' -- OAuth-style authorized scopes +); + +-- Create indexes for efficient lookups +CREATE INDEX IF NOT EXISTS idx_nonces_status ON public.nonces (client_token, user_id, purpose, expires_at) + WHERE used_at IS NULL AND revoked = FALSE; + +-- Enable Row Level Security (RLS) +ALTER TABLE public.nonces ENABLE ROW LEVEL SECURITY; + +-- RLS policies +-- Users can view their own nonces for verification +CREATE POLICY "Users can read their own nonces" + ON public.nonces + FOR SELECT + USING ( + user_id = (select auth.uid()) + ); + +-- Create a function to create a nonce +-- Create a function to create a nonce +create or replace function public.create_nonce ( + p_user_id UUID default null, + p_purpose TEXT default null, + p_expires_in_seconds INTEGER default 3600, -- 1 hour by default + p_metadata JSONB default null, + p_scopes text[] default null, + p_revoke_previous BOOLEAN default true -- New parameter to control automatic revocation +) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER + set + search_path to '' as $$ +DECLARE + v_client_token TEXT; + v_nonce TEXT; + v_expires_at TIMESTAMPTZ; + v_id UUID; + v_plaintext_token TEXT; + v_revoked_count INTEGER; +BEGIN + -- Revoke previous tokens for the same user and purpose if requested + -- This only applies if a user ID is provided (not for anonymous tokens) + IF p_revoke_previous = TRUE AND p_user_id IS NOT NULL THEN + WITH revoked AS ( + UPDATE public.nonces + SET + revoked = TRUE, + revoked_reason = 'Superseded by new token with same purpose' + WHERE + user_id = p_user_id + AND purpose = p_purpose + AND used_at IS NULL + AND revoked = FALSE + AND expires_at > NOW() + RETURNING 1 + ) + SELECT COUNT(*) INTO v_revoked_count FROM revoked; + END IF; + + -- Generate a 6-digit token + v_plaintext_token := (100000 + floor(random() * 900000))::text; + v_client_token := extensions.crypt(v_plaintext_token, extensions.gen_salt('bf')); + + -- Still generate a secure nonce for internal use + v_nonce := encode(extensions.gen_random_bytes(24), 'base64'); + v_nonce := extensions.crypt(v_nonce, extensions.gen_salt('bf')); + + -- Calculate expiration time + v_expires_at := NOW() + (p_expires_in_seconds * interval '1 second'); + + -- Insert the new nonce + INSERT INTO public.nonces ( + client_token, + nonce, + user_id, + expires_at, + metadata, + purpose, + scopes + ) + VALUES ( + v_client_token, + v_nonce, + p_user_id, + v_expires_at, + COALESCE(p_metadata, '{}'::JSONB), + p_purpose, + COALESCE(p_scopes, '{}'::TEXT[]) + ) + RETURNING id INTO v_id; + + -- Return the token information + -- Note: returning the plaintext token, not the hash + RETURN jsonb_build_object( + 'id', v_id, + 'token', v_plaintext_token, + 'expires_at', v_expires_at, + 'revoked_previous_count', COALESCE(v_revoked_count, 0) + ); +END; +$$; + +grant execute on function public.create_nonce to service_role; + +-- Create a function to verify a nonce +create or replace function public.verify_nonce ( + p_token TEXT, + p_purpose TEXT, + p_user_id UUID default null, + p_required_scopes text[] default null, + p_max_verification_attempts INTEGER default 5, + p_ip INET default null, + p_user_agent TEXT default null +) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER + set + SEARCH_PATH to '' as $$ +DECLARE + v_nonce RECORD; + v_matching_count INTEGER; +BEGIN + -- Count how many matching tokens exist before verification attempt + SELECT COUNT(*) + INTO v_matching_count + FROM public.nonces + WHERE purpose = p_purpose; + + -- Update verification attempt counter and tracking info for all matching tokens + UPDATE public.nonces + SET verification_attempts = verification_attempts + 1, + last_verification_at = NOW(), + last_verification_ip = COALESCE(p_ip, last_verification_ip), + last_verification_user_agent = COALESCE(p_user_agent, last_verification_user_agent) + WHERE client_token = extensions.crypt(p_token, client_token) + AND purpose = p_purpose; + + -- Find the nonce by token and purpose + -- Modified to handle user-specific tokens better + SELECT * + INTO v_nonce + FROM public.nonces + WHERE client_token = extensions.crypt(p_token, client_token) + AND purpose = p_purpose + -- Only apply user_id filter if the token was created for a specific user + AND ( + -- Case 1: Anonymous token (user_id is NULL in DB) + (user_id IS NULL) + OR + -- Case 2: User-specific token (check if user_id matches) + (user_id = p_user_id) + ) + AND used_at IS NULL + AND NOT revoked + AND expires_at > NOW(); + + -- Check if nonce exists + IF v_nonce.id IS NULL THEN + RETURN jsonb_build_object( + 'valid', false, + 'message', 'Invalid or expired token' + ); + END IF; + + -- Check if max verification attempts exceeded + IF p_max_verification_attempts > 0 AND v_nonce.verification_attempts > p_max_verification_attempts THEN + -- Automatically revoke the token + UPDATE public.nonces + SET revoked = TRUE, + revoked_reason = 'Maximum verification attempts exceeded' + WHERE id = v_nonce.id; + + RETURN jsonb_build_object( + 'valid', false, + 'message', 'Token revoked due to too many verification attempts', + 'max_attempts_exceeded', true + ); + END IF; + + -- Check scopes if required + IF p_required_scopes IS NOT NULL AND array_length(p_required_scopes, 1) > 0 THEN + -- Fix scope validation to properly check if token scopes contain all required scopes + -- Using array containment check: array1 @> array2 (array1 contains array2) + IF NOT (v_nonce.scopes @> p_required_scopes) THEN + RETURN jsonb_build_object( + 'valid', false, + 'message', 'Token does not have required permissions', + 'token_scopes', v_nonce.scopes, + 'required_scopes', p_required_scopes + ); + END IF; + END IF; + + -- Mark nonce as used + UPDATE public.nonces + SET used_at = NOW() + WHERE id = v_nonce.id; + + -- Return success with metadata + RETURN jsonb_build_object( + 'valid', true, + 'user_id', v_nonce.user_id, + 'metadata', v_nonce.metadata, + 'scopes', v_nonce.scopes, + 'purpose', v_nonce.purpose + ); +END; +$$; + +grant + execute on function public.verify_nonce to authenticated, + service_role; + +-- Create a function to revoke a nonce +CREATE OR REPLACE FUNCTION public.revoke_nonce( + p_id UUID, + p_reason TEXT DEFAULT NULL +) +RETURNS BOOLEAN +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path TO '' +AS $$ +DECLARE + v_affected_rows INTEGER; +BEGIN + UPDATE public.nonces + SET + revoked = TRUE, + revoked_reason = p_reason + WHERE + id = p_id + AND used_at IS NULL + AND NOT revoked + RETURNING 1 INTO v_affected_rows; + + RETURN v_affected_rows > 0; +END; +$$; + +grant execute on function public.revoke_nonce to service_role; + +-- Create a function to clean up expired nonces +CREATE OR REPLACE FUNCTION kit.cleanup_expired_nonces( + p_older_than_days INTEGER DEFAULT 1, + p_include_used BOOLEAN DEFAULT TRUE, + p_include_revoked BOOLEAN DEFAULT TRUE +) +RETURNS INTEGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path TO '' +AS $$ +DECLARE + v_count INTEGER; +BEGIN + -- Count and delete expired or used nonces based on parameters + WITH deleted AS ( + DELETE FROM public.nonces + WHERE + ( + -- Expired and unused tokens + (expires_at < NOW() AND used_at IS NULL) + + -- Used tokens older than specified days (if enabled) + OR (p_include_used = TRUE AND used_at < NOW() - (p_older_than_days * interval '1 day')) + + -- Revoked tokens older than specified days (if enabled) + OR (p_include_revoked = TRUE AND revoked = TRUE AND created_at < NOW() - (p_older_than_days * interval '1 day')) + ) + RETURNING 1 + ) + SELECT COUNT(*) INTO v_count FROM deleted; + + RETURN v_count; +END; +$$; + +-- Create a function to get token status (for administrative use) +CREATE OR REPLACE FUNCTION public.get_nonce_status( + p_id UUID +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path TO '' +AS $$ +DECLARE + v_nonce public.nonces; +BEGIN + SELECT * INTO v_nonce FROM public.nonces WHERE id = p_id; + + IF v_nonce.id IS NULL THEN + RETURN jsonb_build_object('exists', false); + END IF; + + RETURN jsonb_build_object( + 'exists', true, + 'purpose', v_nonce.purpose, + 'user_id', v_nonce.user_id, + 'created_at', v_nonce.created_at, + 'expires_at', v_nonce.expires_at, + 'used_at', v_nonce.used_at, + 'revoked', v_nonce.revoked, + 'revoked_reason', v_nonce.revoked_reason, + 'verification_attempts', v_nonce.verification_attempts, + 'last_verification_at', v_nonce.last_verification_at, + 'last_verification_ip', v_nonce.last_verification_ip, + 'is_valid', (v_nonce.used_at IS NULL AND NOT v_nonce.revoked AND v_nonce.expires_at > NOW()) + ); +END; +$$; + +-- Comments for documentation +COMMENT ON TABLE public.nonces IS 'Table for storing one-time tokens with enhanced security and audit features'; +COMMENT ON FUNCTION public.create_nonce IS 'Creates a new one-time token for a specific purpose with enhanced options'; +COMMENT ON FUNCTION public.verify_nonce IS 'Verifies a one-time token, checks scopes, and marks it as used'; +COMMENT ON FUNCTION public.revoke_nonce IS 'Administratively revokes a token to prevent its use'; +COMMENT ON FUNCTION kit.cleanup_expired_nonces IS 'Cleans up expired, used, or revoked tokens based on parameters'; +COMMENT ON FUNCTION public.get_nonce_status IS 'Retrieves the status of a token for administrative purposes'; diff --git a/supabase/migrations/20250612193815_mfa.sql b/supabase/migrations/20250612193815_mfa.sql new file mode 100644 index 0000000..bc97754 --- /dev/null +++ b/supabase/migrations/20250612193815_mfa.sql @@ -0,0 +1,145 @@ +/* + * ------------------------------------------------------- + * Section: MFA + * We create the policies and functions to enforce MFA + * ------------------------------------------------------- + */ + +/* +* public.is_aal2 +* Check if the user has aal2 access +*/ +create + or replace function public.is_aal2() returns boolean + set + search_path = '' as +$$ +declare + is_aal2 boolean; +begin + select auth.jwt() ->> 'aal' = 'aal2' into is_aal2; + + return coalesce(is_aal2, false); +end +$$ language plpgsql; + +-- Grant access to the function to authenticated users +grant execute on function public.is_aal2() to authenticated; + +/* +* public.is_super_admin +* Check if the user is a super admin. +* A Super Admin is a user that has the role 'super-admin' and has MFA enabled. +*/ +create + or replace function public.is_super_admin() returns boolean + set + search_path = '' as +$$ +declare + is_super_admin boolean; +begin + if not public.is_aal2() then + return false; + end if; + + select (auth.jwt() ->> 'app_metadata')::jsonb ->> 'role' = 'super-admin' into is_super_admin; + + return coalesce(is_super_admin, false); +end +$$ language plpgsql; + +-- Grant access to the function to authenticated users +grant execute on function public.is_super_admin() to authenticated; + +/* +* public.is_mfa_compliant +* Check if the user meets MFA requirements if they have MFA enabled. +* If the user has MFA enabled, then the user must have aal2 enabled. Otherwise, the user must have aal1 enabled (default behavior). +*/ +create or replace function public.is_mfa_compliant() returns boolean + set search_path = '' as +$$ +begin + return array[(select auth.jwt()->>'aal')] <@ ( + select + case + when count(id) > 0 then array['aal2'] + else array['aal1', 'aal2'] + end as aal + from auth.mfa_factors + where ((select auth.uid()) = auth.mfa_factors.user_id) and auth.mfa_factors.status = 'verified' + ); +end +$$ language plpgsql security definer; + +-- Grant access to the function to authenticated users +grant execute on function public.is_mfa_compliant() to authenticated; + +-- MFA Restrictions: +-- the following policies are applied to the tables as a +-- restrictive policy to ensure that if MFA is enabled, then the policy will be applied. +-- For users that have not enabled MFA, the policy will not be applied and will keep the default behavior. + +-- Restrict access to accounts if MFA is enabled +create policy restrict_mfa_accounts + on public.accounts + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to accounts memberships if MFA is enabled +create policy restrict_mfa_accounts_memberships + on public.accounts_memberships + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to subscriptions if MFA is enabled +create policy restrict_mfa_subscriptions + on public.subscriptions + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to subscription items if MFA is enabled +create policy restrict_mfa_subscription_items + on public.subscription_items + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to role permissions if MFA is enabled +create policy restrict_mfa_role_permissions + on public.role_permissions + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to invitations if MFA is enabled +create policy restrict_mfa_invitations + on public.invitations + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to orders if MFA is enabled +create policy restrict_mfa_orders + on public.orders + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to orders items if MFA is enabled +create policy restrict_mfa_order_items + on public.order_items + as restrictive + to authenticated + using (public.is_mfa_compliant()); + +-- Restrict access to orders if MFA is enabled +create policy restrict_mfa_notifications + on public.notifications + as restrictive + to authenticated + using (public.is_mfa_compliant()); \ No newline at end of file diff --git a/supabase/migrations/20250612193836_super_admin.sql b/supabase/migrations/20250612193836_super_admin.sql new file mode 100644 index 0000000..4fdaf20 --- /dev/null +++ b/supabase/migrations/20250612193836_super_admin.sql @@ -0,0 +1,73 @@ +/* + * ------------------------------------------------------- + * Section: Super Admin + * We create the policies and functions to enforce super admin access + * ------------------------------------------------------- + */ + +-- the following policies are applied to the tables as a permissive policy to ensure that +-- super admins can access all tables (view only). + +-- Allow Super Admins to access the accounts table +create policy super_admins_access_accounts + on public.accounts + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the accounts memberships table +create policy super_admins_access_accounts_memberships + on public.accounts_memberships + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the subscriptions table +create policy super_admins_access_subscriptions + on public.subscriptions + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the subscription items table +create policy super_admins_access_subscription_items + on public.subscription_items + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the invitations items table +create policy super_admins_access_invitations + on public.invitations + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the orders table +create policy super_admins_access_orders + on public.orders + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the order items table +create policy super_admins_access_order_items + on public.order_items + as permissive + for select + to authenticated + using (public.is_super_admin()); + +-- Allow Super Admins to access the role permissions table +create policy super_admins_access_role_permissions + on public.role_permissions + as permissive + for select + to authenticated + using (public.is_super_admin()); \ No newline at end of file diff --git a/supabase/migrations/20250612193929_account_functions.sql b/supabase/migrations/20250612193929_account_functions.sql new file mode 100644 index 0000000..13f6d8b --- /dev/null +++ b/supabase/migrations/20250612193929_account_functions.sql @@ -0,0 +1,126 @@ +/* + * ------------------------------------------------------- + * Section: Account Functions + * We create the schema for the functions. Functions are the custom functions for the application. + * ------------------------------------------------------- + */ + + +-- +-- VIEW "user_account_workspace": +-- we create a view to load the general app data for the authenticated +-- user which includes the user accounts and memberships +create or replace view + public.user_account_workspace + with + (security_invoker = true) as +select + accounts.id as id, + accounts.name as name, + accounts.picture_url as picture_url, + ( + select + status + from + public.subscriptions + where + account_id = accounts.id + limit + 1 + ) as subscription_status +from + public.accounts +where + primary_owner_user_id = (select auth.uid ()) + and accounts.is_personal_account = true +limit + 1; + +grant + select + on public.user_account_workspace to authenticated, + service_role; + +-- +-- VIEW "user_accounts": +-- we create a view to load the user's accounts and memberships +-- useful to display the user's accounts in the app +create or replace view + public.user_accounts (id, name, picture_url, slug, role) + with + (security_invoker = true) as +select + account.id, + account.name, + account.picture_url, + account.slug, + membership.account_role +from + public.accounts account + join public.accounts_memberships membership on account.id = membership.account_id +where + membership.user_id = (select auth.uid ()) + and account.is_personal_account = false + and account.id in ( + select + account_id + from + public.accounts_memberships + where + user_id = (select auth.uid ()) +); + +grant + select + on public.user_accounts to authenticated, + service_role; + +-- +-- Function "public.team_account_workspace" +-- Load all the data for a team account workspace +create or replace function public.team_account_workspace(account_slug text) +returns table ( + id uuid, + name varchar(255), + picture_url varchar(1000), + slug text, + role varchar(50), + role_hierarchy_level int, + primary_owner_user_id uuid, + subscription_status public.subscription_status, + permissions public.app_permissions[] +) +set search_path to '' +as $$ +begin + return QUERY + select + accounts.id, + accounts.name, + accounts.picture_url, + accounts.slug, + accounts_memberships.account_role, + roles.hierarchy_level, + accounts.primary_owner_user_id, + subscriptions.status, + array_agg(role_permissions.permission) + from + public.accounts + join public.accounts_memberships on accounts.id = accounts_memberships.account_id + left join public.subscriptions on accounts.id = subscriptions.account_id + join public.roles on accounts_memberships.account_role = roles.name + left join public.role_permissions on accounts_memberships.account_role = role_permissions.role + where + accounts.slug = account_slug + and public.accounts_memberships.user_id = (select auth.uid()) + group by + accounts.id, + accounts_memberships.account_role, + subscriptions.status, + roles.hierarchy_level; +end; +$$ language plpgsql; + +grant +execute on function public.team_account_workspace (text) to authenticated, +service_role; diff --git a/supabase/templates/change-email-address.html b/supabase/templates/change-email-address.html new file mode 100644 index 0000000..3fd0c2b --- /dev/null +++ b/supabase/templates/change-email-address.html @@ -0,0 +1,8 @@ +
Confirm Change of Email | Makerkit
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
\ No newline at end of file diff --git a/supabase/templates/confirm-email.html b/supabase/templates/confirm-email.html new file mode 100644 index 0000000..99c59b5 --- /dev/null +++ b/supabase/templates/confirm-email.html @@ -0,0 +1,8 @@ +
Confirm your email - Makerkit
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
\ No newline at end of file diff --git a/supabase/templates/invite-user.html b/supabase/templates/invite-user.html new file mode 100644 index 0000000..add06d2 --- /dev/null +++ b/supabase/templates/invite-user.html @@ -0,0 +1,8 @@ +
You have bee invited to join - Makerkit
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
\ No newline at end of file diff --git a/supabase/templates/magic-link.html b/supabase/templates/magic-link.html new file mode 100644 index 0000000..eb3c7c9 --- /dev/null +++ b/supabase/templates/magic-link.html @@ -0,0 +1,8 @@ +
Your sign in link to Makerkit
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
\ No newline at end of file diff --git a/supabase/templates/reset-password.html b/supabase/templates/reset-password.html new file mode 100644 index 0000000..ce6ed5a --- /dev/null +++ b/supabase/templates/reset-password.html @@ -0,0 +1,8 @@ +
Reset your password | Makerkit
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
\ No newline at end of file