From 22f78b9a86b94532cc7016453e299c7a5770e02e Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Mon, 3 Mar 2025 11:38:32 +0700 Subject: [PATCH] Cursor rules v2 (#200) * Add new Cursor rules based on new format --- .cursor/rules/accounts-context.mdc | 77 ++++ .cursor/rules/data-fetching.mdc | 91 +++++ .cursor/rules/database.mdc | 304 ++++++++++++++++ .cursor/rules/forms.mdc | 145 ++++++++ .cursor/rules/page-creation.mdc | 322 +++++++++++++++++ .cursor/rules/permissions.mdc | 69 ++++ .cursor/rules/project-structure.mdc | 238 +++++++++++++ .cursor/rules/route-handlers.mdc | 51 +++ .cursor/rules/server-actions.mdc | 61 ++++ .cursor/rules/super-admin.mdc | 208 +++++++++++ .cursor/rules/team-account-context.mdc | 80 +++++ .cursor/rules/ui.mdc | 160 +++++++++ .cursorrules | 463 ------------------------- 13 files changed, 1806 insertions(+), 463 deletions(-) create mode 100644 .cursor/rules/accounts-context.mdc create mode 100644 .cursor/rules/data-fetching.mdc create mode 100644 .cursor/rules/database.mdc create mode 100644 .cursor/rules/forms.mdc create mode 100644 .cursor/rules/page-creation.mdc create mode 100644 .cursor/rules/permissions.mdc create mode 100644 .cursor/rules/project-structure.mdc create mode 100644 .cursor/rules/route-handlers.mdc create mode 100644 .cursor/rules/server-actions.mdc create mode 100644 .cursor/rules/super-admin.mdc create mode 100644 .cursor/rules/team-account-context.mdc create mode 100644 .cursor/rules/ui.mdc delete mode 100644 .cursorrules diff --git a/.cursor/rules/accounts-context.mdc b/.cursor/rules/accounts-context.mdc new file mode 100644 index 000000000..ce632be4a --- /dev/null +++ b/.cursor/rules/accounts-context.mdc @@ -0,0 +1,77 @@ +--- +description: Personal Accounts context and functionality +globs: apps/*/app/home/(user),packages/features/accounts/** +alwaysApply: false +--- + +# Personal Account Context + +This rule provides guidance for working with personal account related components in the application. + +The user/personal account context in the application lives under the path `app/home/(user)`. Under this context, we identify the user using Supabase Auth. + +We can use the `requireUserInServerComponent` to retrieve the relative Supabase User object and identify the user. [require-user-in-server-component.ts](mdc:apps/web/lib/server/require-user-in-server-component.ts) + +### Client Components + +In a Client Component, we can access the `UserWorkspaceContext` and use the `user` object to identify the user. + +We can use it like this: + +```tsx +import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace'; +``` + +This utility only works in paths under `apps/*/app/home/(user)`. + +## Guidelines + +### Components and Structure +- Personal account components are used in the `/home/(user)` route +- Reusable components should be in `packages/features/accounts/src/components` +- Settings-related components should be in `packages/features/accounts/src/components/personal-account-settings` + +### State Management +- Use the `UserWorkspaceContext` to access user workspace data +- Personal account data can be fetched using `usePersonalAccountData` hook +- Mutations should use React Query's `useMutation` hooks + +### Authentication Flow +- User authentication status is available via `useUser` hook +- Account deletion requires OTP verification +- Password updates may require reauthentication + +### Feature Flags +- Personal account features are controlled via `featureFlagsConfig` [feature-flags.config.ts](mdc:apps/web/config/feature-flags.config.ts) +- Key flags: + - `enableAccountDeletion` + - `enablePasswordUpdate` + - `enablePersonalAccountBilling` + - `enableNotifications` + +## Personal Account API + +The API for the personal account is [api.ts](mdc:packages/features/accounts/src/server/api.ts) + +A class that provides methods for interacting with account-related data in the database. Initializes a new instance of the `AccountsApi` class with a Supabase client. + +### AccountsApi +```typescript +constructor(client: SupabaseClient) +``` + +### Methods +- `getAccount(id: string)` - Get account by ID +- `getAccountWorkspace()` - Get current user's account workspace +- `loadUserAccounts()` - Get all accounts for current user +- `getSubscription(accountId: string)` - Get account subscription +- `getOrder(accountId: string)` - Get account order +- `getCustomerId(accountId: string)` - Get account customer ID + +## Database + +When applying Database rules [database.mdc](mdc:.cursor/rules/database.mdc) must ensure the authenticated user matches the account ID of the entity + +```sql +account_id = (select auth.uid()) +``` diff --git a/.cursor/rules/data-fetching.mdc b/.cursor/rules/data-fetching.mdc new file mode 100644 index 000000000..39ef72bf3 --- /dev/null +++ b/.cursor/rules/data-fetching.mdc @@ -0,0 +1,91 @@ +--- +description: Fetch data from the Database using the Supabase Clients +globs: apps/**,packages/** +alwaysApply: false +--- + +# Data Fetching + +## General Data Flow +- In a Server Component context, please use the Supabase Client directly for data fetching +- In a Client Component context, please use the `useQuery` hook from the "@tanstack/react-query" package + +Data Flow works in the following way: + +1. Server Component uses the Supabase Client to fetch data. +2. Data is rendered in Server Components or passed down to Client Components when absolutely necessary to use a client component (e.g. when using React Hooks or any interaction with the DOM). + +```tsx +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +async function ServerComponent() { + const client = getSupabaseServerClient(); + const { data, error } = await client.from('notes').select('*'); + + // use data +} +``` + +or pass down the data to a Client Component: + +```tsx +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +export default function ServerComponent() { + const supabase = getSupabaseServerClient(); + const { data, error } = await supabase.from('notes').select('*'); + + if (error) { + return ; + } + + return ; +} +``` + +## Supabase Clients +- In a Server Component context, use the `getSupabaseServerClient` function from the "@kit/supabase/server-client" package [server-client.ts](mdc:packages/supabase/src/clients/server-client.ts) +- In a Client Component context, use the `useSupabase` hook from the "@kit/supabase/hooks/use-supabase" package. + +### Admin Actions + +Only in rare cases suggest using the Admin client `getSupabaseServerAdminClient` when needing to bypass RLS from the package `@kit/supabase/server-admin-client` [server-admin-client.ts](mdc:packages/supabase/src/clients/server-admin-client.ts) + +## React Query + +When using `useQuery`, make sure to define the data fetching hook. Create two components: one that fetches the data and one that displays the data. For example a good usage is [roles-data-provider.tsx](mdc:packages/features/team-accounts/src/components/members/roles-data-provider.tsx) as shown in [update-member-role-dialog.tsx](mdc:packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx) + +## Error Handling + +- Logging using the `@kit/shared/logger` package [logger.ts](mdc:packages/shared/src/logger/logger.ts) +- Don't swallow errors, always handle them appropriately +- Handle promises and async/await gracefully +- Consider the unhappy path and handle errors appropriately +- Context without sensitive data + +```tsx +'use server'; + +import { getLogger } from '@kit/shared/logger'; + +export async function myServerAction() { + const logger = await getLogger(); + + logger.info('Request started...'); + + try { + // your code here + await someAsyncFunction(); + + logger.info('Request succeeded...'); + } catch (error) { + logger.error('Request failed...'); + + // handle error + } + + return { + success: true, + }; +} +``` \ No newline at end of file diff --git a/.cursor/rules/database.mdc b/.cursor/rules/database.mdc new file mode 100644 index 000000000..f225685ee --- /dev/null +++ b/.cursor/rules/database.mdc @@ -0,0 +1,304 @@ +--- +description: Detailed Database Schema and Architecture +globs: +alwaysApply: true +--- + +# Database Rules + +## Database Architecture +- Supabase uses Postgres +- We strive to create a safe, robust, performant schema +- Accounts are the general concept of a user account, defined by the having the same ID as Supabase Auth's users (personal). They can be a team account or a personal account. +- Generally speaking, other tables will be used to store data related to the account. For example, a table `notes` would have a foreign key `account_id` to link it to an account. + +## Migrations +- Migration files are placed at `apps//supabase/migrations` +- The main migration schema can be found at [20221215192558_schema.sql](mdc:apps/web/supabase/migrations/20221215192558_schema.sql) +- Use the command `pnpm --filter web supabase migrations new ` for creating well timestamped migrations + +## Security & RLS +- Using RLS, we must ensure that only the account owner can access the data. Always write safe RLS policies and ensure that the policies are enforced. +- Unless specified, always enable RLS when creating a table. Propose the required RLS policies ensuring the safety of the data. +- Always consider any required constraints and triggers are in place for data consistency +- Always consider the compromises you need to make and explain them so I can make an educated decision. Follow up with the considerations make and explain them. +- Always consider the security of the data and explain the security implications of the data. +- Always use Postgres schemas explicitly (e.g., `public.accounts`) +- Consider the required compromises between simplicity, functionality and developer experience. However, never compromise on security, which is paramount and fundamental. + +## Schema Overview + +Makerkit uses a Supabase Postgres database with a well-defined schema focused on multi-tenancy through the concepts of accounts (both personal and team) and robust permission systems. + +### Core Entity Relationships + +1. **User ↔ Account**: + - Each user has a personal account (1:1) + - Users can belong to multiple team accounts (M:N through `accounts_memberships`) + - Accounts can have multiple users (M:N through `accounts_memberships`) + +2. **Account ↔ Role**: + - Each user has a role in each account they belong to + - Roles define permissions through `role_permissions` + +3. **Subscription System**: + - Accounts can have subscriptions + - Subscriptions have multiple subscription items + - Billing providers include Stripe, Lemon Squeezy, and Paddle + +4. **Invitation System**: + - Team accounts can invite users via email + - Invitations specify roles for the invited user + +5. **One-Time Tokens**: + - Used for secure verification processes + - Generic system that can be used for various purposes + +## Table Relationships + +``` +auth.users +├── public.accounts (personal_account=true, id=user_id) +└── public.accounts_memberships + └── public.accounts (personal_account=false) + └── public.roles (hierarchy_level) + └── public.role_permissions + └── app_permissions (enum) +``` + +``` +public.accounts +├── public.billing_customers +│ └── public.subscriptions +│ └── public.subscription_items +└── public.invitations +``` + +``` +public.nonces +└── auth.users (optional relationship) +``` + +## Schema Overview + +Makerkit implements a multi-tenant SaaS architecture through a robust account and permission system: + +1. **Core Entities**: + - `auth.users`: Supabase Auth users + - `public.accounts`: Both personal and team accounts + - `public.accounts_memberships`: Links users to accounts with roles + - `public.roles` and `public.role_permissions`: Define permission hierarchy + - `public.invitations`: For inviting users to team accounts + +2. **Billing System**: + - `public.billing_customers`: Account's connection to billing providers + - `public.subscriptions` and `public.subscription_items`: For subscription tracking + - `public.orders` and `public.order_items`: For one-time purchases + +3. **Security**: + - `public.nonces`: One-time tokens for secure operations + +## Database Best Practices + +### Security + +- **Always enable RLS** on new tables unless explicitly instructed otherwise +- **Create proper RLS policies** for all CRUD operations following existing patterns +- **Always associate data with accounts** using a foreign key to ensure proper access control +- **Use explicit schema references** (`public.table_name` not just `table_name`) +- **Place internal functions in the `kit` schema** + +### Data Access Patterns + +- Use `has_role_on_account(account_id, role?)` to check membership +- Use `has_permission(user_id, account_id, permission)` for permission checks +- Use `is_account_owner(account_id)` to identify account ownership + +### SQL Coding Style + +- Use explicit transactions for multi-step operations +- Follow existing naming conventions: + - Tables: snake_case, plural nouns (`accounts`, `subscriptions`) + - Functions: snake_case, verb phrases (`create_team_account`, `verify_nonce`) + - Triggers: descriptive action names (`set_slug_from_account_name`) +- Document functions and complex SQL with comments +- Use parameterized queries to prevent SQL injection + +### Common Patterns + +- **Account Lookup**: Typically by `id` (UUID) or `slug` (for team accounts) +- **Permission Check**: Always verify proper permissions before mutations +- **Timestamp Automation**: Use the `trigger_set_timestamps()` function +- **User Tracking**: Use the `trigger_set_user_tracking()` function +- **Configuration**: Use `is_set(field_name)` to check enabled features + +## Best Practices for Database Code + +### 1. RLS Policy Management + +- **Always Enable RLS**: Always enable RLS for your tables unless you have a specific reason not to. + ```sql + ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY; + ``` + +- **Follow the Standard Policies Pattern**: Use the existing structure for policies: + ```sql + -- SELECT policy + CREATE POLICY "my_table_read" ON public.my_table FOR SELECT + TO authenticated USING ( + account_id = (select auth.uid()) OR + public.has_role_on_account(account_id) + ); + + -- INSERT/UPDATE/DELETE policies follow similar patterns + ``` + +### 2. Account Association + +- **Associate Data with Accounts**: Always link data to accounts using a foreign key: + ```sql + CREATE TABLE public.my_data ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID REFERENCES public.accounts(id) ON DELETE CASCADE NOT NULL, + /* other fields */ + ); + ``` + +### 3. Permission System + +- **Use the Permission System**: Leverage the built-in permission system for access control: + ```sql + -- Check if a user has a specific permission + SELECT public.has_permission( + auth.uid(), + account_id, + 'my.permission'::public.app_permissions + ); + ``` + +### 4. Schema Organization + +- **Use Schemas Explicitly**: Always use schema prefixes explicitly: + ```sql + -- Good + SELECT * FROM public.accounts; + + -- Avoid + SELECT * FROM accounts; + ``` + +- **Put Internal Functions in 'kit' Schema**: Use the 'kit' schema for internal helper functions + ```sql + CREATE OR REPLACE FUNCTION kit.my_helper_function() + RETURNS void AS $$ + -- function body + $$ LANGUAGE plpgsql; + ``` + +### 5. Types and Constraints + +- **Use Enums for Constrained Values**: Create and use enum types for values with a fixed set: + ```sql + CREATE TYPE public.my_status AS ENUM('active', 'inactive', 'pending'); + + CREATE TABLE public.my_table ( + status public.my_status NOT NULL DEFAULT 'pending' + ); + ``` + +- **Apply Appropriate Constraints**: Use constraints to ensure data integrity: + ```sql + CREATE TABLE public.my_table ( + email VARCHAR(255) NOT NULL CHECK (email ~* '^.+@.+\..+$'), + count INTEGER NOT NULL CHECK (count >= 0), + /* other fields */ + ); + ``` + +### 6. Authentication and User Management + +- **Use Supabase Auth**: Leverage auth.users for identity management +- **Handle User Creation**: Use triggers like `kit.setup_new_user` to set up user data after registration + +### 7. Function Security + +- **Apply Security Definer Carefully**: For functions that need elevated privileges: + ```sql + CREATE OR REPLACE FUNCTION public.my_function() + RETURNS void + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = '' AS $$ + -- function body + $$; + ``` + +- **Set Proper Function Permissions**: + ```sql + GRANT EXECUTE ON FUNCTION public.my_function() TO authenticated, service_role; + ``` + +### 8. Error Handling and Validation + +- **Use Custom Error Messages**: Return meaningful errors: + ```sql + IF NOT validation_passed THEN + RAISE EXCEPTION 'Validation failed: %', error_message; + END IF; + ``` + +### 9. Triggers for Automation + +- **Use Triggers for Derived Data**: Automate updates to derived fields: + ```sql + CREATE TRIGGER update_timestamp + BEFORE UPDATE ON public.my_table + FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamps(); + ``` + +### 10. View Structure for Commonly Used Queries + +- **Create Views for Complex Joins**: As done with `user_account_workspace` + ```sql + CREATE OR REPLACE VIEW public.my_view + WITH (security_invoker = true) AS + SELECT ... + ``` + +## Key Functions to Know + +1. **Account Access** + - `public.has_role_on_account(account_id, account_role)` + - `public.is_account_owner(account_id)` + - `public.is_team_member(account_id, user_id)` + +2. **Permissions** + - `public.has_permission(user_id, account_id, permission_name)` + - `public.has_more_elevated_role(target_user_id, target_account_id, role_name)` + +3. **Team Management** + - `public.create_team_account(account_name)` + +4. **Billing & Subscriptions** + - `public.has_active_subscription(target_account_id)` + +5. **One-Time Tokens** + - `public.create_nonce(...)` + - `public.verify_nonce(...)` + - `public.revoke_nonce(...)` + +6. **Super Admins** + - `public.is_super_admin()` + +7. **MFA**: + - `public.is_aal2()` + - `public.is_mfa_compliant()` + +## Configuration Control + +- **Use the `config` Table**: The application has a central configuration table +- **Check Features with `public.is_set(field_name)`**: + ```sql + -- Check if team accounts are enabled + SELECT public.is_set('enable_team_accounts'); + ``` \ No newline at end of file diff --git a/.cursor/rules/forms.mdc b/.cursor/rules/forms.mdc new file mode 100644 index 000000000..0af6a8d16 --- /dev/null +++ b/.cursor/rules/forms.mdc @@ -0,0 +1,145 @@ +--- +description: Writing Forms with Shadcn UI, Server Actions, Zod +globs: apps/**/*.tsx,packages/**/*.tsx +alwaysApply: false +--- + +# Forms + +- Use React Hook Form for form validation and submission. +- Use Zod for form validation. +- Use the `zodResolver` function to resolve the Zod schema to the form. + +Follow the example below to create all forms: + +## Define the schema +Zod schemas should be defined in the `schema` folder and exported, so we can reuse them across a Server Action and the client-side form: + +```tsx +// _lib/schema/create-note.schema.ts +import { z } from 'zod'; + +export const CreateNoteSchema = z.object({ + title: z.string().min(1), + content: z.string().min(1), +}); +``` + +## Create the Server Action + +```tsx +// _lib/server/server-actions.ts +'use server'; + +import { z } from 'zod'; +import { enhanceAction } from '@kit/next/actions'; +import { CreateNoteSchema } from '../schema/create-note.schema'; + +const CreateNoteSchema = z.object({ + title: z.string().min(1), + content: z.string().min(1), +}); + +export const createNoteAction = enhanceAction( + async function (data, user) { + // 1. "data" has been validated against the Zod schema, and it's safe to use + // 2. "user" is the authenticated user + + // ... your code here + return { + success: true, + }; + }, + { + auth: true, + schema: CreateNoteSchema, + }, +); +``` + +## Create the Form Component + +Then create a client component to handle the form submission: + +```tsx +// _components/create-note-form.tsx +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form'; + +import { CreateNoteSchema } from '../_lib/schema/create-note.schema'; + +export function CreateNoteForm() { + const [pending, startTransition] = useTransition(); + + const form = useForm({ + resolver: zodResolver(CreateNoteSchema), + defaultValues: { + title: '', + content: '', + }, + }); + + const onSubmit = (data) => { + startTransition(async () => { + try { + await createNoteAction(data); + } catch { + // handle error + } + }); + }; + + return ( +
+ + ( + + + Title + + + + + + + + + )} /> + + ( + + + Content + + + +