nextauthz 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +1986 -0
- package/dist/index.mjs +1969 -0
- package/package.json +26 -0
- package/readme.md +180 -0
- package/src/AuthGuard.tsx +67 -0
- package/src/AuthProvider.tsx +89 -0
- package/src/RoleGuard.tsx +37 -0
- package/src/index.ts +16 -0
- package/src/myAuth.ts +7 -0
- package/store/useGuardStore.ts +39 -0
- package/tsconfig.json +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextauthz",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
10
|
+
"dev": "tsup src/index.ts --watch"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/react": "^19.2.14",
|
|
17
|
+
"@types/react-dom": "^19.2.3",
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"next": "^16.1.6",
|
|
22
|
+
"react-token-manager": "^1.0.6",
|
|
23
|
+
"tsup": "^8.5.1",
|
|
24
|
+
"zustand": "^5.0.11"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
## Auth System Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This authentication system provides:
|
|
6
|
+
|
|
7
|
+
** Global auth state
|
|
8
|
+
|
|
9
|
+
** Token persistence
|
|
10
|
+
|
|
11
|
+
** Typed AuthProvider & useAuth
|
|
12
|
+
|
|
13
|
+
** Route protection
|
|
14
|
+
|
|
15
|
+
** Role-based access
|
|
16
|
+
|
|
17
|
+
** Configurable storage system
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
npm install nextauthz
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## AuthProvider & useAuth
|
|
25
|
+
|
|
26
|
+
Purpose
|
|
27
|
+
|
|
28
|
+
Wrap your application with AuthProvider to provide global auth state (user, loading, error) and helper functions (login, logout, setUser).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
import { createAppAuth } from 'nextauthz'
|
|
32
|
+
|
|
33
|
+
// Choose storage type
|
|
34
|
+
const { AuthProvider } = createAppAuth('localStorage')
|
|
35
|
+
// Options: 'localStorage' | 'sessionStorage' | 'cookie'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
If no storage is passed, default is:
|
|
39
|
+
|
|
40
|
+
createAppAuth() // defaults to 'cookie'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function App({ children }: { children: React.ReactNode }) {
|
|
44
|
+
return <AuthProvider>{children}</AuthProvider>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default App
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Hook: useAuth
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
import { useAuth } from 'nextauthz'
|
|
55
|
+
|
|
56
|
+
const { user, login, logout, setUser, loading, error } = useAuth()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Available Properties
|
|
60
|
+
|
|
61
|
+
| Name | Type | Description | |
|
|
62
|
+
| --------- | ------------------------ | -------------------------------------------- | -------------------------- |
|
|
63
|
+
| `user` | `User | null` | Current authenticated user |
|
|
64
|
+
| `login` | `(tokens, user) => void` | Login and save tokens + user | |
|
|
65
|
+
| `logout` | `() => void` | Clear auth tokens and reset state | |
|
|
66
|
+
| `setUser` | `(user) => void` | Update user state manually | |
|
|
67
|
+
| `loading` | `boolean` | True while restoring user from token storage | |
|
|
68
|
+
| `error` | `Error | null` | Last auth error |
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
Example: Logging in
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
const handleLogin = async () => {
|
|
75
|
+
const tokens = {
|
|
76
|
+
access_token: 'abc123',
|
|
77
|
+
refresh_token: 'xyz789',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const user = {
|
|
81
|
+
id: 1,
|
|
82
|
+
name: 'John',
|
|
83
|
+
role: 'admin',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
login(tokens, user)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## AuthGuard
|
|
92
|
+
|
|
93
|
+
Purpose
|
|
94
|
+
|
|
95
|
+
Protect routes/pages to ensure only authenticated users can access them.
|
|
96
|
+
|
|
97
|
+
Props
|
|
98
|
+
|
|
99
|
+
| Name | Type | Default | Description |
|
|
100
|
+
| -------------- | ----------------------- | -------------- | -------------------------------------- |
|
|
101
|
+
| `children` | `ReactNode` | required | Components to render if authenticated |
|
|
102
|
+
| `redirectTo` | `string` | `/login` | Page to redirect unauthenticated users |
|
|
103
|
+
| `tokenKey` | `string` | `access_token` | Token key to validate |
|
|
104
|
+
| `refreshToken` | `() => Promise<string>` | optional | Function to refresh expired token |
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Usage Example
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
import AuthGuard from 'nextauthz'
|
|
111
|
+
|
|
112
|
+
function DashboardPage() {
|
|
113
|
+
return (
|
|
114
|
+
<AuthGuard>
|
|
115
|
+
<h1>Dashboard</h1>
|
|
116
|
+
</AuthGuard>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
Behavior:
|
|
121
|
+
|
|
122
|
+
** Redirects to /login if the user is not authenticated.
|
|
123
|
+
|
|
124
|
+
** Supports token refresh with refreshToken callback.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## RoleGuard
|
|
128
|
+
|
|
129
|
+
Purpose
|
|
130
|
+
|
|
131
|
+
Restrict access to specific roles after authentication.
|
|
132
|
+
|
|
133
|
+
| Name | Type | Default | Description |
|
|
134
|
+
| -------------- | ----------- | --------------- | -------------------------------------------- |
|
|
135
|
+
| `children` | `ReactNode` | required | Components to render if user role is allowed |
|
|
136
|
+
| `allowedRoles` | `string[]` | required | Roles allowed to access this page |
|
|
137
|
+
| `redirectTo` | `string` | `/unauthorized` | Redirect page for unauthorized roles |
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
Usage Example
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
import RoleGuard from '@/auth/RoleGuard'
|
|
144
|
+
import { useAuth } from '@/auth'
|
|
145
|
+
|
|
146
|
+
function AdminPage() {
|
|
147
|
+
return (
|
|
148
|
+
<RoleGuard allowedRoles={['admin']}>
|
|
149
|
+
<h1>Admin Dashboard</h1>
|
|
150
|
+
</RoleGuard>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Storage Options
|
|
156
|
+
|
|
157
|
+
You can configure token storage:
|
|
158
|
+
|
|
159
|
+
| Storage Type | Description |
|
|
160
|
+
| ---------------- | ------------------------------- |
|
|
161
|
+
| `localStorage` | Persists until manually cleared |
|
|
162
|
+
| `sessionStorage` | Clears on tab close |
|
|
163
|
+
| `cookie` | Cookie-based storage (default) |
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Why NextAuthZ?
|
|
167
|
+
|
|
168
|
+
** Zero boilerplate
|
|
169
|
+
|
|
170
|
+
** Fully typed
|
|
171
|
+
|
|
172
|
+
** Storage configurable
|
|
173
|
+
|
|
174
|
+
** Clean architecture
|
|
175
|
+
|
|
176
|
+
** Works with Next.js App Router
|
|
177
|
+
|
|
178
|
+
** Lightweight
|
|
179
|
+
|
|
180
|
+
** No opinionated backend
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useTokenManager } from 'react-token-manager'
|
|
6
|
+
import { useAuthStore } from '../store/useGuardStore'
|
|
7
|
+
|
|
8
|
+
type AuthGuardProps = {
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
redirectTo?: string
|
|
11
|
+
tokenKey?: string
|
|
12
|
+
refreshToken?: () => Promise<string | null>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const AuthGuard = ({
|
|
16
|
+
children,
|
|
17
|
+
redirectTo = '/login',
|
|
18
|
+
tokenKey = 'access_token',
|
|
19
|
+
refreshToken,
|
|
20
|
+
}: AuthGuardProps) => {
|
|
21
|
+
const manager = useTokenManager()
|
|
22
|
+
const router = useRouter()
|
|
23
|
+
const isAuthChecked = useAuthStore((state) => state.isAuthChecked)
|
|
24
|
+
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
|
|
25
|
+
const error = useAuthStore((state) => state.error)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const checkAuth = async () => {
|
|
29
|
+
try {
|
|
30
|
+
let token = manager.getSingleToken(tokenKey)
|
|
31
|
+
|
|
32
|
+
if (!token || manager.isExpired(token)) {
|
|
33
|
+
if (refreshToken) {
|
|
34
|
+
const newToken = await refreshToken()
|
|
35
|
+
if (newToken) {
|
|
36
|
+
manager.setTokens({ [tokenKey]: newToken })
|
|
37
|
+
token = newToken
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isValid = token && !manager.isExpired(token)
|
|
43
|
+
useAuthStore.getState().setAuth(Boolean(isValid))
|
|
44
|
+
|
|
45
|
+
if (!isValid) {
|
|
46
|
+
router.replace(redirectTo)
|
|
47
|
+
}
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
useAuthStore.getState().setError(err instanceof Error ? err : new Error(String(err)))
|
|
50
|
+
useAuthStore.getState().setAuth(false)
|
|
51
|
+
router.replace(redirectTo)
|
|
52
|
+
} finally {
|
|
53
|
+
useAuthStore.getState().setAuthChecked(true)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
checkAuth()
|
|
58
|
+
}, [manager, router, redirectTo, tokenKey, refreshToken])
|
|
59
|
+
|
|
60
|
+
if (!isAuthChecked) return <div>Loading...</div>
|
|
61
|
+
if (error) return <div>Error: {error.message}</div>
|
|
62
|
+
if (!isAuthenticated) return null
|
|
63
|
+
|
|
64
|
+
return <>{children}</>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default AuthGuard
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, ReactNode, useState, useEffect } from 'react'
|
|
4
|
+
import { configureTokenManager, useTokenManager } from 'react-token-manager'
|
|
5
|
+
import { useAuthStore } from '../store/useGuardStore'
|
|
6
|
+
|
|
7
|
+
export type AuthContextType<UserType> = {
|
|
8
|
+
user: UserType | any
|
|
9
|
+
login: (tokens: Record<string, string>, user: UserType) => void
|
|
10
|
+
logout: () => void
|
|
11
|
+
setUser: (user: UserType) => void
|
|
12
|
+
loading: boolean
|
|
13
|
+
error: Error | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AuthContextOptions = {
|
|
17
|
+
storage?: 'localStorage' | 'sessionStorage' | 'cookie'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Factory to create typed AuthProvider and useAuth hook
|
|
22
|
+
*/
|
|
23
|
+
export function createAuthContext<UserType>(options?: AuthContextOptions) {
|
|
24
|
+
const storageType = options?.storage || 'cookie'
|
|
25
|
+
configureTokenManager({ storage: storageType })
|
|
26
|
+
const manager = useTokenManager()
|
|
27
|
+
|
|
28
|
+
const AuthContext = createContext<AuthContextType<UserType> | null>(null)
|
|
29
|
+
|
|
30
|
+
const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
31
|
+
const [loading, setLoading] = useState(true)
|
|
32
|
+
const user = useAuthStore((state) => state.user)
|
|
33
|
+
const error = useAuthStore((state) => state.error)
|
|
34
|
+
|
|
35
|
+
// Restore user on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const savedUser = manager.getSingleToken('user')
|
|
38
|
+
if (savedUser) {
|
|
39
|
+
try {
|
|
40
|
+
const parsedUser = JSON.parse(savedUser)
|
|
41
|
+
useAuthStore.getState().setUser(parsedUser)
|
|
42
|
+
useAuthStore.getState().setError(null)
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
useAuthStore.getState().resetAuth()
|
|
45
|
+
useAuthStore.getState().setError(new Error('Failed to parse saved user'))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setLoading(false)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const setUser = (userData: any) => {
|
|
52
|
+
useAuthStore.getState().setUser(userData)
|
|
53
|
+
useAuthStore.getState().setError(null)
|
|
54
|
+
manager.setTokens({ user: JSON.stringify(userData) })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const login = (tokens: Record<string, string>, userData: UserType) => {
|
|
58
|
+
try {
|
|
59
|
+
manager.setTokens(tokens)
|
|
60
|
+
setUser(userData)
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
useAuthStore.getState().setError(err instanceof Error ? err : new Error(String(err)))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const logout = () => {
|
|
67
|
+
try {
|
|
68
|
+
manager.clearTokens()
|
|
69
|
+
useAuthStore.getState().resetAuth()
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
useAuthStore.getState().setError(err instanceof Error ? err : new Error(String(err)))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<AuthContext.Provider value={{ user, login, logout, setUser, loading, error }}>
|
|
77
|
+
{children}
|
|
78
|
+
</AuthContext.Provider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const useAuth = (): AuthContextType<UserType> => {
|
|
83
|
+
const ctx = useContext(AuthContext)
|
|
84
|
+
if (!ctx) throw new Error('useAuth must be used inside AuthProvider')
|
|
85
|
+
return ctx
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { AuthProvider, useAuth }
|
|
89
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useAuth } from './myAuth'
|
|
6
|
+
|
|
7
|
+
type RoleGuardProps = {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
allowedRoles: string[]
|
|
10
|
+
redirectTo?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RoleGuard = ({
|
|
14
|
+
children,
|
|
15
|
+
allowedRoles,
|
|
16
|
+
redirectTo = '/unauthorized',
|
|
17
|
+
}: RoleGuardProps) => {
|
|
18
|
+
const { user, loading } = useAuth()
|
|
19
|
+
const router = useRouter()
|
|
20
|
+
const [isChecking, setIsChecking] = useState(true)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!user) return
|
|
24
|
+
|
|
25
|
+
const hasAccess = allowedRoles.includes(user?.role)
|
|
26
|
+
if (!hasAccess) {
|
|
27
|
+
router.replace(redirectTo)
|
|
28
|
+
}
|
|
29
|
+
setIsChecking(false)
|
|
30
|
+
}, [user, allowedRoles, redirectTo, router])
|
|
31
|
+
|
|
32
|
+
if (loading || !user || isChecking) return <div>Loading...</div>
|
|
33
|
+
|
|
34
|
+
return <>{children}</>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default RoleGuard
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createAuthContext } from './AuthProvider'
|
|
2
|
+
|
|
3
|
+
export type User = {
|
|
4
|
+
[key: string]: any
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Export factory instead of fixed instance
|
|
8
|
+
export function createAppAuth(
|
|
9
|
+
storage: 'localStorage' | 'sessionStorage' | 'cookie' = 'cookie'
|
|
10
|
+
) {
|
|
11
|
+
return createAuthContext<User>({ storage })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { createAuthContext } from './AuthProvider'
|
|
15
|
+
export { default as AuthGuard } from './AuthGuard'
|
|
16
|
+
export { default as RoleGuard } from './RoleGuard'
|
package/src/myAuth.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
|
|
5
|
+
export type User = {
|
|
6
|
+
[key: string]: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type AuthState = {
|
|
10
|
+
user: User | null // user can be null
|
|
11
|
+
isAuthenticated: boolean
|
|
12
|
+
isAuthChecked: boolean
|
|
13
|
+
error: Error | null
|
|
14
|
+
setUser: (user: User | null) => void
|
|
15
|
+
setAuth: (isAuth: boolean) => void
|
|
16
|
+
setAuthChecked: (checked: boolean) => void
|
|
17
|
+
setError: (err: Error | null) => void
|
|
18
|
+
resetAuth: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
22
|
+
user: null,
|
|
23
|
+
isAuthenticated: false,
|
|
24
|
+
isAuthChecked: false,
|
|
25
|
+
error: null,
|
|
26
|
+
|
|
27
|
+
setUser: (user) => set({ user }),
|
|
28
|
+
setAuth: (isAuth) => set({ isAuthenticated: isAuth }),
|
|
29
|
+
setAuthChecked: (checked) => set({ isAuthChecked: checked }),
|
|
30
|
+
setError: (err) => set({ error: err }),
|
|
31
|
+
|
|
32
|
+
resetAuth: () =>
|
|
33
|
+
set({
|
|
34
|
+
user: null,
|
|
35
|
+
isAuthenticated: false,
|
|
36
|
+
isAuthChecked: false,
|
|
37
|
+
error: null,
|
|
38
|
+
}),
|
|
39
|
+
}))
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"jsx": "react-jsx", // important
|
|
7
|
+
"strict": true,
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
}
|
|
13
|
+
}
|