@vibecodiq/cli 0.5.0 → 0.5.1

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.
Files changed (82) hide show
  1. package/dist/foundation/admin_basic/manifest.json +37 -0
  2. package/dist/foundation/admin_basic/migrations/004_user_roles.sql +117 -0
  3. package/dist/foundation/admin_basic/migrations/005_audit_log.sql +34 -0
  4. package/dist/foundation/admin_basic/migrations/006_impersonation_sessions.sql +22 -0
  5. package/dist/foundation/admin_basic/shared/audit.ts +97 -0
  6. package/dist/foundation/admin_basic/shared/guards.ts +58 -0
  7. package/dist/foundation/admin_basic/shared/impersonation.ts +165 -0
  8. package/dist/foundation/admin_basic/shared/permissions.ts +27 -0
  9. package/dist/foundation/admin_basic/shared/roles.ts +151 -0
  10. package/dist/foundation/admin_basic/slices/audit_log/handler.ts +34 -0
  11. package/dist/foundation/admin_basic/slices/audit_log/slice.contract.json +19 -0
  12. package/dist/foundation/admin_basic/slices/dashboard/handler.ts +51 -0
  13. package/dist/foundation/admin_basic/slices/dashboard/slice.contract.json +13 -0
  14. package/dist/foundation/admin_basic/slices/impersonation/handler.ts +61 -0
  15. package/dist/foundation/admin_basic/slices/impersonation/slice.contract.json +21 -0
  16. package/dist/foundation/admin_basic/slices/roles/handler.ts +90 -0
  17. package/dist/foundation/admin_basic/slices/roles/slice.contract.json +21 -0
  18. package/dist/foundation/admin_basic/slices/users/handler.ts +48 -0
  19. package/dist/foundation/admin_basic/slices/users/slice.contract.json +15 -0
  20. package/dist/foundation/auth_basic/manifest.json +32 -0
  21. package/dist/foundation/auth_basic/migrations/001_create_profiles.sql +36 -0
  22. package/dist/foundation/auth_basic/shared/guards.ts +89 -0
  23. package/dist/foundation/auth_basic/shared/hooks.ts +63 -0
  24. package/dist/foundation/auth_basic/shared/middleware.ts +46 -0
  25. package/dist/foundation/auth_basic/shared/server-user.ts +61 -0
  26. package/dist/foundation/auth_basic/shared/session.ts +38 -0
  27. package/dist/foundation/auth_basic/shared/types.ts +29 -0
  28. package/dist/foundation/auth_basic/slices/login/handler.ts +50 -0
  29. package/dist/foundation/auth_basic/slices/login/repository.ts +23 -0
  30. package/dist/foundation/auth_basic/slices/login/schemas.ts +22 -0
  31. package/dist/foundation/auth_basic/slices/login/slice.contract.json +19 -0
  32. package/dist/foundation/auth_basic/slices/login/ui/AuthLogin.tsx +107 -0
  33. package/dist/foundation/auth_basic/slices/login/ui/hook.ts +44 -0
  34. package/dist/foundation/auth_basic/slices/logout/handler.ts +19 -0
  35. package/dist/foundation/auth_basic/slices/logout/slice.contract.json +16 -0
  36. package/dist/foundation/auth_basic/slices/register/handler.ts +61 -0
  37. package/dist/foundation/auth_basic/slices/register/repository.ts +25 -0
  38. package/dist/foundation/auth_basic/slices/register/schemas.ts +29 -0
  39. package/dist/foundation/auth_basic/slices/register/slice.contract.json +21 -0
  40. package/dist/foundation/auth_basic/slices/register/ui/AuthRegister.tsx +118 -0
  41. package/dist/foundation/auth_basic/slices/register/ui/hook.ts +48 -0
  42. package/dist/foundation/auth_basic/slices/reset_password/handler.ts +47 -0
  43. package/dist/foundation/auth_basic/slices/reset_password/schemas.ts +21 -0
  44. package/dist/foundation/auth_basic/slices/reset_password/slice.contract.json +18 -0
  45. package/dist/foundation/auth_basic/slices/reset_password/ui/AuthResetPassword.tsx +79 -0
  46. package/dist/foundation/auth_basic/slices/reset_password/ui/hook.ts +48 -0
  47. package/dist/foundation/db_basic/manifest.json +33 -0
  48. package/dist/foundation/db_basic/shared/seed.ts +27 -0
  49. package/dist/foundation/db_basic/shared/supabase-client.ts +70 -0
  50. package/dist/foundation/db_basic/shared/types.ts +20 -0
  51. package/dist/foundation/db_basic/shared/utils.ts +43 -0
  52. package/dist/foundation/payments_basic/manifest.json +54 -0
  53. package/dist/foundation/payments_basic/migrations/002_create_subscriptions.sql +44 -0
  54. package/dist/foundation/payments_basic/migrations/003_create_entitlements.sql +54 -0
  55. package/dist/foundation/payments_basic/migrations/003b_create_webhook_events.sql +28 -0
  56. package/dist/foundation/payments_basic/shared/entitlement-hooks.ts +50 -0
  57. package/dist/foundation/payments_basic/shared/entitlement-types.ts +29 -0
  58. package/dist/foundation/payments_basic/shared/entitlements.ts +78 -0
  59. package/dist/foundation/payments_basic/shared/guards.ts +110 -0
  60. package/dist/foundation/payments_basic/shared/hooks.ts +45 -0
  61. package/dist/foundation/payments_basic/shared/plans.ts +54 -0
  62. package/dist/foundation/payments_basic/shared/reconciliation.ts +85 -0
  63. package/dist/foundation/payments_basic/shared/resolver.ts +61 -0
  64. package/dist/foundation/payments_basic/shared/stripe-client.ts +15 -0
  65. package/dist/foundation/payments_basic/shared/types.ts +84 -0
  66. package/dist/foundation/payments_basic/shared/webhook-handler.ts +198 -0
  67. package/dist/foundation/payments_basic/shared/webhook-processor.ts +174 -0
  68. package/dist/foundation/payments_basic/slices/cancel/handler.ts +55 -0
  69. package/dist/foundation/payments_basic/slices/cancel/slice.contract.json +17 -0
  70. package/dist/foundation/payments_basic/slices/cancel/ui/hook.ts +45 -0
  71. package/dist/foundation/payments_basic/slices/check_limits/handler.ts +33 -0
  72. package/dist/foundation/payments_basic/slices/check_limits/slice.contract.json +17 -0
  73. package/dist/foundation/payments_basic/slices/subscribe/handler.ts +79 -0
  74. package/dist/foundation/payments_basic/slices/subscribe/repository.ts +32 -0
  75. package/dist/foundation/payments_basic/slices/subscribe/schemas.ts +21 -0
  76. package/dist/foundation/payments_basic/slices/subscribe/slice.contract.json +20 -0
  77. package/dist/foundation/payments_basic/slices/subscribe/ui/BillingSubscribe.tsx +93 -0
  78. package/dist/foundation/payments_basic/slices/subscribe/ui/hook.ts +44 -0
  79. package/dist/foundation/payments_basic/slices/webhook/handler.ts +67 -0
  80. package/dist/foundation/payments_basic/slices/webhook/slice.contract.json +19 -0
  81. package/dist/index.js +19 -18
  82. package/package.json +11 -2
