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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shortcut-next",
3
- "version": "0.2.0",
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 { accessToken, baseURL, refreshAPI, logoutAPI, fallbackPage, requestTimeout } from '../configs/clientConfig'
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(accessToken)
41
- if (token && !config.url?.includes(refreshAPI)) {
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(accessToken, token)
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}${refreshAPI}`, {}, { withCredentials: true })
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
- localStorage.removeItem(accessToken)
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
- // ** Base API path (rewritten to backend in next.config.ts)
2
- const baseURL = '/api'
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 for localStorage/session
5
- const accessToken = 'accessToken'
6
- const refreshAPI = '/refresh-token'
7
- const logoutAPI = '/logout'
9
+ // ** Storage Keys
10
+ storageTokenKeyName: 'accessToken',
11
+ storageRefreshTokenKeyName: 'refreshToken',
12
+ storageUserDataKeyName: 'userData',
8
13
 
9
- // ** Where to redirect after logout
10
- const fallbackPage = '/login'
14
+ // ** Routes
15
+ loginPageURL: '/login',
16
+ homePageURL: '/',
11
17
 
12
- // ** Axios defaults
13
- const requestTimeout = 15000 // 15 seconds
18
+ // ** Request Configuration
19
+ requestTimeout: 15000, // 15 seconds
14
20
 
15
- export {
16
- baseURL,
17
- accessToken,
18
- refreshAPI,
19
- logoutAPI,
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={{ ...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
- <SettingsProvider>
30
- <ThemedProviders client={client}>{children}</ThemedProviders>
31
- </SettingsProvider>
30
+ <AuthProvider>
31
+ <SettingsProvider>
32
+ <ThemedProviders client={client}>{children}</ThemedProviders>
33
+ </SettingsProvider>
34
+ </AuthProvider>
32
35
  )
33
36
  }