shortcut-next 0.2.2 → 0.2.6

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.
Files changed (35) hide show
  1. package/package.json +5 -2
  2. package/templates/base/@core/configs/clientConfig.ts +1 -1
  3. package/templates/base/@core/context/AuthContext.tsx +21 -28
  4. package/templates/base/@core/hooks/useAbility.ts +58 -0
  5. package/templates/base/app/(dashboard)/dashboard/page.tsx +104 -0
  6. package/templates/base/app/(dashboard)/layout.tsx +97 -0
  7. package/templates/base/app/home/page.tsx +112 -0
  8. package/templates/base/app/login/page.tsx +296 -0
  9. package/templates/base/app/unauthorized/page.tsx +120 -0
  10. package/templates/base/components/MSWProvider.tsx +54 -0
  11. package/templates/base/components/auth/LoginForm.tsx +279 -0
  12. package/templates/base/components/auth/SignupForm.tsx +348 -0
  13. package/templates/base/components/loaders/Spinner.tsx +5 -24
  14. package/templates/base/components/ui/ErrorMessage.tsx +17 -0
  15. package/templates/base/components/ui/FormFieldWrapper.tsx +27 -0
  16. package/templates/base/docs/AuthorizationDocumentation.md +348 -0
  17. package/templates/base/lib/abilities/checkAuthorization.ts +74 -0
  18. package/templates/base/lib/abilities/index.ts +27 -0
  19. package/templates/base/lib/abilities/roles.ts +75 -0
  20. package/templates/base/lib/abilities/routeMap.ts +35 -0
  21. package/templates/base/lib/abilities/routeMatcher.ts +117 -0
  22. package/templates/base/lib/abilities/types.ts +68 -0
  23. package/templates/base/lib/mocks/browser.ts +11 -0
  24. package/templates/base/lib/mocks/db.ts +124 -0
  25. package/templates/base/lib/mocks/handlers/auth.ts +203 -0
  26. package/templates/base/lib/mocks/handlers/index.ts +16 -0
  27. package/templates/base/lib/mocks/index.ts +34 -0
  28. package/templates/base/lib/mocks/jwt.ts +99 -0
  29. package/templates/base/middleware.ts +147 -0
  30. package/templates/base/package-lock.json +725 -2
  31. package/templates/base/package.json +13 -2
  32. package/templates/base/providers/AppProviders.tsx +8 -5
  33. package/templates/base/public/locales/ar.json +73 -0
  34. package/templates/base/public/locales/en.json +73 -0
  35. package/templates/base/public/mockServiceWorker.js +349 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shortcut-next",
3
- "version": "0.2.2",
3
+ "version": "0.2.6",
4
4
  "description": "Scaffold Next.js apps with MUI base or Tailwind v4 preset.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -61,6 +61,9 @@
61
61
  "ora": "^8.2.0"
62
62
  },
63
63
  "devDependencies": {
64
- "@types/js-cookie": "^3.0.6"
64
+ "@semantic-release/changelog": "^6.0.3",
65
+ "@semantic-release/git": "^10.0.1",
66
+ "@types/js-cookie": "^3.0.6",
67
+ "semantic-release": "^25.0.3"
65
68
  }
66
69
  }
