ultra-dex 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -74
- package/assets/code-patterns/clerk-middleware.ts +138 -0
- package/assets/code-patterns/prisma-schema.prisma +224 -0
- package/assets/code-patterns/rls-policies.sql +246 -0
- package/assets/code-patterns/server-actions.ts +191 -0
- package/assets/code-patterns/trpc-router.ts +258 -0
- package/assets/cursor-rules/13-ai-integration.mdc +155 -0
- package/assets/cursor-rules/14-server-components.mdc +81 -0
- package/assets/cursor-rules/15-server-actions.mdc +102 -0
- package/assets/cursor-rules/16-edge-middleware.mdc +105 -0
- package/assets/cursor-rules/17-streaming-ssr.mdc +138 -0
- package/bin/ultra-dex.js +38 -1
- package/lib/commands/agents.js +16 -13
- package/lib/commands/banner.js +43 -21
- package/lib/commands/build.js +26 -17
- package/lib/commands/doctor.js +98 -79
- package/lib/commands/generate.js +19 -16
- package/lib/commands/init.js +52 -56
- package/lib/commands/scaffold.js +151 -0
- package/lib/commands/serve.js +15 -13
- package/lib/commands/state.js +43 -70
- package/lib/commands/swarm.js +31 -9
- package/lib/config/theme.js +47 -0
- package/lib/templates/code/clerk-middleware.ts +138 -0
- package/lib/templates/code/prisma-schema.prisma +224 -0
- package/lib/templates/code/rls-policies.sql +246 -0
- package/lib/templates/code/server-actions.ts +191 -0
- package/lib/templates/code/trpc-router.ts +258 -0
- package/lib/themes/doomsday.js +229 -0
- package/lib/ui/index.js +5 -0
- package/lib/ui/interface.js +241 -0
- package/lib/ui/spinners.js +116 -0
- package/lib/ui/theme.js +183 -0
- package/lib/utils/agents.js +32 -0
- package/lib/utils/help.js +64 -0
- package/lib/utils/messages.js +35 -0
- package/lib/utils/progress.js +24 -0
- package/lib/utils/prompts.js +47 -0
- package/lib/utils/spinners.js +46 -0
- package/lib/utils/status.js +31 -0
- package/lib/utils/tables.js +41 -0
- package/lib/utils/theme-state.js +9 -0
- package/lib/utils/version-display.js +32 -0
- package/package.json +10 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Ultra-Dex Production Pattern: Full Prisma Schema
|
|
2
|
+
// Copy to prisma/schema.prisma
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client-js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql"
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// CORE MODELS
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
model User {
|
|
18
|
+
id String @id @default(cuid())
|
|
19
|
+
clerkId String @unique
|
|
20
|
+
email String @unique
|
|
21
|
+
name String?
|
|
22
|
+
imageUrl String?
|
|
23
|
+
role UserRole @default(USER)
|
|
24
|
+
createdAt DateTime @default(now())
|
|
25
|
+
updatedAt DateTime @updatedAt
|
|
26
|
+
|
|
27
|
+
// Relations
|
|
28
|
+
organizationMemberships OrganizationMember[]
|
|
29
|
+
createdOrganizations Organization[] @relation("OrganizationCreator")
|
|
30
|
+
projects Project[]
|
|
31
|
+
comments Comment[]
|
|
32
|
+
activityLogs ActivityLog[]
|
|
33
|
+
|
|
34
|
+
@@index([clerkId])
|
|
35
|
+
@@index([email])
|
|
36
|
+
@@map("users")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enum UserRole {
|
|
40
|
+
USER
|
|
41
|
+
ADMIN
|
|
42
|
+
SUPER_ADMIN
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// MULTI-TENANCY MODELS
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
model Organization {
|
|
50
|
+
id String @id @default(cuid())
|
|
51
|
+
name String
|
|
52
|
+
slug String @unique
|
|
53
|
+
logo String?
|
|
54
|
+
plan Plan @default(FREE)
|
|
55
|
+
createdAt DateTime @default(now())
|
|
56
|
+
updatedAt DateTime @updatedAt
|
|
57
|
+
|
|
58
|
+
// Relations
|
|
59
|
+
creatorId String
|
|
60
|
+
creator User @relation("OrganizationCreator", fields: [creatorId], references: [id])
|
|
61
|
+
members OrganizationMember[]
|
|
62
|
+
projects Project[]
|
|
63
|
+
invoices Invoice[]
|
|
64
|
+
|
|
65
|
+
@@index([slug])
|
|
66
|
+
@@map("organizations")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
model OrganizationMember {
|
|
70
|
+
id String @id @default(cuid())
|
|
71
|
+
role MemberRole @default(MEMBER)
|
|
72
|
+
joinedAt DateTime @default(now())
|
|
73
|
+
organizationId String
|
|
74
|
+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
75
|
+
userId String
|
|
76
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
77
|
+
|
|
78
|
+
@@unique([organizationId, userId])
|
|
79
|
+
@@map("organization_members")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
enum MemberRole {
|
|
83
|
+
OWNER
|
|
84
|
+
ADMIN
|
|
85
|
+
MEMBER
|
|
86
|
+
VIEWER
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
enum Plan {
|
|
90
|
+
FREE
|
|
91
|
+
PRO
|
|
92
|
+
ENTERPRISE
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// PROJECT MODELS
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
model Project {
|
|
100
|
+
id String @id @default(cuid())
|
|
101
|
+
name String
|
|
102
|
+
description String?
|
|
103
|
+
status ProjectStatus @default(ACTIVE)
|
|
104
|
+
createdAt DateTime @default(now())
|
|
105
|
+
updatedAt DateTime @updatedAt
|
|
106
|
+
|
|
107
|
+
// Relations
|
|
108
|
+
organizationId String
|
|
109
|
+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
110
|
+
ownerId String
|
|
111
|
+
owner User @relation(fields: [ownerId], references: [id])
|
|
112
|
+
tasks Task[]
|
|
113
|
+
comments Comment[]
|
|
114
|
+
|
|
115
|
+
@@index([organizationId])
|
|
116
|
+
@@map("projects")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
enum ProjectStatus {
|
|
120
|
+
ACTIVE
|
|
121
|
+
ARCHIVED
|
|
122
|
+
COMPLETED
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
model Task {
|
|
126
|
+
id String @id @default(cuid())
|
|
127
|
+
title String
|
|
128
|
+
description String?
|
|
129
|
+
status TaskStatus @default(TODO)
|
|
130
|
+
priority Priority @default(MEDIUM)
|
|
131
|
+
dueDate DateTime?
|
|
132
|
+
createdAt DateTime @default(now())
|
|
133
|
+
updatedAt DateTime @updatedAt
|
|
134
|
+
|
|
135
|
+
// Relations
|
|
136
|
+
projectId String
|
|
137
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
138
|
+
comments Comment[]
|
|
139
|
+
|
|
140
|
+
@@index([projectId])
|
|
141
|
+
@@index([status])
|
|
142
|
+
@@map("tasks")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
enum TaskStatus {
|
|
146
|
+
TODO
|
|
147
|
+
IN_PROGRESS
|
|
148
|
+
IN_REVIEW
|
|
149
|
+
DONE
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
enum Priority {
|
|
153
|
+
LOW
|
|
154
|
+
MEDIUM
|
|
155
|
+
HIGH
|
|
156
|
+
URGENT
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
model Comment {
|
|
160
|
+
id String @id @default(cuid())
|
|
161
|
+
content String
|
|
162
|
+
createdAt DateTime @default(now())
|
|
163
|
+
updatedAt DateTime @updatedAt
|
|
164
|
+
|
|
165
|
+
// Relations
|
|
166
|
+
authorId String
|
|
167
|
+
author User @relation(fields: [authorId], references: [id])
|
|
168
|
+
projectId String?
|
|
169
|
+
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
170
|
+
taskId String?
|
|
171
|
+
task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
172
|
+
|
|
173
|
+
@@map("comments")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// BILLING MODELS
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
model Invoice {
|
|
181
|
+
id String @id @default(cuid())
|
|
182
|
+
stripeId String @unique
|
|
183
|
+
amount Int
|
|
184
|
+
currency String @default("usd")
|
|
185
|
+
status InvoiceStatus @default(PENDING)
|
|
186
|
+
paidAt DateTime?
|
|
187
|
+
createdAt DateTime @default(now())
|
|
188
|
+
|
|
189
|
+
// Relations
|
|
190
|
+
organizationId String
|
|
191
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
192
|
+
|
|
193
|
+
@@index([organizationId])
|
|
194
|
+
@@map("invoices")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
enum InvoiceStatus {
|
|
198
|
+
PENDING
|
|
199
|
+
PAID
|
|
200
|
+
FAILED
|
|
201
|
+
REFUNDED
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =============================================================================
|
|
205
|
+
// AUDIT LOG
|
|
206
|
+
// =============================================================================
|
|
207
|
+
|
|
208
|
+
model ActivityLog {
|
|
209
|
+
id String @id @default(cuid())
|
|
210
|
+
action String
|
|
211
|
+
entity String
|
|
212
|
+
entityId String
|
|
213
|
+
metadata Json?
|
|
214
|
+
createdAt DateTime @default(now())
|
|
215
|
+
|
|
216
|
+
// Relations
|
|
217
|
+
userId String
|
|
218
|
+
user User @relation(fields: [userId], references: [id])
|
|
219
|
+
|
|
220
|
+
@@index([userId])
|
|
221
|
+
@@index([entity, entityId])
|
|
222
|
+
@@index([createdAt])
|
|
223
|
+
@@map("activity_logs")
|
|
224
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
-- Ultra-Dex Production Pattern: Row-Level Security Policies
|
|
2
|
+
-- Run after Prisma migrations to add RLS
|
|
3
|
+
|
|
4
|
+
-- =============================================================================
|
|
5
|
+
-- ENABLE RLS ON TABLES
|
|
6
|
+
-- =============================================================================
|
|
7
|
+
|
|
8
|
+
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
|
9
|
+
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
|
|
10
|
+
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
|
|
11
|
+
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
12
|
+
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
|
13
|
+
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
|
|
14
|
+
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
|
|
15
|
+
ALTER TABLE activity_logs ENABLE ROW LEVEL SECURITY;
|
|
16
|
+
|
|
17
|
+
-- =============================================================================
|
|
18
|
+
-- HELPER FUNCTIONS
|
|
19
|
+
-- =============================================================================
|
|
20
|
+
|
|
21
|
+
-- Get current user's clerk ID from session
|
|
22
|
+
CREATE OR REPLACE FUNCTION auth.clerk_id()
|
|
23
|
+
RETURNS TEXT AS $$
|
|
24
|
+
SELECT current_setting('app.clerk_id', true)::TEXT;
|
|
25
|
+
$$ LANGUAGE SQL STABLE;
|
|
26
|
+
|
|
27
|
+
-- Get current user's internal ID
|
|
28
|
+
CREATE OR REPLACE FUNCTION auth.user_id()
|
|
29
|
+
RETURNS TEXT AS $$
|
|
30
|
+
SELECT id FROM users WHERE clerk_id = auth.clerk_id();
|
|
31
|
+
$$ LANGUAGE SQL STABLE;
|
|
32
|
+
|
|
33
|
+
-- Check if user is member of organization
|
|
34
|
+
CREATE OR REPLACE FUNCTION auth.is_org_member(org_id TEXT)
|
|
35
|
+
RETURNS BOOLEAN AS $$
|
|
36
|
+
SELECT EXISTS (
|
|
37
|
+
SELECT 1 FROM organization_members om
|
|
38
|
+
JOIN users u ON u.id = om.user_id
|
|
39
|
+
WHERE om.organization_id = org_id
|
|
40
|
+
AND u.clerk_id = auth.clerk_id()
|
|
41
|
+
);
|
|
42
|
+
$$ LANGUAGE SQL STABLE;
|
|
43
|
+
|
|
44
|
+
-- Check if user is admin of organization
|
|
45
|
+
CREATE OR REPLACE FUNCTION auth.is_org_admin(org_id TEXT)
|
|
46
|
+
RETURNS BOOLEAN AS $$
|
|
47
|
+
SELECT EXISTS (
|
|
48
|
+
SELECT 1 FROM organization_members om
|
|
49
|
+
JOIN users u ON u.id = om.user_id
|
|
50
|
+
WHERE om.organization_id = org_id
|
|
51
|
+
AND u.clerk_id = auth.clerk_id()
|
|
52
|
+
AND om.role IN ('OWNER', 'ADMIN')
|
|
53
|
+
);
|
|
54
|
+
$$ LANGUAGE SQL STABLE;
|
|
55
|
+
|
|
56
|
+
-- =============================================================================
|
|
57
|
+
-- USERS POLICIES
|
|
58
|
+
-- =============================================================================
|
|
59
|
+
|
|
60
|
+
-- Users can read their own data
|
|
61
|
+
CREATE POLICY "users_select_own" ON users
|
|
62
|
+
FOR SELECT
|
|
63
|
+
USING (clerk_id = auth.clerk_id());
|
|
64
|
+
|
|
65
|
+
-- Users can update their own data
|
|
66
|
+
CREATE POLICY "users_update_own" ON users
|
|
67
|
+
FOR UPDATE
|
|
68
|
+
USING (clerk_id = auth.clerk_id());
|
|
69
|
+
|
|
70
|
+
-- Admins can read all users
|
|
71
|
+
CREATE POLICY "users_select_admin" ON users
|
|
72
|
+
FOR SELECT
|
|
73
|
+
USING (
|
|
74
|
+
EXISTS (
|
|
75
|
+
SELECT 1 FROM users u
|
|
76
|
+
WHERE u.clerk_id = auth.clerk_id()
|
|
77
|
+
AND u.role IN ('ADMIN', 'SUPER_ADMIN')
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- =============================================================================
|
|
82
|
+
-- ORGANIZATIONS POLICIES
|
|
83
|
+
-- =============================================================================
|
|
84
|
+
|
|
85
|
+
-- Members can view their organizations
|
|
86
|
+
CREATE POLICY "organizations_select_members" ON organizations
|
|
87
|
+
FOR SELECT
|
|
88
|
+
USING (auth.is_org_member(id));
|
|
89
|
+
|
|
90
|
+
-- Only admins can update organizations
|
|
91
|
+
CREATE POLICY "organizations_update_admin" ON organizations
|
|
92
|
+
FOR UPDATE
|
|
93
|
+
USING (auth.is_org_admin(id));
|
|
94
|
+
|
|
95
|
+
-- Only owners can delete organizations
|
|
96
|
+
CREATE POLICY "organizations_delete_owner" ON organizations
|
|
97
|
+
FOR DELETE
|
|
98
|
+
USING (
|
|
99
|
+
EXISTS (
|
|
100
|
+
SELECT 1 FROM organization_members om
|
|
101
|
+
JOIN users u ON u.id = om.user_id
|
|
102
|
+
WHERE om.organization_id = organizations.id
|
|
103
|
+
AND u.clerk_id = auth.clerk_id()
|
|
104
|
+
AND om.role = 'OWNER'
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
-- =============================================================================
|
|
109
|
+
-- ORGANIZATION MEMBERS POLICIES
|
|
110
|
+
-- =============================================================================
|
|
111
|
+
|
|
112
|
+
-- Members can view org membership
|
|
113
|
+
CREATE POLICY "org_members_select" ON organization_members
|
|
114
|
+
FOR SELECT
|
|
115
|
+
USING (auth.is_org_member(organization_id));
|
|
116
|
+
|
|
117
|
+
-- Only admins can add/remove members
|
|
118
|
+
CREATE POLICY "org_members_insert" ON organization_members
|
|
119
|
+
FOR INSERT
|
|
120
|
+
WITH CHECK (auth.is_org_admin(organization_id));
|
|
121
|
+
|
|
122
|
+
CREATE POLICY "org_members_delete" ON organization_members
|
|
123
|
+
FOR DELETE
|
|
124
|
+
USING (auth.is_org_admin(organization_id));
|
|
125
|
+
|
|
126
|
+
-- =============================================================================
|
|
127
|
+
-- PROJECTS POLICIES
|
|
128
|
+
-- =============================================================================
|
|
129
|
+
|
|
130
|
+
-- Members can view projects
|
|
131
|
+
CREATE POLICY "projects_select" ON projects
|
|
132
|
+
FOR SELECT
|
|
133
|
+
USING (auth.is_org_member(organization_id));
|
|
134
|
+
|
|
135
|
+
-- Members (non-viewers) can create projects
|
|
136
|
+
CREATE POLICY "projects_insert" ON projects
|
|
137
|
+
FOR INSERT
|
|
138
|
+
WITH CHECK (
|
|
139
|
+
EXISTS (
|
|
140
|
+
SELECT 1 FROM organization_members om
|
|
141
|
+
JOIN users u ON u.id = om.user_id
|
|
142
|
+
WHERE om.organization_id = projects.organization_id
|
|
143
|
+
AND u.clerk_id = auth.clerk_id()
|
|
144
|
+
AND om.role IN ('OWNER', 'ADMIN', 'MEMBER')
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
-- Members can update projects they own, admins can update any
|
|
149
|
+
CREATE POLICY "projects_update" ON projects
|
|
150
|
+
FOR UPDATE
|
|
151
|
+
USING (
|
|
152
|
+
(owner_id = auth.user_id()) OR auth.is_org_admin(organization_id)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
-- Only admins can delete projects
|
|
156
|
+
CREATE POLICY "projects_delete" ON projects
|
|
157
|
+
FOR DELETE
|
|
158
|
+
USING (auth.is_org_admin(organization_id));
|
|
159
|
+
|
|
160
|
+
-- =============================================================================
|
|
161
|
+
-- TASKS POLICIES
|
|
162
|
+
-- =============================================================================
|
|
163
|
+
|
|
164
|
+
-- Inherit from project access
|
|
165
|
+
CREATE POLICY "tasks_select" ON tasks
|
|
166
|
+
FOR SELECT
|
|
167
|
+
USING (
|
|
168
|
+
EXISTS (
|
|
169
|
+
SELECT 1 FROM projects p
|
|
170
|
+
WHERE p.id = tasks.project_id
|
|
171
|
+
AND auth.is_org_member(p.organization_id)
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE POLICY "tasks_insert" ON tasks
|
|
176
|
+
FOR INSERT
|
|
177
|
+
WITH CHECK (
|
|
178
|
+
EXISTS (
|
|
179
|
+
SELECT 1 FROM projects p
|
|
180
|
+
JOIN organization_members om ON om.organization_id = p.organization_id
|
|
181
|
+
JOIN users u ON u.id = om.user_id
|
|
182
|
+
WHERE p.id = tasks.project_id
|
|
183
|
+
AND u.clerk_id = auth.clerk_id()
|
|
184
|
+
AND om.role IN ('OWNER', 'ADMIN', 'MEMBER')
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
CREATE POLICY "tasks_update" ON tasks
|
|
189
|
+
FOR UPDATE
|
|
190
|
+
USING (
|
|
191
|
+
EXISTS (
|
|
192
|
+
SELECT 1 FROM projects p
|
|
193
|
+
WHERE p.id = tasks.project_id
|
|
194
|
+
AND auth.is_org_member(p.organization_id)
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE POLICY "tasks_delete" ON tasks
|
|
199
|
+
FOR DELETE
|
|
200
|
+
USING (
|
|
201
|
+
EXISTS (
|
|
202
|
+
SELECT 1 FROM projects p
|
|
203
|
+
WHERE p.id = tasks.project_id
|
|
204
|
+
AND auth.is_org_admin(p.organization_id)
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
-- =============================================================================
|
|
209
|
+
-- INVOICES POLICIES
|
|
210
|
+
-- =============================================================================
|
|
211
|
+
|
|
212
|
+
-- Only org admins can view invoices
|
|
213
|
+
CREATE POLICY "invoices_select" ON invoices
|
|
214
|
+
FOR SELECT
|
|
215
|
+
USING (auth.is_org_admin(organization_id));
|
|
216
|
+
|
|
217
|
+
-- =============================================================================
|
|
218
|
+
-- ACTIVITY LOGS POLICIES
|
|
219
|
+
-- =============================================================================
|
|
220
|
+
|
|
221
|
+
-- Users can view their own activity
|
|
222
|
+
CREATE POLICY "activity_logs_select_own" ON activity_logs
|
|
223
|
+
FOR SELECT
|
|
224
|
+
USING (user_id = auth.user_id());
|
|
225
|
+
|
|
226
|
+
-- Admins can view all activity in their orgs
|
|
227
|
+
-- (would need to add org_id to activity_logs for this)
|
|
228
|
+
|
|
229
|
+
-- =============================================================================
|
|
230
|
+
-- USAGE: Set session context before queries
|
|
231
|
+
-- =============================================================================
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
-- In your API routes or middleware, set the clerk_id:
|
|
235
|
+
|
|
236
|
+
-- For Prisma with raw queries:
|
|
237
|
+
await prisma.$executeRaw`SELECT set_config('app.clerk_id', ${clerkId}, true)`;
|
|
238
|
+
|
|
239
|
+
-- Then all subsequent queries will respect RLS policies
|
|
240
|
+
|
|
241
|
+
-- For Supabase:
|
|
242
|
+
const { data } = await supabase
|
|
243
|
+
.from('projects')
|
|
244
|
+
.select('*')
|
|
245
|
+
// RLS automatically applied based on auth.uid()
|
|
246
|
+
*/
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Ultra-Dex Production Pattern: Next.js 15 Server Actions
|
|
2
|
+
// Copy this file to your app/actions/ directory
|
|
3
|
+
|
|
4
|
+
'use server';
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { revalidatePath } from 'next/cache';
|
|
8
|
+
import { redirect } from 'next/navigation';
|
|
9
|
+
import { auth } from '@clerk/nextjs/server';
|
|
10
|
+
import { prisma } from '@/lib/prisma';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// SCHEMA DEFINITIONS
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
const CreateUserSchema = z.object({
|
|
17
|
+
email: z.string().email('Invalid email address'),
|
|
18
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
19
|
+
role: z.enum(['USER', 'ADMIN']).default('USER'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const UpdateUserSchema = z.object({
|
|
23
|
+
id: z.string().uuid(),
|
|
24
|
+
name: z.string().min(2).optional(),
|
|
25
|
+
email: z.string().email().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// ACTION RESPONSE TYPE
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
type ActionResponse<T = void> =
|
|
33
|
+
| { success: true; data: T }
|
|
34
|
+
| { success: false; error: string; fieldErrors?: Record<string, string[]> };
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// USER ACTIONS
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
export async function createUser(
|
|
41
|
+
prevState: ActionResponse<{ id: string }> | null,
|
|
42
|
+
formData: FormData
|
|
43
|
+
): Promise<ActionResponse<{ id: string }>> {
|
|
44
|
+
const { userId } = await auth();
|
|
45
|
+
|
|
46
|
+
if (!userId) {
|
|
47
|
+
return { success: false, error: 'Unauthorized' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rawData = {
|
|
51
|
+
email: formData.get('email'),
|
|
52
|
+
name: formData.get('name'),
|
|
53
|
+
role: formData.get('role') || 'USER',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const validated = CreateUserSchema.safeParse(rawData);
|
|
57
|
+
|
|
58
|
+
if (!validated.success) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: 'Validation failed',
|
|
62
|
+
fieldErrors: validated.error.flatten().fieldErrors,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const user = await prisma.user.create({
|
|
68
|
+
data: validated.data,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(JSON.stringify({
|
|
72
|
+
level: 'info',
|
|
73
|
+
event: 'user_created',
|
|
74
|
+
userId: user.id,
|
|
75
|
+
createdBy: userId,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
revalidatePath('/users');
|
|
79
|
+
return { success: true, data: { id: user.id } };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Failed to create user:', error);
|
|
82
|
+
return { success: false, error: 'Failed to create user' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function updateUser(
|
|
87
|
+
prevState: ActionResponse | null,
|
|
88
|
+
formData: FormData
|
|
89
|
+
): Promise<ActionResponse> {
|
|
90
|
+
const { userId } = await auth();
|
|
91
|
+
|
|
92
|
+
if (!userId) {
|
|
93
|
+
return { success: false, error: 'Unauthorized' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rawData = {
|
|
97
|
+
id: formData.get('id'),
|
|
98
|
+
name: formData.get('name'),
|
|
99
|
+
email: formData.get('email'),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const validated = UpdateUserSchema.safeParse(rawData);
|
|
103
|
+
|
|
104
|
+
if (!validated.success) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
error: 'Validation failed',
|
|
108
|
+
fieldErrors: validated.error.flatten().fieldErrors,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await prisma.user.update({
|
|
114
|
+
where: { id: validated.data.id },
|
|
115
|
+
data: {
|
|
116
|
+
...(validated.data.name && { name: validated.data.name }),
|
|
117
|
+
...(validated.data.email && { email: validated.data.email }),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
revalidatePath('/users');
|
|
122
|
+
revalidatePath(`/users/${validated.data.id}`);
|
|
123
|
+
return { success: true, data: undefined };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('Failed to update user:', error);
|
|
126
|
+
return { success: false, error: 'Failed to update user' };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function deleteUser(id: string): Promise<ActionResponse> {
|
|
131
|
+
const { userId } = await auth();
|
|
132
|
+
|
|
133
|
+
if (!userId) {
|
|
134
|
+
return { success: false, error: 'Unauthorized' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await prisma.user.delete({
|
|
139
|
+
where: { id },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
console.log(JSON.stringify({
|
|
143
|
+
level: 'info',
|
|
144
|
+
event: 'user_deleted',
|
|
145
|
+
deletedUserId: id,
|
|
146
|
+
deletedBy: userId,
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
revalidatePath('/users');
|
|
150
|
+
return { success: true, data: undefined };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Failed to delete user:', error);
|
|
153
|
+
return { success: false, error: 'Failed to delete user' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// USAGE IN COMPONENTS
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
// In a Client Component:
|
|
163
|
+
|
|
164
|
+
'use client';
|
|
165
|
+
|
|
166
|
+
import { useActionState } from 'react';
|
|
167
|
+
import { createUser } from '@/app/actions/user';
|
|
168
|
+
|
|
169
|
+
export function CreateUserForm() {
|
|
170
|
+
const [state, formAction, isPending] = useActionState(createUser, null);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<form action={formAction}>
|
|
174
|
+
<input name="name" placeholder="Name" required />
|
|
175
|
+
<input name="email" type="email" placeholder="Email" required />
|
|
176
|
+
|
|
177
|
+
{state?.error && (
|
|
178
|
+
<p className="text-red-500">{state.error}</p>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{state?.fieldErrors?.email && (
|
|
182
|
+
<p className="text-red-500">{state.fieldErrors.email[0]}</p>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
<button type="submit" disabled={isPending}>
|
|
186
|
+
{isPending ? 'Creating...' : 'Create User'}
|
|
187
|
+
</button>
|
|
188
|
+
</form>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
*/
|