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.
Files changed (28) hide show
  1. package/commands/interface/boilerplate.js +16 -2
  2. package/package.json +1 -1
  3. package/templates/interface-boilerplate/app/api/auth/[...all]/route.ts +52 -0
  4. package/templates/interface-boilerplate/app/auth/callback/page.tsx +34 -0
  5. package/templates/interface-boilerplate/app/dashboard/page.tsx +53 -0
  6. package/templates/interface-boilerplate/app/favicon.ico +0 -0
  7. package/templates/interface-boilerplate/app/globals.css +26 -0
  8. package/templates/interface-boilerplate/app/layout.tsx +34 -0
  9. package/templates/interface-boilerplate/app/page.tsx +50 -0
  10. package/templates/interface-boilerplate/app/settings/page.tsx +71 -0
  11. package/templates/interface-boilerplate/app/sign-in/page.tsx +19 -0
  12. package/templates/interface-boilerplate/app/sign-up/page.tsx +19 -0
  13. package/templates/interface-boilerplate/components/auth/sign-in-form.tsx +137 -0
  14. package/templates/interface-boilerplate/components/auth/sign-up-form.tsx +225 -0
  15. package/templates/interface-boilerplate/components/ui/badge.tsx +36 -0
  16. package/templates/interface-boilerplate/components/ui/button.tsx +57 -0
  17. package/templates/interface-boilerplate/components/ui/card.tsx +76 -0
  18. package/templates/interface-boilerplate/components/ui/input.tsx +22 -0
  19. package/templates/interface-boilerplate/eslint.config.mjs +18 -0
  20. package/templates/interface-boilerplate/lib/auth-client.ts +18 -0
  21. package/templates/interface-boilerplate/lib/auth.config.ts +111 -0
  22. package/templates/interface-boilerplate/lib/utils.ts +6 -0
  23. package/templates/interface-boilerplate/middleware.ts +60 -0
  24. package/templates/interface-boilerplate/next.config.ts +7 -0
  25. package/templates/interface-boilerplate/package-lock.json +6855 -0
  26. package/templates/interface-boilerplate/package.json +32 -0
  27. package/templates/interface-boilerplate/postcss.config.mjs +7 -0
  28. 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,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -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
+ };
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;