fix company creation for admin and inviting of new employees

This commit is contained in:
Danel Kungla
2025-07-31 12:27:30 +03:00
parent 87363051cd
commit a39c21e4e7
18 changed files with 496 additions and 57 deletions

View File

@@ -163,9 +163,9 @@ export function PersonalAccountDropdown({
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<If condition={accounts.length > 0}>
<DropdownMenuSeparator />
<span className="text-muted-foreground px-2 text-xs">
<Trans
i18nKey={'teams:yourTeams'}

View File

@@ -20,7 +20,6 @@ import { Button } from '@kit/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -29,10 +28,13 @@ import {
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
import { createCompanyAccountAction } from '../lib/server/admin-server-actions';
import { CreateCompanySchema, CreateCompanySchemaType } from '../lib/server/schema/create-company.schema';
import { Trans } from '@kit/ui/trans';
import {
CreateCompanySchema,
CreateCompanySchemaType,
} from '../lib/server/schema/create-company.schema';
export function AdminCreateCompanyDialog(props: React.PropsWithChildren) {
const [pending, startTransition] = useTransition();
@@ -58,14 +60,9 @@ export function AdminCreateCompanyDialog(props: React.PropsWithChildren) {
setOpen(false);
setError(null);
} else {
setError('Something went wrong with company creation');
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Error');
}
@@ -100,17 +97,17 @@ export function AdminCreateCompanyDialog(props: React.PropsWithChildren) {
</If>
<FormField
name={'name'}
name="name"
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'teams:teamNameLabel'} />
<Trans i18nKey="teams:teamNameLabel" />
</FormLabel>
<FormControl>
<Input
data-test={'create-team-name-input'}
data-test="create-team-name-input"
required
minLength={2}
maxLength={50}
@@ -119,9 +116,31 @@ export function AdminCreateCompanyDialog(props: React.PropsWithChildren) {
/>
</FormControl>
<FormDescription>
<Trans i18nKey={'teams:teamNameDescription'} />
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
name="ownerPersonalCode"
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey="teams:teamOwnerPersonalCodeLabel" />
</FormLabel>
<FormControl>
<Input
data-test="create-team-owner-personal-code-input"
required
minLength={2}
maxLength={50}
placeholder={''}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -239,7 +239,7 @@ export const resetPasswordAction = adminAction(
);
export const createCompanyAccountAction = enhanceAction(
async ({ name }, user) => {
async ({ name, ownerPersonalCode }, user) => {
const logger = await getLogger();
const client = getSupabaseServerClient();
const service = createCreateCompanyAccountService(client);
@@ -254,7 +254,7 @@ export const createCompanyAccountAction = enhanceAction(
const { data, error } = await service.createNewOrganizationAccount({
name,
userId: user.id,
ownerPersonalCode,
});
if (error) {
@@ -266,8 +266,7 @@ export const createCompanyAccountAction = enhanceAction(
}
logger.info(ctx, `Company account created`);
redirect(`/home/${data.slug}/settings`);
redirect(`/admin/accounts/${data.id}`);
},
{
schema: CreateCompanySchema,

View File

@@ -46,7 +46,11 @@ export const CompanyNameSchema = z
*/
export const CreateCompanySchema = z.object({
name: CompanyNameSchema,
ownerPersonalCode: z
.string()
.regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
message: 'Invalid Estonian personal code format',
}),
});
export type CreateCompanySchemaType = z.infer<typeof CreateCompanySchema>;

View File

@@ -16,7 +16,10 @@ class CreateTeamAccountService {
constructor(private readonly client: SupabaseClient<Database>) {}
async createNewOrganizationAccount(params: { name: string; userId: string }) {
async createNewOrganizationAccount(params: {
name: string;
ownerPersonalCode: string;
}) {
const logger = await getLogger();
const ctx = { ...params, namespace: this.namespace };
@@ -26,12 +29,13 @@ class CreateTeamAccountService {
.schema('medreport')
.rpc('create_team_account', {
account_name: params.name,
new_personal_code: params.ownerPersonalCode,
});
if (error) {
logger.error(
{
error,
error: error.message,
...ctx,
},
`Error creating company account`,

View File

@@ -34,20 +34,18 @@ import {
import { Spinner } from '@kit/ui/spinner';
import { Trans } from '@kit/ui/trans';
import pathsConfig from '~/config/paths.config';
export function MultiFactorChallengeContainer({
paths,
userId,
}: React.PropsWithChildren<{
userId: string;
paths: {
redirectPath: string;
};
}>) {
const router = useRouter();
const verifyMFAChallenge = useVerifyMFAChallenge({
onSuccess: () => {
router.replace('/');
router.replace(pathsConfig.app.home);
},
});
@@ -206,6 +204,10 @@ function useVerifyMFAChallenge({ onSuccess }: { onSuccess: () => void }) {
});
if (response.error) {
console.warn(
{ error: response.error.message },
'Failed to verify MFA challenge',
);
throw response.error;
}

View File

@@ -2,6 +2,7 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { getLogger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
type Notification = Database['medreport']['Tables']['notifications'];
@@ -14,12 +15,17 @@ class NotificationsService {
constructor(private readonly client: SupabaseClient<Database>) {}
async createNotification(params: Notification['Insert']) {
const logger = await getLogger();
const { error } = await this.client
.schema('medreport')
.from('notifications')
.insert(params);
if (error) {
logger.error(
{ ...params },
`Could not create notification: ${error.message}`,
);
throw error;
}
}

View File

@@ -54,8 +54,9 @@ export const createInvitationsAction = enhanceAction(
);
}
const { data: invitations, error: invitationError } =
await serviceClient.rpc('get_invitations_with_account_ids', {
const { data: invitations, error: invitationError } = await serviceClient
.schema('medreport')
.rpc('get_invitations_with_account_ids', {
company_id: company[0].id,
personal_codes: personalCodes,
});

View File

@@ -51,7 +51,10 @@ class AccountInvitationsService {
});
if (error) {
logger.error(ctx, `Failed to remove invitation`);
logger.error(
{ ...ctx, error: error.message },
`Failed to remove invitation`,
);
throw error;
}
@@ -184,7 +187,7 @@ class AccountInvitationsService {
throw new Error('Account not found');
}
console.log('property', invitations, accountSlug);
const response = await this.client
.schema('medreport')
.rpc('add_invitations_to_account', {

View File

@@ -218,10 +218,24 @@ export type Database = {
{
foreignKeyName: "account_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: true
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "account_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "account_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
accounts: {
@@ -316,7 +330,13 @@ export type Database = {
user_id?: string
}
Relationships: [
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
@@ -932,6 +952,7 @@ export type Database = {
id: number
invite_token: string
invited_by: string
personal_code: string | null
role: string
updated_at: string
}
@@ -943,6 +964,7 @@ export type Database = {
id?: number
invite_token: string
invited_by: string
personal_code?: string | null
role: string
updated_at?: string
}
@@ -954,6 +976,7 @@ export type Database = {
id?: number
invite_token?: string
invited_by?: string
personal_code?: string | null
role?: string
updated_at?: string
}
@@ -1543,7 +1566,7 @@ export type Database = {
add_invitations_to_account: {
Args: {
account_slug: string
invitations: Database["public"]["CompositeTypes"]["invitation"][]
invitations: Database["medreport"]["CompositeTypes"]["invitation"][]
}
Returns: Database["medreport"]["Tables"]["invitations"]["Row"][]
}
@@ -1561,6 +1584,7 @@ export type Database = {
id: number
invite_token: string
invited_by: string
personal_code: string | null
role: string
updated_at: string
}
@@ -1577,7 +1601,9 @@ export type Database = {
Returns: Json
}
create_team_account: {
Args: { account_name: string }
Args:
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: {
city: string | null
created_at: string | null
@@ -1609,6 +1635,7 @@ export type Database = {
created_at: string
updated_at: string
expires_at: string
personal_code: string
inviter_name: string
inviter_email: string
}[]
@@ -1633,6 +1660,14 @@ export type Database = {
Args: Record<PropertyKey, never>
Returns: Json
}
get_invitations_with_account_ids: {
Args: { company_id: string; personal_codes: string[] }
Returns: {
invite_token: string
personal_code: string
account_id: string
}[]
}
get_nonce_status: {
Args: { p_id: string }
Returns: Json
@@ -1846,7 +1881,11 @@ export type Database = {
| "paused"
}
CompositeTypes: {
[_ in never]: never
invitation: {
email: string | null
role: string | null
personal_code: string | null
}
}
}
public: {
@@ -7531,14 +7570,6 @@ export type Database = {
[_ in never]: never
}
Functions: {
get_invitations_with_account_ids: {
Args: { company_id: string; personal_codes: string[] }
Returns: {
invite_token: string
personal_code: string
account_id: string
}[]
}
has_permission: {
Args: {
user_id: string