Merge branch 'main' into B2B-65
This commit is contained in:
9
.env
9
.env
@@ -33,9 +33,9 @@ NEXT_PUBLIC_LOCALES_PATH=apps/web/public/locales
|
|||||||
# FEATURE FLAGS
|
# FEATURE FLAGS
|
||||||
NEXT_PUBLIC_ENABLE_THEME_TOGGLE=true
|
NEXT_PUBLIC_ENABLE_THEME_TOGGLE=true
|
||||||
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION=true
|
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION=true
|
||||||
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=true
|
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=false
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=false
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=false
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
||||||
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
||||||
@@ -47,3 +47,6 @@ NEXT_TELEMETRY_DISABLED=1
|
|||||||
LOGGER=pino
|
LOGGER=pino
|
||||||
|
|
||||||
NEXT_PUBLIC_DEFAULT_LOCALE=et
|
NEXT_PUBLIC_DEFAULT_LOCALE=et
|
||||||
|
|
||||||
|
NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom
|
||||||
|
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
|
||||||
@@ -10,12 +10,6 @@ SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhY
|
|||||||
SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET
|
SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET
|
||||||
|
|
||||||
# EMAILS
|
# EMAILS
|
||||||
EMAIL_SENDER="Makerkit <admin@makerkit.dev>"
|
|
||||||
EMAIL_PORT=54325
|
|
||||||
EMAIL_HOST=localhost
|
|
||||||
EMAIL_TLS=false
|
|
||||||
EMAIL_USER=user
|
|
||||||
EMAIL_PASSWORD=password
|
|
||||||
|
|
||||||
# CONTACT FORM
|
# CONTACT FORM
|
||||||
CONTACT_EMAIL=test@makerkit.dev
|
CONTACT_EMAIL=test@makerkit.dev
|
||||||
@@ -25,3 +19,9 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|||||||
|
|
||||||
# MAILER
|
# MAILER
|
||||||
MAILER_PROVIDER=nodemailer
|
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)
|
||||||
@@ -8,3 +8,10 @@ MEDIPOST_URL=your-medipost-url
|
|||||||
MEDIPOST_USER=your-medipost-user
|
MEDIPOST_USER=your-medipost-user
|
||||||
MEDIPOST_PASSWORD=your-medipost-password
|
MEDIPOST_PASSWORD=your-medipost-password
|
||||||
MEDIPOST_RECIPIENT=your-medipost-recipient
|
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)
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link';
|
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 { ArrowRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { CtaButton, Hero } from '@kit/ui/marketing';
|
import { CtaButton, Hero } from '@kit/ui/marketing';
|
||||||
@@ -13,7 +13,7 @@ function Home() {
|
|||||||
<div className={'mt-4 flex flex-col space-y-24 py-14'}>
|
<div className={'mt-4 flex flex-col space-y-24 py-14'}>
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<Hero
|
<Hero
|
||||||
title={<MedReportTitle />}
|
title={<MedReportLogo />}
|
||||||
subtitle={
|
subtitle={
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'marketing:heroSubtitle'} />
|
<Trans i18nKey={'marketing:heroSubtitle'} />
|
||||||
|
|||||||
@@ -4,20 +4,19 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { MedReportTitle } from '@/components/med-report-title';
|
import { MedReportLogo } from "@/components/med-report-title";
|
||||||
import { SubmitButton } from '@/components/ui/submit-button';
|
import React from "react";
|
||||||
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { submitCompanyRegistration } from '@/lib/services/register-company.service';
|
import { useForm } from "react-hook-form";
|
||||||
import { CompanySubmitData } from '@/lib/types/company';
|
import { companySchema } from "@/lib/validations/companySchema";
|
||||||
import { companySchema } from '@/lib/validations/companySchema';
|
import { CompanySubmitData } from "@/lib/types/company";
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { submitCompanyRegistration } from "@/lib/services/register-company.service";
|
||||||
import { useForm } from 'react-hook-form';
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { Label } from "@kit/ui/label";
|
||||||
|
import { Input } from "@kit/ui/input";
|
||||||
import { FormItem } from '@kit/ui/form';
|
import { SubmitButton } from "@/components/ui/submit-button";
|
||||||
import { Input } from '@kit/ui/input';
|
import { FormItem } from "@kit/ui/form";
|
||||||
import { Label } from '@kit/ui/label';
|
import { Trans } from "@kit/ui/trans";
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
export default function RegisterCompany() {
|
export default function RegisterCompany() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { MedReportTitle } from '@/components/med-report-title';
|
import { MedReportLogo } from '@/components/med-report-title';
|
||||||
import { Button } from '@/packages/ui/src/shadcn/button';
|
|
||||||
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Button } from '@kit/ui/button';
|
||||||
|
|
||||||
export default function CompanyRegistrationSuccess() {
|
export default function CompanyRegistrationSuccess() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
|
useSidebar,
|
||||||
} from '@kit/ui/shadcn-sidebar';
|
} from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
import { AppLogo } from '~/components/app-logo';
|
import { AppLogo } from '~/components/app-logo';
|
||||||
@@ -22,11 +23,12 @@ import { ProfileAccountDropdownContainer } from '~/components/personal-account-d
|
|||||||
|
|
||||||
export function AdminSidebar() {
|
export function AdminSidebar() {
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
|
const { open } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader className={'m-2'}>
|
<SidebarHeader className={'m-2'}>
|
||||||
<AppLogo href={'/admin'} className="max-w-full" />
|
<AppLogo href={'/admin'} className="max-w-full" compact={!open} />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
|||||||
|
|
||||||
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
||||||
import { AdminCreateUserDialog } from '@kit/admin/components/admin-create-user-dialog';
|
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 { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||||
@@ -30,10 +31,14 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader description={<AppBreadcrumbs />}>
|
<PageHeader description={<AppBreadcrumbs />}>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end gap-2">
|
||||||
<AdminCreateUserDialog>
|
<AdminCreateUserDialog>
|
||||||
<Button data-test="admin-create-user-button">Create User</Button>
|
<Button data-test="admin-create-user-button">Create Personal Account</Button>
|
||||||
</AdminCreateUserDialog>
|
</AdminCreateUserDialog>
|
||||||
|
<AdminCreateCompanyDialog>
|
||||||
|
<Button>Create Company Account</Button>
|
||||||
|
</AdminCreateCompanyDialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "app/globals.css",
|
"css": "styles/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
|
|||||||
@@ -1,47 +1,36 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { MedReportLogo } from './med-report-title';
|
||||||
|
|
||||||
function LogoImage({
|
function LogoImage({
|
||||||
className,
|
className,
|
||||||
width = 105,
|
compact = false,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
compact?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return <MedReportLogo compact={compact} className={className} />;
|
||||||
<svg
|
|
||||||
width={width}
|
|
||||||
className={cn(`w-[80px] lg:w-[95px]`, className)}
|
|
||||||
viewBox="0 0 733 140"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
className={'fill-primary dark:fill-white'}
|
|
||||||
d="M119.081 138V73.209C119.081 67.551 117.08 62.79 113.078 58.926C109.214 55.062 104.453 53.13 98.7951 53.13C93.2751 53.13 88.4451 55.062 84.3051 58.926C80.3031 62.652 78.3021 67.344 78.3021 73.002V138H59.4651V73.002C59.4651 67.206 57.5331 62.514 53.6691 58.926C49.5291 55.062 44.6301 53.13 38.9721 53.13C33.4521 53.13 28.7601 55.062 24.8961 58.926C20.7561 63.066 18.6861 67.965 18.6861 73.623V138H0.0560548V36.984H18.6861V44.643C21.0321 41.745 24.0681 39.33 27.7941 37.398C31.6581 35.466 35.3841 34.5 38.9721 34.5C45.0441 34.5 50.5641 35.742 55.5321 38.226C60.6381 40.572 65.0541 43.884 68.7801 48.162C72.5061 43.884 76.9221 40.572 82.0281 38.226C87.1341 35.742 92.7231 34.5 98.7951 34.5C104.177 34.5 109.214 35.466 113.906 37.398C118.598 39.33 122.738 42.09 126.326 45.678C129.914 49.266 132.674 53.475 134.606 58.305C136.676 62.997 137.711 67.965 137.711 73.209V138H119.081ZM242.173 138V122.268C237.757 127.374 232.651 131.445 226.855 134.481C221.059 137.517 214.918 139.035 208.432 139.035C201.256 139.035 194.494 137.724 188.146 135.102C181.936 132.48 176.416 128.754 171.586 123.924C166.756 119.232 162.961 113.712 160.201 107.364C157.579 100.878 156.268 94.116 156.268 87.078C156.268 80.04 157.579 73.347 160.201 66.999C162.961 60.513 166.756 54.855 171.586 50.025C176.416 45.195 181.936 41.469 188.146 38.847C194.494 36.225 201.256 34.914 208.432 34.914C215.056 34.914 221.266 36.294 227.062 39.054C232.996 41.814 238.033 45.678 242.173 50.646V36.984H260.803V138H242.173ZM208.432 53.337C203.878 53.337 199.462 54.234 195.184 56.028C191.044 57.684 187.456 60.03 184.42 63.066C181.384 66.102 178.969 69.759 177.175 74.037C175.519 78.177 174.691 82.524 174.691 87.078C174.691 91.632 175.519 95.979 177.175 100.119C178.969 104.259 181.384 107.847 184.42 110.883C187.456 113.919 191.044 116.334 195.184 118.128C199.462 119.784 203.878 120.612 208.432 120.612C212.986 120.612 217.333 119.784 221.473 118.128C225.613 116.334 229.201 113.919 232.237 110.883C235.273 107.847 237.619 104.259 239.275 100.119C241.069 95.979 241.966 91.632 241.966 87.078C241.966 82.524 241.069 78.177 239.275 74.037C237.619 69.759 235.273 66.102 232.237 63.066C229.201 60.03 225.613 57.684 221.473 56.028C217.333 54.234 212.986 53.337 208.432 53.337ZM331.127 138L299.663 99.705V138H281.447V0.344996H299.663V59.754L327.815 33.258H354.932L305.873 78.798L355.139 138H331.127ZM379.299 94.116C379.299 97.428 380.472 100.878 382.818 104.466C385.302 108.054 388.131 111.09 391.305 113.574C397.101 118.128 403.863 120.405 411.591 120.405C423.873 120.405 433.878 114.471 441.606 102.603L457.338 111.918C451.956 120.612 445.332 127.305 437.466 131.997C429.6 136.689 420.975 139.035 411.591 139.035C404.553 139.035 397.86 137.724 391.512 135.102C385.164 132.342 379.575 128.547 374.745 123.717C369.915 118.887 366.12 113.298 363.36 106.95C360.738 100.602 359.427 93.909 359.427 86.871C359.427 79.833 360.738 73.14 363.36 66.792C366.12 60.306 369.915 54.648 374.745 49.818C379.437 44.988 384.957 41.262 391.305 38.64C397.791 36.018 404.553 34.707 411.591 34.707C418.629 34.707 425.322 36.018 431.67 38.64C438.156 41.262 443.745 44.988 448.437 49.818C458.649 60.306 463.755 72.45 463.755 86.25C463.755 88.734 463.548 91.356 463.134 94.116H379.299ZM411.591 51.681C405.933 51.681 400.62 52.923 395.652 55.407C390.684 57.891 386.682 61.203 383.646 65.343C380.748 69.345 379.299 73.623 379.299 78.177H443.883C443.883 73.623 442.365 69.345 439.329 65.343C436.431 61.203 432.498 57.891 427.53 55.407C422.562 52.923 417.249 51.681 411.591 51.681ZM528.543 54.372C525.231 52.854 522.264 52.095 519.642 52.095C514.122 52.095 509.568 54.027 505.98 57.891C502.116 62.031 500.184 66.792 500.184 72.174V138H482.382V72.174C482.382 64.722 484.245 57.891 487.971 51.681C491.835 45.471 497.079 40.641 503.703 37.191C508.671 34.845 513.984 33.672 519.642 33.672C524.196 33.672 528.543 34.5 532.683 36.156C536.823 37.812 541.17 40.503 545.724 44.229L528.543 54.372ZM610.092 138L578.628 99.705V138H560.412V0.344996H578.628V59.754L606.78 33.258H633.897L584.838 78.798L634.104 138H610.092ZM656.049 19.596C653.427 19.596 651.15 18.699 649.218 16.905C647.424 14.973 646.527 12.696 646.527 10.074C646.527 7.45199 647.424 5.24399 649.218 3.44999C651.15 1.51799 653.427 0.551993 656.049 0.551993C658.671 0.551993 660.879 1.51799 662.673 3.44999C664.605 5.24399 665.571 7.45199 665.571 10.074C665.571 12.696 664.605 14.973 662.673 16.905C660.879 18.699 658.671 19.596 656.049 19.596ZM647.562 138V34.5H664.95V138H647.562ZM717.4 53.13V138H699.805V53.13H684.28V34.5H699.805V0.344996H717.4V34.5H732.925V53.13H717.4Z"
|
|
||||||
fill="url(#paint0_linear_1666_2)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppLogo({
|
export function AppLogo({
|
||||||
href,
|
href,
|
||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
|
compact = false,
|
||||||
}: {
|
}: {
|
||||||
href?: string | null;
|
href?: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
compact?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (href === null) {
|
if (href === null) {
|
||||||
return <LogoImage className={className} />;
|
return <LogoImage className={className} compact={compact} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link aria-label={label ?? 'Home Page'} href={href ?? '/'} prefetch={true}>
|
<Link aria-label={label ?? 'Home Page'} href={href ?? '/'} prefetch={true}>
|
||||||
<LogoImage className={className} />
|
<LogoImage className={className} compact={compact} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { MedReportSmallLogo } from "@/public/assets/med-report-small-logo";
|
import { MedReportSmallLogo } from "@/public/assets/med-report-small-logo";
|
||||||
|
|
||||||
export const MedReportTitle = () => (
|
export const MedReportLogo = ({ className, compact = false }: { className?: string, compact?: boolean }) => (
|
||||||
<div className="flex gap-2 justify-center">
|
<div className={cn('flex gap-2 justify-center', className)}>
|
||||||
<MedReportSmallLogo />
|
<MedReportSmallLogo />
|
||||||
<span className="text-foreground text-lg font-semibold tracking-tighter">
|
{!compact && <span className="text-foreground text-lg font-semibold tracking-tighter">
|
||||||
MedReport
|
MedReport
|
||||||
</span>
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createClient as createCustomClient } from '@supabase/supabase-js';
|
import { createClient as createCustomClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
@@ -126,6 +127,7 @@ async function syncData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codes: any[] = [];
|
||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
// SAVE ANALYSIS GROUP
|
// SAVE ANALYSIS GROUP
|
||||||
const { data: insertedAnalysisGroup, error } = await supabase
|
const { data: insertedAnalysisGroup, error } = await supabase
|
||||||
@@ -148,7 +150,8 @@ async function syncData() {
|
|||||||
const analysisGroupId = insertedAnalysisGroup[0].id;
|
const analysisGroupId = insertedAnalysisGroup[0].id;
|
||||||
|
|
||||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||||
const codes = analysisGroupCodes.map((kood) => ({
|
codes.push(
|
||||||
|
...analysisGroupCodes.map((kood) => ({
|
||||||
hk_code: kood.HkKood,
|
hk_code: kood.HkKood,
|
||||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||||
coefficient: kood.Koefitsient,
|
coefficient: kood.Koefitsient,
|
||||||
@@ -156,7 +159,8 @@ async function syncData() {
|
|||||||
analysis_group_id: analysisGroupId,
|
analysis_group_id: analysisGroupId,
|
||||||
analysis_element_id: null,
|
analysis_element_id: null,
|
||||||
analysis_id: null,
|
analysis_id: null,
|
||||||
}));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||||
|
|
||||||
@@ -229,7 +233,7 @@ async function syncData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertedAnalysisId = insertedAnalysis[0].id;
|
const insertedAnalysisId = insertedAnalysis[0].id;
|
||||||
if (analysisElement.Kood) {
|
if (analysis.Kood) {
|
||||||
const analysisCodes = toArray(analysis.Kood);
|
const analysisCodes = toArray(analysis.Kood);
|
||||||
|
|
||||||
codes.push(
|
codes.push(
|
||||||
@@ -249,6 +253,8 @@ async function syncData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await supabase.from('codes').upsert(codes);
|
||||||
|
|
||||||
await supabase.schema('audit').from('sync_entries').insert({
|
await supabase.schema('audit').from('sync_entries').insert({
|
||||||
operation: 'ANALYSES_SYNC',
|
operation: 'ANALYSES_SYNC',
|
||||||
status: 'SUCCESS',
|
status: 'SUCCESS',
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"@types/react-dom": "19.1.5",
|
"@types/react-dom": "19.1.5",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.0.7",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.57.0",
|
||||||
@@ -100,8 +101,7 @@
|
|||||||
"tailwindcss": "4.1.7",
|
"tailwindcss": "4.1.7",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"yup": "^1.6.1",
|
"yup": "^1.6.1"
|
||||||
"dotenv": "^16.5.0"
|
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
"prettier": "@kit/prettier-config",
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { cn } from '@kit/ui/utils';
|
|||||||
|
|
||||||
import { CreateTeamAccountDialog } from '../../../team-accounts/src/components/create-team-account-dialog';
|
import { CreateTeamAccountDialog } from '../../../team-accounts/src/components/create-team-account-dialog';
|
||||||
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
|
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
|
||||||
|
import { useUserWorkspace } from '../hooks/use-user-workspace';
|
||||||
|
|
||||||
interface AccountSelectorProps {
|
interface AccountSelectorProps {
|
||||||
accounts: Array<{
|
accounts: Array<{
|
||||||
@@ -63,6 +64,7 @@ export function AccountSelector({
|
|||||||
const [isCreatingAccount, setIsCreatingAccount] = useState<boolean>(false);
|
const [isCreatingAccount, setIsCreatingAccount] = useState<boolean>(false);
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
const personalData = usePersonalAccountData(userId);
|
const personalData = usePersonalAccountData(userId);
|
||||||
|
const { user } = useUserWorkspace();
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
return selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
|
return selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
|
||||||
@@ -89,6 +91,16 @@ export function AccountSelector({
|
|||||||
<PersonIcon className="h-5 w-5" />
|
<PersonIcon className="h-5 w-5" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
@@ -172,7 +184,6 @@ export function AccountSelector({
|
|||||||
>
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder={t('searchAccount')} className="h-9" />
|
<CommandInput placeholder={t('searchAccount')} className="h-9" />
|
||||||
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -251,7 +262,7 @@ export function AccountSelector({
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<If condition={features.enableTeamCreation}>
|
<If condition={features.enableTeamCreation && isSuperAdmin}>
|
||||||
<div className={'p-1'}>
|
<div className={'p-1'}>
|
||||||
<Button
|
<Button
|
||||||
data-test={'create-team-account-trigger'}
|
data-test={'create-team-account-trigger'}
|
||||||
@@ -274,7 +285,7 @@ export function AccountSelector({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<If condition={features.enableTeamCreation}>
|
<If condition={features.enableTeamCreation && isSuperAdmin}>
|
||||||
<CreateTeamAccountDialog
|
<CreateTeamAccountDialog
|
||||||
isOpen={isCreatingAccount}
|
isOpen={isCreatingAccount}
|
||||||
setIsOpen={setIsCreatingAccount}
|
setIsOpen={setIsCreatingAccount}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ async function PersonalAccountPage(props: { account: Account }) {
|
|||||||
<SubscriptionsTable accountId={props.account.id} />
|
<SubscriptionsTable accountId={props.account.id} />
|
||||||
|
|
||||||
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
||||||
<Heading level={6}>Teams</Heading>
|
<Heading level={6}>Companies</Heading>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<AdminMembershipsTable memberships={memberships} />
|
<AdminMembershipsTable memberships={memberships} />
|
||||||
@@ -205,16 +205,15 @@ async function TeamAccountPage(props: {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge variant={'outline'}>Team Account</Badge>
|
<Badge variant={'outline'}>Company Account</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className={'flex flex-col gap-y-8'}>
|
<div className={'flex flex-col gap-y-8'}>
|
||||||
<SubscriptionsTable accountId={props.account.id} />
|
|
||||||
|
|
||||||
<div className={'flex flex-col gap-y-2.5'}>
|
<div className={'flex flex-col gap-y-2.5'}>
|
||||||
<Heading level={6}>Team Members</Heading>
|
<Heading level={6}>Company Employees</Heading>
|
||||||
|
|
||||||
<AdminMembersTable members={members} />
|
<AdminMembersTable members={members} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ function AccountsTableFilters(props: {
|
|||||||
<SelectLabel>Account Type</SelectLabel>
|
<SelectLabel>Account Type</SelectLabel>
|
||||||
|
|
||||||
<SelectItem value={'all'}>All accounts</SelectItem>
|
<SelectItem value={'all'}>All accounts</SelectItem>
|
||||||
<SelectItem value={'team'}>Team</SelectItem>
|
<SelectItem value={'team'}>Company</SelectItem>
|
||||||
<SelectItem value={'personal'}>Personal</SelectItem>
|
<SelectItem value={'personal'}>Personal</SelectItem>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -183,7 +183,7 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
id: 'type',
|
id: 'type',
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
cell: ({ row }) => {
|
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<Account>[] {
|
|||||||
<If condition={!isPersonalAccount}>
|
<If condition={!isPersonalAccount}>
|
||||||
<AdminDeleteAccountDialog accountId={row.original.id}>
|
<AdminDeleteAccountDialog accountId={row.original.id}>
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
Delete Team Account
|
Delete Company Account
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</AdminDeleteAccountDialog>
|
</AdminDeleteAccountDialog>
|
||||||
</If>
|
</If>
|
||||||
|
|||||||
@@ -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<string | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<CreateCompanySchemaType>({
|
||||||
|
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 (
|
||||||
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||||
|
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Create New Company Account</AlertDialogTitle>
|
||||||
|
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Complete the form below to create a new company account.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
data-test={'admin-create-user-form'}
|
||||||
|
className={'flex flex-col space-y-4'}
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<If condition={!!error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name={'name'}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
data-test={'create-team-name-input'}
|
||||||
|
required
|
||||||
|
minLength={2}
|
||||||
|
maxLength={50}
|
||||||
|
placeholder={''}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
<Trans i18nKey={'teams:teamNameDescription'} />
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
|
<Button disabled={pending} type={'submit'}>
|
||||||
|
{pending ? 'Creating...' : 'Create Company'}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
|||||||
const result = await createUserAction(data);
|
const result = await createUserAction(data);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('User creates successfully');
|
toast.success('User created successfully');
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ export async function AdminDashboard() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Team Accounts</CardTitle>
|
<CardTitle>Company Accounts</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
The number of team accounts that have been created.
|
The number of company accounts that have been created.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@@ -49,43 +49,6 @@ export async function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Paying Customers</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
The number of paying customers with active subscriptions.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<div className={'flex justify-between'}>
|
|
||||||
<Figure>{data.subscriptions}</Figure>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Trials</CardTitle>
|
|
||||||
|
|
||||||
<CardDescription>
|
|
||||||
The number of trial subscriptions currently active.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<div className={'flex justify-between'}>
|
|
||||||
<Figure>{data.trials}</Figure>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className={'text-muted-foreground w-max text-xs'}>
|
|
||||||
The above data is estimated and may not be 100% accurate.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function getColumns(): ColumnDef<Memberships>[] {
|
|||||||
{
|
{
|
||||||
header: 'Role',
|
header: 'Role',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return row.original.role;
|
return row.original.role === 'owner' ? 'HR' : 'Employee';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { ResetPasswordSchema } from './schema/reset-password.schema';
|
|||||||
import { createAdminAccountsService } from './services/admin-accounts.service';
|
import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||||
import { adminAction } from './utils/admin-action';
|
import { adminAction } from './utils/admin-action';
|
||||||
|
import { CreateCompanySchema } from './schema/create-company.schema';
|
||||||
|
import { createCreateCompanyAccountService } from './services/admin-create-company-account.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name banUserAction
|
* @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() {
|
function revalidateAdmin() {
|
||||||
revalidatePath('/admin', 'layout');
|
revalidatePath('/admin', 'layout');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<typeof CreateCompanySchema>;
|
||||||
|
|
||||||
@@ -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<Database>,
|
||||||
|
) {
|
||||||
|
return new CreateTeamAccountService(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateTeamAccountService {
|
||||||
|
private readonly namespace = 'accounts.create-team-account';
|
||||||
|
|
||||||
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,9 +38,7 @@ export const createTeamAccountAction = enhanceAction(
|
|||||||
|
|
||||||
logger.info(ctx, `Team account created`);
|
logger.info(ctx, `Team account created`);
|
||||||
|
|
||||||
const accountHomePath = '/home/' + data.slug;
|
redirect(`/home/${data.slug}/settings`);
|
||||||
|
|
||||||
redirect(accountHomePath);
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
schema: CreateTeamSchema,
|
schema: CreateTeamSchema,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "./tailwind.config.ts",
|
"config": "./tailwind.config.ts",
|
||||||
"css": "../../apps/web/styles/globals.css",
|
"css": "../../styles/globals.css",
|
||||||
"baseColor": "slate",
|
"baseColor": "slate",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
packages:
|
packages:
|
||||||
- packages/**
|
- packages/**
|
||||||
- tooling/*
|
- tooling/*
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@sentry/cli'
|
||||||
|
- '@tailwindcss/oxide'
|
||||||
|
- protobufjs
|
||||||
|
- sharp
|
||||||
|
- supabase
|
||||||
|
- unrs-resolver
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
"sendingEmailCode": "Sending code...",
|
"sendingEmailCode": "Sending code...",
|
||||||
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
||||||
"emailPlaceholder": "your@email.com",
|
"emailPlaceholder": "your@email.com",
|
||||||
"inviteAlertHeading": "You have been invited to join a team",
|
"inviteAlertHeading": "You have been invited to join a company",
|
||||||
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the team.",
|
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
|
||||||
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Terms of Service",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Proceed to App",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
||||||
"manageTeamPlan": "Manage your Team Plan",
|
"manageTeamPlan": "Manage your Company Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Base Plan",
|
||||||
"billingInterval": {
|
"billingInterval": {
|
||||||
"label": "Choose your billing interval",
|
"label": "Choose your billing interval",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Proceed to Payment",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Start Trial",
|
||||||
"perTeamMember": "Per team member",
|
"perTeamMember": "Per company employee",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "Per {{unit}} usage",
|
||||||
"teamMembers": "Team Members",
|
"teamMembers": "Company Employees",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "above {{ previousTier }} {{ unit }}",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"homeTabLabel": "Home",
|
"homeTabLabel": "Home",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Welcome to your home page",
|
||||||
"accountMembers": "Team Members",
|
"accountMembers": "Company Employees",
|
||||||
"membersTabDescription": "Here you can manage the members of your team.",
|
"membersTabDescription": "Here you can manage the employees of your company.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Billing",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Manage your billing and subscription",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Dashboard",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"members": "Members",
|
"members": "Employees",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"label": "Owner"
|
"label": "Owner"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Member"
|
"label": "Employee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
|||||||
@@ -4,26 +4,26 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "Settings",
|
"pageTitle": "Settings",
|
||||||
"pageDescription": "Manage your Team details",
|
"pageDescription": "Manage your Company details",
|
||||||
"teamLogo": "Team Logo",
|
"teamLogo": "Company Logo",
|
||||||
"teamLogoDescription": "Update your team's logo to make it easier to identify",
|
"teamLogoDescription": "Update your company's logo to make it easier to identify",
|
||||||
"teamName": "Team Name",
|
"teamName": "Company Name",
|
||||||
"teamNameDescription": "Update your team's name",
|
"teamNameDescription": "Update your company's name",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Members"
|
"pageTitle": "Employees"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Billing"
|
||||||
},
|
},
|
||||||
"yourTeams": "Your Teams ({{teamsCount}})",
|
"yourTeams": "Your Companies ({{teamsCount}})",
|
||||||
"createTeam": "Create a Team",
|
"createTeam": "Create a Company",
|
||||||
"creatingTeam": "Creating Team...",
|
"creatingTeam": "Creating Company...",
|
||||||
"personalAccount": "Personal Account",
|
"personalAccount": "Personal Account",
|
||||||
"searchAccount": "Search Account...",
|
"searchAccount": "Search Account...",
|
||||||
"membersTabLabel": "Members",
|
"membersTabLabel": "Employees",
|
||||||
"memberName": "Name",
|
"memberName": "Name",
|
||||||
"youLabel": "You",
|
"youLabel": "You",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
@@ -31,108 +31,108 @@
|
|||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Owner",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited at",
|
"invitedAtLabel": "Invited at",
|
||||||
"inviteMembersPageSubheading": "Invite members to your Team",
|
"inviteMembersPageSubheading": "Invite employees to your Company",
|
||||||
"createTeamModalHeading": "Create Team",
|
"createTeamModalHeading": "Create Company",
|
||||||
"createTeamModalDescription": "Create a new Team to manage your projects and members.",
|
"createTeamModalDescription": "Create a new Company to manage your projects and employees.",
|
||||||
"teamNameLabel": "Team Name",
|
"teamNameLabel": "Company Name",
|
||||||
"teamNameDescription": "Your team name should be unique and descriptive",
|
"teamNameDescription": "Your company name should be unique and descriptive",
|
||||||
"createTeamSubmitLabel": "Create Team",
|
"createTeamSubmitLabel": "Create Company",
|
||||||
"createTeamSuccess": "Team created successfully",
|
"createTeamSuccess": "Company created successfully",
|
||||||
"createTeamError": "Team not created. Please try again.",
|
"createTeamError": "Company not created. Please try again.",
|
||||||
"createTeamLoading": "Creating team...",
|
"createTeamLoading": "Creating company...",
|
||||||
"settingsPageLabel": "General",
|
"settingsPageLabel": "General",
|
||||||
"createTeamDropdownLabel": "New team",
|
"createTeamDropdownLabel": "New company",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove from Account",
|
"removeMember": "Remove from Account",
|
||||||
"inviteMembersSuccess": "Members invited successfully!",
|
"inviteMembersSuccess": "Employees invited successfully!",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
||||||
"inviteMembersLoading": "Inviting members...",
|
"inviteMembersLoading": "Inviting employees...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"removeInviteButtonLabel": "Remove invite",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"addAnotherMemberButtonLabel": "Add another one",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"inviteMembersButtonLabel": "Send Invites",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"removeMemberModalHeading": "You are removing this user",
|
||||||
"removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.",
|
"removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.",
|
||||||
"removeMemberSuccessMessage": "Member removed successfully",
|
"removeMemberSuccessMessage": "Employee removed successfully",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.",
|
||||||
"removeMemberLoadingMessage": "Removing member...",
|
"removeMemberLoadingMessage": "Removing employee...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Team",
|
"removeMemberSubmitLabel": "Remove User from Company",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRole": "Update Role",
|
"updateRole": "Update Role",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"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.",
|
"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",
|
"roleMustBeDifferent": "Role must be different from the current one",
|
||||||
"memberRoleInputLabel": "Member role",
|
"memberRoleInputLabel": "Member role",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"updateRoleDescription": "Pick a role for this member.",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"updateRoleSubmitLabel": "Update Role",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"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.",
|
"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",
|
"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",
|
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
||||||
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
||||||
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
||||||
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
||||||
"transferOwnershipDisclaimer": "You are transferring ownership of the selected team to <b>{{ member }}</b>.",
|
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.",
|
||||||
"transferringOwnership": "Transferring ownership...",
|
"transferringOwnership": "Transferring ownership...",
|
||||||
"transferOwnershipSuccess": "Ownership successfully transferred",
|
"transferOwnershipSuccess": "Ownership successfully transferred",
|
||||||
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
||||||
"deleteInviteSubmitLabel": "Delete Invite",
|
"deleteInviteSubmitLabel": "Delete Invite",
|
||||||
"youBadgeLabel": "You",
|
"youBadgeLabel": "You",
|
||||||
"updateTeamLoadingMessage": "Updating Team...",
|
"updateTeamLoadingMessage": "Updating Company...",
|
||||||
"updateTeamSuccessMessage": "Team successfully updated",
|
"updateTeamSuccessMessage": "Company successfully updated",
|
||||||
"updateTeamErrorMessage": "Could not update Team. Please try again.",
|
"updateTeamErrorMessage": "Could not update Company. Please try again.",
|
||||||
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
||||||
"teamNameInputLabel": "Team Name",
|
"teamNameInputLabel": "Company Name",
|
||||||
"teamLogoInputHeading": "Upload your team's Logo",
|
"teamLogoInputHeading": "Upload your company's Logo",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your team logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
||||||
"updateTeamSubmitLabel": "Update Team",
|
"updateTeamSubmitLabel": "Update Company",
|
||||||
"inviteMembersHeading": "Invite Members to your Team",
|
"inviteMembersHeading": "Invite Employees to your Company",
|
||||||
"inviteMembersDescription": "Invite members to your team by entering their email and role.",
|
"inviteMembersDescription": "Invite employees to your company by entering their email and role.",
|
||||||
"emailPlaceholder": "member@email.com",
|
"emailPlaceholder": "employee@email.com",
|
||||||
"membersPageHeading": "Members",
|
"membersPageHeading": "Employees",
|
||||||
"inviteMembersButton": "Invite Members",
|
"inviteMembersButton": "Invite Employees",
|
||||||
"invitingMembers": "Inviting members...",
|
"invitingMembers": "Inviting employees...",
|
||||||
"inviteMembersSuccessMessage": "Members invited successfully",
|
"inviteMembersSuccessMessage": "Employees invited successfully",
|
||||||
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
"inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"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",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading members...",
|
"loadingMembers": "Loading employees...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
"loadMembersError": "Sorry, we couldn't fetch your company's employees.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.",
|
||||||
"loadingInvitedMembers": "Loading invited members...",
|
"loadingInvitedMembers": "Loading invited employees...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneSubheading": "Delete or leave your team",
|
"dangerZoneSubheading": "Delete or leave your company",
|
||||||
"deleteTeam": "Delete Team",
|
"deleteTeam": "Delete Company",
|
||||||
"deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.",
|
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.",
|
||||||
"deletingTeam": "Deleting team",
|
"deletingTeam": "Deleting company",
|
||||||
"deleteTeamModalHeading": "Deleting Team",
|
"deleteTeamModalHeading": "Deleting Company",
|
||||||
"deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.",
|
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.",
|
||||||
"deleteTeamInputField": "Type the name of the team to confirm",
|
"deleteTeamInputField": "Type the name of the company to confirm",
|
||||||
"leaveTeam": "Leave Team",
|
"leaveTeam": "Leave Company",
|
||||||
"leavingTeamModalHeading": "Leaving Team",
|
"leavingTeamModalHeading": "Leaving Company",
|
||||||
"leavingTeamModalDescription": "You are about to leave this team. You will no longer have access to it.",
|
"leavingTeamModalDescription": "You are about to leave this company. 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.",
|
"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 team {{ teamName }}. This action cannot be undone.",
|
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.",
|
||||||
"leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.",
|
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your team.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your team.",
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
||||||
"searchMembersPlaceholder": "Search members",
|
"searchMembersPlaceholder": "Search employees",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your team.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your team. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.",
|
"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 member.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.",
|
||||||
"searchInvitations": "Search Invitations",
|
"searchInvitations": "Search Invitations",
|
||||||
"updateInvitation": "Update Invitation",
|
"updateInvitation": "Update Invitation",
|
||||||
"removeInvitation": "Remove Invitation",
|
"removeInvitation": "Remove Invitation",
|
||||||
@@ -144,20 +144,20 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inviteStatus": "Status",
|
"inviteStatus": "Status",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"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",
|
"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.",
|
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
||||||
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
||||||
"signInWithDifferentAccount": "Sign in with a different account",
|
"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.",
|
"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}}",
|
"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}}",
|
"continueAs": "Continue as {{email}}",
|
||||||
"joinTeamAccount": "Join Team",
|
"joinTeamAccount": "Join Company",
|
||||||
"joiningTeam": "Joining team...",
|
"joiningTeam": "Joining company...",
|
||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.",
|
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
||||||
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
"sendingEmailCode": "Sending code...",
|
"sendingEmailCode": "Sending code...",
|
||||||
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
||||||
"emailPlaceholder": "your@email.com",
|
"emailPlaceholder": "your@email.com",
|
||||||
"inviteAlertHeading": "You have been invited to join a team",
|
"inviteAlertHeading": "You have been invited to join a company",
|
||||||
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the team.",
|
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
|
||||||
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Terms of Service",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Proceed to App",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
||||||
"manageTeamPlan": "Manage your Team Plan",
|
"manageTeamPlan": "Manage your Company Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Base Plan",
|
||||||
"billingInterval": {
|
"billingInterval": {
|
||||||
"label": "Choose your billing interval",
|
"label": "Choose your billing interval",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Proceed to Payment",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Start Trial",
|
||||||
"perTeamMember": "Per team member",
|
"perTeamMember": "Per company employee",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "Per {{unit}} usage",
|
||||||
"teamMembers": "Team Members",
|
"teamMembers": "Company Employees",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "above {{ previousTier }} {{ unit }}",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"homeTabLabel": "Home",
|
"homeTabLabel": "Home",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Welcome to your home page",
|
||||||
"accountMembers": "Team Members",
|
"accountMembers": "Company Employees",
|
||||||
"membersTabDescription": "Here you can manage the members of your team.",
|
"membersTabDescription": "Here you can manage the employees of your company.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Billing",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Manage your billing and subscription",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Dashboard",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"members": "Members",
|
"members": "Employees",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"label": "Owner"
|
"label": "Owner"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Member"
|
"label": "Employee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
|||||||
@@ -36,5 +36,5 @@
|
|||||||
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
||||||
"footerDescription": "Here you can add a description about your company or product",
|
"footerDescription": "Here you can add a description about your company or product",
|
||||||
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
||||||
"heroSubtitle": "Lihtne, mugav ja kiire ülevaade Sinu tervise seisundist"
|
"heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,26 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "Settings",
|
"pageTitle": "Settings",
|
||||||
"pageDescription": "Manage your Team details",
|
"pageDescription": "Manage your Company details",
|
||||||
"teamLogo": "Team Logo",
|
"teamLogo": "Company Logo",
|
||||||
"teamLogoDescription": "Update your team's logo to make it easier to identify",
|
"teamLogoDescription": "Update your company's logo to make it easier to identify",
|
||||||
"teamName": "Team Name",
|
"teamName": "Company Name",
|
||||||
"teamNameDescription": "Update your team's name",
|
"teamNameDescription": "Update your company's name",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Members"
|
"pageTitle": "Employees"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Billing"
|
||||||
},
|
},
|
||||||
"yourTeams": "Your Teams ({{teamsCount}})",
|
"yourTeams": "Your Companies ({{teamsCount}})",
|
||||||
"createTeam": "Create a Team",
|
"createTeam": "Create a Company",
|
||||||
"creatingTeam": "Creating Team...",
|
"creatingTeam": "Creating Company...",
|
||||||
"personalAccount": "Personal Account",
|
"personalAccount": "Personal Account",
|
||||||
"searchAccount": "Search Account...",
|
"searchAccount": "Search Account...",
|
||||||
"membersTabLabel": "Members",
|
"membersTabLabel": "Employees",
|
||||||
"memberName": "Name",
|
"memberName": "Name",
|
||||||
"youLabel": "You",
|
"youLabel": "You",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
@@ -31,108 +31,108 @@
|
|||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Owner",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited at",
|
"invitedAtLabel": "Invited at",
|
||||||
"inviteMembersPageSubheading": "Invite members to your Team",
|
"inviteMembersPageSubheading": "Invite employees to your Company",
|
||||||
"createTeamModalHeading": "Create Team",
|
"createTeamModalHeading": "Create Company",
|
||||||
"createTeamModalDescription": "Create a new Team to manage your projects and members.",
|
"createTeamModalDescription": "Create a new Company to manage your projects and employees.",
|
||||||
"teamNameLabel": "Team Name",
|
"teamNameLabel": "Company Name",
|
||||||
"teamNameDescription": "Your team name should be unique and descriptive",
|
"teamNameDescription": "Your company name should be unique and descriptive",
|
||||||
"createTeamSubmitLabel": "Create Team",
|
"createTeamSubmitLabel": "Create Company",
|
||||||
"createTeamSuccess": "Team created successfully",
|
"createTeamSuccess": "Company created successfully",
|
||||||
"createTeamError": "Team not created. Please try again.",
|
"createTeamError": "Company not created. Please try again.",
|
||||||
"createTeamLoading": "Creating team...",
|
"createTeamLoading": "Creating company...",
|
||||||
"settingsPageLabel": "General",
|
"settingsPageLabel": "General",
|
||||||
"createTeamDropdownLabel": "New team",
|
"createTeamDropdownLabel": "New company",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove from Account",
|
"removeMember": "Remove from Account",
|
||||||
"inviteMembersSuccess": "Members invited successfully!",
|
"inviteMembersSuccess": "Employees invited successfully!",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
||||||
"inviteMembersLoading": "Inviting members...",
|
"inviteMembersLoading": "Inviting employees...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"removeInviteButtonLabel": "Remove invite",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"addAnotherMemberButtonLabel": "Add another one",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"inviteMembersButtonLabel": "Send Invites",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"removeMemberModalHeading": "You are removing this user",
|
||||||
"removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.",
|
"removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.",
|
||||||
"removeMemberSuccessMessage": "Member removed successfully",
|
"removeMemberSuccessMessage": "Employee removed successfully",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.",
|
||||||
"removeMemberLoadingMessage": "Removing member...",
|
"removeMemberLoadingMessage": "Removing employee...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Team",
|
"removeMemberSubmitLabel": "Remove User from Company",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRole": "Update Role",
|
"updateRole": "Update Role",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"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.",
|
"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",
|
"roleMustBeDifferent": "Role must be different from the current one",
|
||||||
"memberRoleInputLabel": "Member role",
|
"memberRoleInputLabel": "Member role",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"updateRoleDescription": "Pick a role for this member.",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"updateRoleSubmitLabel": "Update Role",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"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.",
|
"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",
|
"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",
|
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
||||||
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
||||||
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
||||||
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
||||||
"transferOwnershipDisclaimer": "You are transferring ownership of the selected team to <b>{{ member }}</b>.",
|
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.",
|
||||||
"transferringOwnership": "Transferring ownership...",
|
"transferringOwnership": "Transferring ownership...",
|
||||||
"transferOwnershipSuccess": "Ownership successfully transferred",
|
"transferOwnershipSuccess": "Ownership successfully transferred",
|
||||||
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
||||||
"deleteInviteSubmitLabel": "Delete Invite",
|
"deleteInviteSubmitLabel": "Delete Invite",
|
||||||
"youBadgeLabel": "You",
|
"youBadgeLabel": "You",
|
||||||
"updateTeamLoadingMessage": "Updating Team...",
|
"updateTeamLoadingMessage": "Updating Company...",
|
||||||
"updateTeamSuccessMessage": "Team successfully updated",
|
"updateTeamSuccessMessage": "Company successfully updated",
|
||||||
"updateTeamErrorMessage": "Could not update Team. Please try again.",
|
"updateTeamErrorMessage": "Could not update Company. Please try again.",
|
||||||
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
||||||
"teamNameInputLabel": "Team Name",
|
"teamNameInputLabel": "Company Name",
|
||||||
"teamLogoInputHeading": "Upload your team's Logo",
|
"teamLogoInputHeading": "Upload your company's Logo",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your team logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
||||||
"updateTeamSubmitLabel": "Update Team",
|
"updateTeamSubmitLabel": "Update Company",
|
||||||
"inviteMembersHeading": "Invite Members to your Team",
|
"inviteMembersHeading": "Invite Employees to your Company",
|
||||||
"inviteMembersDescription": "Invite members to your team by entering their email and role.",
|
"inviteMembersDescription": "Invite employees to your company by entering their email and role.",
|
||||||
"emailPlaceholder": "member@email.com",
|
"emailPlaceholder": "employee@email.com",
|
||||||
"membersPageHeading": "Members",
|
"membersPageHeading": "Employees",
|
||||||
"inviteMembersButton": "Invite Members",
|
"inviteMembersButton": "Invite Employees",
|
||||||
"invitingMembers": "Inviting members...",
|
"invitingMembers": "Inviting employees...",
|
||||||
"inviteMembersSuccessMessage": "Members invited successfully",
|
"inviteMembersSuccessMessage": "Employees invited successfully",
|
||||||
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
"inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"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",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading members...",
|
"loadingMembers": "Loading employees...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
"loadMembersError": "Sorry, we couldn't fetch your company's employees.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.",
|
||||||
"loadingInvitedMembers": "Loading invited members...",
|
"loadingInvitedMembers": "Loading invited employees...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneSubheading": "Delete or leave your team",
|
"dangerZoneSubheading": "Delete or leave your company",
|
||||||
"deleteTeam": "Delete Team",
|
"deleteTeam": "Delete Company",
|
||||||
"deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.",
|
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.",
|
||||||
"deletingTeam": "Deleting team",
|
"deletingTeam": "Deleting company",
|
||||||
"deleteTeamModalHeading": "Deleting Team",
|
"deleteTeamModalHeading": "Deleting Company",
|
||||||
"deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.",
|
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.",
|
||||||
"deleteTeamInputField": "Type the name of the team to confirm",
|
"deleteTeamInputField": "Type the name of the company to confirm",
|
||||||
"leaveTeam": "Leave Team",
|
"leaveTeam": "Leave Company",
|
||||||
"leavingTeamModalHeading": "Leaving Team",
|
"leavingTeamModalHeading": "Leaving Company",
|
||||||
"leavingTeamModalDescription": "You are about to leave this team. You will no longer have access to it.",
|
"leavingTeamModalDescription": "You are about to leave this company. 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.",
|
"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 team {{ teamName }}. This action cannot be undone.",
|
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.",
|
||||||
"leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.",
|
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your team.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your team.",
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
||||||
"searchMembersPlaceholder": "Search members",
|
"searchMembersPlaceholder": "Search employees",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your team.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your team. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.",
|
"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 member.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.",
|
||||||
"searchInvitations": "Search Invitations",
|
"searchInvitations": "Search Invitations",
|
||||||
"updateInvitation": "Update Invitation",
|
"updateInvitation": "Update Invitation",
|
||||||
"removeInvitation": "Remove Invitation",
|
"removeInvitation": "Remove Invitation",
|
||||||
@@ -144,20 +144,20 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inviteStatus": "Status",
|
"inviteStatus": "Status",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"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",
|
"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.",
|
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
||||||
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
||||||
"signInWithDifferentAccount": "Sign in with a different account",
|
"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.",
|
"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}}",
|
"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}}",
|
"continueAs": "Continue as {{email}}",
|
||||||
"joinTeamAccount": "Join Team",
|
"joinTeamAccount": "Join Company",
|
||||||
"joiningTeam": "Joining team...",
|
"joiningTeam": "Joining company...",
|
||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.",
|
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
||||||
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
"sendingEmailCode": "Sending code...",
|
"sendingEmailCode": "Sending code...",
|
||||||
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
||||||
"emailPlaceholder": "your@email.com",
|
"emailPlaceholder": "your@email.com",
|
||||||
"inviteAlertHeading": "You have been invited to join a team",
|
"inviteAlertHeading": "You have been invited to join a company",
|
||||||
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the team.",
|
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
|
||||||
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Terms of Service",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Proceed to App",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
||||||
"manageTeamPlan": "Manage your Team Plan",
|
"manageTeamPlan": "Manage your Company Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Base Plan",
|
||||||
"billingInterval": {
|
"billingInterval": {
|
||||||
"label": "Choose your billing interval",
|
"label": "Choose your billing interval",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Proceed to Payment",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Start Trial",
|
||||||
"perTeamMember": "Per team member",
|
"perTeamMember": "Per company employee",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "Per {{unit}} usage",
|
||||||
"teamMembers": "Team Members",
|
"teamMembers": "Company Employees",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "above {{ previousTier }} {{ unit }}",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"homeTabLabel": "Home",
|
"homeTabLabel": "Home",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Welcome to your home page",
|
||||||
"accountMembers": "Team Members",
|
"accountMembers": "Company Employees",
|
||||||
"membersTabDescription": "Here you can manage the members of your team.",
|
"membersTabDescription": "Here you can manage the employees of your company.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Billing",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Manage your billing and subscription",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Dashboard",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"members": "Members",
|
"members": "Employees",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"label": "Owner"
|
"label": "Owner"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Member"
|
"label": "Employee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
|||||||
@@ -36,5 +36,5 @@
|
|||||||
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
||||||
"footerDescription": "Here you can add a description about your company or product",
|
"footerDescription": "Here you can add a description about your company or product",
|
||||||
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
||||||
"heroSubtitle": "Простой, удобный и быстрый обзор вашего состояния здоровья"
|
"heroSubtitle": "A simple, convenient, and quick overview of your health condition"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,26 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "Settings",
|
"pageTitle": "Settings",
|
||||||
"pageDescription": "Manage your Team details",
|
"pageDescription": "Manage your Company details",
|
||||||
"teamLogo": "Team Logo",
|
"teamLogo": "Company Logo",
|
||||||
"teamLogoDescription": "Update your team's logo to make it easier to identify",
|
"teamLogoDescription": "Update your company's logo to make it easier to identify",
|
||||||
"teamName": "Team Name",
|
"teamName": "Company Name",
|
||||||
"teamNameDescription": "Update your team's name",
|
"teamNameDescription": "Update your company's name",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Members"
|
"pageTitle": "Employees"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Billing"
|
||||||
},
|
},
|
||||||
"yourTeams": "Your Teams ({{teamsCount}})",
|
"yourTeams": "Your Companies ({{teamsCount}})",
|
||||||
"createTeam": "Create a Team",
|
"createTeam": "Create a Company",
|
||||||
"creatingTeam": "Creating Team...",
|
"creatingTeam": "Creating Company...",
|
||||||
"personalAccount": "Personal Account",
|
"personalAccount": "Personal Account",
|
||||||
"searchAccount": "Search Account...",
|
"searchAccount": "Search Account...",
|
||||||
"membersTabLabel": "Members",
|
"membersTabLabel": "Employees",
|
||||||
"memberName": "Name",
|
"memberName": "Name",
|
||||||
"youLabel": "You",
|
"youLabel": "You",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
@@ -31,108 +31,108 @@
|
|||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Owner",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited at",
|
"invitedAtLabel": "Invited at",
|
||||||
"inviteMembersPageSubheading": "Invite members to your Team",
|
"inviteMembersPageSubheading": "Invite employees to your Company",
|
||||||
"createTeamModalHeading": "Create Team",
|
"createTeamModalHeading": "Create Company",
|
||||||
"createTeamModalDescription": "Create a new Team to manage your projects and members.",
|
"createTeamModalDescription": "Create a new Company to manage your projects and employees.",
|
||||||
"teamNameLabel": "Team Name",
|
"teamNameLabel": "Company Name",
|
||||||
"teamNameDescription": "Your team name should be unique and descriptive",
|
"teamNameDescription": "Your company name should be unique and descriptive",
|
||||||
"createTeamSubmitLabel": "Create Team",
|
"createTeamSubmitLabel": "Create Company",
|
||||||
"createTeamSuccess": "Team created successfully",
|
"createTeamSuccess": "Company created successfully",
|
||||||
"createTeamError": "Team not created. Please try again.",
|
"createTeamError": "Company not created. Please try again.",
|
||||||
"createTeamLoading": "Creating team...",
|
"createTeamLoading": "Creating company...",
|
||||||
"settingsPageLabel": "General",
|
"settingsPageLabel": "General",
|
||||||
"createTeamDropdownLabel": "New team",
|
"createTeamDropdownLabel": "New company",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove from Account",
|
"removeMember": "Remove from Account",
|
||||||
"inviteMembersSuccess": "Members invited successfully!",
|
"inviteMembersSuccess": "Employees invited successfully!",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
||||||
"inviteMembersLoading": "Inviting members...",
|
"inviteMembersLoading": "Inviting employees...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"removeInviteButtonLabel": "Remove invite",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"addAnotherMemberButtonLabel": "Add another one",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"inviteMembersButtonLabel": "Send Invites",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"removeMemberModalHeading": "You are removing this user",
|
||||||
"removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.",
|
"removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.",
|
||||||
"removeMemberSuccessMessage": "Member removed successfully",
|
"removeMemberSuccessMessage": "Employee removed successfully",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.",
|
||||||
"removeMemberLoadingMessage": "Removing member...",
|
"removeMemberLoadingMessage": "Removing employee...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Team",
|
"removeMemberSubmitLabel": "Remove User from Company",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRole": "Update Role",
|
"updateRole": "Update Role",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"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.",
|
"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",
|
"roleMustBeDifferent": "Role must be different from the current one",
|
||||||
"memberRoleInputLabel": "Member role",
|
"memberRoleInputLabel": "Member role",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"updateRoleDescription": "Pick a role for this member.",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"updateRoleSubmitLabel": "Update Role",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"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.",
|
"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",
|
"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",
|
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
||||||
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
||||||
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
||||||
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
||||||
"transferOwnershipDisclaimer": "You are transferring ownership of the selected team to <b>{{ member }}</b>.",
|
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.",
|
||||||
"transferringOwnership": "Transferring ownership...",
|
"transferringOwnership": "Transferring ownership...",
|
||||||
"transferOwnershipSuccess": "Ownership successfully transferred",
|
"transferOwnershipSuccess": "Ownership successfully transferred",
|
||||||
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
||||||
"deleteInviteSubmitLabel": "Delete Invite",
|
"deleteInviteSubmitLabel": "Delete Invite",
|
||||||
"youBadgeLabel": "You",
|
"youBadgeLabel": "You",
|
||||||
"updateTeamLoadingMessage": "Updating Team...",
|
"updateTeamLoadingMessage": "Updating Company...",
|
||||||
"updateTeamSuccessMessage": "Team successfully updated",
|
"updateTeamSuccessMessage": "Company successfully updated",
|
||||||
"updateTeamErrorMessage": "Could not update Team. Please try again.",
|
"updateTeamErrorMessage": "Could not update Company. Please try again.",
|
||||||
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
||||||
"teamNameInputLabel": "Team Name",
|
"teamNameInputLabel": "Company Name",
|
||||||
"teamLogoInputHeading": "Upload your team's Logo",
|
"teamLogoInputHeading": "Upload your company's Logo",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your team logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
||||||
"updateTeamSubmitLabel": "Update Team",
|
"updateTeamSubmitLabel": "Update Company",
|
||||||
"inviteMembersHeading": "Invite Members to your Team",
|
"inviteMembersHeading": "Invite Employees to your Company",
|
||||||
"inviteMembersDescription": "Invite members to your team by entering their email and role.",
|
"inviteMembersDescription": "Invite employees to your company by entering their email and role.",
|
||||||
"emailPlaceholder": "member@email.com",
|
"emailPlaceholder": "employee@email.com",
|
||||||
"membersPageHeading": "Members",
|
"membersPageHeading": "Employees",
|
||||||
"inviteMembersButton": "Invite Members",
|
"inviteMembersButton": "Invite Employees",
|
||||||
"invitingMembers": "Inviting members...",
|
"invitingMembers": "Inviting employees...",
|
||||||
"inviteMembersSuccessMessage": "Members invited successfully",
|
"inviteMembersSuccessMessage": "Employees invited successfully",
|
||||||
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
"inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"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",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading members...",
|
"loadingMembers": "Loading employees...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
"loadMembersError": "Sorry, we couldn't fetch your company's employees.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.",
|
||||||
"loadingInvitedMembers": "Loading invited members...",
|
"loadingInvitedMembers": "Loading invited employees...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneSubheading": "Delete or leave your team",
|
"dangerZoneSubheading": "Delete or leave your company",
|
||||||
"deleteTeam": "Delete Team",
|
"deleteTeam": "Delete Company",
|
||||||
"deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.",
|
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.",
|
||||||
"deletingTeam": "Deleting team",
|
"deletingTeam": "Deleting company",
|
||||||
"deleteTeamModalHeading": "Deleting Team",
|
"deleteTeamModalHeading": "Deleting Company",
|
||||||
"deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.",
|
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.",
|
||||||
"deleteTeamInputField": "Type the name of the team to confirm",
|
"deleteTeamInputField": "Type the name of the company to confirm",
|
||||||
"leaveTeam": "Leave Team",
|
"leaveTeam": "Leave Company",
|
||||||
"leavingTeamModalHeading": "Leaving Team",
|
"leavingTeamModalHeading": "Leaving Company",
|
||||||
"leavingTeamModalDescription": "You are about to leave this team. You will no longer have access to it.",
|
"leavingTeamModalDescription": "You are about to leave this company. 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.",
|
"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 team {{ teamName }}. This action cannot be undone.",
|
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.",
|
||||||
"leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.",
|
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your team.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your team.",
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
||||||
"searchMembersPlaceholder": "Search members",
|
"searchMembersPlaceholder": "Search employees",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your team.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your team. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.",
|
"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 member.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.",
|
||||||
"searchInvitations": "Search Invitations",
|
"searchInvitations": "Search Invitations",
|
||||||
"updateInvitation": "Update Invitation",
|
"updateInvitation": "Update Invitation",
|
||||||
"removeInvitation": "Remove Invitation",
|
"removeInvitation": "Remove Invitation",
|
||||||
@@ -144,20 +144,20 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inviteStatus": "Status",
|
"inviteStatus": "Status",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"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",
|
"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.",
|
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
||||||
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
||||||
"signInWithDifferentAccount": "Sign in with a different account",
|
"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.",
|
"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}}",
|
"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}}",
|
"continueAs": "Continue as {{email}}",
|
||||||
"joinTeamAccount": "Join Team",
|
"joinTeamAccount": "Join Company",
|
||||||
"joiningTeam": "Joining team...",
|
"joiningTeam": "Joining company...",
|
||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.",
|
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
||||||
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ Example of usage
|
|||||||
--color-text-foreground -> className="[property name]-text-foreground" -> className="text-text-foreground", border-text-foreground
|
--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
|
||||||
@@ -69,7 +69,7 @@ enabled = true
|
|||||||
# Port to use for Supabase Studio.
|
# Port to use for Supabase Studio.
|
||||||
port = 54323
|
port = 54323
|
||||||
# External URL of the API server that frontend connects to.
|
# 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 to use for Supabase AI in the Supabase Studio.
|
||||||
openai_api_key = "env(OPENAI_API_KEY)"
|
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.
|
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||||
# smtp_port = 54325
|
# smtp_port = 54325
|
||||||
# pop3_port = 54326
|
# pop3_port = 54326
|
||||||
# admin_email = "admin@email.com"
|
# admin_email = ""
|
||||||
# sender_name = "Admin"
|
# sender_name = "Admin"
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
@@ -107,7 +107,7 @@ enabled = true
|
|||||||
# in emails.
|
# in emails.
|
||||||
site_url = "http://127.0.0.1:3000"
|
site_url = "http://127.0.0.1:3000"
|
||||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
# 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).
|
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||||
jwt_expiry = 3600
|
jwt_expiry = 3600
|
||||||
# If disabled, the refresh token will never expire.
|
# If disabled, the refresh token will never expire.
|
||||||
@@ -129,7 +129,7 @@ password_requirements = ""
|
|||||||
|
|
||||||
[auth.rate_limit]
|
[auth.rate_limit]
|
||||||
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
|
# 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.
|
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
|
||||||
sms_sent = 30
|
sms_sent = 30
|
||||||
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
|
# 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
|
enable_signup = true
|
||||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
# 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.
|
# 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.
|
# If enabled, users need to confirm their email address before signing in.
|
||||||
enable_confirmations = false
|
enable_confirmations = false
|
||||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
# 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"
|
# sender_name = "Admin"
|
||||||
|
|
||||||
# Uncomment to customize email template
|
# Uncomment to customize email template
|
||||||
# [auth.email.template.invite]
|
[auth.email.template.invite]
|
||||||
# subject = "You have been invited"
|
subject = "You have been invited"
|
||||||
# content_path = "./supabase/templates/invite.html"
|
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]
|
[auth.sms]
|
||||||
# Allow/disallow new user signups via SMS to your project.
|
# Allow/disallow new user signups via SMS to your project.
|
||||||
@@ -220,13 +237,13 @@ max_enrolled_factors = 10
|
|||||||
|
|
||||||
# Control MFA via App Authenticator (TOTP)
|
# Control MFA via App Authenticator (TOTP)
|
||||||
[auth.mfa.totp]
|
[auth.mfa.totp]
|
||||||
enroll_enabled = false
|
enroll_enabled = true
|
||||||
verify_enabled = false
|
verify_enabled = true
|
||||||
|
|
||||||
# Configure MFA via Phone Messaging
|
# Configure MFA via Phone Messaging
|
||||||
[auth.mfa.phone]
|
[auth.mfa.phone]
|
||||||
enroll_enabled = false
|
enroll_enabled = true
|
||||||
verify_enabled = false
|
verify_enabled = true
|
||||||
otp_length = 6
|
otp_length = 6
|
||||||
template = "Your code is {{ .Code }}"
|
template = "Your code is {{ .Code }}"
|
||||||
max_frequency = "5s"
|
max_frequency = "5s"
|
||||||
|
|||||||
349
supabase/migrations/20250612193715_nonces.sql
Normal file
349
supabase/migrations/20250612193715_nonces.sql
Normal file
@@ -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';
|
||||||
145
supabase/migrations/20250612193815_mfa.sql
Normal file
145
supabase/migrations/20250612193815_mfa.sql
Normal file
@@ -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());
|
||||||
73
supabase/migrations/20250612193836_super_admin.sql
Normal file
73
supabase/migrations/20250612193836_super_admin.sql
Normal file
@@ -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());
|
||||||
126
supabase/migrations/20250612193929_account_functions.sql
Normal file
126
supabase/migrations/20250612193929_account_functions.sql
Normal file
@@ -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;
|
||||||
8
supabase/templates/change-email-address.html
Normal file
8
supabase/templates/change-email-address.html
Normal file
File diff suppressed because one or more lines are too long
8
supabase/templates/confirm-email.html
Normal file
8
supabase/templates/confirm-email.html
Normal file
File diff suppressed because one or more lines are too long
8
supabase/templates/invite-user.html
Normal file
8
supabase/templates/invite-user.html
Normal file
File diff suppressed because one or more lines are too long
8
supabase/templates/magic-link.html
Normal file
8
supabase/templates/magic-link.html
Normal file
File diff suppressed because one or more lines are too long
8
supabase/templates/reset-password.html
Normal file
8
supabase/templates/reset-password.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user