luxlabs 1.0.0 → 1.0.2
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/commands/interface/boilerplate.js +16 -2
- package/package.json +1 -1
- package/templates/interface-boilerplate/app/api/auth/[...all]/route.ts +52 -0
- package/templates/interface-boilerplate/app/auth/callback/page.tsx +34 -0
- package/templates/interface-boilerplate/app/dashboard/page.tsx +53 -0
- package/templates/interface-boilerplate/app/favicon.ico +0 -0
- package/templates/interface-boilerplate/app/globals.css +26 -0
- package/templates/interface-boilerplate/app/layout.tsx +34 -0
- package/templates/interface-boilerplate/app/page.tsx +50 -0
- package/templates/interface-boilerplate/app/settings/page.tsx +71 -0
- package/templates/interface-boilerplate/app/sign-in/page.tsx +19 -0
- package/templates/interface-boilerplate/app/sign-up/page.tsx +19 -0
- package/templates/interface-boilerplate/components/auth/sign-in-form.tsx +137 -0
- package/templates/interface-boilerplate/components/auth/sign-up-form.tsx +225 -0
- package/templates/interface-boilerplate/components/ui/badge.tsx +36 -0
- package/templates/interface-boilerplate/components/ui/button.tsx +57 -0
- package/templates/interface-boilerplate/components/ui/card.tsx +76 -0
- package/templates/interface-boilerplate/components/ui/input.tsx +22 -0
- package/templates/interface-boilerplate/eslint.config.mjs +18 -0
- package/templates/interface-boilerplate/lib/auth-client.ts +18 -0
- package/templates/interface-boilerplate/lib/auth.config.ts +111 -0
- package/templates/interface-boilerplate/lib/utils.ts +6 -0
- package/templates/interface-boilerplate/middleware.ts +60 -0
- package/templates/interface-boilerplate/next.config.ts +7 -0
- package/templates/interface-boilerplate/package-lock.json +6855 -0
- package/templates/interface-boilerplate/package.json +32 -0
- package/templates/interface-boilerplate/postcss.config.mjs +7 -0
- package/templates/interface-boilerplate/tsconfig.json +34 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { authClient } from "@/lib/auth-client";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export function SignUpForm() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [name, setName] = useState("");
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [password, setPassword] = useState("");
|
|
12
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
13
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
14
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState("");
|
|
17
|
+
|
|
18
|
+
const handleAccountSubmit = async (e: React.FormEvent) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setError("");
|
|
21
|
+
|
|
22
|
+
if (password !== confirmPassword) {
|
|
23
|
+
setError("Passwords do not match");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (password.length < 8) {
|
|
28
|
+
setError("Password must be at least 8 characters");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await authClient.signUp.email({
|
|
36
|
+
email,
|
|
37
|
+
password,
|
|
38
|
+
name,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.push("/dashboard");
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
if (err.message?.includes("already exists") || err.message?.includes("UNIQUE constraint")) {
|
|
44
|
+
setError("An account with this email already exists. Please sign in instead.");
|
|
45
|
+
} else {
|
|
46
|
+
setError(err.message || "Failed to create account");
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleGoogleSignUp = async () => {
|
|
54
|
+
setError("");
|
|
55
|
+
try {
|
|
56
|
+
await authClient.signIn.social({
|
|
57
|
+
provider: "google",
|
|
58
|
+
callbackURL: "/dashboard",
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setError("Failed to sign up with Google");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-6">
|
|
67
|
+
{/* Google Sign Up */}
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleGoogleSignUp}
|
|
70
|
+
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|
71
|
+
>
|
|
72
|
+
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
|
73
|
+
<path
|
|
74
|
+
fill="#4285F4"
|
|
75
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
76
|
+
/>
|
|
77
|
+
<path
|
|
78
|
+
fill="#34A853"
|
|
79
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
80
|
+
/>
|
|
81
|
+
<path
|
|
82
|
+
fill="#FBBC05"
|
|
83
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
84
|
+
/>
|
|
85
|
+
<path
|
|
86
|
+
fill="#EA4335"
|
|
87
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
88
|
+
/>
|
|
89
|
+
</svg>
|
|
90
|
+
Continue with Google
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<div className="relative">
|
|
94
|
+
<div className="absolute inset-0 flex items-center">
|
|
95
|
+
<div className="w-full border-t border-gray-300" />
|
|
96
|
+
</div>
|
|
97
|
+
<div className="relative flex justify-center text-sm">
|
|
98
|
+
<span className="bg-white px-2 text-gray-500">Or sign up with email</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Sign Up Form */}
|
|
103
|
+
<form onSubmit={handleAccountSubmit} className="space-y-4">
|
|
104
|
+
<div>
|
|
105
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
106
|
+
Name
|
|
107
|
+
</label>
|
|
108
|
+
<input
|
|
109
|
+
type="text"
|
|
110
|
+
value={name}
|
|
111
|
+
onChange={(e) => setName(e.target.value)}
|
|
112
|
+
required
|
|
113
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
114
|
+
placeholder="John Doe"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
120
|
+
Email
|
|
121
|
+
</label>
|
|
122
|
+
<input
|
|
123
|
+
type="email"
|
|
124
|
+
value={email}
|
|
125
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
126
|
+
required
|
|
127
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
128
|
+
placeholder="you@example.com"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div>
|
|
133
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
134
|
+
Password
|
|
135
|
+
</label>
|
|
136
|
+
<div className="relative">
|
|
137
|
+
<input
|
|
138
|
+
type={showPassword ? "text" : "password"}
|
|
139
|
+
value={password}
|
|
140
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
141
|
+
required
|
|
142
|
+
minLength={8}
|
|
143
|
+
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
144
|
+
placeholder="••••••••"
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
149
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
150
|
+
>
|
|
151
|
+
{showPassword ? (
|
|
152
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
153
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
154
|
+
</svg>
|
|
155
|
+
) : (
|
|
156
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
157
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
158
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
159
|
+
</svg>
|
|
160
|
+
)}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
164
|
+
Must be at least 8 characters
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div>
|
|
169
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
170
|
+
Confirm Password
|
|
171
|
+
</label>
|
|
172
|
+
<div className="relative">
|
|
173
|
+
<input
|
|
174
|
+
type={showConfirmPassword ? "text" : "password"}
|
|
175
|
+
value={confirmPassword}
|
|
176
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
177
|
+
required
|
|
178
|
+
minLength={8}
|
|
179
|
+
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
180
|
+
placeholder="••••••••"
|
|
181
|
+
/>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
185
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
186
|
+
>
|
|
187
|
+
{showConfirmPassword ? (
|
|
188
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
189
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
190
|
+
</svg>
|
|
191
|
+
) : (
|
|
192
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
193
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
194
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
195
|
+
</svg>
|
|
196
|
+
)}
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{error && (
|
|
202
|
+
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
|
203
|
+
{error}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<button
|
|
208
|
+
type="submit"
|
|
209
|
+
disabled={isLoading}
|
|
210
|
+
className="w-full px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
211
|
+
>
|
|
212
|
+
{isLoading ? "Creating account..." : "Sign Up"}
|
|
213
|
+
</button>
|
|
214
|
+
</form>
|
|
215
|
+
|
|
216
|
+
{/* Sign In Link */}
|
|
217
|
+
<div className="text-center text-sm text-gray-600">
|
|
218
|
+
Already have an account?{" "}
|
|
219
|
+
<a href="/sign-in" className="text-gray-900 hover:underline font-medium">
|
|
220
|
+
Sign in
|
|
221
|
+
</a>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
"border-transparent bg-gray-900 text-white shadow hover:bg-gray-800",
|
|
13
|
+
secondary:
|
|
14
|
+
"border-transparent bg-gray-100 text-gray-900 hover:bg-gray-200",
|
|
15
|
+
destructive:
|
|
16
|
+
"border-transparent bg-red-600 text-white shadow hover:bg-red-700",
|
|
17
|
+
outline: "text-gray-900 border-gray-300",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export interface BadgeProps
|
|
27
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
28
|
+
VariantProps<typeof badgeVariants> {}
|
|
29
|
+
|
|
30
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-gray-900 text-white shadow hover:bg-gray-800",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-red-600 text-white shadow-sm hover:bg-red-700",
|
|
16
|
+
outline:
|
|
17
|
+
"border border-gray-300 bg-white shadow-sm hover:bg-gray-50",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-gray-100 text-gray-900 shadow-sm hover:bg-gray-200",
|
|
20
|
+
ghost: "hover:bg-gray-100",
|
|
21
|
+
link: "text-gray-900 underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2",
|
|
25
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
26
|
+
lg: "h-10 rounded-md px-8",
|
|
27
|
+
icon: "h-9 w-9",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
size: "default",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export interface ButtonProps
|
|
38
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
39
|
+
VariantProps<typeof buttonVariants> {
|
|
40
|
+
asChild?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
44
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
45
|
+
const Comp = asChild ? Slot : "button"
|
|
46
|
+
return (
|
|
47
|
+
<Comp
|
|
48
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
49
|
+
ref={ref}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
Button.displayName = "Button"
|
|
56
|
+
|
|
57
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"rounded-xl border border-gray-200 bg-white shadow",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Card.displayName = "Card"
|
|
19
|
+
|
|
20
|
+
const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
))
|
|
30
|
+
CardHeader.displayName = "CardHeader"
|
|
31
|
+
|
|
32
|
+
const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLDivElement,
|
|
34
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<div
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
))
|
|
42
|
+
CardTitle.displayName = "CardTitle"
|
|
43
|
+
|
|
44
|
+
const CardDescription = React.forwardRef<
|
|
45
|
+
HTMLDivElement,
|
|
46
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("text-sm text-gray-500", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
))
|
|
54
|
+
CardDescription.displayName = "CardDescription"
|
|
55
|
+
|
|
56
|
+
const CardContent = React.forwardRef<
|
|
57
|
+
HTMLDivElement,
|
|
58
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
61
|
+
))
|
|
62
|
+
CardContent.displayName = "CardContent"
|
|
63
|
+
|
|
64
|
+
const CardFooter = React.forwardRef<
|
|
65
|
+
HTMLDivElement,
|
|
66
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<div
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
))
|
|
74
|
+
CardFooter.displayName = "CardFooter"
|
|
75
|
+
|
|
76
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
6
|
+
({ className, type, ...props }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type={type}
|
|
10
|
+
className={cn(
|
|
11
|
+
"flex h-9 w-full rounded-md border border-gray-300 bg-white px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
ref={ref}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
Input.displayName = "Input"
|
|
21
|
+
|
|
22
|
+
export { Input }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
|
+
// Auth client configured to use the Lux Studio API
|
|
4
|
+
// The API URL and Org ID are injected at build/runtime
|
|
5
|
+
const baseURL = process.env.NEXT_PUBLIC_LUX_API_URL || "https://v2.uselux.ai";
|
|
6
|
+
const orgId = process.env.NEXT_PUBLIC_LUX_ORG_ID || "";
|
|
7
|
+
|
|
8
|
+
export const authClient = createAuthClient({
|
|
9
|
+
baseURL: `${baseURL}/api/auth`,
|
|
10
|
+
fetchOptions: {
|
|
11
|
+
headers: {
|
|
12
|
+
"X-Org-Id": orgId,
|
|
13
|
+
},
|
|
14
|
+
credentials: "include",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const { signIn, signUp, signOut, useSession } = authClient;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file defines which routes require authentication and which are public.
|
|
5
|
+
* Update this file to control access to different parts of your application.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const authConfig = {
|
|
9
|
+
/**
|
|
10
|
+
* Routes that require authentication
|
|
11
|
+
* Users will be redirected to sign-in if not authenticated
|
|
12
|
+
*/
|
|
13
|
+
protectedRoutes: [
|
|
14
|
+
"/dashboard",
|
|
15
|
+
"/profile",
|
|
16
|
+
"/settings",
|
|
17
|
+
"/account",
|
|
18
|
+
"/auth/callback", // Organization setup after OAuth
|
|
19
|
+
// Add more protected routes here
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Auth pages (sign-in, sign-up, etc.)
|
|
24
|
+
* Authenticated users will be redirected away from these pages
|
|
25
|
+
*/
|
|
26
|
+
authRoutes: [
|
|
27
|
+
"/sign-in",
|
|
28
|
+
"/sign-up",
|
|
29
|
+
"/verify-email",
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Public routes that anyone can access
|
|
34
|
+
* These routes are accessible whether authenticated or not
|
|
35
|
+
*/
|
|
36
|
+
publicRoutes: [
|
|
37
|
+
"/",
|
|
38
|
+
"/about",
|
|
39
|
+
"/pricing",
|
|
40
|
+
"/contact",
|
|
41
|
+
"/blog",
|
|
42
|
+
"/docs",
|
|
43
|
+
"/accept-invite",
|
|
44
|
+
// Add more public routes here
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Default redirect destinations
|
|
49
|
+
*/
|
|
50
|
+
redirects: {
|
|
51
|
+
// Where to redirect after successful sign-in
|
|
52
|
+
afterSignIn: "/dashboard",
|
|
53
|
+
|
|
54
|
+
// Where to redirect authenticated users who try to access auth pages
|
|
55
|
+
afterAuth: "/dashboard",
|
|
56
|
+
|
|
57
|
+
// Where to redirect unauthenticated users
|
|
58
|
+
toSignIn: "/sign-in",
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Route matching options
|
|
63
|
+
*/
|
|
64
|
+
options: {
|
|
65
|
+
// If true, "/dashboard" will also match "/dashboard/settings"
|
|
66
|
+
matchPrefixes: true,
|
|
67
|
+
|
|
68
|
+
// If true, "/" is treated as a public route even if not in publicRoutes
|
|
69
|
+
rootIsPublic: true,
|
|
70
|
+
},
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Helper function to check if a route matches a pattern
|
|
75
|
+
*/
|
|
76
|
+
export function isRouteMatch(pathname: string, pattern: string, matchPrefix = true): boolean {
|
|
77
|
+
if (matchPrefix) {
|
|
78
|
+
return pathname === pattern || pathname.startsWith(pattern + "/");
|
|
79
|
+
}
|
|
80
|
+
return pathname === pattern;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Helper function to determine route type
|
|
85
|
+
*/
|
|
86
|
+
export function getRouteType(pathname: string): "protected" | "auth" | "public" {
|
|
87
|
+
const { protectedRoutes, authRoutes, publicRoutes, options } = authConfig;
|
|
88
|
+
|
|
89
|
+
// Check if it's a protected route
|
|
90
|
+
if (protectedRoutes.some(route => isRouteMatch(pathname, route, options.matchPrefixes))) {
|
|
91
|
+
return "protected";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check if it's an auth route
|
|
95
|
+
if (authRoutes.some(route => isRouteMatch(pathname, route, options.matchPrefixes))) {
|
|
96
|
+
return "auth";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if it's explicitly public
|
|
100
|
+
if (publicRoutes.some(route => isRouteMatch(pathname, route, options.matchPrefixes))) {
|
|
101
|
+
return "public";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Special case for root
|
|
105
|
+
if (pathname === "/" && options.rootIsPublic) {
|
|
106
|
+
return "public";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Default: treat as public (change to "protected" for whitelist approach)
|
|
110
|
+
return "public";
|
|
111
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
import { authConfig, getRouteType } from "@/lib/auth.config";
|
|
4
|
+
|
|
5
|
+
export async function middleware(request: NextRequest) {
|
|
6
|
+
const { pathname } = request.nextUrl;
|
|
7
|
+
|
|
8
|
+
// Check if user has a session cookie
|
|
9
|
+
const sessionToken = request.cookies.get("better-auth.session_token");
|
|
10
|
+
const isAuthenticated = !!sessionToken;
|
|
11
|
+
|
|
12
|
+
// Special handling for API routes
|
|
13
|
+
if (pathname.startsWith("/api/")) {
|
|
14
|
+
// API auth routes are always allowed
|
|
15
|
+
if (pathname.startsWith("/api/auth/")) {
|
|
16
|
+
return NextResponse.next();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// All other API routes require authentication
|
|
20
|
+
if (!isAuthenticated) {
|
|
21
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return NextResponse.next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Page route handling (non-API)
|
|
28
|
+
// Determine route type from config
|
|
29
|
+
const routeType = getRouteType(pathname);
|
|
30
|
+
|
|
31
|
+
// Handle auth routes (signin, signup, etc.)
|
|
32
|
+
// Redirect authenticated users away from auth pages
|
|
33
|
+
if (routeType === "auth" && isAuthenticated) {
|
|
34
|
+
return NextResponse.redirect(new URL(authConfig.redirects.afterAuth, request.url));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle protected routes
|
|
38
|
+
// Redirect unauthenticated users to signin
|
|
39
|
+
if (routeType === "protected" && !isAuthenticated) {
|
|
40
|
+
const signInUrl = new URL(authConfig.redirects.toSignIn, request.url);
|
|
41
|
+
// Preserve the original destination for redirect after sign-in
|
|
42
|
+
signInUrl.searchParams.set("callbackUrl", pathname);
|
|
43
|
+
return NextResponse.redirect(signInUrl);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Allow access to public routes
|
|
47
|
+
return NextResponse.next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const config = {
|
|
51
|
+
matcher: [
|
|
52
|
+
/*
|
|
53
|
+
* Match all request paths except:
|
|
54
|
+
* - _next/static (static files)
|
|
55
|
+
* - _next/image (image optimization)
|
|
56
|
+
* - favicon.ico (favicon file)
|
|
57
|
+
*/
|
|
58
|
+
"/((?!_next/static|_next/image|favicon.ico).*)",
|
|
59
|
+
],
|
|
60
|
+
};
|