nobalmako 1.0.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.
Files changed (123) hide show
  1. package/README.md +112 -0
  2. package/components.json +22 -0
  3. package/dist/nobalmako.js +272 -0
  4. package/drizzle/0000_pink_spiral.sql +126 -0
  5. package/drizzle/meta/0000_snapshot.json +1027 -0
  6. package/drizzle/meta/_journal.json +13 -0
  7. package/drizzle.config.ts +10 -0
  8. package/eslint.config.mjs +18 -0
  9. package/next.config.ts +7 -0
  10. package/package.json +80 -0
  11. package/postcss.config.mjs +7 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/server/index.ts +118 -0
  18. package/src/app/api/api-keys/[id]/route.ts +147 -0
  19. package/src/app/api/api-keys/route.ts +151 -0
  20. package/src/app/api/audit-logs/route.ts +84 -0
  21. package/src/app/api/auth/forgot-password/route.ts +47 -0
  22. package/src/app/api/auth/login/route.ts +99 -0
  23. package/src/app/api/auth/logout/route.ts +15 -0
  24. package/src/app/api/auth/me/route.ts +23 -0
  25. package/src/app/api/auth/mfa/setup/route.ts +33 -0
  26. package/src/app/api/auth/mfa/verify/route.ts +45 -0
  27. package/src/app/api/auth/register/route.ts +140 -0
  28. package/src/app/api/auth/reset-password/route.ts +52 -0
  29. package/src/app/api/auth/update/route.ts +71 -0
  30. package/src/app/api/auth/verify/route.ts +39 -0
  31. package/src/app/api/environments/route.ts +227 -0
  32. package/src/app/api/team-members/route.ts +385 -0
  33. package/src/app/api/teams/route.ts +217 -0
  34. package/src/app/api/variable-history/route.ts +218 -0
  35. package/src/app/api/variables/route.ts +476 -0
  36. package/src/app/api/webhooks/route.ts +77 -0
  37. package/src/app/api-keys/APIKeysClient.tsx +316 -0
  38. package/src/app/api-keys/page.tsx +10 -0
  39. package/src/app/api-reference/page.tsx +324 -0
  40. package/src/app/audit-log/AuditLogClient.tsx +229 -0
  41. package/src/app/audit-log/page.tsx +10 -0
  42. package/src/app/auth/forgot-password/page.tsx +121 -0
  43. package/src/app/auth/login/LoginForm.tsx +145 -0
  44. package/src/app/auth/login/page.tsx +11 -0
  45. package/src/app/auth/register/RegisterForm.tsx +156 -0
  46. package/src/app/auth/register/page.tsx +16 -0
  47. package/src/app/auth/reset-password/page.tsx +160 -0
  48. package/src/app/dashboard/DashboardClient.tsx +219 -0
  49. package/src/app/dashboard/page.tsx +11 -0
  50. package/src/app/docs/page.tsx +251 -0
  51. package/src/app/favicon.ico +0 -0
  52. package/src/app/globals.css +123 -0
  53. package/src/app/layout.tsx +35 -0
  54. package/src/app/page.tsx +231 -0
  55. package/src/app/profile/ProfileClient.tsx +230 -0
  56. package/src/app/profile/page.tsx +10 -0
  57. package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
  58. package/src/app/project/[id]/page.tsx +17 -0
  59. package/src/bin/nobalmako.ts +341 -0
  60. package/src/components/ApiKeysManager.tsx +529 -0
  61. package/src/components/AppLayout.tsx +193 -0
  62. package/src/components/BulkActions.tsx +138 -0
  63. package/src/components/CreateEnvironmentDialog.tsx +207 -0
  64. package/src/components/CreateTeamDialog.tsx +174 -0
  65. package/src/components/CreateVariableDialog.tsx +311 -0
  66. package/src/components/DeleteEnvironmentDialog.tsx +104 -0
  67. package/src/components/DeleteTeamDialog.tsx +112 -0
  68. package/src/components/DeleteVariableDialog.tsx +103 -0
  69. package/src/components/EditEnvironmentDialog.tsx +202 -0
  70. package/src/components/EditMemberDialog.tsx +143 -0
  71. package/src/components/EditTeamDialog.tsx +178 -0
  72. package/src/components/EditVariableDialog.tsx +231 -0
  73. package/src/components/ImportVariablesDialog.tsx +347 -0
  74. package/src/components/InviteMemberDialog.tsx +191 -0
  75. package/src/components/LeaveProjectDialog.tsx +111 -0
  76. package/src/components/MFASettings.tsx +136 -0
  77. package/src/components/ProjectDiff.tsx +123 -0
  78. package/src/components/Providers.tsx +24 -0
  79. package/src/components/RemoveMemberDialog.tsx +112 -0
  80. package/src/components/SearchDialog.tsx +276 -0
  81. package/src/components/SecurityOverview.tsx +92 -0
  82. package/src/components/TeamMembersManager.tsx +103 -0
  83. package/src/components/VariableHistoryDialog.tsx +265 -0
  84. package/src/components/WebhooksManager.tsx +169 -0
  85. package/src/components/ui/alert-dialog.tsx +160 -0
  86. package/src/components/ui/alert.tsx +59 -0
  87. package/src/components/ui/avatar.tsx +53 -0
  88. package/src/components/ui/badge.tsx +46 -0
  89. package/src/components/ui/button.tsx +62 -0
  90. package/src/components/ui/card.tsx +92 -0
  91. package/src/components/ui/checkbox.tsx +32 -0
  92. package/src/components/ui/dialog.tsx +143 -0
  93. package/src/components/ui/dropdown-menu.tsx +257 -0
  94. package/src/components/ui/input.tsx +21 -0
  95. package/src/components/ui/label.tsx +24 -0
  96. package/src/components/ui/select.tsx +190 -0
  97. package/src/components/ui/separator.tsx +28 -0
  98. package/src/components/ui/sonner.tsx +37 -0
  99. package/src/components/ui/switch.tsx +31 -0
  100. package/src/components/ui/table.tsx +117 -0
  101. package/src/components/ui/tabs.tsx +66 -0
  102. package/src/components/ui/textarea.tsx +18 -0
  103. package/src/hooks/use-api-keys.ts +95 -0
  104. package/src/hooks/use-audit-logs.ts +58 -0
  105. package/src/hooks/use-auth.tsx +121 -0
  106. package/src/hooks/use-environments.ts +33 -0
  107. package/src/hooks/use-project-permissions.ts +49 -0
  108. package/src/hooks/use-team-members.ts +30 -0
  109. package/src/hooks/use-teams.ts +33 -0
  110. package/src/hooks/use-variables.ts +38 -0
  111. package/src/lib/audit.ts +36 -0
  112. package/src/lib/auth.ts +108 -0
  113. package/src/lib/crypto.ts +39 -0
  114. package/src/lib/db.ts +15 -0
  115. package/src/lib/dynamic-providers.ts +19 -0
  116. package/src/lib/email.ts +110 -0
  117. package/src/lib/mail.ts +51 -0
  118. package/src/lib/permissions.ts +51 -0
  119. package/src/lib/schema.ts +240 -0
  120. package/src/lib/seed.ts +107 -0
  121. package/src/lib/utils.ts +6 -0
  122. package/src/lib/webhooks.ts +42 -0
  123. package/tsconfig.json +34 -0
