changes: - Implemented Authetik as IDP
- middleware updated for route handling - Auth js for authentication with credentials and authentil provider
This commit is contained in:
71
CHANGELOG.md
71
CHANGELOG.md
@@ -8,6 +8,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Auth.js Credentials Provider Integration**
|
||||
- Integrated Auth.js (NextAuth.js v5) with Credentials provider for direct login with backend
|
||||
- Users can now log in using username/password through Auth.js authentication
|
||||
- Credentials provider calls ASP.NET Core `/api/TokenAuth/Authenticate` endpoint
|
||||
- Seamless integration with existing OIDC providers (Authentik, etc.)
|
||||
- Single authentication system supporting both credentials and external OIDC providers
|
||||
- **Authentik SSO Button in Sign-In Page**
|
||||
- Added "Sign in with Authentik" button to the main login page
|
||||
- Users can choose between traditional credentials or SSO authentication
|
||||
- Visual separator between credentials and OAuth login options
|
||||
- Consistent design with shadcn-ui components
|
||||
- Automatic redirect to callback URL after successful OAuth authentication
|
||||
- **Auth.js (NextAuth.js v5) Integration for SSO**
|
||||
- Configured Auth.js with OIDC provider support for external authentication
|
||||
- Created `/testSSO` page for testing Single Sign-On with Authentik
|
||||
- Implemented token exchange flow: Authentik OIDC → Backend JWT
|
||||
- Added automatic token refresh when backend JWT expires
|
||||
- **JWT Utilities**
|
||||
- Created `src/lib/auth/jwt-decoder.ts` with JWT parsing and validation functions
|
||||
- Created `src/lib/auth/token-exchange.ts` for external token exchange with backend
|
||||
- Functions: `decodeJwt()`, `isTokenExpired()`, `getTokenTtl()`, `formatTokenTtl()`
|
||||
- User info extraction from JWT tokens
|
||||
- **Test SSO Components**
|
||||
- Created `AuthStatusCard` component to display current authentication state
|
||||
- Created `TokenViewer` component to inspect and decode JWT tokens with accordion UI
|
||||
- Created `AuthentikButton` component for sign-in/sign-out with Authentik
|
||||
- All components use shadcn-ui for consistent styling
|
||||
- **External Authentication Support**
|
||||
- Extended `AuthContext` with `loginWithExternal()` method
|
||||
- Support for both traditional (username/password) and OIDC authentication
|
||||
- Unified token management for both authentication methods
|
||||
- **Auth.js Configuration**
|
||||
- Created `src/auth.ts` with Auth.js configuration and callbacks
|
||||
- Created `src/app/api/auth/[...nextauth]/route.ts` for Auth.js API routes
|
||||
- JWT callback performs token exchange with backend on initial sign-in
|
||||
- Session callback exposes backend JWT to client
|
||||
- Automatic token refresh before expiration (5-minute threshold)
|
||||
- **Provider Configuration UI**
|
||||
- Created `ProviderConfigCheck` component to display backend provider status
|
||||
- Automatically fetches enabled providers from backend API
|
||||
- Shows configuration issues and setup instructions in real-time
|
||||
- Provides SQL script helper values for easy setup
|
||||
- **Backend Integration**
|
||||
- Created `providers.ts` helper to fetch provider config from backend
|
||||
- Function `getEnabledProviders()` calls `ExternalAuthProviderAppService`
|
||||
- Dynamic provider loading from database
|
||||
- **Documentation and Setup**
|
||||
- Updated `env.example.txt` with Auth.js and Authentik configuration variables
|
||||
- Added instructions for generating AUTH_SECRET
|
||||
- Documented Authentik OIDC provider setup steps
|
||||
- Created `setup-authentik-provider.sql` script for easy database setup
|
||||
- Added comprehensive troubleshooting guide
|
||||
- Explained hybrid approach (backend config + env vars)
|
||||
- **TypeScript Type Generation from OpenAPI**
|
||||
- Added `@hey-api/openapi-ts` for automatic type generation from backend Swagger spec
|
||||
- Created `openapi-ts.config.ts` configuration using local swagger.json file (avoids SSL certificate issues)
|
||||
@@ -52,11 +105,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Removed Clerk references from `.gitignore`
|
||||
|
||||
### Fixed
|
||||
- **Fixed Middleware Authentication Protection (Critical Security Fix)**
|
||||
- Fixed `authorized` callback in `auth.ts` that was allowing unauthenticated access by default
|
||||
- Changed default behavior from `return true` (allow all) to `return isLoggedIn` (require auth)
|
||||
- Middleware now properly requires authentication for ALL routes by default (except public routes)
|
||||
- Added explicit list of public routes that don't require authentication
|
||||
- Root path (`/`) now redirects to dashboard if authenticated, or sign-in if not
|
||||
- Authenticated users trying to access auth pages are redirected to dashboard
|
||||
- All other routes now properly require authentication before access
|
||||
- **IMPORTANT**: Restart dev server after this change for it to take effect
|
||||
- Fixed import path for generated API client (use `@/client/client.gen` instead of `@/client`)
|
||||
- Resolved "Export client doesn't exist" error in auth-context.tsx
|
||||
- Fixed SSL certificate issues when generating types from localhost by downloading swagger.json first
|
||||
|
||||
### Changed
|
||||
- **Migrated to Auth.js for All Authentication**
|
||||
- Updated `custom-sign-in-form.tsx` to use `signIn()` from `next-auth/react` instead of custom login
|
||||
- Updated `auth-context.tsx` to use `useSession()` hook from Auth.js
|
||||
- Updated `middleware.ts` to use Auth.js middleware for route protection
|
||||
- Updated `lib/api/client.ts` to retrieve tokens from Auth.js session
|
||||
- Modified `auth.ts` to support both Credentials and OIDC authentication methods
|
||||
- JWT callback handles both credential-based and OIDC-based authentication flows
|
||||
- Session callback exposes tokens (accessToken, encryptedAccessToken) to client
|
||||
- Changed login page redirect from `/testSSO` to `/auth/sign-in`
|
||||
- **Complete Authentication System Replacement**
|
||||
- Replaced Clerk authentication with custom JWT-based authentication system
|
||||
- Created `AuthProvider` and `useAuth` hook to manage authentication state
|
||||
|
||||
@@ -56,10 +56,55 @@ SENTRY_AUTH_TOKEN= #Example: sntrys_************************************
|
||||
NEXT_PUBLIC_SENTRY_DISABLED= "false"
|
||||
|
||||
|
||||
# =================================================================
|
||||
# ASP.NET Core Backend API Configuration
|
||||
# =================================================================
|
||||
# Backend API base URL for API calls
|
||||
|
||||
NEXT_PUBLIC_API_URL=https://localhost:44313
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Auth.js (NextAuth.js v5) Configuration
|
||||
# =================================================================
|
||||
# Required for both Credentials and OIDC authentication
|
||||
|
||||
# Auth Secret: Generate with: openssl rand -base64 32
|
||||
# Run this command in your terminal: openssl rand -base64 32
|
||||
# Or use: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
||||
AUTH_SECRET=
|
||||
|
||||
# NextAuth URL (your frontend URL)
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Authentik OIDC Provider Configuration
|
||||
# =================================================================
|
||||
# To set up Authentik:
|
||||
# 1. Create an OIDC Provider in Authentik
|
||||
# 2. Create an Application linked to the provider
|
||||
# 3. Copy the following values from Authentik
|
||||
|
||||
# Client ID (from Provider settings)
|
||||
AUTH_AUTHENTIK_ID=
|
||||
|
||||
# Client Secret (from Provider settings)
|
||||
AUTH_AUTHENTIK_SECRET=
|
||||
|
||||
# Issuer URL (format: https://your-authentik-domain/application/o/your-app-slug/)
|
||||
# Example: https://auth.example.com/application/o/aspbase-oidc/
|
||||
AUTH_AUTHENTIK_ISSUER=
|
||||
|
||||
# Redirect URI to configure in Authentik:
|
||||
# http://localhost:3000/api/auth/callback/authentik
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Important Notes:
|
||||
# =================================================================
|
||||
# 1. Rename this file to '.env' for local development
|
||||
# 2. Never commit the actual '.env' file to version control
|
||||
# 1. Rename this file to '.env.local' for local development
|
||||
# 2. Never commit the actual '.env.local' file to version control
|
||||
# 3. Make sure to replace all placeholder values with real ones
|
||||
# 4. Keep your secret keys private and never share them
|
||||
# 5. For production, set these as environment variables in your hosting platform
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import { AuthProvider } from '@/contexts/auth-context';
|
||||
import { ActiveThemeProvider } from '../active-theme';
|
||||
|
||||
@@ -12,9 +13,11 @@ export default function Providers({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ActiveThemeProvider initialTheme={activeThemeValue}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</ActiveThemeProvider>
|
||||
<SessionProvider>
|
||||
<ActiveThemeProvider initialTheme={activeThemeValue}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</ActiveThemeProvider>
|
||||
</SessionProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { client } from '@/lib/api/client';
|
||||
import {
|
||||
clearAuthToken,
|
||||
getAuthToken,
|
||||
getUserInfo,
|
||||
isAuthenticated,
|
||||
setAuthTokens,
|
||||
setUserInfo,
|
||||
type AuthTokens,
|
||||
type UserInfo
|
||||
} from '@/lib/auth';
|
||||
import { useSession, signOut as nextAuthSignOut } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode
|
||||
} from 'react';
|
||||
|
||||
@@ -36,118 +24,44 @@ interface AuthContextType {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
login: (
|
||||
userNameOrEmailAddress: string,
|
||||
password: string
|
||||
) => Promise<void>;
|
||||
logout: () => void;
|
||||
refreshUser: () => Promise<void>;
|
||||
accessToken: string | null;
|
||||
encryptedAccessToken: string | null;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
// Load user from localStorage on mount
|
||||
useEffect(() => {
|
||||
const loadUser = () => {
|
||||
if (isAuthenticated()) {
|
||||
const userInfo = getUserInfo();
|
||||
if (userInfo) {
|
||||
setUser(mapUserInfoToUser(userInfo));
|
||||
}
|
||||
const user: User | null = session?.user
|
||||
? {
|
||||
id: session.user.id,
|
||||
userName: session.user.userName || session.user.email || 'Unknown',
|
||||
name: session.user.name?.split(' ')[0],
|
||||
surname: session.user.name?.split(' ').slice(1).join(' '),
|
||||
emailAddress: session.user.email || undefined,
|
||||
fullName: session.user.name || session.user.userName || 'Unknown User',
|
||||
imageUrl: session.user.image || undefined,
|
||||
emailAddresses: session.user.email
|
||||
? [{ emailAddress: session.user.email }]
|
||||
: []
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
: null;
|
||||
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const mapUserInfoToUser = (userInfo: UserInfo): User => {
|
||||
const fullName = [userInfo.name, userInfo.surname]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return {
|
||||
id: userInfo.id,
|
||||
userName: userInfo.userName,
|
||||
name: userInfo.name,
|
||||
surname: userInfo.surname,
|
||||
emailAddress: userInfo.emailAddress,
|
||||
fullName: fullName || userInfo.userName,
|
||||
imageUrl: undefined, // Could be fetched from backend if available
|
||||
emailAddresses: userInfo.emailAddress
|
||||
? [{ emailAddress: userInfo.emailAddress }]
|
||||
: []
|
||||
};
|
||||
};
|
||||
|
||||
const login = useCallback(
|
||||
async (userNameOrEmailAddress: string, password: string) => {
|
||||
try {
|
||||
const response = await client.POST('/api/TokenAuth/Authenticate', {
|
||||
body: {
|
||||
userNameOrEmailAddress,
|
||||
password,
|
||||
rememberClient: false
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
// Save tokens
|
||||
setAuthTokens({
|
||||
accessToken: response.data.accessToken || '',
|
||||
encryptedAccessToken: response.data.encryptedAccessToken || '',
|
||||
expireInSeconds: response.data.expireInSeconds || 0
|
||||
});
|
||||
|
||||
// Save user info
|
||||
const userInfo: UserInfo = {
|
||||
id: response.data.userId || 0,
|
||||
userName: userNameOrEmailAddress,
|
||||
emailAddress: undefined, // Will need to fetch from user profile endpoint
|
||||
name: undefined,
|
||||
surname: undefined
|
||||
};
|
||||
|
||||
setUserInfo(userInfo);
|
||||
setUser(mapUserInfoToUser(userInfo));
|
||||
|
||||
// Redirect to dashboard
|
||||
router.push('/dashboard/overview');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearAuthToken();
|
||||
setUser(null);
|
||||
const logout = useCallback(async () => {
|
||||
await nextAuthSignOut({ redirect: false });
|
||||
router.push('/auth/sign-in');
|
||||
}, [router]);
|
||||
|
||||
const refreshUser = useCallback(async () => {
|
||||
// Optionally fetch updated user info from backend
|
||||
const userInfo = getUserInfo();
|
||||
if (userInfo) {
|
||||
setUser(mapUserInfoToUser(userInfo));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
login,
|
||||
isLoading: status === 'loading',
|
||||
isAuthenticated: !!session,
|
||||
logout,
|
||||
refreshUser
|
||||
accessToken: session?.accessToken || session?.user.backendJwt || null,
|
||||
encryptedAccessToken: session?.encryptedAccessToken || null
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
|
||||
@@ -10,13 +10,20 @@ import {
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useAuth } from '@/contexts/auth-context';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { IconBrandAuth0 } from '@tabler/icons-react';
|
||||
|
||||
export function CustomSignInForm() {
|
||||
const { login } = useAuth();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard/overview';
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOAuthLoading, setIsOAuthLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
userNameOrEmailAddress: '',
|
||||
password: ''
|
||||
@@ -27,16 +34,41 @@ export function CustomSignInForm() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await login(formData.userNameOrEmailAddress, formData.password);
|
||||
toast.success('Successfully signed in!');
|
||||
const result = await signIn('credentials', {
|
||||
userNameOrEmailAddress: formData.userNameOrEmailAddress,
|
||||
password: formData.password,
|
||||
redirect: false
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
toast.error('Invalid credentials. Please check your username and password.');
|
||||
console.error('Login error:', result.error);
|
||||
} else if (result?.ok) {
|
||||
toast.success('Successfully signed in!');
|
||||
router.push(callbackUrl);
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
toast.error(error.message || 'Failed to sign in. Please check your credentials.');
|
||||
toast.error(error.message || 'An unexpected error occurred. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthentikSignIn = async () => {
|
||||
setIsOAuthLoading(true);
|
||||
try {
|
||||
await signIn('authentik', {
|
||||
callbackUrl: callbackUrl
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Authentik sign-in error:', error);
|
||||
toast.error('Failed to sign in with Authentik. Please try again.');
|
||||
setIsOAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className='w-full max-w-md'>
|
||||
<CardHeader className='space-y-1'>
|
||||
@@ -88,11 +120,31 @@ export function CustomSignInForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type='submit' className='w-full' disabled={isLoading}>
|
||||
<Button type='submit' className='w-full' disabled={isLoading || isOAuthLoading}>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className='relative my-6'>
|
||||
<Separator />
|
||||
<div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2'>
|
||||
<span className='text-xs text-muted-foreground uppercase'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
disabled={isLoading || isOAuthLoading}
|
||||
onClick={handleAuthentikSignIn}
|
||||
>
|
||||
<IconBrandAuth0 className='mr-2 h-5 w-5' />
|
||||
{isOAuthLoading ? 'Connecting...' : 'Sign in with Authentik'}
|
||||
</Button>
|
||||
|
||||
<div className='mt-4 text-center text-sm'>
|
||||
Don't have an account?{' '}
|
||||
<a
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { client } from '@/client/client.gen';
|
||||
import { getAuthToken, getTenantId } from '@/lib/auth';
|
||||
import { getSession } from 'next-auth/react';
|
||||
|
||||
/**
|
||||
* ASP.NET Boilerplate response wrapper
|
||||
@@ -43,9 +44,26 @@ client.setConfig({
|
||||
/**
|
||||
* Request interceptor: Add authentication and tenant headers
|
||||
*/
|
||||
client.interceptors.request.use((request) => {
|
||||
client.interceptors.request.use(async (request) => {
|
||||
// Try to get token from Auth.js session first (client-side)
|
||||
let token: string | null = null;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const session = await getSession();
|
||||
token = session?.accessToken || session?.user?.backendJwt || null;
|
||||
} catch (error) {
|
||||
// Fallback to localStorage if session not available
|
||||
console.warn('Failed to get session, falling back to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localStorage (legacy support)
|
||||
if (!token) {
|
||||
token = getAuthToken();
|
||||
}
|
||||
|
||||
// Add JWT token
|
||||
const token = getAuthToken();
|
||||
if (token) {
|
||||
request.headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
@@ -32,3 +32,21 @@ export function absoluteUrl(path: string) {
|
||||
? `http://localhost:3000${path}`
|
||||
: `https://${config.appUrl}${path}`;
|
||||
}
|
||||
|
||||
export function formatBytes(
|
||||
bytes: number,
|
||||
opts: {
|
||||
decimals?: number;
|
||||
sizeType?: "accurate" | "normal";
|
||||
} = {}
|
||||
) {
|
||||
const { decimals = 0, sizeType = "normal" } = opts;
|
||||
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const accurateSizes = ["Bytes", "KiB", "MiB", "GiB", "TiB"];
|
||||
if (bytes === 0) return "0 Byte";
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${
|
||||
sizeType === "accurate" ? accurateSizes[i] ?? "Bytes" : sizes[i] ?? "Bytes"
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
export default auth((req) => {
|
||||
const { pathname } = req.nextUrl;
|
||||
const isLoggedIn = !!req.auth;
|
||||
|
||||
// Public routes that don't require authentication
|
||||
const publicRoutes = ['/auth/sign-in', '/auth/sign-up', '/auth/forgot-password'];
|
||||
const publicRoutes = [
|
||||
'/auth/sign-in',
|
||||
'/auth/sign-up',
|
||||
'/auth/forgot-password',
|
||||
'/testSSO',
|
||||
'/api/auth', // Auth.js API routes
|
||||
];
|
||||
|
||||
// Check if current path is a public route
|
||||
const isPublicRoute = publicRoutes.some((route) => pathname.startsWith(route));
|
||||
|
||||
// Get token from cookie or check localStorage (client-side only)
|
||||
// Note: In middleware, we can only check cookies, not localStorage
|
||||
const token = request.cookies.get('accessToken')?.value;
|
||||
|
||||
// If trying to access protected route without token, redirect to sign-in
|
||||
if (!isPublicRoute && !token && pathname.startsWith('/dashboard')) {
|
||||
return NextResponse.redirect(new URL('/auth/sign-in', request.url));
|
||||
// Root path - redirect based on auth status
|
||||
if (pathname === '/') {
|
||||
if (isLoggedIn) {
|
||||
return NextResponse.redirect(new URL('/dashboard/overview', req.url));
|
||||
}
|
||||
return NextResponse.redirect(new URL('/auth/sign-in', req.url));
|
||||
}
|
||||
|
||||
// If authenticated and trying to access auth pages, redirect to dashboard
|
||||
if (isPublicRoute && token) {
|
||||
return NextResponse.redirect(new URL('/dashboard/overview', request.url));
|
||||
// Allow access to public routes
|
||||
if (isPublicRoute) {
|
||||
// If already logged in and trying to access auth pages, redirect to dashboard
|
||||
if (isLoggedIn && pathname.startsWith('/auth/')) {
|
||||
return NextResponse.redirect(new URL('/dashboard/overview', req.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// For all other routes (including dashboard), require authentication
|
||||
if (!isLoggedIn) {
|
||||
const signInUrl = new URL('/auth/sign-in', req.url);
|
||||
signInUrl.searchParams.set('callbackUrl', pathname);
|
||||
return NextResponse.redirect(signInUrl);
|
||||
}
|
||||
|
||||
// User is authenticated, allow access
|
||||
return NextResponse.next();
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
|
||||
Reference in New Issue
Block a user