feat: enhance team account creation with policy checks and UI updates (#436)

* feat: enhance team account creation with policy checks and UI updates
This commit is contained in:
Giancarlo Buomprisco
2026-01-06 12:50:18 +01:00
committed by GitHub
parent ab57b24518
commit 5237d34e6f
14 changed files with 223 additions and 39 deletions

View File

@@ -14,7 +14,6 @@
"./hooks/*": "./src/hooks/*.ts",
"./webhooks": "./src/server/services/webhooks/index.ts",
"./policies": "./src/server/policies/index.ts",
"./policies/orchestrator": "./src/server/policies/orchestrator.ts",
"./services/account-invitations.service": "./src/server/services/account-invitations.service.ts"
},
"dependencies": {

View File

@@ -61,7 +61,7 @@ export function CreateTeamAccountDialog(
}
function CreateOrganizationAccountForm(props: { onClose: () => void }) {
const [error, setError] = useState<boolean>();
const [error, setError] = useState<{ message?: string } | undefined>();
const [pending, startTransition] = useTransition();
const form = useForm({
@@ -78,14 +78,14 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
onSubmit={form.handleSubmit((data) => {
startTransition(async () => {
try {
const { error } = await createTeamAccountAction(data);
const result = await createTeamAccountAction(data);
if (error) {
setError(true);
if (result.error) {
setError({ message: result.message });
}
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
} catch (e) {
if (!isRedirectError(e)) {
setError({});
}
}
});
@@ -93,7 +93,7 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
>
<div className={'flex flex-col space-y-4'}>
<If condition={error}>
<CreateOrganizationErrorAlert />
<CreateOrganizationErrorAlert message={error?.message} />
</If>
<FormField
@@ -150,7 +150,7 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
);
}
function CreateOrganizationErrorAlert() {
function CreateOrganizationErrorAlert(props: { message?: string }) {
return (
<Alert variant={'destructive'}>
<AlertTitle>
@@ -158,7 +158,11 @@ function CreateOrganizationErrorAlert() {
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'teams:createTeamErrorMessage'} />
{props.message ? (
<Trans i18nKey={props.message} defaults={props.message} />
) : (
<Trans i18nKey={'teams:createTeamErrorMessage'} />
)}
</AlertDescription>
</Alert>
);

View File

@@ -7,6 +7,7 @@ import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { CreateTeamSchema } from '../../schema/create-team.schema';
import { createAccountCreationPolicyEvaluator } from '../policies';
import { createCreateTeamAccountService } from '../services/create-team-account.service';
export const createTeamAccountAction = enhanceAction(
@@ -23,19 +24,40 @@ export const createTeamAccountAction = enhanceAction(
logger.info(ctx, `Creating team account...`);
const { data, error } = await service.createNewOrganizationAccount({
// Check policies before creating
const evaluator = createAccountCreationPolicyEvaluator();
if (await evaluator.hasPoliciesForStage('submission')) {
const policyContext = {
timestamp: new Date().toISOString(),
userId: user.id,
accountName: name,
};
const result = await evaluator.canCreateAccount(
policyContext,
'submission',
);
if (!result.allowed) {
logger.warn(
{ ...ctx, reasons: result.reasons },
`Policy denied team account creation`,
);
return {
error: true,
message: result.reasons[0] ?? 'Policy denied account creation',
};
}
}
// Service throws on error, so no need to check for error
const { data } = await service.createNewOrganizationAccount({
name,
userId: user.id,
});
if (error) {
logger.error({ ...ctx, error }, `Failed to create team account`);
return {
error: true,
};
}
logger.info(ctx, `Team account created`);
const accountHomePath = '/home/' + data.slug;

View File

@@ -0,0 +1,10 @@
import 'server-only';
import { createPolicyRegistry } from '@kit/policies';
/**
* Feature-specific registry for create account policies.
*/
export const createAccountPolicyRegistry = createPolicyRegistry();
// Register policies here

View File

@@ -0,0 +1,50 @@
import 'server-only';
import type { EvaluationResult } from '@kit/policies';
import { createPoliciesEvaluator } from '@kit/policies';
import { createAccountPolicyRegistry } from './create-account-policies';
import type { FeaturePolicyCreateAccountContext } from './feature-policy-create-account-context';
export interface CreateAccountPolicyEvaluator {
hasPoliciesForStage(stage: 'preliminary' | 'submission'): Promise<boolean>;
canCreateAccount(
context: FeaturePolicyCreateAccountContext,
stage: 'preliminary' | 'submission',
): Promise<EvaluationResult>;
}
/**
* Creates a create account policy evaluator
*/
export function createAccountCreationPolicyEvaluator(): CreateAccountPolicyEvaluator {
const evaluator =
createPoliciesEvaluator<FeaturePolicyCreateAccountContext>();
return {
/**
* Checks if there are any create account policies for the given stage
* @param stage - The stage to check if there are any policies for
*/
async hasPoliciesForStage(stage: 'preliminary' | 'submission') {
return evaluator.hasPoliciesForStage(createAccountPolicyRegistry, stage);
},
/**
* Evaluates the create account policies for the given context and stage
* @param context - The context for the create account policy
* @param stage - The stage to evaluate the policies for
*/
async canCreateAccount(
context: FeaturePolicyCreateAccountContext,
stage: 'preliminary' | 'submission',
) {
return evaluator.evaluate(
createAccountPolicyRegistry,
context,
'ALL',
stage,
);
},
};
}

View File

@@ -0,0 +1,13 @@
import type { PolicyContext } from '@kit/policies';
/**
* Minimal context for create account policies.
* Policies can fetch additional data internally using getSupabaseServerClient().
*/
export interface FeaturePolicyCreateAccountContext extends PolicyContext {
/** The ID of the user creating the account */
userId: string;
/** The name of the account being created (empty string for preliminary checks) */
accountName: string;
}

View File

@@ -1,7 +1,10 @@
// Invitation policies
export { createInvitationsPolicyEvaluator } from './invitation-policies';
// Context building
export { createInvitationContextBuilder } from './invitation-context-builder';
// Type exports
export type { FeaturePolicyInvitationContext } from './feature-policy-invitation-context';
// Create account policies
export { createAccountCreationPolicyEvaluator } from './create-account-policy-evaluator';
export type { CreateAccountPolicyEvaluator } from './create-account-policy-evaluator';
export { createAccountPolicyRegistry } from './create-account-policies';
export type { FeaturePolicyCreateAccountContext } from './feature-policy-create-account-context';