diff --git a/actions/admin.ts b/actions/admin.ts
new file mode 100644
index 0000000..bd8fcaa
--- /dev/null
+++ b/actions/admin.ts
@@ -0,0 +1,14 @@
+'use server'
+
+import { currentRole } from '@/lib/auth'
+import { UserRole } from '@prisma/client'
+
+export const admin = async () => {
+ const role = await currentRole()
+
+ if (role === UserRole.ADMIN) {
+ return { success: `Allowed Server Action for ${UserRole.ADMIN}` }
+ }
+
+ return { error: `Forbidden Server Action for ${UserRole.ADMIN}` }
+}
\ No newline at end of file
diff --git a/app/[locale]/(protected)/_components/navbar.tsx b/app/[locale]/(protected)/_components/navbar.tsx
index c9fa979..d78efe5 100644
--- a/app/[locale]/(protected)/_components/navbar.tsx
+++ b/app/[locale]/(protected)/_components/navbar.tsx
@@ -3,45 +3,31 @@
import { Button } from '@/components/ui/button'
import Link from 'next/link'
-import { USER_PROFILE_URL } from '@/config/routes'
+import { CABINET_ROUTES, USER_PROFILE_URL } from '@/config/routes'
import { usePathname } from 'next/navigation'
import UserButton from '@/components/auth/user-button'
import LocaleSwitcher from '@/components/locale-switcher'
-const Navbar = () => {
+export const Navbar = () => {
const pathname = usePathname()
- //
+
+ console.log(USER_PROFILE_URL)
+
return (
)
}
-
-export default Navbar
diff --git a/app/[locale]/(protected)/cabinet/admin/page.tsx b/app/[locale]/(protected)/cabinet/admin/page.tsx
new file mode 100644
index 0000000..a46cb7a
--- /dev/null
+++ b/app/[locale]/(protected)/cabinet/admin/page.tsx
@@ -0,0 +1,68 @@
+'use client'
+
+import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import { RoleGate } from '@/components/auth/role-gate'
+import FormSuccess from '@/components/form-success'
+import { UserRole } from '@prisma/client'
+import { Button } from '@/components/ui/button'
+import { toast } from 'sonner'
+import { admin } from '@/actions/admin'
+
+const AdminPage = () => {
+ const onServerActionClick = () => {
+ admin()
+ .then((data) => {
+ if (data.error) {
+ toast.error(data.error)
+ }
+
+ if (data.success) {
+ toast.success(data.success)
+ }
+ })
+ }
+
+ const onApiRouteClick = () => {
+ fetch('/api/admin')
+ .then((response) => {
+ if (response.ok) {
+ toast.success('Allow API Route')
+ } else {
+ toast.error('Forbidden API Route')
+ }
+ })
+ }
+
+ return (
+
+
+
+ 🔑 Admin
+
+
+
+
+
+
+
+
+ Admin-only API Route
+
+
+
+
+
+ Admin-only Server Action
+
+
+
+
+
+ )
+}
+
+export default AdminPage
diff --git a/app/[locale]/(protected)/cabinet/client/page.tsx b/app/[locale]/(protected)/cabinet/client/page.tsx
new file mode 100644
index 0000000..d6b448a
--- /dev/null
+++ b/app/[locale]/(protected)/cabinet/client/page.tsx
@@ -0,0 +1,14 @@
+'use client'
+
+import { UserInfo } from '@/components/cabinet/user-info'
+import { useCurrentUser } from '@/hooks/useCurrentUser'
+
+const ClientPage = ({ params }: any) => {
+ const user = useCurrentUser()
+
+ return (
+
+ )
+}
+
+export default ClientPage
diff --git a/app/[locale]/(protected)/cabinet/page.tsx b/app/[locale]/(protected)/cabinet/page.tsx
index 2941f4e..edb211d 100644
--- a/app/[locale]/(protected)/cabinet/page.tsx
+++ b/app/[locale]/(protected)/cabinet/page.tsx
@@ -1,10 +1,9 @@
'use client'
import { logout } from '@/actions/logout'
-import { useCurrentUser } from '@/hooks/useCurrentUser'
-const CabinetPage = () => {
- const user = useCurrentUser()
+const CabinetPage = ({ params }: any) => {
+
const btnOnClick = () => logout()
return (
diff --git a/app/[locale]/(protected)/cabinet/server/page.tsx b/app/[locale]/(protected)/cabinet/server/page.tsx
new file mode 100644
index 0000000..45c88dd
--- /dev/null
+++ b/app/[locale]/(protected)/cabinet/server/page.tsx
@@ -0,0 +1,15 @@
+'use server'
+
+import { currentUser } from '@/lib/auth'
+import { UserInfo } from '@/components/cabinet/user-info'
+
+const ServerPage = async () => {
+ const user = await currentUser()
+
+ return (
+
+ )
+}
+
+export default ServerPage
+
diff --git a/app/[locale]/(protected)/layout.tsx b/app/[locale]/(protected)/layout.tsx
index 5d057b2..dd05e25 100644
--- a/app/[locale]/(protected)/layout.tsx
+++ b/app/[locale]/(protected)/layout.tsx
@@ -1,4 +1,4 @@
-import Navbar from '@/app/[locale]/(protected)/_components/navbar'
+import { Navbar } from '@/app/[locale]/(protected)/_components/navbar'
interface ProtectedLayoutProps {
children: React.ReactNode;
diff --git a/app/[locale]/auth/layout.tsx b/app/[locale]/auth/layout.tsx
index 38739e9..8486545 100644
--- a/app/[locale]/auth/layout.tsx
+++ b/app/[locale]/auth/layout.tsx
@@ -10,7 +10,6 @@ type Props = {
const AuthLayout = ({ children }: Props) => {
return (
<>
-
{children}
diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css
index 2de449b..819a466 100644
--- a/app/[locale]/globals.css
+++ b/app/[locale]/globals.css
@@ -30,7 +30,7 @@ input[aria-invalid='false']:not(:placeholder-shown) {
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
- --primary: 221.2 83.2% 53.3%;
+ --primary: 200 98% 39%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
index 5b9f0b7..2bf47ed 100644
--- a/app/[locale]/layout.tsx
+++ b/app/[locale]/layout.tsx
@@ -6,6 +6,8 @@ import { lc } from '@/lib/utils'
import './globals.css'
import { SessionProvider } from 'next-auth/react'
import { auth } from '@/config/auth'
+import Navbar from '@/components/auth/navbar'
+import { Toaster } from '@/components/ui/sonner'
const inter = Inter({ subsets: ['cyrillic'] })
@@ -24,6 +26,8 @@ export default async function RootLayout ({ params: { locale }, children }: Read
+
+
{children}
diff --git a/app/api/admin/route.ts b/app/api/admin/route.ts
new file mode 100644
index 0000000..d97e59e
--- /dev/null
+++ b/app/api/admin/route.ts
@@ -0,0 +1,10 @@
+import { NextResponse } from 'next/server'
+import { currentRole } from '@/lib/auth'
+import { UserRole } from '@prisma/client'
+
+export async function GET () {
+ const role = await currentRole()
+ const status: number = role === UserRole.ADMIN ? 200 : 403
+
+ return new NextResponse(null, { status })
+}
\ No newline at end of file
diff --git a/app/robots.ts b/app/robots.ts
index 7c95284..86fe8a1 100644
--- a/app/robots.ts
+++ b/app/robots.ts
@@ -15,7 +15,7 @@ export default function robots (): MetadataRoute.Robots {
{
userAgent: '*',
allow: ['/'],
- disallow: ['/auth/', '/api/'],
+ disallow: ['/auth/', '/api/', '/en/auth/', '/en/api/'],
crawlDelay: 3,
},
],
diff --git a/components/auth/card-wrapper.tsx b/components/auth/card-wrapper.tsx
index 5e13294..ee6f281 100644
--- a/components/auth/card-wrapper.tsx
+++ b/components/auth/card-wrapper.tsx
@@ -26,7 +26,7 @@ export const CardWrapper = ({
}: Props) => {
return (
+ className={`shadow-2xl max-w-[430px] w-full sm:min-w-[430px]`}>
@@ -34,7 +34,7 @@ export const CardWrapper = ({
{children}
{showSocial &&
-
+
diff --git a/components/auth/logout-button.tsx b/components/auth/logout-button.tsx
index 4261585..58fddbb 100644
--- a/components/auth/logout-button.tsx
+++ b/components/auth/logout-button.tsx
@@ -10,7 +10,7 @@ const LogoutButton = ({ children }: LogoutButtonProps) => {
const onClick = () => logout()
return (
-
+
{children}
)
diff --git a/components/auth/role-gate.tsx b/components/auth/role-gate.tsx
new file mode 100644
index 0000000..37e9b33
--- /dev/null
+++ b/components/auth/role-gate.tsx
@@ -0,0 +1,23 @@
+'use client'
+
+import { UserRole } from '@prisma/client'
+import { useCurrentRole } from '@/hooks/useCurrentRole'
+import FormError from '@/components/form-error'
+
+interface RoleGateProps {
+ children: React.ReactNode
+ allowedRole: UserRole
+}
+
+export const RoleGate = ({
+ children,
+ allowedRole,
+}: RoleGateProps) => {
+ const role = useCurrentRole()
+
+ if (role !== allowedRole) {
+ return
+ }
+
+ return <>{children}>
+}
\ No newline at end of file
diff --git a/components/auth/user-button.tsx b/components/auth/user-button.tsx
index eaa18b5..32ff3b0 100644
--- a/components/auth/user-button.tsx
+++ b/components/auth/user-button.tsx
@@ -3,26 +3,29 @@
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { FaUser } from 'react-icons/fa6'
import { IoExitOutline } from 'react-icons/io5'
import LogoutButton from '@/components/auth/logout-button'
+const fallbackInitials = (name?: string | null | undefined): string => {
+ return (name ?? '').split(' ').map((w: string) => w[0]).join('').toUpperCase()
+}
+
const UserButton = () => {
const user = useCurrentUser()
return (
-
+
-
-
+
+ {fallbackInitials(user?.name)}
-
+
-
+
Logout
diff --git a/components/cabinet/user-info.tsx b/components/cabinet/user-info.tsx
new file mode 100644
index 0000000..147c5e2
--- /dev/null
+++ b/components/cabinet/user-info.tsx
@@ -0,0 +1,44 @@
+import { type ExtendedUser } from '@/config/auth'
+import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+
+interface UserInfoProps {
+ user?: ExtendedUser
+ label: string
+}
+
+export const UserInfo = ({ user, label }: UserInfoProps) => {
+ return (
+
+
+
+ {label}
+
+
+
+
+ ID
+ {user?.id}
+
+
+ Name
+ {user?.name}
+
+
+ Email
+ {user?.email}
+
+
+ Role
+ {user?.role}
+
+
+ Two factor authentication
+
+ {user?.isTwoFactorEnabled ? 'ON' : 'OFF'}
+
+
+
+
+ )
+}
diff --git a/components/form-error.tsx b/components/form-error.tsx
index e90988a..51d4f79 100644
--- a/components/form-error.tsx
+++ b/components/form-error.tsx
@@ -1,15 +1,14 @@
import { TriangleAlert } from 'lucide-react'
-type Props = {
+type FormErrorProps = {
message?: string
}
-const FormError = ({ message }: Props) => {
+const FormError = ({ message }: FormErrorProps) => {
if (!message) return null
return (
-
+
diff --git a/components/form-success.tsx b/components/form-success.tsx
index f799daf..e51b565 100644
--- a/components/form-success.tsx
+++ b/components/form-success.tsx
@@ -1,15 +1,14 @@
import { CircleCheck } from 'lucide-react'
-type Props = {
+type FormSuccessProps = {
message?: string
}
-const FormSuccess = ({ message }: Props) => {
+const FormSuccess = ({ message }: FormSuccessProps) => {
if (!message) return null
return (
-
+
diff --git a/components/locale-switcher.tsx b/components/locale-switcher.tsx
index 3dd6250..e9d3724 100644
--- a/components/locale-switcher.tsx
+++ b/components/locale-switcher.tsx
@@ -7,15 +7,18 @@ import { ChangeEvent } from 'react'
export default function LocaleSwitcher () {
const changeLocale = useChangeLocale()
const locale = useCurrentLocale()
- const selectHandler = (e: ChangeEvent
) => changeLocale(e.target.value as loc)
+ const selectHandler = (e: ChangeEvent) =>
+ changeLocale(e.target.value as loc)
+
// {cn(styles['yo-locale-switcher'], 'pr-4')}
return (//@ts-ignore
- )
}
\ No newline at end of file
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..eb48095
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline: 'text-foreground',
+ success:
+ 'border-transparent bg-emerald-500 text-primary-foreground hover:bg-emerald-500/80',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge ({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
new file mode 100644
index 0000000..452f4d9
--- /dev/null
+++ b/components/ui/sonner.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ )
+}
+
+export { Toaster }
diff --git a/config/auth.ts b/config/auth.ts
index ea736b4..028df28 100644
--- a/config/auth.ts
+++ b/config/auth.ts
@@ -1,4 +1,4 @@
-import NextAuth from 'next-auth'
+import NextAuth, { type DefaultSession } from 'next-auth'
import { UserRole } from '@prisma/client'
import { PrismaAdapter } from '@auth/prisma-adapter'
import db from '@/lib/db'
@@ -9,9 +9,16 @@ import { getCurrentLocale } from '@/locales/server'
import { type loc } from '@/config/locales'
import { getTwoFactorConfirmationByUserId } from '@/data/two-factor-confirmation'
+export type ExtendedUser = DefaultSession['user'] & {
+ role: UserRole,
+ locale: loc,
+ isTwoFactorEnabled?: boolean,
+ image?: string
+}
+
declare module 'next-auth' {
interface Session {
- user: { role: UserRole, locale: loc, image?: string }
+ user: ExtendedUser
}
}
@@ -67,6 +74,10 @@ export const {
session.user.role = token.role as UserRole
}
+ if (session.user) {
+ session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean
+ }
+
session.user.locale = getCurrentLocale()
return session
@@ -79,6 +90,7 @@ export const {
if (!existingUser) return token
token.role = existingUser.role
+ token.isTwoFactorEnabled = existingUser.isTwoFactorEnabled
return token
},
diff --git a/config/locales.ts b/config/locales.ts
index 252f45a..c107496 100644
--- a/config/locales.ts
+++ b/config/locales.ts
@@ -37,4 +37,6 @@ const LC: Locale[] = [
const locales: loc[] = LC.map((locale: Locale) => locale.code)
-export { locales, defaultLocale, fallbackLocale, LC, importLocales, type loc }
\ No newline at end of file
+const SKIP_I18N_URLS = '/api/'
+
+export { locales, defaultLocale, fallbackLocale, LC, importLocales, type loc, SKIP_I18N_URLS }
\ No newline at end of file
diff --git a/config/routes.ts b/config/routes.ts
index 488ba12..9a7be5b 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -1,6 +1,9 @@
import { UUID_V4_REGEX } from '@/config/validation'
export const USER_PROFILE_URL: string = '/cabinet'
+export const USER_SERVER_URL: string = `${USER_PROFILE_URL}/server`
+export const USER_CLIENT_URL: string = `${USER_PROFILE_URL}/client`
+export const USER_ADMIN_URL: string = `${USER_PROFILE_URL}/admin`
export const AUTH_URL: string = '/auth/'
export const AUTH_LOGIN_URL: string = `${AUTH_URL}login`
export const AUTH_REGISTER_URL: string = `${AUTH_URL}register`
@@ -9,6 +12,8 @@ export const AUTH_ERROR_URL: string = `${AUTH_URL}error`
export const AUTH_USER_VERIFICATION_URL: string = `${AUTH_URL}user-verification`
export const AUTH_NEW_PASSWORD_URL: string = `${AUTH_URL}new-password`
+export const CABINET_ROUTES: string[] = [USER_SERVER_URL, USER_CLIENT_URL, USER_ADMIN_URL, USER_PROFILE_URL] as const
+
/**
* An array of routes that accessible to the public.
* These routes do not requite authentication.
@@ -31,10 +36,10 @@ export const authRoutesRegEx = [
/**
* The prefix for API authentication routes.
- * Routes that start with this prefix are used for API authentication purpose.
+ * Routes that start with this prefix are used for API authentication purposes.
* @type {string}
*/
-export const apiAuthPrefix: string = '/api/auth'
+export const apiAuthPrefixRegEx: string = '/api/(auth|admin)'
/**
* The default redirect path after logging in.
diff --git a/hooks/useCurrentRole.ts b/hooks/useCurrentRole.ts
new file mode 100644
index 0000000..e1e4113
--- /dev/null
+++ b/hooks/useCurrentRole.ts
@@ -0,0 +1,7 @@
+import { useSession } from 'next-auth/react'
+
+export const useCurrentRole = () => {
+ const session = useSession()
+
+ return session.data?.user?.role
+}
\ No newline at end of file
diff --git a/lib/CSP.ts b/lib/CSP.ts
new file mode 100644
index 0000000..ff794ed
--- /dev/null
+++ b/lib/CSP.ts
@@ -0,0 +1,60 @@
+// @Docs https://content-security-policy.com/
+
+import { NextRequest, NextResponse } from 'next/server'
+
+export class CSP {
+ private readonly request: any
+ private readonly on: boolean
+ private readonly request_headers?: Headers
+ private readonly csp_with_nonce?: string
+ private next_response?: NextResponse
+
+ constructor (request: any, on?: boolean) {
+ this.request = request
+ this.on = on ?? true
+
+ if (this.on) {
+ const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
+ this.csp_with_nonce = CSP.contentSecurityPolicyHeaderValue(nonce)
+ this.request_headers = new Headers(this.request.headers)
+ this.request_headers.set('x-nonce', nonce)
+ this.request_headers.set('x-xxx', '123')
+ this.request_headers.set('Content-Security-Policy', this.csp_with_nonce)
+ }
+ }
+
+ private static contentSecurityPolicyHeaderValue (nonce: string): string {
+ //style-src 'self' 'nonce-${nonce}';
+ return `
+ default-src 'self';
+ script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
+ style-src 'self' 'unsafe-inline';
+ img-src 'self' blob: data: avatars.githubusercontent.com *.googleusercontent.com;
+ connect-src 'self';
+ font-src 'self';
+ object-src 'none';
+ base-uri 'self';
+ form-action 'self';
+ frame-ancestors 'none';
+ upgrade-insecure-requests`.replace(/\s{2,}/g, ' ').trim()
+ }
+
+ next (middleware?: (request: NextRequest) => NextResponse): NextResponse {
+ if (!this.on) {
+ return middleware ? middleware(this.request) : NextResponse.next()
+ }
+
+ if (middleware) {
+ const reqNext = new NextRequest(this.request, { headers: this.request_headers })
+ this.next_response = middleware(reqNext)
+ } else {
+ this.next_response = NextResponse.next({
+ request: { headers: this.request_headers },
+ })
+ }
+
+ this.next_response.headers.set('Content-Security-Policy', this.csp_with_nonce as string)
+
+ return this.next_response
+ }
+}
diff --git a/lib/auth.ts b/lib/auth.ts
new file mode 100644
index 0000000..f3f25e1
--- /dev/null
+++ b/lib/auth.ts
@@ -0,0 +1,13 @@
+import { auth } from '@/config/auth'
+
+export const currentUser = async () => {
+ const session = await auth()
+
+ return session?.user
+}
+
+export const currentRole = async () => {
+ const session = await auth()
+
+ return session?.user?.role
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index 8cba2e3..a36843a 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -21,6 +21,8 @@ export const testPathnameRegex = (
const pattern: string = `^(/(${locales.join('|')}))?(${pages.flatMap(
(p) => (p === '/' ? ['', '/'] : p)).join('|')})/?$`
+ //console.log(pattern)
+
return RegExp(pattern, 'is').test(pathName)
}
diff --git a/middleware.ts b/middleware.ts
index 16b2874..b2102d6 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -3,64 +3,56 @@ import NextAuth from 'next-auth'
import { NextRequest, NextResponse } from 'next/server'
import { defaultLocale, locales } from '@/config/locales'
import authConfig from '@/auth.config'
-import { apiAuthPrefix, AUTH_LOGIN_URL, authRoutesRegEx, DEFAULT_LOGIN_REDIRECT, publicRoutes } from '@/config/routes'
+import { apiAuthPrefixRegEx, AUTH_LOGIN_URL, authRoutesRegEx, DEFAULT_LOGIN_REDIRECT, publicRoutes } from '@/config/routes'
import { testPathnameRegex } from '@/lib/utils'
import { createI18nMiddleware } from 'next-international/middleware'
+import { CSP } from '@/lib/CSP'
interface AppRouteHandlerFnContext {
params?: Record;
}
+const I18nMiddleware = createI18nMiddleware({
+ locales, defaultLocale, urlMappingStrategy: 'rewriteDefault',
+})
+
+const { auth } = NextAuth(authConfig)
+
export const middleware = (request: NextRequest, event: AppRouteHandlerFnContext): NextResponse | null => {
- return NextAuth(authConfig).auth((request): any => {
+ return auth((request): any => {
+ //const csp = new CSP(request, process.env.NODE_ENV === 'production')
+ const csp = new CSP(request, false)
const { nextUrl }: { nextUrl: NextURL } = request
- const isLoggedIn: boolean = !!request.auth
- const isApiAuthRoute: boolean = nextUrl.pathname.startsWith(apiAuthPrefix)
- const isPublicRoute: boolean = testPathnameRegex(publicRoutes, nextUrl.pathname)
- const isAuthRoute: boolean = testPathnameRegex(authRoutesRegEx, nextUrl.pathname)
- if (isApiAuthRoute) {
- return null
+ if (nextUrl.pathname.match(apiAuthPrefixRegEx)) {
+ return csp.next()
}
- const I18nMiddleware = createI18nMiddleware({
- locales, defaultLocale, urlMappingStrategy: 'rewriteDefault',
- })
+ const isLoggedIn: boolean = !!request.auth
+ const isPublicRoute: boolean = testPathnameRegex(publicRoutes, nextUrl.pathname)
+ const isAuthRoute: boolean = testPathnameRegex(authRoutesRegEx, nextUrl.pathname)
if (isAuthRoute) {
if (isLoggedIn) {
return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
- return I18nMiddleware(request)
+ return csp.next(I18nMiddleware)
}
if (!isLoggedIn && !isPublicRoute) {
return NextResponse.redirect(new URL(AUTH_LOGIN_URL, nextUrl))
}
- return I18nMiddleware(request)
+ return csp.next(I18nMiddleware)
})(request, event) as NextResponse
}
-// export const config = {
-// matcher: [
-// /*
-// * Match all request paths except for the ones starting with:
-// * - api (API routes)
-// * - _next/static (static files)
-// * - _next/image (image optimization files)
-// * - favicon.ico (favicon file)
-// */
-// {
-// source: '/((?!.+\\.[\\w]+$|api|_next/image|favicon.ico|robots.txt|trpc).*)', missing: [
-// { type: 'header', key: 'next-router-prefetch' }, { type: 'header', key: 'purpose', value: 'prefetch' }],
-// }],
-// }
-
export const config = {
matcher: [
- '/((?!.+\\.[\\w]+$|_next|_next/image|_next/static).*)', '/(api|trpc)(.*)',
+ '/((?!.+\\.[\\w]+$|_next|_next/image|_next/static|favicon.ico|robots.txt).*)',
+ '/',
+ '/(api|trpc)(.*)',
],
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 838e34a..b9d838f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"next": "14.1.4",
"next-auth": "^5.0.0-beta.16",
"next-international": "^1.2.4",
+ "next-themes": "^0.3.0",
"nodemailer": "^6.9.13",
"pino": "^9.0.0",
"pino-http": "^9.0.0",
@@ -34,6 +35,7 @@
"react-loader-spinner": "^6.1.6",
"sharp": "^0.33.3",
"shart": "^0.0.4",
+ "sonner": "^1.4.41",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
@@ -4985,6 +4987,15 @@
"server-only": "^0.0.1"
}
},
+ "node_modules/next-themes": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
+ "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -6333,6 +6344,15 @@
"atomic-sleep": "^1.0.0"
}
},
+ "node_modules/sonner": {
+ "version": "1.4.41",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz",
+ "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==",
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
diff --git a/package.json b/package.json
index 87136bb..aca4dba 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
"version": "0.1.1",
"private": true,
"scripts": {
- "dev": "next dev",
+ "dev": "chcp 65001 && next dev",
"build": "next build",
- "start": "next start",
+ "start": "chcp 65001 && next start",
"lint": "next lint",
"browserslist:update": "npx update-browserslist-db@latest",
"browserslist": "npx browserslist"
@@ -32,6 +32,7 @@
"next": "14.1.4",
"next-auth": "^5.0.0-beta.16",
"next-international": "^1.2.4",
+ "next-themes": "^0.3.0",
"nodemailer": "^6.9.13",
"pino": "^9.0.0",
"pino-http": "^9.0.0",
@@ -42,6 +43,7 @@
"react-loader-spinner": "^6.1.6",
"sharp": "^0.33.3",
"shart": "^0.0.4",
+ "sonner": "^1.4.41",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
diff --git a/tsconfig.json b/tsconfig.json
index c061b48..c42ff89 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -29,6 +29,9 @@
"@/config/*": [
"./config/*"
],
+ "@/hooks/*": [
+ "./hooks/*"
+ ],
"@/data/*": [
"./data/*"
],