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.
- package/README.md +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- 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
|
+
}
|