Feature Policies API + Invitations Policies (#375)

- Added Feature Policy API: a declarative system to enable/disable/modify default behavior in the SaaS kit
- Team invitation policies with pre-checks using the Feature Policy API: Invite Members dialog now shows loading, errors, and clear reasons when invitations are blocked
- Version bump to 2.16.0 and widespread dependency updates (Supabase, React types, react-i18next, etc.).
- Added comprehensive docs for the new policy system and orchestrators.
- Subscription cancellations now trigger immediate invoicing explicitly
This commit is contained in:
Giancarlo Buomprisco
2025-09-30 12:36:19 +08:00
committed by GitHub
parent 3c13b5ec1e
commit 1dd6fdad22
53 changed files with 3908 additions and 1128 deletions

View File

@@ -8,11 +8,11 @@
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
},
"dependencies": {
"@ai-sdk/openai": "^2.0.34",
"@ai-sdk/openai": "^2.0.38",
"@faker-js/faker": "^10.0.0",
"@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "5.90.2",
"ai": "5.0.51",
"ai": "5.0.56",
"lucide-react": "^0.544.0",
"next": "15.5.4",
"nodemailer": "^7.0.6",
@@ -32,7 +32,7 @@
"@tailwindcss/postcss": "^4.1.13",
"@types/node": "^24.5.2",
"@types/nodemailer": "7.0.1",
"@types/react": "19.1.13",
"@types/react": "19.1.15",
"@types/react-dom": "19.1.9",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"pino-pretty": "13.0.0",

View File

@@ -12,7 +12,7 @@
"author": "Makerkit",
"devDependencies": {
"@playwright/test": "^1.55.1",
"@supabase/supabase-js": "2.57.4",
"@supabase/supabase-js": "2.58.0",
"@types/node": "^24.5.2",
"dotenv": "17.2.2",
"node-html-parser": "^7.0.1",

View File

@@ -115,18 +115,17 @@ export class InvitationsPageObject {
async acceptInvitation() {
console.log('Accepting invitation...');
const click = this.page
.locator('[data-test="join-team-form"] button[type="submit"]')
.click();
const response = this.page.waitForResponse((response) => {
return (
response.url().includes('/join') &&
response.request().method() === 'POST'
);
});
await Promise.all([click, response]);
await Promise.all([
this.page
.locator('[data-test="join-team-form"] button[type="submit"]')
.click(),
this.page.waitForResponse((response) => {
return (
response.url().includes('/join') &&
response.request().method() === 'POST'
);
}),
]);
console.log('Invitation accepted');
}

View File

@@ -13,7 +13,7 @@ test.describe('Team Invitation with MFA Flow', () => {
const invitations = new InvitationsPageObject(page);
await auth.loginAsUser({
email: 'test@makerkit.dev',
email: 'owner@makerkit.dev',
});
const teamName = `test-team-${Math.random().toString(36).substring(2, 15)}`;

View File

@@ -0,0 +1,69 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { enhanceRouteHandler } from '@kit/next/routes';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import {
createInvitationContextBuilder,
createInvitationsPolicyEvaluator,
} from '@kit/team-accounts/policies';
export const GET = enhanceRouteHandler(
async function ({ params, user }) {
const client = getSupabaseServerClient();
const { account } = z.object({ account: z.string() }).parse(params);
try {
// Evaluate with standard evaluator
const evaluator = createInvitationsPolicyEvaluator();
const hasPolicies = await evaluator.hasPoliciesForStage('preliminary');
if (!hasPolicies) {
return NextResponse.json({
allowed: true,
reasons: [],
metadata: {
policiesEvaluated: 0,
timestamp: new Date().toISOString(),
noPoliciesConfigured: true,
},
});
}
// Build context for policy evaluation (empty invitations for testing)
const contextBuilder = createInvitationContextBuilder(client);
const context = await contextBuilder.buildContext(
{
invitations: [],
accountSlug: account,
},
user,
);
// validate against policies
const result = await evaluator.canInvite(context, 'preliminary');
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{
allowed: false,
reasons: [
error instanceof Error ? error.message : 'Unknown error occurred',
],
metadata: {
error: true,
originalError:
error instanceof Error ? error.message : String(error),
},
},
{ status: 500 },
);
}
},
{
auth: true,
},
);

View File

@@ -56,7 +56,7 @@
"@marsidev/react-turnstile": "^1.3.1",
"@nosecone/next": "1.0.0-beta.12",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.57.4",
"@supabase/supabase-js": "2.58.0",
"@tanstack/react-query": "5.90.2",
"@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
@@ -67,7 +67,7 @@
"react": "19.1.1",
"react-dom": "19.1.1",
"react-hook-form": "^7.63.0",
"react-i18next": "^15.7.3",
"react-i18next": "^16.0.0",
"recharts": "2.15.3",
"tailwind-merge": "^3.3.1",
"zod": "^3.25.74"
@@ -79,13 +79,13 @@
"@next/bundle-analyzer": "15.5.4",
"@tailwindcss/postcss": "^4.1.13",
"@types/node": "^24.5.2",
"@types/react": "19.1.13",
"@types/react": "19.1.15",
"@types/react-dom": "19.1.9",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"cssnano": "^7.1.1",
"pino-pretty": "13.0.0",
"prettier": "^3.6.2",
"supabase": "2.45.5",
"supabase": "2.47.2",
"tailwindcss": "4.1.13",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.2"

View File

@@ -159,5 +159,17 @@
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.",
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
"reservedNameError": "This name is reserved. Please choose a different one.",
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
"checkingPolicies": "Loading. Please wait...",
"policyCheckError": "We are unable to verify invitations restrictions. Please try again.",
"invitationsBlockedMultiple": "Invitations are currently not allowed for the following reasons:",
"invitationsBlockedDefault": "Invitations are currently not allowed due to policy restrictions.",
"policyErrors": {
"subscriptionRequired": "An active subscription is required to invite team members.",
"paddleTrialRestriction": "Cannot invite members during trial period with per-seat billing on Paddle."
},
"policyRemediation": {
"subscriptionRequired": "Please upgrade your plan or activate your subscription",
"paddleTrialRestriction": "Wait until trial period ends or upgrade to full plan"
}
}