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,47 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import crypto from 'crypto';
3
+ import { db } from '@/lib/db';
4
+ import { users } from '@/lib/schema';
5
+ import { eq } from 'drizzle-orm';
6
+ import { sendEmail, emailTemplates } from '@/lib/email';
7
+
8
+ export async function POST(request: NextRequest) {
9
+ try {
10
+ const { email } = await request.json();
11
+
12
+ if (!email) {
13
+ return NextResponse.json({ error: 'Email is required' }, { status: 400 });
14
+ }
15
+
16
+ const [user] = await db
17
+ .select()
18
+ .from(users)
19
+ .where(eq(users.email, email.toLowerCase()))
20
+ .limit(1);
21
+
22
+ // For security reasons, don't reveal if user exists
23
+ if (!user) {
24
+ return NextResponse.json({ message: 'If an account exists, a reset link has been sent' });
25
+ }
26
+
27
+ const resetToken = crypto.randomBytes(32).toString('hex');
28
+ const resetTokenExpiry = new Date(Date.now() + 3600000); // 1 hour from now
29
+
30
+ await db.update(users)
31
+ .set({
32
+ resetToken,
33
+ resetTokenExpiry,
34
+ updatedAt: new Date()
35
+ })
36
+ .where(eq(users.id, user.id));
37
+
38
+ // Send the email
39
+ const template = emailTemplates.passwordReset(user.name, resetToken);
40
+ await sendEmail({ to: user.email, ...template });
41
+
42
+ return NextResponse.json({ message: 'If an account exists, a reset link has been sent' });
43
+ } catch (error) {
44
+ console.error('Forgot password error:', error);
45
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
46
+ }
47
+ }
@@ -0,0 +1,99 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import bcrypt from 'bcryptjs';
3
+ import jwt from 'jsonwebtoken';
4
+ import { db } from '@/lib/db';
5
+ import { users } from '@/lib/schema';
6
+ import { eq } from 'drizzle-orm';
7
+ import speakeasy from 'speakeasy';
8
+
9
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
10
+
11
+ export async function POST(request: NextRequest) {
12
+ try {
13
+ const { email, password, mfaToken } = await request.json();
14
+
15
+ if (!email || !password) {
16
+ return NextResponse.json(
17
+ { error: 'Email and password are required' },
18
+ { status: 400 }
19
+ );
20
+ }
21
+
22
+ // Find user by email
23
+ const [user] = await db
24
+ .select()
25
+ .from(users)
26
+ .where(eq(users.email, email))
27
+ .limit(1);
28
+
29
+ if (!user) {
30
+ return NextResponse.json(
31
+ { error: 'Invalid credentials' },
32
+ { status: 401 }
33
+ );
34
+ }
35
+
36
+ // Verify password
37
+ const isValidPassword = await bcrypt.compare(password, user.password);
38
+ if (!isValidPassword) {
39
+ return NextResponse.json(
40
+ { error: 'Invalid credentials' },
41
+ { status: 401 }
42
+ );
43
+ }
44
+
45
+ // Verify MFA if enabled
46
+ if (user.mfaEnabled) {
47
+ if (!mfaToken) {
48
+ return NextResponse.json({
49
+ mfaRequired: true,
50
+ message: 'MFA token required'
51
+ }, { status: 200 }); // Status 200 but signaling UI to show MFA input
52
+ }
53
+
54
+ const verified = speakeasy.totp.verify({
55
+ secret: user.mfaSecret!,
56
+ encoding: 'base32',
57
+ token: mfaToken,
58
+ });
59
+
60
+ if (!verified) {
61
+ return NextResponse.json({ error: 'Invalid MFA token' }, { status: 401 });
62
+ }
63
+ }
64
+
65
+ // Generate JWT token
66
+ const token = jwt.sign(
67
+ { userId: user.id, email: user.email },
68
+ JWT_SECRET,
69
+ { expiresIn: '7d' }
70
+ );
71
+
72
+ // Create response with token and optional raw token for CLI
73
+ const response = NextResponse.json({
74
+ message: 'Login successful',
75
+ token: token, // Include token in JSON body for CLI/login
76
+ user: {
77
+ id: user.id,
78
+ name: user.name,
79
+ email: user.email,
80
+ },
81
+ });
82
+
83
+ // Set HTTP-only cookie
84
+ response.cookies.set('auth-token', token, {
85
+ httpOnly: true,
86
+ secure: process.env.NODE_ENV === 'production',
87
+ sameSite: 'lax',
88
+ maxAge: 60 * 60 * 24 * 7, // 7 days
89
+ });
90
+
91
+ return response;
92
+ } catch (error) {
93
+ console.error('Login error:', error);
94
+ return NextResponse.json(
95
+ { error: 'Internal server error' },
96
+ { status: 500 }
97
+ );
98
+ }
99
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function POST() {
4
+ const response = NextResponse.json({ message: 'Logged out successfully' });
5
+
6
+ // Clear the auth cookie
7
+ response.cookies.set('auth-token', '', {
8
+ httpOnly: true,
9
+ secure: process.env.NODE_ENV === 'production',
10
+ sameSite: 'lax',
11
+ maxAge: 0,
12
+ });
13
+
14
+ return response;
15
+ }
@@ -0,0 +1,23 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+
4
+ export async function GET() {
5
+ try {
6
+ const user = await getUserFromToken();
7
+
8
+ if (!user) {
9
+ return NextResponse.json(
10
+ { error: 'Not authenticated' },
11
+ { status: 401 }
12
+ );
13
+ }
14
+
15
+ return NextResponse.json({ user });
16
+ } catch (error) {
17
+ console.error('Auth check error:', error);
18
+ return NextResponse.json(
19
+ { error: 'Internal server error' },
20
+ { status: 500 }
21
+ );
22
+ }
23
+ }
@@ -0,0 +1,33 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { users } from '@/lib/schema';
5
+ import { eq } from 'drizzle-orm';
6
+ import speakeasy from 'speakeasy';
7
+ import QRCode from 'qrcode';
8
+
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const user = await getUserFromToken();
12
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
13
+
14
+ const secret = speakeasy.generateSecret({
15
+ name: `Nobalmako (${user.email})`,
16
+ });
17
+
18
+ const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url || '');
19
+
20
+ // We don't enable it yet, just store the secret temporarily or return it
21
+ // For this implementation, we'll store it in the user record
22
+ await db.update(users)
23
+ .set({ mfaSecret: secret.base32 })
24
+ .where(eq(users.id, user.id));
25
+
26
+ return NextResponse.json({
27
+ qrCode: qrCodeUrl,
28
+ secret: secret.base32
29
+ });
30
+ } catch (error) {
31
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
32
+ }
33
+ }
@@ -0,0 +1,45 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { users } from '@/lib/schema';
5
+ import { eq } from 'drizzle-orm';
6
+ import speakeasy from 'speakeasy';
7
+ import { sendEmail, emailTemplates } from '@/lib/email';
8
+
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const user = await getUserFromToken();
12
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
13
+
14
+ const { token } = await request.json();
15
+ if (!token) return NextResponse.json({ error: 'Token is required' }, { status: 400 });
16
+
17
+ const [userData] = await db.select().from(users).where(eq(users.id, user.id)).limit(1);
18
+
19
+ if (!userData.mfaSecret) {
20
+ return NextResponse.json({ error: 'MFA not setup' }, { status: 400 });
21
+ }
22
+
23
+ const verified = speakeasy.totp.verify({
24
+ secret: userData.mfaSecret,
25
+ encoding: 'base32',
26
+ token,
27
+ });
28
+
29
+ if (verified) {
30
+ await db.update(users)
31
+ .set({ mfaEnabled: true })
32
+ .where(eq(users.id, user.id));
33
+
34
+ // Notify user via email
35
+ const template = emailTemplates.mfaEnabled(userData.name);
36
+ await sendEmail({ to: userData.email, ...template });
37
+
38
+ return NextResponse.json({ success: true });
39
+ } else {
40
+ return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
41
+ }
42
+ } catch (error) {
43
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
44
+ }
45
+ }
@@ -0,0 +1,140 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import bcrypt from 'bcryptjs';
3
+ import crypto from 'crypto';
4
+ import { db } from '@/lib/db';
5
+ import { users, invitations, teamMembers } from '@/lib/schema';
6
+ import { eq, and, gt } from 'drizzle-orm';
7
+ import { sendEmail, emailTemplates } from '@/lib/email';
8
+
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const { name, email, password, confirmPassword, inviteToken } = await request.json();
12
+
13
+ if (!name || !email || !password || !confirmPassword) {
14
+ return NextResponse.json(
15
+ { error: 'All fields are required' },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ if (password !== confirmPassword) {
21
+ return NextResponse.json(
22
+ { error: 'Passwords do not match' },
23
+ { status: 400 }
24
+ );
25
+ }
26
+
27
+ if (password.length < 6) {
28
+ return NextResponse.json(
29
+ { error: 'Password must be at least 6 characters long' },
30
+ { status: 400 }
31
+ );
32
+ }
33
+
34
+ // Check if user already exists
35
+ const [existingUser] = await db
36
+ .select()
37
+ .from(users)
38
+ .where(eq(users.email, email))
39
+ .limit(1);
40
+
41
+ if (existingUser) {
42
+ return NextResponse.json(
43
+ { error: 'User with this email already exists' },
44
+ { status: 409 }
45
+ );
46
+ }
47
+
48
+ // Verify invite token if provided
49
+ let invitationData = null;
50
+ if (inviteToken) {
51
+ const [invitation] = await db
52
+ .select()
53
+ .from(invitations)
54
+ .where(and(
55
+ eq(invitations.token, inviteToken),
56
+ eq(invitations.status, 'pending'),
57
+ gt(invitations.expiresAt, new Date())
58
+ ))
59
+ .limit(1);
60
+
61
+ if (!invitation) {
62
+ return NextResponse.json(
63
+ { error: 'Invalid or expired invitation token' },
64
+ { status: 400 }
65
+ );
66
+ }
67
+
68
+ // Ensure the email matches the invitation
69
+ if (invitation.email.toLowerCase() !== email.toLowerCase()) {
70
+ return NextResponse.json(
71
+ { error: 'This invitation was sent to a different email address' },
72
+ { status: 400 }
73
+ );
74
+ }
75
+ invitationData = invitation;
76
+ }
77
+
78
+ // Hash password
79
+ const hashedPassword = await bcrypt.hash(password, 12);
80
+ const verificationToken = crypto.randomBytes(32).toString('hex');
81
+
82
+ // Create user and handle invitation in a transaction
83
+ const result = await db.transaction(async (tx) => {
84
+ const [newUser] = await tx
85
+ .insert(users)
86
+ .values({
87
+ name,
88
+ email: email.toLowerCase(),
89
+ password: hashedPassword,
90
+ verificationToken,
91
+ })
92
+ .returning();
93
+
94
+ if (invitationData) {
95
+ // Add user to team
96
+ await tx.insert(teamMembers).values({
97
+ teamId: invitationData.teamId,
98
+ userId: newUser.id,
99
+ role: invitationData.role,
100
+ });
101
+
102
+ // Update invitation status
103
+ await tx.update(invitations)
104
+ .set({ status: 'accepted', updatedAt: new Date() })
105
+ .where(eq(invitations.id, invitationData.id));
106
+ }
107
+
108
+ return newUser;
109
+ });
110
+
111
+ // Send Welcome & Verification Emails
112
+ try {
113
+ const welcome = emailTemplates.welcome(result.name);
114
+ const verify = emailTemplates.verification(result.name, verificationToken);
115
+
116
+ await Promise.all([
117
+ sendEmail({ to: result.email, ...welcome }),
118
+ sendEmail({ to: result.email, ...verify })
119
+ ]);
120
+ } catch (emailError) {
121
+ console.error('Failed to send registration emails:', emailError);
122
+ // We don't fail registration if emails fail, but we log it
123
+ }
124
+
125
+ return NextResponse.json({
126
+ message: 'User created successfully',
127
+ user: {
128
+ id: result.id,
129
+ name: result.name,
130
+ email: result.email,
131
+ },
132
+ });
133
+ } catch (error) {
134
+ console.error('Registration error:', error);
135
+ return NextResponse.json(
136
+ { error: 'Internal server error' },
137
+ { status: 500 }
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,52 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import bcrypt from 'bcryptjs';
3
+ import { db } from '@/lib/db';
4
+ import { users } from '@/lib/schema';
5
+ import { eq, and, gt } from 'drizzle-orm';
6
+
7
+ export async function POST(request: NextRequest) {
8
+ try {
9
+ const { token, password, confirmPassword } = await request.json();
10
+
11
+ if (!token || !password || !confirmPassword) {
12
+ return NextResponse.json({ error: 'All fields are required' }, { status: 400 });
13
+ }
14
+
15
+ if (password !== confirmPassword) {
16
+ return NextResponse.json({ error: 'Passwords do not match' }, { status: 400 });
17
+ }
18
+
19
+ if (password.length < 6) {
20
+ return NextResponse.json({ error: 'Password must be at least 6 characters' }, { status: 400 });
21
+ }
22
+
23
+ const [user] = await db
24
+ .select()
25
+ .from(users)
26
+ .where(and(
27
+ eq(users.resetToken, token),
28
+ gt(users.resetTokenExpiry, new Date())
29
+ ))
30
+ .limit(1);
31
+
32
+ if (!user) {
33
+ return NextResponse.json({ error: 'Invalid or expired reset token' }, { status: 400 });
34
+ }
35
+
36
+ const hashedPassword = await bcrypt.hash(password, 12);
37
+
38
+ await db.update(users)
39
+ .set({
40
+ password: hashedPassword,
41
+ resetToken: null,
42
+ resetTokenExpiry: null,
43
+ updatedAt: new Date()
44
+ })
45
+ .where(eq(users.id, user.id));
46
+
47
+ return NextResponse.json({ message: 'Password reset successfully' });
48
+ } catch (error) {
49
+ console.error('Reset password error:', error);
50
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
51
+ }
52
+ }
@@ -0,0 +1,71 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { db } from '@/lib/db';
3
+ import { users } from '@/lib/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { getUserFromToken } from '@/lib/auth';
6
+ import bcrypt from 'bcryptjs';
7
+
8
+ export async function PUT(req: NextRequest) {
9
+ try {
10
+ const user = await getUserFromToken();
11
+ if (!user) {
12
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
13
+ }
14
+
15
+ const { name } = await req.json();
16
+
17
+ if (!name) {
18
+ return NextResponse.json({ error: 'Name is required' }, { status: 400 });
19
+ }
20
+
21
+ await db.update(users)
22
+ .set({ name })
23
+ .where(eq(users.id, user.id));
24
+
25
+ return NextResponse.json({ success: true });
26
+ } catch (error) {
27
+ console.error('Profile update failed:', error);
28
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
29
+ }
30
+ }
31
+
32
+ export async function PATCH(req: NextRequest) {
33
+ try {
34
+ const user = await getUserFromToken();
35
+ if (!user) {
36
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
37
+ }
38
+
39
+ const { currentPassword, newPassword } = await req.json();
40
+
41
+ if (!currentPassword || !newPassword) {
42
+ return NextResponse.json({ error: 'All fields are required' }, { status: 400 });
43
+ }
44
+
45
+ // Get user with password for comparison
46
+ const [dbUser] = await db.select().from(users).where(eq(users.id, user.id));
47
+
48
+ if (!dbUser) {
49
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
50
+ }
51
+
52
+ // Verify current password
53
+ const isPasswordMatch = await bcrypt.compare(currentPassword, dbUser.password);
54
+ if (!isPasswordMatch) {
55
+ return NextResponse.json({ error: 'Incorrect current password' }, { status: 400 });
56
+ }
57
+
58
+ // Hash new password
59
+ const hashedPassword = await bcrypt.hash(newPassword, 10);
60
+
61
+ // Update password
62
+ await db.update(users)
63
+ .set({ password: hashedPassword })
64
+ .where(eq(users.id, user.id));
65
+
66
+ return NextResponse.json({ success: true });
67
+ } catch (error) {
68
+ console.error('Password change failed:', error);
69
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
70
+ }
71
+ }
@@ -0,0 +1,39 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { db } from '@/lib/db';
3
+ import { users } from '@/lib/schema';
4
+ import { eq } from 'drizzle-orm';
5
+
6
+ export async function GET(request: NextRequest) {
7
+ try {
8
+ const { searchParams } = new URL(request.url);
9
+ const token = searchParams.get('token');
10
+
11
+ if (!token) {
12
+ return NextResponse.json({ error: 'Token is required' }, { status: 400 });
13
+ }
14
+
15
+ const [user] = await db
16
+ .select()
17
+ .from(users)
18
+ .where(eq(users.verificationToken, token))
19
+ .limit(1);
20
+
21
+ if (!user) {
22
+ return NextResponse.json({ error: 'Invalid or expired token' }, { status: 400 });
23
+ }
24
+
25
+ await db.update(users)
26
+ .set({
27
+ emailVerified: new Date(),
28
+ verificationToken: null,
29
+ updatedAt: new Date()
30
+ })
31
+ .where(eq(users.id, user.id));
32
+
33
+ // Redirect to dashboard or login with a success message
34
+ return NextResponse.redirect(new URL('/auth/login?verified=true', request.url));
35
+ } catch (error) {
36
+ console.error('Verification error:', error);
37
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
38
+ }
39
+ }