diff --git a/apps/web/app/home/(user)/_components/home-accounts-list.tsx b/apps/web/app/home/(user)/_components/home-accounts-list.tsx
index 3d6c36302..7c8e30dbb 100644
--- a/apps/web/app/home/(user)/_components/home-accounts-list.tsx
+++ b/apps/web/app/home/(user)/_components/home-accounts-list.tsx
@@ -19,10 +19,12 @@ import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
import { HomeAddAccountButton } from './home-add-account-button';
export function HomeAccountsList() {
- const { accounts } = use(loadUserWorkspace());
+ const { accounts, canCreateTeamAccount } = use(loadUserWorkspace());
if (!accounts.length) {
- return ;
+ return (
+
+ );
}
return (
@@ -42,12 +44,17 @@ export function HomeAccountsList() {
);
}
-function HomeAccountsListEmptyState() {
+function HomeAccountsListEmptyState(props: {
+ canCreateTeamAccount: { allowed: boolean; reason?: string };
+}) {
return (
-
+
diff --git a/apps/web/app/home/(user)/_components/home-add-account-button.tsx b/apps/web/app/home/(user)/_components/home-add-account-button.tsx
index 4a2cabba8..5d94cf5be 100644
--- a/apps/web/app/home/(user)/_components/home-add-account-button.tsx
+++ b/apps/web/app/home/(user)/_components/home-add-account-button.tsx
@@ -4,19 +4,54 @@ import { useState } from 'react';
import { CreateTeamAccountDialog } from '@kit/team-accounts/components';
import { Button } from '@kit/ui/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@kit/ui/tooltip';
import { Trans } from '@kit/ui/trans';
-export function HomeAddAccountButton(props: { className?: string }) {
+interface HomeAddAccountButtonProps {
+ className?: string;
+ canCreateTeamAccount?: {
+ allowed: boolean;
+ reason?: string;
+ };
+}
+
+export function HomeAddAccountButton(props: HomeAddAccountButtonProps) {
const [isAddingAccount, setIsAddingAccount] = useState(false);
+ const canCreate = props.canCreateTeamAccount?.allowed ?? true;
+ const reason = props.canCreateTeamAccount?.reason;
+
+ const button = (
+
+ );
+
return (
<>
-
+ {!canCreate && reason ? (
+
+
+
+ {button}
+
+
+
+
+
+
+ ) : (
+ button
+ )}
void }) {
- const [error, setError] = useState();
+ 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 }) {
>
-
+
void }) {
);
}
-function CreateOrganizationErrorAlert() {
+function CreateOrganizationErrorAlert(props: { message?: string }) {
return (
@@ -158,7 +158,11 @@ function CreateOrganizationErrorAlert() {
-
+ {props.message ? (
+
+ ) : (
+
+ )}
);
diff --git a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts
index a741c2049..66257d220 100644
--- a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts
+++ b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts
@@ -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;
diff --git a/packages/features/team-accounts/src/server/policies/create-account-policies.ts b/packages/features/team-accounts/src/server/policies/create-account-policies.ts
new file mode 100644
index 000000000..afac1f740
--- /dev/null
+++ b/packages/features/team-accounts/src/server/policies/create-account-policies.ts
@@ -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
diff --git a/packages/features/team-accounts/src/server/policies/create-account-policy-evaluator.ts b/packages/features/team-accounts/src/server/policies/create-account-policy-evaluator.ts
new file mode 100644
index 000000000..6cbb71c05
--- /dev/null
+++ b/packages/features/team-accounts/src/server/policies/create-account-policy-evaluator.ts
@@ -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;
+ canCreateAccount(
+ context: FeaturePolicyCreateAccountContext,
+ stage: 'preliminary' | 'submission',
+ ): Promise;
+}
+
+/**
+ * Creates a create account policy evaluator
+ */
+export function createAccountCreationPolicyEvaluator(): CreateAccountPolicyEvaluator {
+ const evaluator =
+ createPoliciesEvaluator();
+
+ 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,
+ );
+ },
+ };
+}
diff --git a/packages/features/team-accounts/src/server/policies/feature-policy-create-account-context.ts b/packages/features/team-accounts/src/server/policies/feature-policy-create-account-context.ts
new file mode 100644
index 000000000..9485f65b1
--- /dev/null
+++ b/packages/features/team-accounts/src/server/policies/feature-policy-create-account-context.ts
@@ -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;
+}
diff --git a/packages/features/team-accounts/src/server/policies/index.ts b/packages/features/team-accounts/src/server/policies/index.ts
index 9906edd22..3fdd053f2 100644
--- a/packages/features/team-accounts/src/server/policies/index.ts
+++ b/packages/features/team-accounts/src/server/policies/index.ts
@@ -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';
diff --git a/packages/next/package.json b/packages/next/package.json
index a1e90216b..68070f01c 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -21,6 +21,7 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "catalog:",
+ "@types/node": "catalog:",
"next": "catalog:",
"zod": "catalog:"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e09a71f9f..072c7f742 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -906,6 +906,9 @@ importers:
'@tanstack/react-query':
specifier: 'catalog:'
version: 5.90.12(react@19.2.3)
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.0.3
'@types/react':
specifier: 'catalog:'
version: 19.2.7
@@ -1313,6 +1316,9 @@ importers:
'@supabase/supabase-js':
specifier: 'catalog:'
version: 2.89.0
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.0.3
next:
specifier: 'catalog:'
version: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -14442,7 +14448,7 @@ snapshots:
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -14481,7 +14487,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -14496,7 +14502,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9