@@ -13,7 +13,7 @@ const authConfig = {
13
13
 
14
14
  // ** Routes
15
15
  loginPageURL: '/login',
16
- homePageURL: '/',
16
+ homePageURL: '/home',
17
17
 
18
18
  // ** Request Configuration
19
19
  requestTimeout: 15000, // 15 seconds
@@ -75,11 +75,9 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
75
75
  try {
76
76
  setIsLoading(true)
77
77
 
78
- const response = await axios.post<AuthResponse>(
79
- `${authConfig.baseURL}${authConfig.loginEndpoint}`,
80
- credentials,
81
- { timeout: authConfig.requestTimeout }
82
- )
78
+ const response = await axios.post<AuthResponse>(`${authConfig.baseURL}${authConfig.loginEndpoint}`, credentials, {
79
+ timeout: authConfig.requestTimeout
80
+ })
83
81
 
84
82
  const { user: userData, accessToken, refreshToken } = response.data
85
83
 
@@ -98,12 +96,12 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
98
96
 
99
97
  // Redirect to home page
100
98
  router.push(authConfig.homePageURL)
101
-
102
99
  } catch (error) {
103
100
  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.'
101
+ const message =
102
+ axios.isAxiosError(error) && error.response?.data?.message
103
+ ? error.response.data.message
104
+ : 'Login failed. Please try again.'
107
105
 
108
106
  onError?.(message)
109
107
  } finally {
@@ -139,12 +137,12 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
139
137
 
140
138
  // Redirect to home page
141
139
  router.push(authConfig.homePageURL)
142
-
143
140
  } catch (error) {
144
141
  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.'
142
+ const message =
143
+ axios.isAxiosError(error) && error.response?.data?.message
144
+ ? error.response.data.message
145
+ : 'Signup failed. Please try again.'
148
146
 
149
147
  onError?.(message)
150
148
  } finally {
@@ -154,18 +152,17 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
154
152
 
155
153
  // ** Logout function
156
154
  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`
155
+ // Clear all auth data regardless of API call result
156
+ setUser(null)
157
+ localStorage.removeItem(authConfig.storageTokenKeyName)
158
+ localStorage.removeItem(authConfig.storageUserDataKeyName)
159
+ localStorage.removeItem(authConfig.storageRefreshTokenKeyName)
165
160
 
166
- // Redirect to login page
167
- router.push(authConfig.loginPageURL)
161
+ // Clear cookie
162
+ document.cookie = `${authConfig.cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`
168
163
 
164
+ // Redirect to login page
165
+ router.push(authConfig.loginPageURL)
169
166
  }
170
167
 
171
168
  // ** Context value
@@ -180,11 +177,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
180
177
  setLoading: setIsLoading
181
178
  }
182
179
 
183
- return (
184
- <AuthContext.Provider value={contextValue}>
185
- {children}
186
- </AuthContext.Provider>
187
- )
180
+ return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
188
181
  }
189
182
 
190
183
  // ** Auth Hook
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import { useAuth } from '@/@core/context/AuthContext'
5
+ import { defineAbilitiesFor } from '@/lib/abilities'
6
+ import type { AppAbility, UserRole } from '@/lib/abilities'
7
+
8
+ /**
9
+ * Hook to get CASL ability instance for the current user
10
+ *
11
+ * Usage:
12
+ * ```tsx
13
+ * const ability = useAbility()
14
+ *
15
+ * if (ability.can('read', 'Users')) {
16
+ * // Show users link
17
+ * }
18
+ *
19
+ * if (ability.can('manage', 'Settings')) {
20
+ * // Show settings link
21
+ * }
22
+ * ```
23
+ *
24
+ * @returns CASL AppAbility instance
25
+ */
26
+ export function useAbility(): AppAbility {
27
+ const { user } = useAuth()
28
+
29
+ const ability = useMemo(() => {
30
+ const role = (user?.role as UserRole) || 'viewer'
31
+ return defineAbilitiesFor(role)
32
+ }, [user?.role])
33
+
34
+ return ability
35
+ }
36
+
37
+ /**
38
+ * Hook to check if current user can perform an action
39
+ *
40
+ * Convenience wrapper around useAbility for simple permission checks.
41
+ *
42
+ * Usage:
43
+ * ```tsx
44
+ * const canReadUsers = useCan('read', 'Users')
45
+ * const canManageSettings = useCan('manage', 'Settings')
46
+ * ```
47
+ *
48
+ * @param action - The action to check
49
+ * @param subject - The subject to check against
50
+ * @returns boolean indicating if the user can perform the action
51
+ */
52
+ export function useCan(
53
+ action: 'read' | 'create' | 'update' | 'delete' | 'manage',
54
+ subject: 'Dashboard' | 'Users' | 'Settings' | 'Reports' | 'Tickets' | 'all'
55
+ ): boolean {
56
+ const ability = useAbility()
57
+ return ability.can(action, subject)
58
+ }
@@ -0,0 +1,104 @@
1
+ 'use client'
2
+
3
+ import { Box, Container, Typography, Grid, Card, CardContent } from '@mui/material'
4
+ import { Users, Ticket, Settings, BarChart3 } from 'lucide-react'
5
+ import Link from 'next/link'
6
+
7
+ const dashboardCards = [
8
+ {
9
+ title: 'Users',
10
+ description: 'Manage system users',
11
+ icon: Users,
12
+ href: '/dashboard/users',
13
+ color: 'primary',
14
+ roles: ['admin', 'manager']
15
+ },
16
+ {
17
+ title: 'Tickets',
18
+ description: 'View and manage tickets',
19
+ icon: Ticket,
20
+ href: '/dashboard/tickets',
21
+ color: 'secondary',
22
+ roles: ['admin', 'manager', 'agent']
23
+ },
24
+ {
25
+ title: 'Reports',
26
+ description: 'View analytics and reports',
27
+ icon: BarChart3,
28
+ href: '/dashboard/reports',
29
+ color: 'info',
30
+ roles: ['admin', 'manager', 'agent', 'viewer']
31
+ },
32
+ {
33
+ title: 'Settings',
34
+ description: 'Configure application settings',
35
+ icon: Settings,
36
+ href: '/dashboard/settings',
37
+ color: 'warning',
38
+ roles: ['admin']
39
+ }
40
+ ]
41
+
42
+ /**
43
+ * Dashboard Home Page
44
+ *
45
+ * This page is accessible to all authenticated users.
46
+ * The navigation cards shown here are just examples -
47
+ * actual access control is enforced by middleware.
48
+ */
49
+ export default function DashboardPage() {
50
+ return (
51
+ <Container maxWidth='lg' sx={{ py: 4 }}>
52
+ <Typography variant='h4' fontWeight={700} gutterBottom>
53
+ Dashboard
54
+ </Typography>
55
+ <Typography variant='body1' color='text.secondary' sx={{ mb: 4 }}>
56
+ Welcome to your dashboard. Select a section to get started.
57
+ </Typography>
58
+
59
+ <Grid container spacing={3}>
60
+ {dashboardCards.map(card => (
61
+ <Grid size={{ xs: 12, sm: 6, md: 3 }} key={card.title}>
62
+ <Card
63
+ component={Link}
64
+ href={card.href}
65
+ sx={{
66
+ height: '100%',
67
+ textDecoration: 'none',
68
+ transition: 'transform 0.2s, box-shadow 0.2s',
69
+ '&:hover': {
70
+ transform: 'translateY(-4px)',
71
+ boxShadow: 4
72
+ }
73
+ }}
74
+ >
75
+ <CardContent sx={{ textAlign: 'center', py: 4 }}>
76
+ <Box
77
+ sx={{
78
+ width: 56,
79
+ height: 56,
80
+ borderRadius: 2,
81
+ bgcolor: `${card.color}.lighter`,
82
+ display: 'flex',
83
+ alignItems: 'center',
84
+ justifyContent: 'center',
85
+ mx: 'auto',
86
+ mb: 2
87
+ }}
88
+ >
89
+ <card.icon size={28} />
90
+ </Box>
91
+ <Typography variant='h6' fontWeight={600}>
92
+ {card.title}
93
+ </Typography>
94
+ <Typography variant='body2' color='text.secondary'>
95
+ {card.description}
96
+ </Typography>
97
+ </CardContent>
98
+ </Card>
99
+ </Grid>
100
+ ))}
101
+ </Grid>
102
+ </Container>
103
+ )
104
+ }
@@ -0,0 +1,97 @@
1
+ import { cookies } from 'next/headers'
2
+ import { redirect } from 'next/navigation'
3
+ import { decodeJwt } from 'jose'
4
+ import type { UserRole } from '@/lib/abilities'
5
+
6
+ /**
7
+ * JWT payload structure
8
+ */
9
+ interface JWTPayload {
10
+ sub: string
11
+ email?: string
12
+ role?: string
13
+ name?: string
14
+ exp?: number
15
+ }
16
+
17
+ /**
18
+ * Session data extracted from JWT
19
+ */
20
+ interface Session {
21
+ userId: string
22
+ role: UserRole
23
+ name?: string
24
+ email?: string
25
+ }
26
+
27
+ /**
28
+ * Extract session from cookies (server-side)
29
+ *
30
+ * This is the defense-in-depth check that runs in addition to middleware.
31
+ * It ensures no protected content is ever rendered without a valid session.
32
+ */
33
+ async function getServerSession(): Promise<Session | null> {
34
+ const cookieStore = await cookies()
35
+ const token = cookieStore.get('accessToken')?.value
36
+
37
+ if (!token) {
38
+ return null
39
+ }
40
+
41
+ try {
42
+ const payload = decodeJwt(token) as JWTPayload
43
+
44
+ // Check expiration
45
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
46
+ return null
47
+ }
48
+
49
+ const validRoles: UserRole[] = ['admin', 'manager', 'agent', 'viewer']
50
+ const role = validRoles.includes(payload.role as UserRole)
51
+ ? (payload.role as UserRole)
52
+ : 'viewer'
53
+
54
+ return {
55
+ userId: payload.sub,
56
+ role,
57
+ name: payload.name,
58
+ email: payload.email,
59
+ }
60
+ } catch {
61
+ return null
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Dashboard Layout
67
+ *
68
+ * This is a Server Component that provides defense-in-depth for all dashboard routes.
69
+ * Even if middleware is bypassed, this layout ensures no protected content renders
70
+ * without a valid session.
71
+ *
72
+ * The layout wraps all routes in the (dashboard) route group.
73
+ */
74
+ export default async function DashboardLayout({
75
+ children,
76
+ }: {
77
+ children: React.ReactNode
78
+ }) {
79
+ const session = await getServerSession()
80
+
81
+ // Defense-in-depth: redirect if no session
82
+ // This should rarely trigger since middleware handles it first
83
+ if (!session) {
84
+ redirect('/login?returnUrl=/dashboard')
85
+ }
86
+
87
+ return (
88
+ <div className="dashboard-layout">
89
+ {/*
90
+ Add dashboard navigation/sidebar here
91
+ Navigation items can be filtered based on session.role
92
+ using the useAbility hook in client components
93
+ */}
94
+ <main className="dashboard-content">{children}</main>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,112 @@
1
+ 'use client'
2
+
3
+ import { Container, Typography, Paper, Box, Chip, Button } from '@mui/material'
4
+ import { LogOut } from 'lucide-react'
5
+ import { useAuth } from '@/@core/context/AuthContext'
6
+ import { useCan } from '@/@core/hooks/useAbility'
7
+
8
+ // Role-specific home components
9
+ function AdminHome() {
10
+ return (
11
+ <Paper sx={{ p: 4, bgcolor: 'error.50', border: '2px solid', borderColor: 'error.main' }}>
12
+ <Typography variant='h5' color='error.main' gutterBottom>
13
+ Admin Dashboard
14
+ </Typography>
15
+ <Typography variant='body1' color='text.secondary'>
16
+ Welcome, Admin! You have full access to all system features including user management, settings, reports, and
17
+ tickets.
18
+ </Typography>
19
+ </Paper>
20
+ )
21
+ }
22
+
23
+ function ManagerHome() {
24
+ return (
25
+ <Paper sx={{ p: 4, bgcolor: 'warning.50', border: '2px solid', borderColor: 'warning.main' }}>
26
+ <Typography variant='h5' color='warning.main' gutterBottom>
27
+ Manager Dashboard
28
+ </Typography>
29
+ <Typography variant='body1' color='text.secondary'>
30
+ Welcome, Manager! You can manage users, tickets, and reports. Settings are restricted to admins.
31
+ </Typography>
32
+ </Paper>
33
+ )
34
+ }
35
+
36
+ function AgentHome() {
37
+ return (
38
+ <Paper sx={{ p: 4, bgcolor: 'info.50', border: '2px solid', borderColor: 'info.main' }}>
39
+ <Typography variant='h5' color='info.main' gutterBottom>
40
+ Agent Dashboard
41
+ </Typography>
42
+ <Typography variant='body1' color='text.secondary'>
43
+ Welcome, Agent! You can handle tickets and view reports. User management and settings are restricted.
44
+ </Typography>
45
+ </Paper>
46
+ )
47
+ }
48
+
49
+ function ViewerHome() {
50
+ return (
51
+ <Paper sx={{ p: 4, bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main' }}>
52
+ <Typography variant='h5' color='success.main' gutterBottom>
53
+ Viewer Dashboard
54
+ </Typography>
55
+ <Typography variant='body1' color='text.secondary'>
56
+ Welcome, Viewer! You have read-only access to the dashboard and reports.
57
+ </Typography>
58
+ </Paper>
59
+ )
60
+ }
61
+
62
+ export default function HomePage() {
63
+ const { user, logout } = useAuth()
64
+
65
+ // Use CASL abilities to determine which component to render
66
+ // Each check is based on unique permissions for that role
67
+ const canManageSettings = useCan('manage', 'Settings') // Only admin
68
+ const canManageUsers = useCan('manage', 'Users') // Admin & Manager
69
+ const canManageTickets = useCan('manage', 'Tickets') // Admin, Manager & Agent
70
+
71
+ // Determine the appropriate component based on abilities (most privileged first)
72
+ const getRoleInfo = () => {
73
+ if (canManageSettings) {
74
+ return { Component: AdminHome, label: 'ADMIN', color: 'error' as const }
75
+ }
76
+ if (canManageUsers) {
77
+ return { Component: ManagerHome, label: 'MANAGER', color: 'warning' as const }
78
+ }
79
+ if (canManageTickets) {
80
+ return { Component: AgentHome, label: 'AGENT', color: 'info' as const }
81
+ }
82
+ return { Component: ViewerHome, label: 'VIEWER', color: 'success' as const }
83
+ }
84
+
85
+ const { Component: RoleComponent, label, color } = getRoleInfo()
86
+
87
+ const handleLogout = () => {
88
+ logout()
89
+ }
90
+
91
+ return (
92
+ <Container maxWidth='md' sx={{ py: 6 }}>
93
+ <Box sx={{ mb: 4, textAlign: 'center' }}>
94
+ <Typography variant='h3' gutterBottom>
95
+ Welcome Home
96
+ </Typography>
97
+ <Typography variant='body1' color='text.secondary' sx={{ mb: 2 }}>
98
+ Hello, {user?.name || 'User'}!
99
+ </Typography>
100
+ <Chip label={label} color={color} size='medium' />
101
+ </Box>
102
+
103
+ <RoleComponent />
104
+
105
+ <Box sx={{ mt: 4, textAlign: 'center' }}>
106
+ <Button variant='outlined' color='error' startIcon={<LogOut size={18} />} onClick={handleLogout}>
107
+ Logout
108
+ </Button>
109
+ </Box>
110
+ </Container>
111
+ )
112
+ }