@@ -0,0 +1,217 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { teams, teamMembers } from '@/lib/schema';
5
+ import { createAuditLog } from '@/lib/audit';
6
+ import { eq, or, exists, and } from 'drizzle-orm';
7
+
8
+ export async function GET() {
9
+ try {
10
+ const user = await getUserFromToken();
11
+
12
+ if (!user) {
13
+ return NextResponse.json(
14
+ { error: 'Not authenticated' },
15
+ { status: 401 }
16
+ );
17
+ }
18
+
19
+ // Get teams where user is owner or member
20
+ const userTeams = await db
21
+ .select()
22
+ .from(teams)
23
+ .where(
24
+ or(
25
+ eq(teams.ownerId, user.id),
26
+ exists(
27
+ db.select()
28
+ .from(teamMembers)
29
+ .where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
30
+ )
31
+ )
32
+ );
33
+
34
+ return NextResponse.json({ teams: userTeams });
35
+ } catch (error) {
36
+ console.error('Get teams error:', error);
37
+ return NextResponse.json(
38
+ { error: 'Internal server error' },
39
+ { status: 500 }
40
+ );
41
+ }
42
+ }
43
+
44
+ export async function POST(request: NextRequest) {
45
+ try {
46
+ const user = await getUserFromToken();
47
+
48
+ if (!user) {
49
+ return NextResponse.json(
50
+ { error: 'Not authenticated' },
51
+ { status: 401 }
52
+ );
53
+ }
54
+
55
+ const { name, description, color } = await request.json();
56
+
57
+ if (!name) {
58
+ return NextResponse.json(
59
+ { error: 'Name is required' },
60
+ { status: 400 }
61
+ );
62
+ }
63
+
64
+ const [newTeam] = await db.insert(teams).values({
65
+ name,
66
+ description,
67
+ color: color || '#3b82f6',
68
+ ownerId: user.id,
69
+ }).returning();
70
+
71
+ // Audit log
72
+ await createAuditLog({
73
+ userId: user.id,
74
+ teamId: newTeam.id,
75
+ action: 'create',
76
+ resourceType: 'team',
77
+ resourceId: newTeam.id,
78
+ newValue: { name, description, color: color || '#3b82f6' },
79
+ });
80
+
81
+ return NextResponse.json({ team: newTeam });
82
+ } catch (error) {
83
+ console.error('Create team error:', error);
84
+ return NextResponse.json(
85
+ { error: 'Internal server error' },
86
+ { status: 500 }
87
+ );
88
+ }
89
+ }
90
+ export async function PUT(request: NextRequest) {
91
+ try {
92
+ const user = await getUserFromToken();
93
+
94
+ if (!user) {
95
+ return NextResponse.json(
96
+ { error: 'Not authenticated' },
97
+ { status: 401 }
98
+ );
99
+ }
100
+
101
+ const { id, name, description, color } = await request.json();
102
+
103
+ if (!id) {
104
+ return NextResponse.json(
105
+ { error: 'Team ID is required' },
106
+ { status: 400 }
107
+ );
108
+ }
109
+
110
+ if (!name) {
111
+ return NextResponse.json(
112
+ { error: 'Name is required' },
113
+ { status: 400 }
114
+ );
115
+ }
116
+
117
+ // Check if user owns the team
118
+ const existingTeam = await db
119
+ .select()
120
+ .from(teams)
121
+ .where(eq(teams.id, id))
122
+ .limit(1);
123
+
124
+ if (!existingTeam.length || existingTeam[0].ownerId !== user.id) {
125
+ return NextResponse.json(
126
+ { error: 'Team not found or access denied' },
127
+ { status: 404 }
128
+ );
129
+ }
130
+
131
+ const [updatedTeam] = await db
132
+ .update(teams)
133
+ .set({
134
+ name,
135
+ description,
136
+ color: color || existingTeam[0].color,
137
+ updatedAt: new Date(),
138
+ })
139
+ .where(eq(teams.id, id))
140
+ .returning();
141
+
142
+ // Audit log
143
+ await createAuditLog({
144
+ userId: user.id,
145
+ teamId: id,
146
+ action: 'update',
147
+ resourceType: 'team',
148
+ resourceId: id,
149
+ oldValue: { name: existingTeam[0].name, description: existingTeam[0].description, color: existingTeam[0].color },
150
+ newValue: { name, description, color: color || existingTeam[0].color },
151
+ });
152
+
153
+ return NextResponse.json({ team: updatedTeam });
154
+ } catch (error) {
155
+ console.error('Update team error:', error);
156
+ return NextResponse.json(
157
+ { error: 'Internal server error' },
158
+ { status: 500 }
159
+ );
160
+ }
161
+ }
162
+
163
+ export async function DELETE(request: NextRequest) {
164
+ try {
165
+ const user = await getUserFromToken();
166
+
167
+ if (!user) {
168
+ return NextResponse.json(
169
+ { error: 'Not authenticated' },
170
+ { status: 401 }
171
+ );
172
+ }
173
+
174
+ const { id } = await request.json();
175
+
176
+ if (!id) {
177
+ return NextResponse.json(
178
+ { error: 'Team ID is required' },
179
+ { status: 400 }
180
+ );
181
+ }
182
+
183
+ // Check if user owns the team
184
+ const existingTeam = await db
185
+ .select()
186
+ .from(teams)
187
+ .where(eq(teams.id, id))
188
+ .limit(1);
189
+
190
+ if (!existingTeam.length || existingTeam[0].ownerId !== user.id) {
191
+ return NextResponse.json(
192
+ { error: 'Team not found or access denied' },
193
+ { status: 404 }
194
+ );
195
+ }
196
+
197
+ await db.delete(teams).where(eq(teams.id, id));
198
+
199
+ // Audit log
200
+ await createAuditLog({
201
+ userId: user.id,
202
+ teamId: id,
203
+ action: 'delete',
204
+ resourceType: 'team',
205
+ resourceId: id,
206
+ oldValue: { name: existingTeam[0].name, description: existingTeam[0].description },
207
+ });
208
+
209
+ return NextResponse.json({ success: true });
210
+ } catch (error) {
211
+ console.error('Delete team error:', error);
212
+ return NextResponse.json(
213
+ { error: 'Internal server error' },
214
+ { status: 500 }
215
+ );
216
+ }
217
+ }
@@ -0,0 +1,218 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { variableHistory, teams, environmentVariables, users, teamMembers } from '@/lib/schema';
5
+ import { eq, and, desc, or, exists } from 'drizzle-orm';
6
+ import { decrypt } from '@/lib/crypto';
7
+ import { hasPermission, Permissions } from '@/lib/permissions';
8
+
9
+ export async function GET(request: NextRequest) {
10
+ try {
11
+ const user = await getUserFromToken();
12
+
13
+ if (!user) {
14
+ return NextResponse.json(
15
+ { error: 'Not authenticated' },
16
+ { status: 401 }
17
+ );
18
+ }
19
+
20
+ const { searchParams } = new URL(request.url);
21
+ const variableId = searchParams.get('variableId');
22
+ const teamId = searchParams.get('teamId');
23
+
24
+ if (!variableId && !teamId) {
25
+ return NextResponse.json(
26
+ { error: 'Variable ID or Team ID is required' },
27
+ { status: 400 }
28
+ );
29
+ }
30
+
31
+ let whereCondition;
32
+ if (variableId) {
33
+ // Check if user has access to this variable
34
+ const variableCheck = await db
35
+ .select()
36
+ .from(environmentVariables)
37
+ .innerJoin(teams, eq(environmentVariables.teamId, teams.id))
38
+ .where(
39
+ and(
40
+ eq(environmentVariables.id, variableId),
41
+ or(
42
+ eq(teams.ownerId, user.id),
43
+ exists(
44
+ db.select()
45
+ .from(teamMembers)
46
+ .where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
47
+ )
48
+ )
49
+ )
50
+ )
51
+ .limit(1);
52
+
53
+ if (!variableCheck.length) {
54
+ return NextResponse.json(
55
+ { error: 'Variable not found or access denied' },
56
+ { status: 404 }
57
+ );
58
+ }
59
+ whereCondition = eq(variableHistory.variableId, variableId);
60
+ } else {
61
+ // Check if user owns or is a member of the team
62
+ if (!teamId) {
63
+ return NextResponse.json(
64
+ { error: 'Team ID is required' },
65
+ { status: 400 }
66
+ );
67
+ }
68
+
69
+ const teamCheck = await db
70
+ .select()
71
+ .from(teams)
72
+ .where(
73
+ and(
74
+ eq(teams.id, teamId),
75
+ or(
76
+ eq(teams.ownerId, user.id),
77
+ exists(
78
+ db.select()
79
+ .from(teamMembers)
80
+ .where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
81
+ )
82
+ )
83
+ )
84
+ )
85
+ .limit(1);
86
+
87
+ if (!teamCheck.length) {
88
+ return NextResponse.json(
89
+ { error: 'Team not found or access denied' },
90
+ { status: 403 }
91
+ );
92
+ }
93
+ whereCondition = eq(variableHistory.teamId, teamId);
94
+ }
95
+
96
+ // Get variable history with user details
97
+ const history = await db
98
+ .select({
99
+ id: variableHistory.id,
100
+ variableId: variableHistory.variableId,
101
+ key: variableHistory.key,
102
+ value: variableHistory.value,
103
+ description: variableHistory.description,
104
+ isSecret: variableHistory.isSecret,
105
+ changeType: variableHistory.changeType,
106
+ createdAt: variableHistory.createdAt,
107
+ changer: {
108
+ id: users.id,
109
+ name: users.name,
110
+ email: users.email,
111
+ },
112
+ })
113
+ .from(variableHistory)
114
+ .innerJoin(users, eq(variableHistory.changedBy, users.id))
115
+ .where(whereCondition)
116
+ .orderBy(desc(variableHistory.createdAt))
117
+ .limit(100); // Limit to last 100 changes
118
+
119
+ // Decrypt history values
120
+ const decryptedHistory = history.map(h => ({
121
+ ...h,
122
+ value: decrypt(h.value)
123
+ }));
124
+
125
+ return NextResponse.json({ history: decryptedHistory });
126
+ } catch (error) {
127
+ console.error('Get variable history error:', error);
128
+ return NextResponse.json(
129
+ { error: 'Internal server error' },
130
+ { status: 500 }
131
+ );
132
+ }
133
+ }
134
+ export async function POST(request: NextRequest) {
135
+ try {
136
+ const user = await getUserFromToken();
137
+
138
+ if (!user) {
139
+ return NextResponse.json(
140
+ { error: 'Not authenticated' },
141
+ { status: 401 }
142
+ );
143
+ }
144
+
145
+ const { historyId } = await request.json();
146
+
147
+ if (!historyId) {
148
+ return NextResponse.json(
149
+ { error: 'History ID is required' },
150
+ { status: 400 }
151
+ );
152
+ }
153
+
154
+ // Get the history entry
155
+ const [historyEntry] = await db
156
+ .select()
157
+ .from(variableHistory)
158
+ .where(eq(variableHistory.id, historyId))
159
+ .limit(1);
160
+
161
+ if (!historyEntry) {
162
+ return NextResponse.json(
163
+ { error: 'History entry not found' },
164
+ { status: 404 }
165
+ );
166
+ }
167
+
168
+ // Verify user has permission to manage secrets in this team
169
+ const canManage = await hasPermission(user.id, historyEntry.teamId, Permissions.MANAGE_SECRETS);
170
+
171
+ if (!canManage) {
172
+ return NextResponse.json(
173
+ { error: 'Forbidden: You do not have permission to manage secrets in this project' },
174
+ { status: 403 }
175
+ );
176
+ }
177
+
178
+ // Roll back the variable
179
+ const [updatedVariable] = await db
180
+ .update(environmentVariables)
181
+ .set({
182
+ value: historyEntry.value, // It's already encrypted in history
183
+ description: historyEntry.description,
184
+ isSecret: historyEntry.isSecret,
185
+ updatedBy: user.id,
186
+ updatedAt: new Date(),
187
+ })
188
+ .where(eq(environmentVariables.id, historyEntry.variableId))
189
+ .returning();
190
+
191
+ // Create a new history entry for the rollback
192
+ await db.insert(variableHistory).values({
193
+ variableId: historyEntry.variableId,
194
+ teamId: historyEntry.teamId,
195
+ environmentId: historyEntry.environmentId,
196
+ key: historyEntry.key,
197
+ value: historyEntry.value, // Already encrypted
198
+ description: historyEntry.description,
199
+ isSecret: historyEntry.isSecret,
200
+ changedBy: user.id,
201
+ changeType: 'update',
202
+ });
203
+
204
+ return NextResponse.json({
205
+ message: 'Variable rolled back successfully',
206
+ variable: {
207
+ ...updatedVariable,
208
+ value: decrypt(updatedVariable.value)
209
+ }
210
+ });
211
+ } catch (error) {
212
+ console.error('Rollback variable error:', error);
213
+ return NextResponse.json(
214
+ { error: 'Internal server error' },
215
+ { status: 500 }
216
+ );
217
+ }
218
+ }