@@ -0,0 +1,151 @@
1
+ import { createAdminClient } from '@/shared/db/supabase-client';
2
+
3
+ // --- ASA GENERATED START ---
4
+ // RBAC engine — canonical built-in roles with permission-based guards.
5
+ // Roles: owner, admin, support (defined in 004_user_roles.sql).
6
+ // JWT claims populated via Supabase Custom Access Token Hook.
7
+ // --- ASA GENERATED END ---
8
+
9
+ // --- USER CODE START ---
10
+
11
+ export const BUILTIN_ROLES = ['owner', 'admin', 'support'] as const;
12
+ export type BuiltinRole = typeof BUILTIN_ROLES[number];
13
+
14
+ /**
15
+ * Get all roles assigned to a user.
16
+ */
17
+ export async function getUserRoles(userId: string): Promise<string[]> {
18
+ const supabase = createAdminClient();
19
+ const { data } = await supabase
20
+ .from('user_roles')
21
+ .select('role_id')
22
+ .eq('user_id', userId);
23
+
24
+ return data?.map((r) => r.role_id) ?? [];
25
+ }
26
+
27
+ /**
28
+ * Get all permissions for a user (resolved through their roles).
29
+ */
30
+ export async function getUserPermissions(userId: string): Promise<string[]> {
31
+ const supabase = createAdminClient();
32
+
33
+ const { data } = await supabase
34
+ .from('user_roles')
35
+ .select('role_id, role:roles!inner(id), permissions:role_permissions!inner(permission_id)')
36
+ .eq('user_id', userId);
37
+
38
+ if (!data) return [];
39
+
40
+ const permissions = new Set<string>();
41
+ for (const entry of data) {
42
+ const perms = entry.permissions as unknown as { permission_id: string }[];
43
+ if (Array.isArray(perms)) {
44
+ for (const p of perms) {
45
+ permissions.add(p.permission_id);
46
+ }
47
+ }
48
+ }
49
+
50
+ return Array.from(permissions);
51
+ }
52
+
53
+ /**
54
+ * Check if a user has a specific permission.
55
+ */
56
+ export async function hasPermission(userId: string, permission: string): Promise<boolean> {
57
+ const permissions = await getUserPermissions(userId);
58
+ return permissions.includes(permission);
59
+ }
60
+
61
+ /**
62
+ * Assign a role to a user.
63
+ * Requires the assigner to have ROLES_ASSIGN permission.
64
+ */
65
+ export async function assignRole(
66
+ userId: string,
67
+ roleId: string,
68
+ assignedBy: string,
69
+ ): Promise<{ success: boolean; error?: string }> {
70
+ const supabase = createAdminClient();
71
+
72
+ // Validate role exists
73
+ const { data: role } = await supabase
74
+ .from('roles')
75
+ .select('id')
76
+ .eq('id', roleId)
77
+ .single();
78
+
79
+ if (!role) {
80
+ return { success: false, error: `Role '${roleId}' does not exist` };
81
+ }
82
+
83
+ const { error } = await supabase
84
+ .from('user_roles')
85
+ .upsert(
86
+ { user_id: userId, role_id: roleId, assigned_by: assignedBy },
87
+ { onConflict: 'user_id,role_id' },
88
+ );
89
+
90
+ if (error) {
91
+ return { success: false, error: error.message };
92
+ }
93
+
94
+ return { success: true };
95
+ }
96
+
97
+ /**
98
+ * Remove a role from a user.
99
+ */
100
+ export async function removeRole(
101
+ userId: string,
102
+ roleId: string,
103
+ ): Promise<{ success: boolean; error?: string }> {
104
+ const supabase = createAdminClient();
105
+
106
+ const { error } = await supabase
107
+ .from('user_roles')
108
+ .delete()
109
+ .eq('user_id', userId)
110
+ .eq('role_id', roleId);
111
+
112
+ if (error) {
113
+ return { success: false, error: error.message };
114
+ }
115
+
116
+ return { success: true };
117
+ }
118
+
119
+ /**
120
+ * Supabase Custom Access Token Hook handler.
121
+ * Add this as a Postgres function + hook in Supabase Dashboard.
122
+ *
123
+ * SQL for the hook function:
124
+ * ```sql
125
+ * CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event JSONB)
126
+ * RETURNS JSONB LANGUAGE plpgsql AS $$
127
+ * DECLARE
128
+ * claims JSONB;
129
+ * user_roles TEXT[];
130
+ * user_perms TEXT[];
131
+ * BEGIN
132
+ * SELECT ARRAY_AGG(ur.role_id) INTO user_roles
133
+ * FROM public.user_roles ur WHERE ur.user_id = (event->>'user_id')::UUID;
134
+ *
135
+ * SELECT ARRAY_AGG(DISTINCT rp.permission_id) INTO user_perms
136
+ * FROM public.user_roles ur
137
+ * JOIN public.role_permissions rp ON rp.role_id = ur.role_id
138
+ * WHERE ur.user_id = (event->>'user_id')::UUID;
139
+ *
140
+ * claims := event->'claims';
141
+ * claims := jsonb_set(claims, '{user_roles}', COALESCE(to_jsonb(user_roles), '[]'));
142
+ * claims := jsonb_set(claims, '{user_permissions}', COALESCE(to_jsonb(user_perms), '[]'));
143
+ *
144
+ * event := jsonb_set(event, '{claims}', claims);
145
+ * RETURN event;
146
+ * END;
147
+ * $$;
148
+ * ```
149
+ */
150
+
151
+ // --- USER CODE END ---
@@ -0,0 +1,34 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { requirePermission } from '@/shared/admin/guards';
3
+ import { PERMISSIONS } from '@/shared/admin/permissions';
4
+ import { queryAuditLog } from '@/shared/admin/audit';
5
+
6
+ // --- ASA GENERATED START ---
7
+ // Route handler for admin/audit-log slice.
8
+ // Filterable, paginated audit log viewer.
9
+ // Security: requires admin.audit.view permission (server-side).
10
+ // --- ASA GENERATED END ---
11
+
12
+ // --- USER CODE START ---
13
+
14
+ export async function GET(request: NextRequest): Promise<NextResponse> {
15
+ const { response } = await requirePermission(request, PERMISSIONS.AUDIT_VIEW);
16
+ if (response) return response;
17
+
18
+ const url = new URL(request.url);
19
+
20
+ const result = await queryAuditLog({
21
+ actor_id: url.searchParams.get('actor_id') ?? undefined,
22
+ subject_id: url.searchParams.get('subject_id') ?? undefined,
23
+ action: url.searchParams.get('action') ?? undefined,
24
+ resource_type: url.searchParams.get('resource_type') ?? undefined,
25
+ from_date: url.searchParams.get('from') ?? undefined,
26
+ to_date: url.searchParams.get('to') ?? undefined,
27
+ limit: parseInt(url.searchParams.get('limit') ?? '50'),
28
+ offset: parseInt(url.searchParams.get('offset') ?? '0'),
29
+ });
30
+
31
+ return NextResponse.json(result);
32
+ }
33
+
34
+ // --- USER CODE END ---
@@ -0,0 +1,19 @@
1
+ {
2
+ "domain": "admin",
3
+ "slice": "audit-log",
4
+ "type": "route",
5
+ "has_ui": true,
6
+ "has_repository": true,
7
+ "description": "Audit log viewer — filterable, paginated, immutable log of all admin actions.",
8
+ "input": {
9
+ "actor_id": "string (optional)",
10
+ "action": "string (optional)",
11
+ "from_date": "string (optional)",
12
+ "to_date": "string (optional)"
13
+ },
14
+ "output": {
15
+ "entries": "array",
16
+ "total": "number"
17
+ },
18
+ "side_effects": []
19
+ }
@@ -0,0 +1,51 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { requirePermission } from '@/shared/admin/guards';
3
+ import { PERMISSIONS } from '@/shared/admin/permissions';
4
+ import { createAdminClient } from '@/shared/db/supabase-client';
5
+
6
+ // --- ASA GENERATED START ---
7
+ // Route handler for admin/dashboard slice.
8
+ // Returns overview stats for admin dashboard.
9
+ // Security: requires admin.access permission (server-side).
10
+ // --- ASA GENERATED END ---
11
+
12
+ // --- USER CODE START ---
13
+
14
+ export async function GET(request: NextRequest): Promise<NextResponse> {
15
+ const { response } = await requirePermission(request, PERMISSIONS.ADMIN_ACCESS);
16
+ if (response) return response;
17
+
18
+ const supabase = createAdminClient();
19
+
20
+ // Parallel queries for dashboard stats
21
+ const [usersResult, subsResult, recentActivityResult] = await Promise.all([
22
+ supabase.from('profiles').select('id', { count: 'exact', head: true }),
23
+ supabase.from('subscriptions').select('plan, status'),
24
+ supabase
25
+ .from('audit_log')
26
+ .select('action, actor_email, created_at')
27
+ .order('created_at', { ascending: false })
28
+ .limit(10),
29
+ ]);
30
+
31
+ const totalUsers = usersResult.count ?? 0;
32
+ const subscriptions = subsResult.data ?? [];
33
+
34
+ const planCounts: Record<string, number> = {};
35
+ const statusCounts: Record<string, number> = {};
36
+ for (const sub of subscriptions) {
37
+ planCounts[sub.plan] = (planCounts[sub.plan] ?? 0) + 1;
38
+ statusCounts[sub.status] = (statusCounts[sub.status] ?? 0) + 1;
39
+ }
40
+
41
+ return NextResponse.json({
42
+ stats: {
43
+ total_users: totalUsers,
44
+ plans: planCounts,
45
+ statuses: statusCounts,
46
+ },
47
+ recent_activity: recentActivityResult.data ?? [],
48
+ });
49
+ }
50
+
51
+ // --- USER CODE END ---
@@ -0,0 +1,13 @@
1
+ {
2
+ "domain": "admin",
3
+ "slice": "dashboard",
4
+ "type": "route",
5
+ "has_ui": true,
6
+ "has_repository": true,
7
+ "description": "Admin dashboard — overview stats (users, subscriptions, recent activity).",
8
+ "input": {},
9
+ "output": {
10
+ "stats": "object"
11
+ },
12
+ "side_effects": []
13
+ }
@@ -0,0 +1,61 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { requirePermission } from '@/shared/admin/guards';
3
+ import { PERMISSIONS } from '@/shared/admin/permissions';
4
+ import { startImpersonation, stopImpersonation, getActiveImpersonation } from '@/shared/admin/impersonation';
5
+ import { requireAuth } from '@/shared/auth/guards';
6
+
7
+ // --- ASA GENERATED START ---
8
+ // Route handler for admin/impersonation slice.
9
+ // POST: start impersonation, DELETE: stop, GET: check active session.
10
+ // Security: requires admin.impersonate permission (server-side).
11
+ // --- ASA GENERATED END ---
12
+
13
+ // --- USER CODE START ---
14
+
15
+ export async function POST(request: NextRequest): Promise<NextResponse> {
16
+ const { user, response } = await requirePermission(request, PERMISSIONS.IMPERSONATE);
17
+ if (response) return response;
18
+
19
+ const body = await request.json();
20
+ const { subject_user_id, reason } = body;
21
+
22
+ if (!subject_user_id || !reason) {
23
+ return NextResponse.json(
24
+ { success: false, error: 'subject_user_id and reason are required' },
25
+ { status: 400 },
26
+ );
27
+ }
28
+
29
+ const result = await startImpersonation(
30
+ user!.id,
31
+ user!.email,
32
+ subject_user_id,
33
+ reason,
34
+ );
35
+
36
+ if (result.error) {
37
+ return NextResponse.json({ success: false, error: result.error }, { status: 400 });
38
+ }
39
+
40
+ return NextResponse.json({ success: true, session: result.session });
41
+ }
42
+
43
+ export async function DELETE(request: NextRequest): Promise<NextResponse> {
44
+ const { user, response } = await requireAuth(request);
45
+ if (response) return response;
46
+
47
+ const result = await stopImpersonation(user!.id, user!.email);
48
+
49
+ if (result.error) {
50
+ return NextResponse.json({ success: false, error: result.error }, { status: 400 });
51
+ }
52
+
53
+ return NextResponse.json({ success: true });
54
+ }
55
+
56
+ export async function GET(): Promise<NextResponse> {
57
+ const session = await getActiveImpersonation();
58
+ return NextResponse.json({ active: !!session, session });
59
+ }
60
+
61
+ // --- USER CODE END ---
@@ -0,0 +1,21 @@
1
+ {
2
+ "domain": "admin",
3
+ "slice": "impersonation",
4
+ "type": "route",
5
+ "has_ui": true,
6
+ "has_repository": true,
7
+ "description": "Safe impersonation — start/stop impersonation with reason, TTL, and full audit chain.",
8
+ "input": {
9
+ "subject_user_id": "string",
10
+ "reason": "string"
11
+ },
12
+ "output": {
13
+ "success": "boolean",
14
+ "error": "string | null"
15
+ },
16
+ "side_effects": [
17
+ "Creates impersonation_sessions record",
18
+ "Sets HttpOnly cookie",
19
+ "Logs to audit_log"
20
+ ]
21
+ }
@@ -0,0 +1,90 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { requirePermission } from '@/shared/admin/guards';
3
+ import { PERMISSIONS } from '@/shared/admin/permissions';
4
+ import { assignRole, removeRole, getUserRoles } from '@/shared/admin/roles';
5
+ import { logAuditEvent } from '@/shared/admin/audit';
6
+
7
+ // --- ASA GENERATED START ---
8
+ // Route handler for admin/roles slice.
9
+ // GET: list user roles, POST: assign role, DELETE: remove role.
10
+ // Security: requires admin.roles.assign permission (server-side).
11
+ // --- ASA GENERATED END ---
12
+
13
+ // --- USER CODE START ---
14
+
15
+ export async function GET(request: NextRequest): Promise<NextResponse> {
16
+ const { response } = await requirePermission(request, PERMISSIONS.ROLES_ASSIGN);
17
+ if (response) return response;
18
+
19
+ const url = new URL(request.url);
20
+ const userId = url.searchParams.get('user_id');
21
+
22
+ if (!userId) {
23
+ return NextResponse.json({ error: 'user_id is required' }, { status: 400 });
24
+ }
25
+
26
+ const roles = await getUserRoles(userId);
27
+ return NextResponse.json({ user_id: userId, roles });
28
+ }
29
+
30
+ export async function POST(request: NextRequest): Promise<NextResponse> {
31
+ const { user, response } = await requirePermission(request, PERMISSIONS.ROLES_ASSIGN);
32
+ if (response) return response;
33
+
34
+ const body = await request.json();
35
+ const { user_id, role_id } = body;
36
+
37
+ if (!user_id || !role_id) {
38
+ return NextResponse.json(
39
+ { success: false, error: 'user_id and role_id are required' },
40
+ { status: 400 },
41
+ );
42
+ }
43
+
44
+ const result = await assignRole(user_id, role_id, user!.id);
45
+
46
+ if (result.success) {
47
+ await logAuditEvent({
48
+ actor_id: user!.id,
49
+ actor_email: user!.email,
50
+ subject_id: user_id,
51
+ action: 'role.assign',
52
+ resource_type: 'role',
53
+ resource_id: role_id,
54
+ });
55
+ }
56
+
57
+ return NextResponse.json(result, { status: result.success ? 200 : 400 });
58
+ }
59
+
60
+ export async function DELETE(request: NextRequest): Promise<NextResponse> {
61
+ const { user, response } = await requirePermission(request, PERMISSIONS.ROLES_ASSIGN);
62
+ if (response) return response;
63
+
64
+ const body = await request.json();
65
+ const { user_id, role_id } = body;
66
+
67
+ if (!user_id || !role_id) {
68
+ return NextResponse.json(
69
+ { success: false, error: 'user_id and role_id are required' },
70
+ { status: 400 },
71
+ );
72
+ }
73
+
74
+ const result = await removeRole(user_id, role_id);
75
+
76
+ if (result.success) {
77
+ await logAuditEvent({
78
+ actor_id: user!.id,
79
+ actor_email: user!.email,
80
+ subject_id: user_id,
81
+ action: 'role.remove',
82
+ resource_type: 'role',
83
+ resource_id: role_id,
84
+ });
85
+ }
86
+
87
+ return NextResponse.json(result, { status: result.success ? 200 : 400 });
88
+ }
89
+
90
+ // --- USER CODE END ---
@@ -0,0 +1,21 @@
1
+ {
2
+ "domain": "admin",
3
+ "slice": "roles",
4
+ "type": "route",
5
+ "has_ui": true,
6
+ "has_repository": true,
7
+ "description": "Role management — assign/remove roles for users. Requires admin.roles.assign permission.",
8
+ "input": {
9
+ "user_id": "string",
10
+ "role_id": "string",
11
+ "action": "assign | remove"
12
+ },
13
+ "output": {
14
+ "success": "boolean",
15
+ "error": "string | null"
16
+ },
17
+ "side_effects": [
18
+ "Modifies user_roles table",
19
+ "Logs to audit_log"
20
+ ]
21
+ }
@@ -0,0 +1,48 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { requirePermission } from '@/shared/admin/guards';
3
+ import { PERMISSIONS } from '@/shared/admin/permissions';
4
+ import { createAdminClient } from '@/shared/db/supabase-client';
5
+
6
+ // --- ASA GENERATED START ---
7
+ // Route handler for admin/users slice.
8
+ // Lists users with their profiles and roles.
9
+ // Security: requires admin.users.list permission (server-side).
10
+ // --- ASA GENERATED END ---
11
+
12
+ // --- USER CODE START ---
13
+
14
+ export async function GET(request: NextRequest): Promise<NextResponse> {
15
+ const { user, response } = await requirePermission(request, PERMISSIONS.USERS_LIST);
16
+ if (response) return response;
17
+
18
+ const supabase = createAdminClient();
19
+ const url = new URL(request.url);
20
+ const page = parseInt(url.searchParams.get('page') ?? '1');
21
+ const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '50'), 100);
22
+ const search = url.searchParams.get('search') ?? '';
23
+
24
+ let query = supabase
25
+ .from('profiles')
26
+ .select('id, display_name, role, created_at, updated_at', { count: 'exact' })
27
+ .order('created_at', { ascending: false })
28
+ .range((page - 1) * limit, page * limit - 1);
29
+
30
+ if (search) {
31
+ query = query.or(`display_name.ilike.%${search}%`);
32
+ }
33
+
34
+ const { data: profiles, count, error } = await query;
35
+
36
+ if (error) {
37
+ return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 });
38
+ }
39
+
40
+ return NextResponse.json({
41
+ users: profiles ?? [],
42
+ total: count ?? 0,
43
+ page,
44
+ limit,
45
+ });
46
+ }
47
+
48
+ // --- USER CODE END ---
@@ -0,0 +1,15 @@
1
+ {
2
+ "domain": "admin",
3
+ "slice": "users",
4
+ "type": "route",
5
+ "has_ui": true,
6
+ "has_repository": true,
7
+ "description": "Admin user management — list, view, edit users. Requires admin.users.list permission.",
8
+ "input": {},
9
+ "output": {
10
+ "users": "array"
11
+ },
12
+ "side_effects": [
13
+ "All admin actions are audit-logged"
14
+ ]
15
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "auth-basic",
3
+ "version": "1.0.0",
4
+ "description": "Basic authentication for SaaS apps — Supabase Auth with SSR sessions",
5
+ "min_cli_version": "2.0.0",
6
+ "stack": "asa-native-v1",
7
+ "dependencies": ["db-basic"],
8
+ "package_dependencies": {},
9
+ "package_dev_dependencies": {},
10
+ "slices": [
11
+ { "domain": "auth", "name": "register", "type": "route", "has_ui": true, "has_repository": true },
12
+ { "domain": "auth", "name": "login", "type": "route", "has_ui": true, "has_repository": true },
13
+ { "domain": "auth", "name": "logout", "type": "route", "has_ui": false, "has_repository": false },
14
+ { "domain": "auth", "name": "reset-password", "type": "route", "has_ui": true, "has_repository": true }
15
+ ],
16
+ "shared": [
17
+ { "src": "shared/middleware.ts", "dest": "shared/auth/middleware.ts", "overwrite": false },
18
+ { "src": "shared/session.ts", "dest": "shared/auth/session.ts", "overwrite": false },
19
+ { "src": "shared/guards.ts", "dest": "shared/auth/guards.ts", "overwrite": false },
20
+ { "src": "shared/server-user.ts", "dest": "shared/auth/server-user.ts", "overwrite": false },
21
+ { "src": "shared/hooks.ts", "dest": "shared/auth/hooks.ts", "overwrite": false },
22
+ { "src": "shared/types.ts", "dest": "shared/auth/types.ts", "overwrite": false }
23
+ ],
24
+ "migrations": [
25
+ { "src": "migrations/001_create_profiles.sql", "dest": "shared/db/migrations/001_create_profiles.sql" }
26
+ ],
27
+ "env_vars": [],
28
+ "post_install_notes": [
29
+ "Enable Email/Password provider in Supabase Dashboard → Authentication → Providers",
30
+ "Run migration: Apply shared/db/migrations/001_create_profiles.sql in Supabase SQL Editor"
31
+ ]
32
+ }
@@ -0,0 +1,36 @@
1
+ -- ASA Module: auth-basic
2
+ -- Migration: 001_create_profiles
3
+ -- Creates profiles table extending Supabase auth.users
4
+
5
+ create table if not exists profiles (
6
+ id uuid references auth.users(id) on delete cascade primary key,
7
+ display_name text,
8
+ role text not null default 'user',
9
+ created_at timestamptz default now(),
10
+ updated_at timestamptz default now()
11
+ );
12
+
13
+ -- RLS policies
14
+ alter table profiles enable row level security;
15
+
16
+ create policy "Users can view own profile"
17
+ on profiles for select
18
+ using (auth.uid() = id);
19
+
20
+ create policy "Users can update own profile"
21
+ on profiles for update
22
+ using (auth.uid() = id);
23
+
24
+ -- Trigger: auto-create profile on user signup
25
+ create or replace function handle_new_user()
26
+ returns trigger as $$
27
+ begin
28
+ insert into public.profiles (id, display_name)
29
+ values (new.id, new.raw_user_meta_data->>'display_name');
30
+ return new;
31
+ end;
32
+ $$ language plpgsql security definer;
33
+
34
+ create or replace trigger on_auth_user_created
35
+ after insert on auth.users
36
+ for each row execute function handle_new_user();