ultra-dex 3.1.0 → 3.3.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 +50 -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/cloud.js +780 -0
- package/lib/commands/doctor.js +98 -79
- package/lib/commands/exec.js +434 -0
- package/lib/commands/generate.js +19 -16
- package/lib/commands/github.js +475 -0
- package/lib/commands/init.js +52 -56
- package/lib/commands/scaffold.js +151 -0
- package/lib/commands/search.js +477 -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/mcp/client.js +502 -0
- package/lib/providers/agent-sdk.js +630 -0
- package/lib/providers/anthropic-agents.js +580 -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/browser.js +373 -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 +19 -4
|
@@ -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
|
+
*/
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Ultra-Dex Production Pattern: tRPC Router
|
|
2
|
+
// Copy to server/routers/ directory
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { TRPCError } from '@trpc/server';
|
|
6
|
+
import { router, publicProcedure, protectedProcedure, adminProcedure } from '../trpc';
|
|
7
|
+
import { prisma } from '@/lib/prisma';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// USER ROUTER
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export const userRouter = router({
|
|
14
|
+
// Get current user profile
|
|
15
|
+
me: protectedProcedure.query(async ({ ctx }) => {
|
|
16
|
+
const user = await prisma.user.findUnique({
|
|
17
|
+
where: { clerkId: ctx.userId },
|
|
18
|
+
include: {
|
|
19
|
+
organizationMemberships: {
|
|
20
|
+
include: { organization: true },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return user;
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
// Update current user profile
|
|
33
|
+
update: protectedProcedure
|
|
34
|
+
.input(
|
|
35
|
+
z.object({
|
|
36
|
+
name: z.string().min(2).optional(),
|
|
37
|
+
imageUrl: z.string().url().optional(),
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
.mutation(async ({ ctx, input }) => {
|
|
41
|
+
return prisma.user.update({
|
|
42
|
+
where: { clerkId: ctx.userId },
|
|
43
|
+
data: input,
|
|
44
|
+
});
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
// List all users (admin only)
|
|
48
|
+
list: adminProcedure
|
|
49
|
+
.input(
|
|
50
|
+
z.object({
|
|
51
|
+
limit: z.number().min(1).max(100).default(50),
|
|
52
|
+
cursor: z.string().optional(),
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
.query(async ({ input }) => {
|
|
56
|
+
const users = await prisma.user.findMany({
|
|
57
|
+
take: input.limit + 1,
|
|
58
|
+
cursor: input.cursor ? { id: input.cursor } : undefined,
|
|
59
|
+
orderBy: { createdAt: 'desc' },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let nextCursor: string | undefined;
|
|
63
|
+
if (users.length > input.limit) {
|
|
64
|
+
const nextItem = users.pop();
|
|
65
|
+
nextCursor = nextItem!.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { users, nextCursor };
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// PROJECT ROUTER
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export const projectRouter = router({
|
|
77
|
+
// List projects for current organization
|
|
78
|
+
list: protectedProcedure
|
|
79
|
+
.input(
|
|
80
|
+
z.object({
|
|
81
|
+
organizationId: z.string(),
|
|
82
|
+
status: z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED']).optional(),
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
.query(async ({ ctx, input }) => {
|
|
86
|
+
// Verify user has access to organization
|
|
87
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
88
|
+
where: {
|
|
89
|
+
organizationId: input.organizationId,
|
|
90
|
+
user: { clerkId: ctx.userId },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!membership) {
|
|
95
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: 'Access denied' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return prisma.project.findMany({
|
|
99
|
+
where: {
|
|
100
|
+
organizationId: input.organizationId,
|
|
101
|
+
...(input.status && { status: input.status }),
|
|
102
|
+
},
|
|
103
|
+
include: {
|
|
104
|
+
owner: { select: { name: true, imageUrl: true } },
|
|
105
|
+
_count: { select: { tasks: true } },
|
|
106
|
+
},
|
|
107
|
+
orderBy: { updatedAt: 'desc' },
|
|
108
|
+
});
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
// Get single project
|
|
112
|
+
get: protectedProcedure
|
|
113
|
+
.input(z.object({ id: z.string() }))
|
|
114
|
+
.query(async ({ ctx, input }) => {
|
|
115
|
+
const project = await prisma.project.findUnique({
|
|
116
|
+
where: { id: input.id },
|
|
117
|
+
include: {
|
|
118
|
+
owner: true,
|
|
119
|
+
tasks: { orderBy: { createdAt: 'desc' } },
|
|
120
|
+
organization: true,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!project) {
|
|
125
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Verify access
|
|
129
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
130
|
+
where: {
|
|
131
|
+
organizationId: project.organizationId,
|
|
132
|
+
user: { clerkId: ctx.userId },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!membership) {
|
|
137
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return project;
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
// Create project
|
|
144
|
+
create: protectedProcedure
|
|
145
|
+
.input(
|
|
146
|
+
z.object({
|
|
147
|
+
name: z.string().min(1).max(100),
|
|
148
|
+
description: z.string().max(500).optional(),
|
|
149
|
+
organizationId: z.string(),
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
.mutation(async ({ ctx, input }) => {
|
|
153
|
+
// Verify user has write access to organization
|
|
154
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
155
|
+
where: {
|
|
156
|
+
organizationId: input.organizationId,
|
|
157
|
+
user: { clerkId: ctx.userId },
|
|
158
|
+
role: { in: ['OWNER', 'ADMIN', 'MEMBER'] },
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!membership) {
|
|
163
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const user = await prisma.user.findUnique({
|
|
167
|
+
where: { clerkId: ctx.userId },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return prisma.project.create({
|
|
171
|
+
data: {
|
|
172
|
+
name: input.name,
|
|
173
|
+
description: input.description,
|
|
174
|
+
organizationId: input.organizationId,
|
|
175
|
+
ownerId: user!.id,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
// Update project
|
|
181
|
+
update: protectedProcedure
|
|
182
|
+
.input(
|
|
183
|
+
z.object({
|
|
184
|
+
id: z.string(),
|
|
185
|
+
name: z.string().min(1).max(100).optional(),
|
|
186
|
+
description: z.string().max(500).optional(),
|
|
187
|
+
status: z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED']).optional(),
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
.mutation(async ({ ctx, input }) => {
|
|
191
|
+
const { id, ...data } = input;
|
|
192
|
+
|
|
193
|
+
const project = await prisma.project.findUnique({
|
|
194
|
+
where: { id },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!project) {
|
|
198
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Verify write access
|
|
202
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
203
|
+
where: {
|
|
204
|
+
organizationId: project.organizationId,
|
|
205
|
+
user: { clerkId: ctx.userId },
|
|
206
|
+
role: { in: ['OWNER', 'ADMIN', 'MEMBER'] },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!membership) {
|
|
211
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return prisma.project.update({
|
|
215
|
+
where: { id },
|
|
216
|
+
data,
|
|
217
|
+
});
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Delete project
|
|
221
|
+
delete: protectedProcedure
|
|
222
|
+
.input(z.object({ id: z.string() }))
|
|
223
|
+
.mutation(async ({ ctx, input }) => {
|
|
224
|
+
const project = await prisma.project.findUnique({
|
|
225
|
+
where: { id: input.id },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!project) {
|
|
229
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Only owner/admin can delete
|
|
233
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
234
|
+
where: {
|
|
235
|
+
organizationId: project.organizationId,
|
|
236
|
+
user: { clerkId: ctx.userId },
|
|
237
|
+
role: { in: ['OWNER', 'ADMIN'] },
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!membership) {
|
|
242
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return prisma.project.delete({ where: { id: input.id } });
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// ROOT ROUTER
|
|
251
|
+
// =============================================================================
|
|
252
|
+
|
|
253
|
+
export const appRouter = router({
|
|
254
|
+
user: userRouter,
|
|
255
|
+
project: projectRouter,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export type AppRouter = typeof appRouter;
|