mirror of
https://github.com/langchain-ai/open-agent-platform.git
synced 2026-07-01 20:24:10 -04:00
feat: Add auth
This commit is contained in:
@@ -45,3 +45,5 @@ credentials.json
|
||||
|
||||
# LangGraph API
|
||||
.langgraph_api
|
||||
|
||||
**/.claude/settings.local.json
|
||||
|
||||
@@ -25,4 +25,10 @@ NEXT_PUBLIC_DEPLOYMENTS="[]"
|
||||
NEXT_PUBLIC_TOOLS="[]"
|
||||
|
||||
# The RAG API URL for the platform.
|
||||
NEXT_PUBLIC_RAG_API_URL="http://localhost:8080"
|
||||
NEXT_PUBLIC_RAG_API_URL="http://localhost:8080"
|
||||
|
||||
# Supabase Authentication
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=""
|
||||
NEXT_PUBLIC_SUPABASE_URL="https://<project_id>.supabase.co"
|
||||
# SECRET: DO NOT EXPOSE
|
||||
SUPABASE_SERVICE_ROLE_KEY=""
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Authentication Fixes Summary
|
||||
|
||||
This document summarizes the changes made to fix the authentication system in the Open Agent Platform web application.
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
1. **Middleware Authentication Checks**
|
||||
|
||||
- The middleware was looking for the wrong cookie name (`sb-auth-token` instead of the Supabase standard `sb-<project-ref>-auth-token`)
|
||||
- Added better handling for static asset paths to prevent redirects on resource loading
|
||||
- Implemented a more robust authenticated state detection
|
||||
|
||||
2. **Supabase Authentication Flow**
|
||||
|
||||
- Created a singleton Supabase client to prevent multiple client instances
|
||||
- Improved error handling and logging for authentication operations
|
||||
- Added proper environment variable validation
|
||||
- Fixed session handling and persistence issues
|
||||
|
||||
3. **OAuth Callback Handler**
|
||||
|
||||
- Enhanced error handling in the callback route
|
||||
- Improved redirect handling after authentication
|
||||
- Fixed the OAuth session exchange process
|
||||
|
||||
4. **Environment Variables Setup**
|
||||
|
||||
- Created a dedicated environment variable utility
|
||||
- Added validation for required Supabase environment variables
|
||||
- Improved error reporting for missing configurations
|
||||
|
||||
5. **Sign-in Page Improvements**
|
||||
- Added support for error messages from URL parameters
|
||||
- Improved redirect handling after successful authentication
|
||||
- Enhanced user feedback during the authentication process
|
||||
|
||||
## Key Files Modified
|
||||
|
||||
1. **Middleware (`/src/middleware.ts`)**
|
||||
|
||||
- Updated authentication detection to work with Supabase's cookie naming convention
|
||||
- Improved path filtering to prevent unnecessary redirects
|
||||
|
||||
2. **Supabase Implementation**
|
||||
|
||||
- Created `/src/lib/auth/supabase-client.ts` - Singleton client implementation
|
||||
- Updated `/src/lib/auth/supabase.ts` - Authentication provider implementation
|
||||
- Added `/src/lib/auth/env.ts` - Environment variable utilities
|
||||
|
||||
3. **Authentication Pages**
|
||||
|
||||
- Updated sign-in page to handle errors and redirects better
|
||||
- Enhanced callback handling in `/src/app/api/auth/callback/route.ts`
|
||||
|
||||
4. **Debugging Tools**
|
||||
- Added a debugging component at `/src/components/auth/debug.tsx`
|
||||
- Created a debug page at `/src/app/debug-auth/page.tsx`
|
||||
|
||||
## How to Test the Changes
|
||||
|
||||
1. **Normal Sign-In Flow**
|
||||
|
||||
- Go to `/signin` and sign in with your email and password
|
||||
- You should be redirected to the home page after successful authentication
|
||||
|
||||
2. **Protected Pages**
|
||||
|
||||
- Try to access a protected page while not logged in
|
||||
- You should be redirected to the sign-in page
|
||||
- After signing in, you should be redirected back to the page you tried to access
|
||||
|
||||
3. **Debugging**
|
||||
- Visit `/debug-auth` to see the current authentication state
|
||||
- Use the debug component to test sign-out functionality
|
||||
|
||||
## Configuration Requirements
|
||||
|
||||
Ensure your `.env.local` file contains the following variables:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_SUPABASE_URL="https://<your-project-id>.supabase.co"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY="<your-anon-key>"
|
||||
SUPABASE_SERVICE_ROLE_KEY="<your-service-role-key>" # Optional, for admin operations
|
||||
```
|
||||
|
||||
These variables can be found in your Supabase project settings under API settings.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
The authentication system is designed to be provider-agnostic through the use of interfaces. The current implementation uses Supabase, but the abstraction allows for easy switching to other providers in the future. The key components are:
|
||||
|
||||
1. **Authentication Interface** (`/src/lib/auth/types.ts`)
|
||||
|
||||
- Defines the contract for all authentication providers
|
||||
|
||||
2. **Supabase Implementation** (`/src/lib/auth/supabase.ts`)
|
||||
|
||||
- Implements the authentication interface using Supabase
|
||||
|
||||
3. **Auth Context Provider** (`/src/providers/Auth.tsx`)
|
||||
- Provides authentication state to the entire application
|
||||
- Can be configured with different authentication providers
|
||||
@@ -0,0 +1,67 @@
|
||||
# Supabase Authentication Setup
|
||||
|
||||
This document outlines the steps needed to integrate Supabase authentication into the Open Agent Platform application.
|
||||
|
||||
## Installation
|
||||
|
||||
Add the required Supabase packages to your project:
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables must be set:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_SUPABASE_URL="https://your-supabase-project.supabase.co"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY="your-supabase-anon-key"
|
||||
```
|
||||
|
||||
Add these to your `.env.local` file.
|
||||
|
||||
## Supabase Configuration
|
||||
|
||||
1. Create a new project on [Supabase](https://supabase.com/)
|
||||
2. Enable authentication in the Supabase dashboard:
|
||||
- Go to Authentication → Settings
|
||||
- Configure site URL and redirect URLs
|
||||
- Enable the Email/Password authentication method
|
||||
3. For Google OAuth:
|
||||
- Set up a project in the Google Cloud Console
|
||||
- Create OAuth credentials
|
||||
- Add the client ID and secret to Supabase
|
||||
- Configure the redirect URL in both Google and Supabase
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
The application uses a modular authentication system:
|
||||
|
||||
1. `/src/lib/auth/types.ts` defines the interfaces for authentication
|
||||
2. `/src/lib/auth/supabase.ts` implements these interfaces using Supabase
|
||||
3. `/src/providers/Auth.tsx` provides authentication context to the application
|
||||
|
||||
This design allows you to easily swap out Supabase with another authentication provider by:
|
||||
|
||||
- Creating a new implementation that follows the `AuthProvider` interface
|
||||
- Updating the provider used in `Auth.tsx`
|
||||
|
||||
## Protected Routes
|
||||
|
||||
The application uses a middleware in `/src/middleware.ts` to protect routes that require authentication.
|
||||
|
||||
## Auth Pages
|
||||
|
||||
The following pages are implemented:
|
||||
|
||||
- Sign In: `/app/(auth)/signin/page.tsx`
|
||||
- Sign Up: `/app/(auth)/signup/page.tsx`
|
||||
- Forgot Password: `/app/(auth)/forgot-password/page.tsx`
|
||||
- Reset Password: `/app/(auth)/reset-password/page.tsx`
|
||||
|
||||
These pages use the route group `(auth)` to share a layout that doesn't include the sidebar.
|
||||
|
||||
## OAuth Callback
|
||||
|
||||
The route `/api/auth/callback/route.ts` handles OAuth redirect callbacks from providers like Google.
|
||||
@@ -37,6 +37,8 @@
|
||||
"@radix-ui/react-tabs": "^1.1.7",
|
||||
"@radix-ui/react-toast": "1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.2.3",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const { resetPassword } = useAuthContext();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
if (!email) {
|
||||
setError("Please enter your email address");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { error } = await resetPassword(email);
|
||||
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
setEmail("");
|
||||
} catch (err) {
|
||||
console.error("Password reset error:", err);
|
||||
setError("An unexpected error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container flex min-h-screen items-center justify-center py-10">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl">Reset Password</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Enter your email address and we'll send you a link to reset your
|
||||
password
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{success ? (
|
||||
<Alert className="mb-4">
|
||||
<AlertDescription>
|
||||
If your email address is associated with an account, you will
|
||||
receive an email with instructions to reset your password
|
||||
shortly.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Sending..." : "Send Reset Link"}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Remember your password?{" "}
|
||||
<Link
|
||||
href="/signin"
|
||||
className="text-primary font-medium hover:underline"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// This layout is for auth pages that don't need the sidebar
|
||||
import React from "react";
|
||||
import AuthLayout from "../auth-layout";
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return <AuthLayout>{children}</AuthLayout>;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
import { z } from "zod";
|
||||
|
||||
// Form validation schema
|
||||
const resetPasswordSchema = z
|
||||
.object({
|
||||
password: z
|
||||
.string()
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
|
||||
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
|
||||
.regex(/[0-9]/, "Password must contain at least one number"),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const { updatePassword } = useAuthContext();
|
||||
const router = useRouter();
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [errors, setErrors] = useState<{
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const validateForm = () => {
|
||||
try {
|
||||
resetPasswordSchema.parse({ password, confirmPassword });
|
||||
setErrors({});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const formattedErrors: { password?: string; confirmPassword?: string } =
|
||||
{};
|
||||
error.errors.forEach((err) => {
|
||||
if (err.path[0]) {
|
||||
formattedErrors[err.path[0] as "password" | "confirmPassword"] =
|
||||
err.message;
|
||||
}
|
||||
});
|
||||
setErrors(formattedErrors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setAuthError(null);
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { error } = await updatePassword(password);
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to the sign-in page with a success message
|
||||
router.push(
|
||||
"/signin?message=Your password has been successfully reset. Please sign in with your new password.",
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Password reset error:", err);
|
||||
setAuthError("An unexpected error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container flex min-h-screen items-center justify-center py-10">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl">Reset Password</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Please enter your new password
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">New Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
placeholder="Create a new password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
aria-invalid={!!errors.password}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-destructive text-sm">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<PasswordInput
|
||||
id="confirmPassword"
|
||||
placeholder="Confirm your new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
aria-invalid={!!errors.confirmPassword}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-destructive text-sm">
|
||||
{errors.confirmPassword}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{authError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Resetting..." : "Reset Password"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
export default function SigninPage() {
|
||||
const { signIn, signInWithGoogle, isAuthenticated } = useAuthContext();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isSuccess, setIsSuccess] = useState<boolean>(false);
|
||||
const [showManualRedirect, setShowManualRedirect] = useState<boolean>(false);
|
||||
|
||||
// Handle URL parameters
|
||||
useEffect(() => {
|
||||
// Check for message parameter
|
||||
const urlMessage = searchParams.get("message");
|
||||
if (urlMessage) {
|
||||
setMessage(urlMessage);
|
||||
}
|
||||
|
||||
// Check for error parameter
|
||||
const urlError = searchParams.get("error");
|
||||
if (urlError) {
|
||||
setError(urlError);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
console.log("redirecting to /")
|
||||
router.push("/");
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!email || !password) {
|
||||
setError("Please enter both email and password");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await signIn({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show success message and set up manual redirect timer
|
||||
setIsSuccess(true);
|
||||
console.log("Sign in successful", result);
|
||||
|
||||
// Set a timer to show manual redirect button after 5 seconds
|
||||
setTimeout(() => {
|
||||
setShowManualRedirect(true);
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
console.error("Sign in error:", err);
|
||||
setError("An unexpected error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const { error } = await signInWithGoogle();
|
||||
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
// Show success message for Google sign-in too
|
||||
setIsSuccess(true);
|
||||
|
||||
// Set a timer to show manual redirect button after 5 seconds
|
||||
setTimeout(() => {
|
||||
setShowManualRedirect(true);
|
||||
}, 5000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Google sign in error:", err);
|
||||
setError("An error occurred while signing in with Google.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container flex min-h-screen items-center justify-center py-10">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl">Sign In</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Welcome back to Open Agent Platform
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{message && !isSuccess && (
|
||||
<Alert className="mb-4">
|
||||
<AlertDescription>{message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
<Alert className="mb-4 bg-green-50 text-green-800 border-green-200">
|
||||
<AlertDescription className="flex flex-col gap-2">
|
||||
<span>Success! We're redirecting you to the dashboard...</span>
|
||||
{showManualRedirect && (
|
||||
<Button
|
||||
onClick={() => router.push('/')}
|
||||
variant="outline"
|
||||
className="mt-2 border-green-300 text-green-700 hover:bg-green-100"
|
||||
>
|
||||
Go to Dashboard Now
|
||||
</Button>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-primary text-sm font-medium hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading || isSuccess}
|
||||
>
|
||||
{isLoading ? "Signing in..." : isSuccess ? "Signed In Successfully" : "Sign In"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isLoading || isSuccess}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M20.283 10.356h-8.327v3.451h4.792c-.446 2.193-2.313 3.453-4.792 3.453a5.27 5.27 0 0 1-5.279-5.28 5.27 5.27 0 0 1 5.279-5.279c1.259 0 2.397.447 3.29 1.178l2.6-2.599c-1.584-1.381-3.615-2.233-5.89-2.233a8.908 8.908 0 0 0-8.934 8.934 8.907 8.907 0 0 0 8.934 8.934c4.467 0 8.529-3.249 8.529-8.934 0-.528-.081-1.097-.202-1.625z"></path>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="text-primary font-medium hover:underline"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
// Form validation schema
|
||||
const signupSchema = z
|
||||
.object({
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
companyName: z.string().optional(),
|
||||
email: z.string().email("Please enter a valid email address"),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
|
||||
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
|
||||
.regex(/[0-9]/, "Password must contain at least one number"),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type SignupFormValues = z.infer<typeof signupSchema>;
|
||||
|
||||
export default function SignupPage() {
|
||||
const { signUp, signInWithGoogle } = useAuthContext();
|
||||
const router = useRouter();
|
||||
|
||||
const [formValues, setFormValues] = useState<Partial<SignupFormValues>>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
companyName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<
|
||||
Partial<Record<keyof SignupFormValues, string>>
|
||||
>({});
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const validateForm = () => {
|
||||
try {
|
||||
signupSchema.parse(formValues);
|
||||
setErrors({});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const newErrors: Partial<Record<keyof SignupFormValues, string>> = {};
|
||||
error.errors.forEach((err) => {
|
||||
if (err.path[0]) {
|
||||
newErrors[err.path[0] as keyof SignupFormValues] = err.message;
|
||||
}
|
||||
});
|
||||
setErrors(newErrors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setAuthError(null);
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { error } = await signUp({
|
||||
email: formValues.email!,
|
||||
password: formValues.password!,
|
||||
metadata: {
|
||||
first_name: formValues.firstName,
|
||||
last_name: formValues.lastName,
|
||||
company_name: formValues.companyName || null,
|
||||
name: `${formValues.firstName} ${formValues.lastName}`.trim(),
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// On success, redirect to a confirmation page or dashboard
|
||||
router.push(
|
||||
"/signin?message=Please check your email to confirm your account",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Signup error:", error);
|
||||
setAuthError("An unexpected error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleSignup = async () => {
|
||||
setIsLoading(true);
|
||||
setAuthError(null);
|
||||
|
||||
try {
|
||||
const { error } = await signInWithGoogle();
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
}
|
||||
// The redirect will be handled by the OAuth provider
|
||||
} catch (error) {
|
||||
console.error("Google signup error:", error);
|
||||
setAuthError("An error occurred while signing up with Google.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container flex min-h-screen items-center justify-center py-10">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl">
|
||||
Create an Account
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Sign up to get started with Open Agent Platform
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
placeholder="John"
|
||||
value={formValues.firstName || ""}
|
||||
onChange={handleInputChange}
|
||||
aria-invalid={!!errors.firstName}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<p className="text-destructive text-sm">{errors.firstName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
placeholder="Doe"
|
||||
value={formValues.lastName || ""}
|
||||
onChange={handleInputChange}
|
||||
aria-invalid={!!errors.lastName}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<p className="text-destructive text-sm">{errors.lastName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyName">Company Name <span className="text-muted-foreground text-xs">(Optional)</span></Label>
|
||||
<Input
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
placeholder="Your Company Inc."
|
||||
value={formValues.companyName || ""}
|
||||
onChange={handleInputChange}
|
||||
aria-invalid={!!errors.companyName}
|
||||
/>
|
||||
{errors.companyName && (
|
||||
<p className="text-destructive text-sm">{errors.companyName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={formValues.email || ""}
|
||||
onChange={handleInputChange}
|
||||
aria-invalid={!!errors.email}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-destructive text-sm">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Create a password"
|
||||
value={formValues.password || ""}
|
||||
onChange={handleInputChange}
|
||||
aria-invalid={!!errors.password}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-destructive text-sm">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<PasswordInput
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
value={formValues.confirmPassword || ""}
|
||||
onChange={handleInputChange}
|
||||
aria-invalid={!!errors.confirmPassword}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-destructive text-sm">
|
||||
{errors.confirmPassword}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{authError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Creating Account..." : "Create Account"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
onClick={handleGoogleSignup}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M20.283 10.356h-8.327v3.451h4.792c-.446 2.193-2.313 3.453-4.792 3.453a5.27 5.27 0 0 1-5.279-5.28 5.27 5.27 0 0 1 5.279-5.279c1.259 0 2.397.447 3.29 1.178l2.6-2.599c-1.584-1.381-3.615-2.233-5.89-2.233a8.908 8.908 0 0 0-8.934 8.934 8.907 8.907 0 0 0 8.934 8.934c4.467 0 8.529-3.249 8.529-8.934 0-.528-.081-1.097-.202-1.625z"></path>
|
||||
</svg>
|
||||
Sign up with Google
|
||||
</Button>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/signin"
|
||||
className="text-primary font-medium hover:underline"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSupabaseClient } from "@/lib/auth/supabase-client";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Parse the URL and get the code parameter
|
||||
const requestUrl = new URL(request.url);
|
||||
const code = requestUrl.searchParams.get("code");
|
||||
|
||||
// Get the redirect destination (or default to home)
|
||||
const redirectTo = requestUrl.searchParams.get("redirect") || "/";
|
||||
|
||||
if (code) {
|
||||
// Get Supabase client
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
// Exchange the code for a session
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
||||
|
||||
if (error) {
|
||||
console.error("Error exchanging code for session:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Successfully authenticated
|
||||
console.log("Authentication successful, redirecting to:", redirectTo);
|
||||
} else {
|
||||
console.log("No code parameter found in URL");
|
||||
}
|
||||
|
||||
// Redirect to the requested page or home
|
||||
return NextResponse.redirect(new URL(redirectTo, request.url));
|
||||
} catch (error) {
|
||||
console.error("Auth callback error:", error);
|
||||
|
||||
// In case of error, redirect to sign-in with error message
|
||||
const errorUrl = new URL("/signin", request.url);
|
||||
errorUrl.searchParams.set(
|
||||
"error",
|
||||
"Authentication failed. Please try again.",
|
||||
);
|
||||
|
||||
return NextResponse.redirect(errorUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
// Layout component for pages that don't need the sidebar (auth pages)
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const { isLoading } = useAuthContext();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container flex min-h-screen items-center justify-center py-10">
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<Skeleton className="mx-auto h-8 w-3/4" />
|
||||
<Skeleton className="mx-auto h-4 w-1/2" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { AuthDebug } from "@/components/auth/debug";
|
||||
|
||||
export default function DebugAuthPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-10">
|
||||
<h1 className="mb-6 text-2xl font-bold">Authentication Debug</h1>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
This page allows you to debug the current authentication state. It is
|
||||
only accessible in development mode.
|
||||
</p>
|
||||
|
||||
<AuthDebug />
|
||||
|
||||
<div className="bg-muted/30 mt-8 rounded-lg border p-4">
|
||||
<h2 className="mb-2 text-lg font-medium">Testing Tips</h2>
|
||||
<ul className="list-inside list-disc space-y-1">
|
||||
<li>Use the Sign Out button to clear your current session</li>
|
||||
<li>Check your browser's developer tools to inspect cookies</li>
|
||||
<li>
|
||||
Look for cookies starting with{" "}
|
||||
<code className="bg-muted rounded px-1 py-0.5 text-sm">sb-</code>
|
||||
</li>
|
||||
<li>Use incognito/private browsing for testing new sign-ups</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Inter } from "next/font/google";
|
||||
import React from "react";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { SidebarLayout } from "@/components/sidebar";
|
||||
import { AuthProvider } from "@/providers/Auth";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -25,7 +26,9 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<NuqsAdapter>
|
||||
<SidebarLayout>{children}</SidebarLayout>
|
||||
<AuthProvider>
|
||||
<SidebarLayout>{children}</SidebarLayout>
|
||||
</AuthProvider>
|
||||
</NuqsAdapter>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
/**
|
||||
* Debug component for authentication state
|
||||
* Only use during development
|
||||
*/
|
||||
export function AuthDebug() {
|
||||
const { user, session, isAuthenticated, isLoading, signOut } = useAuthContext();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
// Only enable in development mode
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mt-6 max-w-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Auth Debug Panel</CardTitle>
|
||||
<CardDescription>
|
||||
This panel shows the current authentication state
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<span className="font-medium">Auth Status:</span>
|
||||
<span>
|
||||
{isLoading
|
||||
? "Loading..."
|
||||
: isAuthenticated
|
||||
? "Authenticated ✅"
|
||||
: "Not Authenticated ❌"}
|
||||
</span>
|
||||
|
||||
<span className="font-medium">User ID:</span>
|
||||
<span>{user?.id || "None"}</span>
|
||||
|
||||
<span className="font-medium">Email:</span>
|
||||
<span>{user?.email || "None"}</span>
|
||||
|
||||
<span className="font-medium">Display Name:</span>
|
||||
<span>{user?.displayName || "None"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
{showDetails ? "Hide Details" : "Show Details"}
|
||||
</Button>
|
||||
|
||||
{isAuthenticated && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<div className="max-h-48 overflow-auto text-xs">
|
||||
<pre>Session: {JSON.stringify(session, null, 2)}</pre>
|
||||
<pre>User: {JSON.stringify(user, null, 2)}</pre>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
This panel is only visible in development mode.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -15,11 +15,6 @@ import { SiteHeader } from "./sidebar-header";
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
user: {
|
||||
name: "John Doe",
|
||||
email: "johndoe@langchain.dev",
|
||||
avatar: "",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Chat",
|
||||
@@ -60,7 +55,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavMain items={data.navMain} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
<NavUser />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
User,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
@@ -25,17 +23,56 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useAuthContext } from "@/providers/Auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
}) {
|
||||
|
||||
export function NavUser() {
|
||||
const { isMobile } = useSidebar();
|
||||
const { user: authUser, signOut, isAuthenticated } = useAuthContext();
|
||||
const router = useRouter();
|
||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||
|
||||
// Use auth user if available, otherwise use default user
|
||||
const displayUser = authUser
|
||||
? {
|
||||
name: authUser.displayName || authUser.email?.split("@")[0] || "User",
|
||||
email: authUser.email || "",
|
||||
avatar: authUser.avatarUrl || "",
|
||||
company: authUser.companyName || "",
|
||||
firstName: authUser.firstName || "",
|
||||
lastName: authUser.lastName || "",
|
||||
}
|
||||
: {
|
||||
name: "Guest",
|
||||
email: "Not signed in",
|
||||
avatar: "",
|
||||
company: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
setIsSigningOut(true);
|
||||
const { error } = await signOut();
|
||||
|
||||
if (error) {
|
||||
console.error("Error signing out:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/signin");
|
||||
} catch (err) {
|
||||
console.error("Error during sign out:", err);
|
||||
} finally {
|
||||
setIsSigningOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignIn = () => {
|
||||
router.push("/signin");
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
@@ -43,19 +80,27 @@ export function NavUser({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground h-16"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
src={displayUser.avatar}
|
||||
alt={displayUser.name}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{displayUser.name.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
<span className="truncate font-semibold">
|
||||
{displayUser.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{displayUser.email}</span>
|
||||
{"company" in displayUser && (
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{displayUser.company}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
@@ -70,44 +115,51 @@ export function NavUser({
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
src={displayUser.avatar}
|
||||
alt={displayUser.name}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{displayUser.name.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
<span className="truncate font-semibold">
|
||||
{displayUser.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{displayUser.email}</span>
|
||||
{"company" in displayUser && (
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{displayUser.company}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
|
||||
{isAuthenticated ? (
|
||||
<DropdownMenuItem
|
||||
onClick={handleSignOut}
|
||||
disabled={isSigningOut}
|
||||
>
|
||||
{isSigningOut ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Signing out...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
) : (
|
||||
<DropdownMenuItem onClick={handleSignIn}>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Sign in
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Missing Supabase URL or Anon Key');
|
||||
}
|
||||
|
||||
const supabase = createServerClient(
|
||||
supabaseUrl,
|
||||
supabaseAnonKey,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Do not run code between createServerClient and
|
||||
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
// IMPORTANT: DO NOT REMOVE auth.getUser()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith('/signin') &&
|
||||
!request.nextUrl.pathname.startsWith('/signup')
|
||||
) {
|
||||
console.log("no user, redirecting to /signin", request.nextUrl.pathname, user)
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = '/signin'
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is.
|
||||
// If you're creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
|
||||
let supabaseInstance: ReturnType<typeof createClient> | null = null;
|
||||
|
||||
/**
|
||||
* Get a Supabase client instance (creates a singleton)
|
||||
*
|
||||
* @returns A Supabase client instance
|
||||
*/
|
||||
export function getSupabaseClient() {
|
||||
if (supabaseInstance) return supabaseInstance;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseKey) {
|
||||
throw new Error(
|
||||
"Missing Supabase configuration: NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
||||
);
|
||||
}
|
||||
|
||||
// Use the browser client when in browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
supabaseInstance = createBrowserClient(
|
||||
supabaseUrl,
|
||||
supabaseKey,
|
||||
{
|
||||
cookies: {
|
||||
get(name) {
|
||||
return document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith(`${name}=`))
|
||||
?.split('=')?.[1]
|
||||
},
|
||||
set(name, value, options) {
|
||||
document.cookie = `${name}=${value}; path=${options?.path ?? '/'}; max-age=${options?.maxAge ?? 31536000}`
|
||||
},
|
||||
remove(name, options) {
|
||||
document.cookie = `${name}=; path=${options?.path ?? '/'}; max-age=0`
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// For server-side, use the regular client
|
||||
// The middleware will use createServerClient separately
|
||||
supabaseInstance = createClient(supabaseUrl, supabaseKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return supabaseInstance;
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthCredentials,
|
||||
AuthError,
|
||||
Session,
|
||||
User,
|
||||
AuthStateChangeCallback,
|
||||
AuthProviderOptions,
|
||||
} from "./types";
|
||||
import { getSupabaseClient } from "./supabase-client";
|
||||
|
||||
export class SupabaseAuthProvider implements AuthProvider {
|
||||
private supabase;
|
||||
private options: AuthProviderOptions;
|
||||
|
||||
constructor(options: AuthProviderOptions = {}) {
|
||||
this.supabase = getSupabaseClient();
|
||||
this.options = {
|
||||
shouldPersistSession: true,
|
||||
redirectUrl:
|
||||
typeof window !== "undefined"
|
||||
? `${window.location.origin}/api/auth/callback`
|
||||
: undefined,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to convert Supabase User to our User interface
|
||||
private formatUser(supabaseUser: any): User | null {
|
||||
if (!supabaseUser) return null;
|
||||
|
||||
// Extract metadata from user_metadata
|
||||
const metadata = supabaseUser.user_metadata || {};
|
||||
|
||||
// Determine name - prefer explicit first_name/last_name from our app
|
||||
// but fall back to name from Google/OAuth or email username
|
||||
const firstName = metadata.first_name || (metadata.name?.split(' ')[0]) || null;
|
||||
const lastName = metadata.last_name ||
|
||||
(metadata.name?.split(' ').slice(1).join(' ')) || null;
|
||||
|
||||
// Construct display name from available data
|
||||
const displayName =
|
||||
(firstName && lastName) ? `${firstName} ${lastName}` :
|
||||
metadata.name ||
|
||||
supabaseUser.email?.split("@")[0] ||
|
||||
null;
|
||||
|
||||
return {
|
||||
id: supabaseUser.id,
|
||||
email: supabaseUser.email,
|
||||
displayName,
|
||||
firstName,
|
||||
lastName,
|
||||
companyName: metadata.company_name || null,
|
||||
avatarUrl: metadata.avatar_url || null,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to convert Supabase Session to our Session interface
|
||||
private formatSession(supabaseSession: any): Session | null {
|
||||
if (!supabaseSession) return null;
|
||||
|
||||
return {
|
||||
user: this.formatUser(supabaseSession.user),
|
||||
accessToken: supabaseSession.access_token,
|
||||
refreshToken: supabaseSession.refresh_token,
|
||||
expiresAt: supabaseSession.expires_at,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert Supabase error to our AuthError format
|
||||
private formatError(error: any): AuthError | null {
|
||||
if (!error) return null;
|
||||
|
||||
console.error("Auth error:", error);
|
||||
|
||||
return {
|
||||
message: error.message || "An unknown error occurred",
|
||||
status: error.status,
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
async signUp(credentials: AuthCredentials) {
|
||||
try {
|
||||
const { data, error } = await this.supabase.auth.signUp({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
options: {
|
||||
emailRedirectTo: this.options.redirectUrl,
|
||||
data: credentials.metadata || {},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return {
|
||||
user: this.formatUser(data?.user),
|
||||
session: this.formatSession(data?.session),
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(credentials: AuthCredentials) {
|
||||
try {
|
||||
const { data, error } = await this.supabase.auth.signInWithPassword({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return {
|
||||
user: this.formatUser(data?.user),
|
||||
session: this.formatSession(data?.session),
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async signInWithGoogle() {
|
||||
try {
|
||||
const { data, error } = await this.supabase.auth.signInWithOAuth({
|
||||
provider: "google",
|
||||
options: {
|
||||
redirectTo: this.options.redirectUrl,
|
||||
queryParams: {
|
||||
prompt: "select_account",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// For browser environments, handle the redirect
|
||||
if (typeof window !== "undefined" && data?.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
|
||||
// OAuth returns data.url - handled above in browser environments
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
try {
|
||||
const { error } = await this.supabase.auth.signOut();
|
||||
if (error) throw error;
|
||||
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
return { error: this.formatError(error) };
|
||||
}
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
try {
|
||||
const { data, error } = await this.supabase.auth.getSession();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return this.formatSession(data.session);
|
||||
} catch (error) {
|
||||
console.error("Error getting session:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSession() {
|
||||
try {
|
||||
const { data, error } = await this.supabase.auth.refreshSession();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return this.formatSession(data.session);
|
||||
} catch (error) {
|
||||
console.error("Error refreshing session:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
try {
|
||||
const { data, error } = await this.supabase.auth.getUser();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return this.formatUser(data.user);
|
||||
} catch (error) {
|
||||
console.error("Error getting current user:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(attributes: Partial<User>) {
|
||||
try {
|
||||
// Convert our User attributes to Supabase format
|
||||
const metadata: Record<string, any> = {
|
||||
...attributes.metadata,
|
||||
};
|
||||
|
||||
// Handle specific fields for our application
|
||||
if (attributes.firstName !== undefined) metadata.first_name = attributes.firstName;
|
||||
if (attributes.lastName !== undefined) metadata.last_name = attributes.lastName;
|
||||
if (attributes.companyName !== undefined) metadata.company_name = attributes.companyName;
|
||||
if (attributes.avatarUrl !== undefined) metadata.avatar_url = attributes.avatarUrl;
|
||||
|
||||
// If first and last name are provided, update the name field too
|
||||
if (attributes.firstName && attributes.lastName) {
|
||||
metadata.name = `${attributes.firstName} ${attributes.lastName}`.trim();
|
||||
} else if (attributes.displayName) {
|
||||
metadata.name = attributes.displayName;
|
||||
}
|
||||
|
||||
const supabaseAttributes: any = {
|
||||
email: attributes.email,
|
||||
data: metadata,
|
||||
};
|
||||
|
||||
const { data, error } =
|
||||
await this.supabase.auth.updateUser(supabaseAttributes);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return {
|
||||
user: this.formatUser(data.user),
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
user: null,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(email: string) {
|
||||
try {
|
||||
const { error } = await this.supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${this.options.redirectUrl}/reset-password`,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
return { error: this.formatError(error) };
|
||||
}
|
||||
}
|
||||
|
||||
async updatePassword(newPassword: string) {
|
||||
try {
|
||||
const { error } = await this.supabase.auth.updateUser({
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
return { error: this.formatError(error) };
|
||||
}
|
||||
}
|
||||
|
||||
onAuthStateChange(callback: AuthStateChangeCallback) {
|
||||
const { data } = this.supabase.auth.onAuthStateChange((_event, session) => {
|
||||
callback(this.formatSession(session));
|
||||
});
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
data.subscription.unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string | null;
|
||||
displayName?: string | null;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
companyName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
user: User | null;
|
||||
accessToken: string | null;
|
||||
refreshToken?: string | null;
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
export interface AuthError {
|
||||
message: string;
|
||||
status?: number;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface AuthProviderOptions {
|
||||
redirectUrl?: string;
|
||||
shouldPersistSession?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AuthStateChangeCallback {
|
||||
(session: Session | null): void;
|
||||
}
|
||||
|
||||
export interface AuthProvider {
|
||||
// Core authentication methods
|
||||
signUp: (credentials: AuthCredentials) => Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
|
||||
signIn: (credentials: AuthCredentials) => Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
|
||||
signInWithGoogle: () => Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
|
||||
signOut: () => Promise<{ error: AuthError | null }>;
|
||||
|
||||
// Session management
|
||||
getSession: () => Promise<Session | null>;
|
||||
refreshSession: () => Promise<Session | null>;
|
||||
|
||||
// User operations
|
||||
getCurrentUser: () => Promise<User | null>;
|
||||
updateUser: (attributes: Partial<User>) => Promise<{
|
||||
user: User | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
|
||||
// Password management
|
||||
resetPassword: (email: string) => Promise<{ error: AuthError | null }>;
|
||||
updatePassword: (newPassword: string) => Promise<{ error: AuthError | null }>;
|
||||
|
||||
// Event listeners
|
||||
onAuthStateChange: (callback: AuthStateChangeCallback) => {
|
||||
unsubscribe: () => void;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { updateSession } from "./lib/auth/middleware";
|
||||
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// Skip middleware for static assets and endpoints that handle auth
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - api/auth (auth API routes)
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|api/auth|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { SupabaseAuthProvider } from "@/lib/auth/supabase";
|
||||
import {
|
||||
AuthProvider as CustomAuthProvider,
|
||||
Session,
|
||||
User,
|
||||
AuthCredentials,
|
||||
AuthError,
|
||||
} from "@/lib/auth/types";
|
||||
|
||||
interface AuthContextProps {
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
signIn: (credentials: AuthCredentials) => Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
signUp: (credentials: AuthCredentials) => Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
signInWithGoogle: () => Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
signOut: () => Promise<{ error: AuthError | null }>;
|
||||
resetPassword: (email: string) => Promise<{ error: AuthError | null }>;
|
||||
updatePassword: (newPassword: string) => Promise<{ error: AuthError | null }>;
|
||||
updateUser: (attributes: Partial<User>) => Promise<{
|
||||
user: User | null;
|
||||
error: AuthError | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Create default authentication provider (Supabase in this case)
|
||||
const authProvider = new SupabaseAuthProvider({
|
||||
redirectUrl:
|
||||
typeof window !== "undefined" ? window.location.origin : undefined,
|
||||
});
|
||||
|
||||
// Create auth context
|
||||
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({
|
||||
children,
|
||||
customAuthProvider,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
customAuthProvider?: CustomAuthProvider;
|
||||
}) {
|
||||
// Use the provided auth provider or default to Supabase
|
||||
const provider = customAuthProvider || authProvider;
|
||||
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load initial session on mount
|
||||
useEffect(() => {
|
||||
const initializeAuth = async () => {
|
||||
try {
|
||||
// Get the current session
|
||||
const currentSession = await provider.getSession();
|
||||
setSession(currentSession);
|
||||
|
||||
// If we have a session, get the user
|
||||
if (currentSession?.user) {
|
||||
setUser(currentSession.user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error initializing auth:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
}, [provider]);
|
||||
|
||||
// Set up auth state change listener
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = provider.onAuthStateChange((newSession) => {
|
||||
setSession(newSession);
|
||||
setUser(newSession?.user || null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [provider]);
|
||||
|
||||
const value = {
|
||||
session,
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!session?.user,
|
||||
signIn: provider.signIn.bind(provider),
|
||||
signUp: provider.signUp.bind(provider),
|
||||
signInWithGoogle: provider.signInWithGoogle.bind(provider),
|
||||
signOut: provider.signOut.bind(provider),
|
||||
resetPassword: provider.resetPassword.bind(provider),
|
||||
updatePassword: provider.updatePassword.bind(provider),
|
||||
updateUser: provider.updateUser.bind(provider),
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuthContext() {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuthContext must be used within an AuthProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -2030,6 +2030,88 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/auth-js@npm:2.69.1":
|
||||
version: 2.69.1
|
||||
resolution: "@supabase/auth-js@npm:2.69.1"
|
||||
dependencies:
|
||||
"@supabase/node-fetch": ^2.6.14
|
||||
checksum: fe5e6bc8d63f2b3754077aff87265aaa8de7620c1a6951affda1e75efee2a549fa1003394b4d950aa976e016e5a032517475e51cc0a0edfbb965fd4c5280e592
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/functions-js@npm:2.4.4":
|
||||
version: 2.4.4
|
||||
resolution: "@supabase/functions-js@npm:2.4.4"
|
||||
dependencies:
|
||||
"@supabase/node-fetch": ^2.6.14
|
||||
checksum: cbddd6c0f03de1d3ff1c9b760c028f9101b0221a2dc032000debd1734c725fe0217d56c281437cf71ff30e23a094b47275511b598ef68a39a66b967664faf7cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/node-fetch@npm:2.6.15, @supabase/node-fetch@npm:^2.6.14":
|
||||
version: 2.6.15
|
||||
resolution: "@supabase/node-fetch@npm:2.6.15"
|
||||
dependencies:
|
||||
whatwg-url: ^5.0.0
|
||||
checksum: 9673b49236a56df49eb7ea5cb789cf4e8b1393069b84b4964ac052995e318a34872f428726d128f232139e17c3375a531e45e99edd3e96a25cce60d914b53879
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/postgrest-js@npm:1.19.4":
|
||||
version: 1.19.4
|
||||
resolution: "@supabase/postgrest-js@npm:1.19.4"
|
||||
dependencies:
|
||||
"@supabase/node-fetch": ^2.6.14
|
||||
checksum: 3265d5d563eb4b54ab78f2d7a87c68948b323f520913e8309f367cbb8a714b8d5a519e05d73e96510f8d216ea55b5c21e1822f200f6db0a52cc028ac396f62cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/realtime-js@npm:2.11.2":
|
||||
version: 2.11.2
|
||||
resolution: "@supabase/realtime-js@npm:2.11.2"
|
||||
dependencies:
|
||||
"@supabase/node-fetch": ^2.6.14
|
||||
"@types/phoenix": ^1.5.4
|
||||
"@types/ws": ^8.5.10
|
||||
ws: ^8.18.0
|
||||
checksum: 0fdb63ca0f6e6993523fb1d95c2ed843cf74e49437a7e625702b551c70ce1861964cbffc2937811a54f51410b02a7e257307e4af561d36fcb95803a145b9fa6d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/ssr@npm:^0.6.1":
|
||||
version: 0.6.1
|
||||
resolution: "@supabase/ssr@npm:0.6.1"
|
||||
dependencies:
|
||||
cookie: ^1.0.1
|
||||
peerDependencies:
|
||||
"@supabase/supabase-js": ^2.43.4
|
||||
checksum: 54152ade021fb888634efca55aa6fbc4dee418ce9c7aa4ccd60f794206a83e175b8c75db6db4781fa9a8b064dde76c025592548982913bb57f6566f3883e3f56
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/storage-js@npm:2.7.1":
|
||||
version: 2.7.1
|
||||
resolution: "@supabase/storage-js@npm:2.7.1"
|
||||
dependencies:
|
||||
"@supabase/node-fetch": ^2.6.14
|
||||
checksum: ed8f3a3178856c331b36588f4fff5cbb7f2f89977fff9716ab20b1977d13816bda5a887a316638f2a05ac35fdef46e18eab8a543d6113de76d3a06b15bf9ae8e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@supabase/supabase-js@npm:^2.49.4":
|
||||
version: 2.49.4
|
||||
resolution: "@supabase/supabase-js@npm:2.49.4"
|
||||
dependencies:
|
||||
"@supabase/auth-js": 2.69.1
|
||||
"@supabase/functions-js": 2.4.4
|
||||
"@supabase/node-fetch": 2.6.15
|
||||
"@supabase/postgrest-js": 1.19.4
|
||||
"@supabase/realtime-js": 2.11.2
|
||||
"@supabase/storage-js": 2.7.1
|
||||
checksum: 1f8b8a04abb662eee61d13ea7841b1f689002317d168312a1de31d8b4bc22867841708e7f7b8e9cc87ae36e99fd334fd1f0baf56ecae5ec83b593a0b5502bb0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/counter@npm:0.1.3":
|
||||
version: 0.1.3
|
||||
resolution: "@swc/counter@npm:0.1.3"
|
||||
@@ -2310,6 +2392,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*":
|
||||
version: 22.15.14
|
||||
resolution: "@types/node@npm:22.15.14"
|
||||
dependencies:
|
||||
undici-types: ~6.21.0
|
||||
checksum: bc3d2a28e1cc001171e861da1c6d46d21a40503a99815e293b501f804797c62a543165acff5e22e46ccff8b1659596be71423b0c0640089cc871ad8e9049fedd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:22.15.2":
|
||||
version: 22.15.2
|
||||
resolution: "@types/node@npm:22.15.2"
|
||||
@@ -2319,6 +2410,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/phoenix@npm:^1.5.4":
|
||||
version: 1.6.6
|
||||
resolution: "@types/phoenix@npm:1.6.6"
|
||||
checksum: 9dc897cb9a4cd62f7a0de48855e6cafded5c676e7d78c4d3a9ade4f21ec82b95eb7195caada26a9a3a5d9aba14f0fd875bc3898e086234b20da63991a033f6e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:^19.1.2":
|
||||
version: 19.1.2
|
||||
resolution: "@types/react-dom@npm:19.1.2"
|
||||
@@ -2374,6 +2472,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.10":
|
||||
version: 8.18.1
|
||||
resolution: "@types/ws@npm:8.18.1"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 0331b14cde388e2805af66cad3e3f51857db8e68ed91e5b99750915e96fe7572e58296dc99999331bbcf08f0ff00a227a0bb214e991f53c2a5aca7b0e71173fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.31.1, @typescript-eslint/eslint-plugin@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version: 8.31.1
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.31.1"
|
||||
@@ -3209,6 +3316,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "cookie@npm:1.0.2"
|
||||
checksum: 2c5a6214147ffa7135ce41860c781de17e93128689b0d080d3116468274b3593b607bcd462ac210d3a61f081db3d3b09ae106e18d60b1f529580e95cf2db8a55
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cors@npm:^2.8.5":
|
||||
version: 2.8.5
|
||||
resolution: "cors@npm:2.8.5"
|
||||
@@ -7625,6 +7739,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tr46@npm:~0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "tr46@npm:0.0.3"
|
||||
checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"trim-lines@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "trim-lines@npm:3.0.1"
|
||||
@@ -8206,6 +8327,8 @@ __metadata:
|
||||
"@radix-ui/react-tabs": ^1.1.7
|
||||
"@radix-ui/react-toast": 1.1.0
|
||||
"@radix-ui/react-tooltip": ^1.2.3
|
||||
"@supabase/ssr": ^0.6.1
|
||||
"@supabase/supabase-js": ^2.49.4
|
||||
"@tailwindcss/postcss": ^4.0.13
|
||||
"@types/js-cookie": ^3.0.6
|
||||
"@types/lodash": ^4.17.16
|
||||
@@ -8259,6 +8382,23 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"webidl-conversions@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "webidl-conversions@npm:3.0.1"
|
||||
checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-url@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "whatwg-url@npm:5.0.0"
|
||||
dependencies:
|
||||
tr46: ~0.0.3
|
||||
webidl-conversions: ^3.0.0
|
||||
checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "which-boxed-primitive@npm:1.1.1"
|
||||
@@ -8345,6 +8485,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^8.18.0":
|
||||
version: 8.18.2
|
||||
resolution: "ws@npm:8.18.2"
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ">=5.0.2"
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
checksum: e38beae19ba4d68577ec24eb34fbfab376333fedd10f99b07511a8e842e22dbc102de39adac333a18e4c58868d0703cd5f239b04b345e22402d0ed8c34ea0aa0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xtend@npm:^4.0.0":
|
||||
version: 4.0.2
|
||||
resolution: "xtend@npm:4.0.2"
|
||||
|
||||
Reference in New Issue
Block a user