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,229 @@
1
+ 'use client';
2
+
3
+ import { useState } from "react";
4
+ import AppLayout from "@/components/AppLayout";
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from "@/components/ui/dialog";
16
+ import { Label } from "@/components/ui/label";
17
+ import {
18
+ Activity,
19
+ Search,
20
+ Filter,
21
+ User,
22
+ Clock,
23
+ Globe,
24
+ Shield,
25
+ Database,
26
+ Users,
27
+ AlertCircle,
28
+ ArrowRight,
29
+ ChevronDown
30
+ } from "lucide-react";
31
+ import { useAuditLogs, AuditLog } from "@/hooks/use-audit-logs";
32
+ import { useTeams } from "@/hooks/use-teams";
33
+ import { format } from "date-fns";
34
+ import {
35
+ Select,
36
+ SelectContent,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from "@/components/ui/select";
41
+
42
+ export default function AuditLogClient() {
43
+ const [selectedTeam, setSelectedTeam] = useState<string>("all");
44
+ const { logs, isLoading } = useAuditLogs(selectedTeam === "all" ? undefined : selectedTeam);
45
+ const { teams } = useTeams();
46
+ const [searchQuery, setSearchQuery] = useState("");
47
+
48
+ const filteredLogs = logs.filter(log =>
49
+ log.action.toLowerCase().includes(searchQuery.toLowerCase()) ||
50
+ log.resourceType.toLowerCase().includes(searchQuery.toLowerCase()) ||
51
+ log.userName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
52
+ log.teamName?.toLowerCase().includes(searchQuery.toLowerCase())
53
+ );
54
+
55
+ const getActionBadge = (action: string) => {
56
+ switch (action) {
57
+ case 'create':
58
+ return <Badge className="bg-emerald-500 hover:bg-emerald-600">CREATE</Badge>;
59
+ case 'update':
60
+ return <Badge className="bg-blue-500 hover:bg-blue-600">UPDATE</Badge>;
61
+ case 'delete':
62
+ return <Badge variant="destructive">DELETE</Badge>;
63
+ case 'view':
64
+ return <Badge variant="outline">VIEW</Badge>;
65
+ default:
66
+ return <Badge variant="secondary">{action.toUpperCase()}</Badge>;
67
+ }
68
+ };
69
+
70
+ const getResourceIcon = (type: string) => {
71
+ switch (type) {
72
+ case 'variable':
73
+ return <Database className="h-4 w-4 text-blue-500" />;
74
+ case 'environment':
75
+ return <Shield className="h-4 w-4 text-emerald-500" />;
76
+ case 'team':
77
+ case 'project':
78
+ return <Users className="h-4 w-4 text-purple-500" />;
79
+ default:
80
+ return <Activity className="h-4 w-4 text-muted-foreground" />;
81
+ }
82
+ };
83
+
84
+ return (
85
+ <AppLayout>
86
+ <div className="space-y-8 max-w-6xl mx-auto">
87
+ <div>
88
+ <h1 className="text-3xl font-bold tracking-tight">Audit Log</h1>
89
+ <p className="text-muted-foreground mt-1 text-lg">
90
+ Track all activity and changes across your projects for security and compliance.
91
+ </p>
92
+ </div>
93
+
94
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
95
+ <div className="relative w-full md:w-96">
96
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
97
+ <input
98
+ type="text"
99
+ placeholder="Search activity..."
100
+ className="w-full bg-card border rounded-md pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all"
101
+ value={searchQuery}
102
+ onChange={(e) => setSearchQuery(e.target.value)}
103
+ />
104
+ </div>
105
+ <div className="flex items-center gap-2">
106
+ <Filter className="h-4 w-4 text-muted-foreground mr-1" />
107
+ <Select value={selectedTeam} onValueChange={setSelectedTeam}>
108
+ <SelectTrigger className="w-[200px] bg-card">
109
+ <SelectValue placeholder="Filter by Project" />
110
+ </SelectTrigger>
111
+ <SelectContent>
112
+ <SelectItem value="all">All Projects</SelectItem>
113
+ {teams?.map(team => (
114
+ <SelectItem key={team.id} value={team.id}>{team.name}</SelectItem>
115
+ ))}
116
+ </SelectContent>
117
+ </Select>
118
+ </div>
119
+ </div>
120
+
121
+ <Card className="shadow-sm border-muted overflow-hidden">
122
+ <CardContent className="p-0">
123
+ {isLoading ? (
124
+ <div className="py-20 flex justify-center">
125
+ <div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" />
126
+ </div>
127
+ ) : filteredLogs.length > 0 ? (
128
+ <div className="divide-y divide-border">
129
+ {filteredLogs.map((log) => (
130
+ <div key={log.id} className="p-4 md:p-6 hover:bg-muted/30 transition-colors">
131
+ <div className="flex flex-col md:flex-row md:items-center gap-4">
132
+ <div className="flex-1 space-y-1">
133
+ <div className="flex items-center gap-2">
134
+ {getActionBadge(log.action)}
135
+ <span className="text-sm font-medium flex items-center gap-1.5">
136
+ {getResourceIcon(log.resourceType)}
137
+ {log.resourceType.charAt(0).toUpperCase() + log.resourceType.slice(1)}
138
+ </span>
139
+ <span className="text-muted-foreground">•</span>
140
+ <span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">
141
+ ID: {log.resourceId.slice(0, 8)}
142
+ </span>
143
+ </div>
144
+
145
+ <div className="text-sm text-foreground py-1">
146
+ <span className="font-semibold text-primary">{log.userName || "Unknown User"}</span>
147
+ {" "}performed{" "}
148
+ <span className="font-semibold">{log.action}</span>
149
+ {" "}on{" "}
150
+ <span className="font-medium">
151
+ {format(new Date(log.createdAt), "MMM d, yyyy")}
152
+ </span>
153
+ {log.teamName && (
154
+ <> in <span className="text-purple-600 font-medium">{log.teamName}</span></>
155
+ )}
156
+ </div>
157
+
158
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-2 pt-1">
159
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
160
+ <Clock className="h-3.5 w-3.5" />
161
+ {format(new Date(log.createdAt), "MMM d, yyyy 'at' HH:mm:ss")}
162
+ </div>
163
+ {log.ipAddress && (
164
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
165
+ <Globe className="h-3.5 w-3.5" />
166
+ {log.ipAddress}
167
+ </div>
168
+ )}
169
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
170
+ <User className="h-3.5 w-3.5" />
171
+ {log.userEmail}
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ {(log.oldValue || log.newValue) && (
177
+ <div className="hidden md:block">
178
+ <Dialog>
179
+ <DialogTrigger asChild>
180
+ <Button variant="outline" size="sm" className="h-8 text-xs">
181
+ View Details
182
+ </Button>
183
+ </DialogTrigger>
184
+ <DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
185
+ <DialogHeader>
186
+ <DialogTitle className="text-slate-900">Event Details</DialogTitle>
187
+ <DialogDescription className="text-slate-500">
188
+ Detailed comparison of values for this event.
189
+ </DialogDescription>
190
+ </DialogHeader>
191
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
192
+ <div className="space-y-2">
193
+ <Label className="text-xs uppercase tracking-wider text-slate-400">Previous State</Label>
194
+ <pre className="bg-slate-900 text-slate-100 p-4 rounded-lg text-[11px] overflow-auto max-h-96 min-h-[100px]">
195
+ {log.oldValue ? JSON.stringify(log.oldValue, null, 2) : "No previous state"}
196
+ </pre>
197
+ </div>
198
+ <div className="space-y-2">
199
+ <Label className="text-xs uppercase tracking-wider text-slate-400">New State</Label>
200
+ <pre className="bg-slate-900 text-slate-100 p-4 rounded-lg text-[11px] overflow-auto max-h-96 min-h-[100px]">
201
+ {log.newValue ? JSON.stringify(log.newValue, null, 2) : "No new state"}
202
+ </pre>
203
+ </div>
204
+ </div>
205
+ </DialogContent>
206
+ </Dialog>
207
+ </div>
208
+ )}
209
+ </div>
210
+ </div>
211
+ ))}
212
+ </div>
213
+ ) : (
214
+ <div className="flex flex-col items-center justify-center py-32 text-center px-4">
215
+ <div className="p-4 bg-muted rounded-full mb-4">
216
+ <Activity className="h-10 w-10 text-muted-foreground/50" />
217
+ </div>
218
+ <h3 className="text-xl font-semibold mb-2">No activity recorded</h3>
219
+ <p className="text-muted-foreground max-w-sm text-sm">
220
+ Once you start managing variables and projects, all activity will be logged here.
221
+ </p>
222
+ </div>
223
+ )}
224
+ </CardContent>
225
+ </Card>
226
+ </div>
227
+ </AppLayout>
228
+ );
229
+ }
@@ -0,0 +1,10 @@
1
+ import AuditLogClient from "./AuditLogClient";
2
+
3
+ export const metadata = {
4
+ title: "Audit Log - Nobalmako",
5
+ description: "Monitor access and changes to your environment variables",
6
+ };
7
+
8
+ export default function AuditLogPage() {
9
+ return <AuditLogClient />;
10
+ }
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { Alert, AlertDescription } from "@/components/ui/alert";
10
+ import { Shield, Loader2, ArrowLeft, Mail } from "lucide-react";
11
+
12
+ export default function ForgotPasswordPage() {
13
+ const [email, setEmail] = useState("");
14
+ const [isLoading, setIsLoading] = useState(false);
15
+ const [success, setSuccess] = useState(false);
16
+ const [error, setError] = useState("");
17
+
18
+ const handleSubmit = async (e: React.FormEvent) => {
19
+ e.preventDefault();
20
+ setIsLoading(true);
21
+ setError("");
22
+
23
+ try {
24
+ const response = await fetch("/api/auth/forgot-password", {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({ email }),
28
+ });
29
+
30
+ if (response.ok) {
31
+ setSuccess(true);
32
+ } else {
33
+ const data = await response.json();
34
+ setError(data.error || "Failed to send reset link");
35
+ }
36
+ } catch (err) {
37
+ setError("An unexpected error occurred");
38
+ } finally {
39
+ setIsLoading(false);
40
+ }
41
+ };
42
+
43
+ return (
44
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
45
+ <Card className="w-full max-w-md">
46
+ <CardHeader className="text-center">
47
+ <div className="flex justify-center mb-4">
48
+ <Shield className="h-12 w-12 text-primary" />
49
+ </div>
50
+ <CardTitle className="text-2xl">Reset Password</CardTitle>
51
+ <CardDescription>
52
+ {success
53
+ ? "Check your email for a reset link"
54
+ : "Enter your email address and we'll send you a link to reset your password."}
55
+ </CardDescription>
56
+ </CardHeader>
57
+ <CardContent>
58
+ {success ? (
59
+ <div className="space-y-6 text-center">
60
+ <div className="flex justify-center">
61
+ <div className="h-20 w-20 bg-primary/10 rounded-full flex items-center justify-center">
62
+ <Mail className="h-10 w-10 text-primary" />
63
+ </div>
64
+ </div>
65
+ <p className="text-sm text-muted-foreground">
66
+ We've sent a password reset link to <strong>{email}</strong>.
67
+ The link will expire in 1 hour.
68
+ </p>
69
+ <Button asChild variant="outline" className="w-full">
70
+ <Link href="/auth/login">Back to Sign in</Link>
71
+ </Button>
72
+ </div>
73
+ ) : (
74
+ <form onSubmit={handleSubmit} className="space-y-4">
75
+ {error && (
76
+ <Alert variant="destructive">
77
+ <AlertDescription>{error}</AlertDescription>
78
+ </Alert>
79
+ )}
80
+
81
+ <div className="space-y-2">
82
+ <Label htmlFor="email">Email address</Label>
83
+ <Input
84
+ id="email"
85
+ name="email"
86
+ type="email"
87
+ required
88
+ placeholder="name@example.com"
89
+ value={email}
90
+ onChange={(e) => setEmail(e.target.value)}
91
+ disabled={isLoading}
92
+ />
93
+ </div>
94
+
95
+ <Button type="submit" className="w-full" disabled={isLoading}>
96
+ {isLoading ? (
97
+ <>
98
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
99
+ Sending link...
100
+ </>
101
+ ) : (
102
+ "Send Reset Link"
103
+ )}
104
+ </Button>
105
+
106
+ <div className="text-center">
107
+ <Link
108
+ href="/auth/login"
109
+ className="inline-flex items-center text-sm text-primary hover:underline"
110
+ >
111
+ <ArrowLeft className="mr-2 h-4 w-4" />
112
+ Back to Sign in
113
+ </Link>
114
+ </div>
115
+ </form>
116
+ )}
117
+ </CardContent>
118
+ </Card>
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,145 @@
1
+ 'use client';
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { Alert, AlertDescription } from "@/components/ui/alert";
11
+ import { Shield, Loader2 } from "lucide-react";
12
+ import { useAuth } from "@/hooks/use-auth";
13
+
14
+ export default function LoginForm() {
15
+ const [email, setEmail] = useState("");
16
+ const [password, setPassword] = useState("");
17
+ const [mfaToken, setMfaToken] = useState("");
18
+ const [showMfaInput, setShowMfaInput] = useState(false);
19
+ const [error, setError] = useState("");
20
+ const [isLoading, setIsLoading] = useState(false);
21
+ const { login } = useAuth();
22
+ const router = useRouter();
23
+
24
+ const handleSubmit = async (e: React.FormEvent) => {
25
+ e.preventDefault();
26
+ setError("");
27
+ setIsLoading(true);
28
+
29
+ try {
30
+ const result = await login(email, password, mfaToken);
31
+ if (result.success) {
32
+ router.push("/dashboard");
33
+ } else if (result.mfaRequired) {
34
+ setShowMfaInput(true);
35
+ } else {
36
+ setError(result.error || "Login failed");
37
+ }
38
+ } catch (err) {
39
+ setError("An unexpected error occurred");
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
47
+ <Card className="w-full max-w-md">
48
+ <CardHeader className="text-center">
49
+ <div className="flex justify-center mb-4">
50
+ <Shield className="h-12 w-12 text-primary" />
51
+ </div>
52
+ <CardTitle className="text-2xl">Sign in to Nobalmako</CardTitle>
53
+ <CardDescription>
54
+ Welcome back! Please sign in to your account.
55
+ </CardDescription>
56
+ </CardHeader>
57
+ <CardContent>
58
+ <form onSubmit={handleSubmit} className="space-y-4">
59
+ {error && (
60
+ <Alert variant="destructive">
61
+ <AlertDescription>{error}</AlertDescription>
62
+ </Alert>
63
+ )}
64
+
65
+ <div className="space-y-2">
66
+ <Label htmlFor="email">Email address</Label>
67
+ <Input
68
+ id="email"
69
+ name="email"
70
+ type="email"
71
+ required
72
+ placeholder="Enter your email"
73
+ value={email}
74
+ onChange={(e) => setEmail(e.target.value)}
75
+ disabled={isLoading}
76
+ />
77
+ </div>
78
+
79
+ <div className="space-y-2">
80
+ <div className="flex items-center justify-between">
81
+ <Label htmlFor="password">Password</Label>
82
+ <Link
83
+ href="/auth/forgot-password"
84
+ className="text-xs text-primary hover:underline"
85
+ >
86
+ Forgot password?
87
+ </Link>
88
+ </div>
89
+ <Input
90
+ id="password"
91
+ name="password"
92
+ type="password"
93
+ required
94
+ placeholder="Enter your password"
95
+ value={password}
96
+ onChange={(e) => setPassword(e.target.value)}
97
+ disabled={isLoading}
98
+ />
99
+ </div>
100
+
101
+ {showMfaInput && (
102
+ <div className="space-y-2 animate-in slide-in-from-top duration-300">
103
+ <Label htmlFor="mfaToken">MFA Code</Label>
104
+ <Input
105
+ id="mfaToken"
106
+ name="mfaToken"
107
+ type="text"
108
+ required
109
+ placeholder="6-digit code from app"
110
+ value={mfaToken}
111
+ onChange={(e) => setMfaToken(e.target.value)}
112
+ disabled={isLoading}
113
+ autoFocus
114
+ />
115
+ <p className="text-xs text-muted-foreground">
116
+ Your account has MFA enabled. Please enter the code from your authenticator app.
117
+ </p>
118
+ </div>
119
+ )}
120
+
121
+ <Button type="submit" className="w-full" disabled={isLoading}>
122
+ {isLoading ? (
123
+ <>
124
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
125
+ Signing in...
126
+ </>
127
+ ) : (
128
+ "Sign in"
129
+ )}
130
+ </Button>
131
+ </form>
132
+
133
+ <div className="mt-6 text-center">
134
+ <p className="text-sm text-muted-foreground">
135
+ Don&apos;t have an account?{" "}
136
+ <Link href="/auth/register" className="text-primary hover:underline">
137
+ Sign up
138
+ </Link>
139
+ </p>
140
+ </div>
141
+ </CardContent>
142
+ </Card>
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,11 @@
1
+ import { Metadata } from "next";
2
+ import LoginForm from "./LoginForm";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Login - Nobalmako",
6
+ description: "Sign in to your Nobalmako account",
7
+ };
8
+
9
+ export default function LoginPage() {
10
+ return <LoginForm />;
11
+ }
@@ -0,0 +1,156 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { Alert, AlertDescription } from "@/components/ui/alert";
11
+ import { Shield, Loader2, Info } from "lucide-react";
12
+ import { useAuth } from "@/hooks/use-auth";
13
+
14
+ export default function RegisterForm() {
15
+ const [name, setName] = useState("");
16
+ const [email, setEmail] = useState("");
17
+ const [password, setPassword] = useState("");
18
+ const [confirmPassword, setConfirmPassword] = useState("");
19
+ const [error, setError] = useState("");
20
+ const [isLoading, setIsLoading] = useState(false);
21
+ const { register } = useAuth();
22
+ const router = useRouter();
23
+ const searchParams = useSearchParams();
24
+ const inviteToken = searchParams.get('invite');
25
+
26
+ const handleSubmit = async (e: React.FormEvent) => {
27
+ e.preventDefault();
28
+ setError("");
29
+ setIsLoading(true);
30
+
31
+ try {
32
+ const result = await register(name, email, password, confirmPassword, inviteToken || undefined);
33
+ if (result.success) {
34
+ router.push("/dashboard");
35
+ } else {
36
+ setError(result.error || "Registration failed");
37
+ }
38
+ } catch (err) {
39
+ setError("An unexpected error occurred");
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
47
+ <Card className="w-full max-w-md">
48
+ <CardHeader className="text-center">
49
+ <div className="flex justify-center mb-4">
50
+ <Shield className="h-12 w-12 text-primary" />
51
+ </div>
52
+ <CardTitle className="text-2xl">Create your account</CardTitle>
53
+ <CardDescription>
54
+ {inviteToken
55
+ ? "You've been invited! Join your team on Nobalmako."
56
+ : "Get started with Nobalmako today."}
57
+ </CardDescription>
58
+ </CardHeader>
59
+ <CardContent>
60
+ <form onSubmit={handleSubmit} className="space-y-4">
61
+ {inviteToken && (
62
+ <Alert className="bg-primary/5 border-primary/20 text-primary">
63
+ <Info className="h-4 w-4" />
64
+ <AlertDescription className="text-xs">
65
+ Your account will be automatically added to the team after registration.
66
+ </AlertDescription>
67
+ </Alert>
68
+ )}
69
+
70
+ {error && (
71
+ <Alert variant="destructive">
72
+ <AlertDescription>{error}</AlertDescription>
73
+ </Alert>
74
+ )}
75
+
76
+ <div className="space-y-2">
77
+ <Label htmlFor="name">Full name</Label>
78
+ <Input
79
+ id="name"
80
+ name="name"
81
+ type="text"
82
+ required
83
+ placeholder="Enter your full name"
84
+ value={name}
85
+ onChange={(e) => setName(e.target.value)}
86
+ disabled={isLoading}
87
+ />
88
+ </div>
89
+
90
+ <div className="space-y-2">
91
+ <Label htmlFor="email">Email address</Label>
92
+ <Input
93
+ id="email"
94
+ name="email"
95
+ type="email"
96
+ required
97
+ placeholder="Enter your email"
98
+ value={email}
99
+ onChange={(e) => setEmail(e.target.value)}
100
+ disabled={isLoading}
101
+ />
102
+ </div>
103
+
104
+ <div className="space-y-2">
105
+ <Label htmlFor="password">Password</Label>
106
+ <Input
107
+ id="password"
108
+ name="password"
109
+ type="password"
110
+ required
111
+ placeholder="Create a password"
112
+ value={password}
113
+ onChange={(e) => setPassword(e.target.value)}
114
+ disabled={isLoading}
115
+ />
116
+ </div>
117
+
118
+ <div className="space-y-2">
119
+ <Label htmlFor="confirmPassword">Confirm password</Label>
120
+ <Input
121
+ id="confirmPassword"
122
+ name="confirmPassword"
123
+ type="password"
124
+ required
125
+ placeholder="Confirm your password"
126
+ value={confirmPassword}
127
+ onChange={(e) => setConfirmPassword(e.target.value)}
128
+ disabled={isLoading}
129
+ />
130
+ </div>
131
+
132
+ <Button type="submit" className="w-full" disabled={isLoading}>
133
+ {isLoading ? (
134
+ <>
135
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
136
+ Creating account...
137
+ </>
138
+ ) : (
139
+ "Create account"
140
+ )}
141
+ </Button>
142
+ </form>
143
+
144
+ <div className="mt-6 text-center">
145
+ <p className="text-sm text-muted-foreground">
146
+ Already have an account?{" "}
147
+ <Link href="/auth/login" className="text-primary hover:underline">
148
+ Sign in
149
+ </Link>
150
+ </p>
151
+ </div>
152
+ </CardContent>
153
+ </Card>
154
+ </div>
155
+ );
156
+ }