shortcut-next 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/templates/base/@core/clients/apiClient.ts +18 -11
- package/templates/base/@core/configs/authConfig.ts +47 -0
- package/templates/base/@core/configs/clientConfig.ts +28 -17
- package/templates/base/@core/context/AuthContext.tsx +202 -0
- package/templates/base/providers/AppProviders.tsx +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shortcut-next",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Scaffold Next.js apps with MUI base or Tailwind v4 preset.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -56,7 +56,11 @@
|
|
|
56
56
|
"cac": "^6.7.14",
|
|
57
57
|
"execa": "^9.6.0",
|
|
58
58
|
"fs-extra": "^11.3.1",
|
|
59
|
+
"js-cookie": "^3.0.5",
|
|
59
60
|
"kolorist": "^1.8.0",
|
|
60
61
|
"ora": "^8.2.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/js-cookie": "^3.0.6"
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
|
2
|
-
import {
|
|
2
|
+
import { baseURL, fallbackPage, requestTimeout, authConfig } from '../configs/clientConfig'
|
|
3
3
|
|
|
4
4
|
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
|
5
5
|
_retry?: boolean
|
|
@@ -37,8 +37,8 @@ const processQueue = (error: AxiosError | null, token: string | null = null) =>
|
|
|
37
37
|
// ** --- Request Interceptor
|
|
38
38
|
apiClient.interceptors.request.use((config: CustomAxiosRequestConfig) => {
|
|
39
39
|
if (typeof window !== 'undefined') {
|
|
40
|
-
const token = localStorage.getItem(
|
|
41
|
-
if (token && !config.url?.includes(
|
|
40
|
+
const token = localStorage.getItem(authConfig.storageTokenKeyName)
|
|
41
|
+
if (token && !config.url?.includes(authConfig.refreshEndpoint)) {
|
|
42
42
|
config.headers.Authorization = `Bearer ${token}`
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -74,7 +74,7 @@ apiClient.interceptors.response.use(
|
|
|
74
74
|
const { token } = refreshRes.data
|
|
75
75
|
|
|
76
76
|
if (typeof window !== 'undefined') {
|
|
77
|
-
localStorage.setItem(
|
|
77
|
+
localStorage.setItem(authConfig.storageTokenKeyName, token)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
processQueue(null, token)
|
|
@@ -101,20 +101,27 @@ apiClient.interceptors.response.use(
|
|
|
101
101
|
|
|
102
102
|
// ** --- Refresh function (uses cookies)
|
|
103
103
|
const refreshToken = () => {
|
|
104
|
-
return axios.post<RefreshResponse>(`${baseURL}${
|
|
104
|
+
return axios.post<RefreshResponse>(`${baseURL}${authConfig.refreshEndpoint}`, {}, { withCredentials: true })
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// ** --- Logout function
|
|
108
108
|
const logoutUser = async () => {
|
|
109
|
-
try {
|
|
110
|
-
await axios.post(`${baseURL}${logoutAPI}`, {}, { withCredentials: true })
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error(error)
|
|
113
|
-
}
|
|
114
109
|
if (typeof window !== 'undefined') {
|
|
115
|
-
|
|
110
|
+
// Clear all auth-related storage
|
|
111
|
+
localStorage.removeItem(authConfig.storageTokenKeyName)
|
|
112
|
+
localStorage.removeItem(authConfig.storageUserDataKeyName)
|
|
113
|
+
localStorage.removeItem(authConfig.storageRefreshTokenKeyName)
|
|
114
|
+
|
|
115
|
+
// Clear cookies
|
|
116
|
+
document.cookie = `${authConfig.cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`
|
|
117
|
+
document.cookie = `${authConfig.storageRefreshTokenKeyName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`
|
|
118
|
+
|
|
119
|
+
// Redirect to login page
|
|
116
120
|
window.location.href = fallbackPage
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
export default apiClient
|
|
125
|
+
|
|
126
|
+
// ** Export utilities for external use
|
|
127
|
+
export { logoutUser }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ** Auth Types - Simple and modifiable
|
|
2
|
+
export interface User {
|
|
3
|
+
id: string
|
|
4
|
+
email: string
|
|
5
|
+
name?: string
|
|
6
|
+
role?: string
|
|
7
|
+
[key: string]: unknown // Allow additional custom fields
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LoginCredentials {
|
|
11
|
+
email: string
|
|
12
|
+
password: string
|
|
13
|
+
[key: string]: unknown // Allow additional fields like rememberMe, etc.
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SignupCredentials {
|
|
17
|
+
email: string
|
|
18
|
+
password: string
|
|
19
|
+
name?: string
|
|
20
|
+
[key: string]: unknown // Allow additional fields
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuthResponse {
|
|
24
|
+
user: User
|
|
25
|
+
accessToken: string
|
|
26
|
+
refreshToken?: string
|
|
27
|
+
[key: string]: unknown // Allow additional response fields
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuthContextType {
|
|
31
|
+
// ** State
|
|
32
|
+
user: User | null
|
|
33
|
+
isAuthenticated: boolean
|
|
34
|
+
isLoading: boolean
|
|
35
|
+
|
|
36
|
+
// ** Actions
|
|
37
|
+
login: (credentials: LoginCredentials, onError?: ErrorCallback) => Promise<void>
|
|
38
|
+
signup: (credentials: SignupCredentials, onError?: ErrorCallback) => Promise<void>
|
|
39
|
+
logout: () => Promise<void>
|
|
40
|
+
|
|
41
|
+
// ** Utilities
|
|
42
|
+
setUser: (user: User | null) => void
|
|
43
|
+
setLoading: (loading: boolean) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ** Error callback type
|
|
47
|
+
export type ErrorCallback = (error: string | Record<string, string>) => void
|
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
// **
|
|
2
|
-
const
|
|
1
|
+
// ** Auth Configuration - Easily customizable for different apps
|
|
2
|
+
const authConfig = {
|
|
3
|
+
// ** API Configuration
|
|
4
|
+
baseURL: '/api',
|
|
5
|
+
loginEndpoint: '/auth/login',
|
|
6
|
+
signupEndpoint: '/auth/signup',
|
|
7
|
+
refreshEndpoint: '/auth/refresh',
|
|
3
8
|
|
|
4
|
-
// ** Keys
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
// ** Storage Keys
|
|
10
|
+
storageTokenKeyName: 'accessToken',
|
|
11
|
+
storageRefreshTokenKeyName: 'refreshToken',
|
|
12
|
+
storageUserDataKeyName: 'userData',
|
|
8
13
|
|
|
9
|
-
// **
|
|
10
|
-
|
|
14
|
+
// ** Routes
|
|
15
|
+
loginPageURL: '/login',
|
|
16
|
+
homePageURL: '/',
|
|
11
17
|
|
|
12
|
-
// **
|
|
13
|
-
|
|
18
|
+
// ** Request Configuration
|
|
19
|
+
requestTimeout: 15000, // 15 seconds
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fallbackPage,
|
|
21
|
-
requestTimeout
|
|
21
|
+
// ** Cookie Configuration
|
|
22
|
+
cookieName: 'accessToken',
|
|
23
|
+
cookieMaxAge: 60 * 60 * 24 * 7, // 7 days
|
|
24
|
+
cookieSecure: typeof window !== 'undefined' ? window.location.protocol === 'https:' : true,
|
|
25
|
+
cookieSameSite: 'Strict' as const
|
|
22
26
|
}
|
|
27
|
+
|
|
28
|
+
// ** Legacy exports for backward compatibility
|
|
29
|
+
const baseURL = authConfig.baseURL
|
|
30
|
+
const fallbackPage = authConfig.loginPageURL
|
|
31
|
+
const requestTimeout = authConfig.requestTimeout
|
|
32
|
+
|
|
33
|
+
export { authConfig, baseURL, fallbackPage, requestTimeout }
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
import { authConfig } from '@/@core/configs/clientConfig'
|
|
7
|
+
import type {
|
|
8
|
+
User,
|
|
9
|
+
LoginCredentials,
|
|
10
|
+
SignupCredentials,
|
|
11
|
+
AuthResponse,
|
|
12
|
+
AuthContextType,
|
|
13
|
+
ErrorCallback
|
|
14
|
+
} from '@/@core/configs/authConfig'
|
|
15
|
+
|
|
16
|
+
// ** Default context value
|
|
17
|
+
const defaultProvider: AuthContextType = {
|
|
18
|
+
user: null,
|
|
19
|
+
isAuthenticated: false,
|
|
20
|
+
isLoading: true,
|
|
21
|
+
login: () => Promise.resolve(),
|
|
22
|
+
signup: () => Promise.resolve(),
|
|
23
|
+
logout: () => Promise.resolve(),
|
|
24
|
+
setUser: () => null,
|
|
25
|
+
setLoading: () => null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ** Create context
|
|
29
|
+
const AuthContext = createContext<AuthContextType>(defaultProvider)
|
|
30
|
+
|
|
31
|
+
// ** Auth Provider Props
|
|
32
|
+
interface AuthProviderProps {
|
|
33
|
+
children: ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ** Auth Provider Component
|
|
37
|
+
export const AuthProvider = ({ children }: AuthProviderProps) => {
|
|
38
|
+
// ** State
|
|
39
|
+
const [user, setUser] = useState<User | null>(null)
|
|
40
|
+
const [isLoading, setIsLoading] = useState<boolean>(true)
|
|
41
|
+
|
|
42
|
+
// ** Hooks
|
|
43
|
+
const router = useRouter()
|
|
44
|
+
|
|
45
|
+
// ** Computed
|
|
46
|
+
const isAuthenticated = !!user
|
|
47
|
+
|
|
48
|
+
// ** Initialize auth state on mount
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const initAuth = () => {
|
|
51
|
+
try {
|
|
52
|
+
const token = localStorage.getItem(authConfig.storageTokenKeyName)
|
|
53
|
+
const userData = localStorage.getItem(authConfig.storageUserDataKeyName)
|
|
54
|
+
|
|
55
|
+
if (token && userData) {
|
|
56
|
+
const parsedUser = JSON.parse(userData) as User
|
|
57
|
+
setUser(parsedUser)
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to initialize auth:', error)
|
|
61
|
+
// Clear invalid data
|
|
62
|
+
localStorage.removeItem(authConfig.storageTokenKeyName)
|
|
63
|
+
localStorage.removeItem(authConfig.storageUserDataKeyName)
|
|
64
|
+
localStorage.removeItem(authConfig.storageRefreshTokenKeyName)
|
|
65
|
+
} finally {
|
|
66
|
+
setIsLoading(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
initAuth()
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
// ** Login function
|
|
74
|
+
const login = async (credentials: LoginCredentials, onError?: ErrorCallback): Promise<void> => {
|
|
75
|
+
try {
|
|
76
|
+
setIsLoading(true)
|
|
77
|
+
|
|
78
|
+
const response = await axios.post<AuthResponse>(
|
|
79
|
+
`${authConfig.baseURL}${authConfig.loginEndpoint}`,
|
|
80
|
+
credentials,
|
|
81
|
+
{ timeout: authConfig.requestTimeout }
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const { user: userData, accessToken, refreshToken } = response.data
|
|
85
|
+
|
|
86
|
+
// Store tokens and user data
|
|
87
|
+
localStorage.setItem(authConfig.storageTokenKeyName, accessToken)
|
|
88
|
+
localStorage.setItem(authConfig.storageUserDataKeyName, JSON.stringify(userData))
|
|
89
|
+
|
|
90
|
+
if (refreshToken) {
|
|
91
|
+
localStorage.setItem(authConfig.storageRefreshTokenKeyName, refreshToken)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Set cookie for SSR
|
|
95
|
+
document.cookie = `${authConfig.cookieName}=${accessToken}; path=/; max-age=${authConfig.cookieMaxAge}; SameSite=${authConfig.cookieSameSite}${authConfig.cookieSecure ? '; Secure' : ''}`
|
|
96
|
+
|
|
97
|
+
setUser(userData)
|
|
98
|
+
|
|
99
|
+
// Redirect to home page
|
|
100
|
+
router.push(authConfig.homePageURL)
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Login failed:', error)
|
|
104
|
+
const message = axios.isAxiosError(error) && error.response?.data?.message
|
|
105
|
+
? error.response.data.message
|
|
106
|
+
: 'Login failed. Please try again.'
|
|
107
|
+
|
|
108
|
+
onError?.(message)
|
|
109
|
+
} finally {
|
|
110
|
+
setIsLoading(false)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ** Signup function
|
|
115
|
+
const signup = async (credentials: SignupCredentials, onError?: ErrorCallback): Promise<void> => {
|
|
116
|
+
try {
|
|
117
|
+
setIsLoading(true)
|
|
118
|
+
|
|
119
|
+
const response = await axios.post<AuthResponse>(
|
|
120
|
+
`${authConfig.baseURL}${authConfig.signupEndpoint}`,
|
|
121
|
+
credentials,
|
|
122
|
+
{ timeout: authConfig.requestTimeout }
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const { user: userData, accessToken, refreshToken } = response.data
|
|
126
|
+
|
|
127
|
+
// Store tokens and user data
|
|
128
|
+
localStorage.setItem(authConfig.storageTokenKeyName, accessToken)
|
|
129
|
+
localStorage.setItem(authConfig.storageUserDataKeyName, JSON.stringify(userData))
|
|
130
|
+
|
|
131
|
+
if (refreshToken) {
|
|
132
|
+
localStorage.setItem(authConfig.storageRefreshTokenKeyName, refreshToken)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Set cookie for SSR
|
|
136
|
+
document.cookie = `${authConfig.cookieName}=${accessToken}; path=/; max-age=${authConfig.cookieMaxAge}; SameSite=${authConfig.cookieSameSite}${authConfig.cookieSecure ? '; Secure' : ''}`
|
|
137
|
+
|
|
138
|
+
setUser(userData)
|
|
139
|
+
|
|
140
|
+
// Redirect to home page
|
|
141
|
+
router.push(authConfig.homePageURL)
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Signup failed:', error)
|
|
145
|
+
const message = axios.isAxiosError(error) && error.response?.data?.message
|
|
146
|
+
? error.response.data.message
|
|
147
|
+
: 'Signup failed. Please try again.'
|
|
148
|
+
|
|
149
|
+
onError?.(message)
|
|
150
|
+
} finally {
|
|
151
|
+
setIsLoading(false)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ** Logout function
|
|
156
|
+
const logout = async (): Promise<void> => {
|
|
157
|
+
// Clear all auth data regardless of API call result
|
|
158
|
+
setUser(null)
|
|
159
|
+
localStorage.removeItem(authConfig.storageTokenKeyName)
|
|
160
|
+
localStorage.removeItem(authConfig.storageUserDataKeyName)
|
|
161
|
+
localStorage.removeItem(authConfig.storageRefreshTokenKeyName)
|
|
162
|
+
|
|
163
|
+
// Clear cookie
|
|
164
|
+
document.cookie = `${authConfig.cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`
|
|
165
|
+
|
|
166
|
+
// Redirect to login page
|
|
167
|
+
router.push(authConfig.loginPageURL)
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ** Context value
|
|
172
|
+
const contextValue: AuthContextType = {
|
|
173
|
+
user,
|
|
174
|
+
isAuthenticated,
|
|
175
|
+
isLoading,
|
|
176
|
+
login,
|
|
177
|
+
signup,
|
|
178
|
+
logout,
|
|
179
|
+
setUser,
|
|
180
|
+
setLoading: setIsLoading
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<AuthContext.Provider value={contextValue}>
|
|
185
|
+
{children}
|
|
186
|
+
</AuthContext.Provider>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ** Auth Hook
|
|
191
|
+
export const useAuth = (): AuthContextType => {
|
|
192
|
+
const context = useContext(AuthContext)
|
|
193
|
+
|
|
194
|
+
if (!context) {
|
|
195
|
+
throw new Error('useAuth must be used within an AuthProvider')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return context
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ** Export context for advanced usage
|
|
202
|
+
export { AuthContext }
|
|
@@ -8,11 +8,12 @@ import I18nProvider from '@/providers/I18nProvider'
|
|
|
8
8
|
import HydrationGate from '@/components/HydrationGate'
|
|
9
9
|
import { useSettings } from '@/@core/hooks/useSettings'
|
|
10
10
|
import Spinner from '@/components/loaders/Spinner'
|
|
11
|
+
import { AuthProvider } from '@/@core/context/AuthContext'
|
|
11
12
|
|
|
12
13
|
function ThemedProviders({ children, client }: { children: React.ReactNode; client: QueryClient }) {
|
|
13
14
|
const { settings } = useSettings()
|
|
14
15
|
return (
|
|
15
|
-
<ThemeComponent settings={
|
|
16
|
+
<ThemeComponent settings={settings}>
|
|
16
17
|
<QueryClientProvider client={client}>
|
|
17
18
|
<I18nProvider>
|
|
18
19
|
<HydrationGate fallback={<Spinner />}>{children}</HydrationGate>
|
|
@@ -26,8 +27,10 @@ export default function AppProviders({ children }: { children: React.ReactNode }
|
|
|
26
27
|
const [client] = useState(() => new QueryClient())
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
30
|
+
<AuthProvider>
|
|
31
|
+
<SettingsProvider>
|
|
32
|
+
<ThemedProviders client={client}>{children}</ThemedProviders>
|
|
33
|
+
</SettingsProvider>
|
|
34
|
+
</AuthProvider>
|
|
32
35
|
)
|
|
33
36
|
}
|