nexu-app 2.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/README.md +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export * from './types';
|
|
3
|
+
|
|
4
|
+
// Provider
|
|
5
|
+
export { AuthProvider, useAuthContext } from './providers';
|
|
6
|
+
|
|
7
|
+
// Hooks
|
|
8
|
+
export { useAuth, useUser, useSession, useRequireAuth } from './hooks';
|
|
9
|
+
|
|
10
|
+
// Components
|
|
11
|
+
export { SignInForm, SignUpForm, SocialButtons, SocialButton, ProtectedRoute } from './components';
|
|
12
|
+
|
|
13
|
+
// Utilities
|
|
14
|
+
export { TokenManager, createTokenManager } from './utils/token';
|
|
15
|
+
export { AuthApiClient, createAuthApiClient } from './utils/api';
|
|
16
|
+
export {
|
|
17
|
+
generateState,
|
|
18
|
+
generateCodeVerifier,
|
|
19
|
+
generateCodeChallenge,
|
|
20
|
+
buildOAuthUrl,
|
|
21
|
+
getProviderConfig,
|
|
22
|
+
initiateOAuthFlow,
|
|
23
|
+
parseOAuthCallback,
|
|
24
|
+
} from './utils/oauth';
|
|
25
|
+
|
|
26
|
+
// Re-export for convenience
|
|
27
|
+
export type {
|
|
28
|
+
AuthConfig,
|
|
29
|
+
AuthUser,
|
|
30
|
+
AuthSession,
|
|
31
|
+
AuthState,
|
|
32
|
+
AuthError,
|
|
33
|
+
AuthErrorCode,
|
|
34
|
+
AuthProvider as AuthProviderType,
|
|
35
|
+
SignInCredentials,
|
|
36
|
+
SignUpCredentials,
|
|
37
|
+
AuthResponse,
|
|
38
|
+
UseAuthReturn,
|
|
39
|
+
UseUserReturn,
|
|
40
|
+
UseSessionReturn,
|
|
41
|
+
SignInFormProps,
|
|
42
|
+
SignUpFormProps,
|
|
43
|
+
SocialButtonsProps,
|
|
44
|
+
ProtectedRouteProps,
|
|
45
|
+
} from './types';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Middleware helpers
|
|
2
|
+
export {
|
|
3
|
+
createAuthMiddleware,
|
|
4
|
+
getTokenFromRequest,
|
|
5
|
+
isRequestAuthenticated,
|
|
6
|
+
type AuthMiddlewareConfig,
|
|
7
|
+
} from './middleware';
|
|
8
|
+
|
|
9
|
+
// Server-side helpers (for Server Components)
|
|
10
|
+
export {
|
|
11
|
+
getAccessToken,
|
|
12
|
+
getRefreshToken,
|
|
13
|
+
getSession,
|
|
14
|
+
getUser,
|
|
15
|
+
isAuthenticated,
|
|
16
|
+
hasRole,
|
|
17
|
+
hasPermission,
|
|
18
|
+
} from './server';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
|
|
4
|
+
const ACCESS_TOKEN_KEY = 'auth_access_token';
|
|
5
|
+
|
|
6
|
+
export interface AuthMiddlewareConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Routes that require authentication
|
|
9
|
+
* Supports glob patterns like '/dashboard/*'
|
|
10
|
+
*/
|
|
11
|
+
protectedRoutes?: string[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Routes that should redirect to home if already authenticated
|
|
15
|
+
*/
|
|
16
|
+
authRoutes?: string[];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* URL to redirect unauthenticated users
|
|
20
|
+
* @default '/login'
|
|
21
|
+
*/
|
|
22
|
+
loginUrl?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* URL to redirect authenticated users from auth routes
|
|
26
|
+
* @default '/'
|
|
27
|
+
*/
|
|
28
|
+
homeUrl?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cookie name for access token
|
|
32
|
+
* @default 'auth_access_token'
|
|
33
|
+
*/
|
|
34
|
+
tokenCookieName?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom callback for additional checks
|
|
38
|
+
*/
|
|
39
|
+
onAuthenticated?: (request: NextRequest) => NextResponse | null | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Match a path against a pattern (supports wildcards)
|
|
44
|
+
*/
|
|
45
|
+
function matchPath(path: string, pattern: string): boolean {
|
|
46
|
+
// Exact match
|
|
47
|
+
if (pattern === path) return true;
|
|
48
|
+
|
|
49
|
+
// Wildcard at end (e.g., '/dashboard/*')
|
|
50
|
+
if (pattern.endsWith('/*')) {
|
|
51
|
+
const base = pattern.slice(0, -2);
|
|
52
|
+
return path === base || path.startsWith(base + '/');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Wildcard in middle (e.g., '/api/*/users')
|
|
56
|
+
const regexPattern = pattern.replace(/\*/g, '[^/]+').replace(/\//g, '\\/');
|
|
57
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
58
|
+
return regex.test(path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if path matches any pattern in the list
|
|
63
|
+
*/
|
|
64
|
+
function matchesAnyPattern(path: string, patterns: string[]): boolean {
|
|
65
|
+
return patterns.some(pattern => matchPath(path, pattern));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decode JWT token (basic decode without verification)
|
|
70
|
+
*/
|
|
71
|
+
function decodeToken(token: string): { exp?: number } | null {
|
|
72
|
+
try {
|
|
73
|
+
const parts = token.split('.');
|
|
74
|
+
if (parts.length !== 3) return null;
|
|
75
|
+
|
|
76
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8')) as {
|
|
77
|
+
exp?: number;
|
|
78
|
+
};
|
|
79
|
+
return payload;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if token is expired
|
|
87
|
+
*/
|
|
88
|
+
function isTokenExpired(token: string): boolean {
|
|
89
|
+
const payload = decodeToken(token);
|
|
90
|
+
if (!payload || !payload.exp) return true;
|
|
91
|
+
return Date.now() >= payload.exp * 1000;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create authentication middleware for Next.js
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* // middleware.ts
|
|
100
|
+
* import { createAuthMiddleware } from '@repo/auth/next';
|
|
101
|
+
*
|
|
102
|
+
* export const middleware = createAuthMiddleware({
|
|
103
|
+
* protectedRoutes: ['/dashboard/*', '/settings/*', '/api/protected/*'],
|
|
104
|
+
* authRoutes: ['/login', '/signup', '/forgot-password'],
|
|
105
|
+
* loginUrl: '/login',
|
|
106
|
+
* homeUrl: '/dashboard',
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* export const config = {
|
|
110
|
+
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
111
|
+
* };
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function createAuthMiddleware(config: AuthMiddlewareConfig = {}) {
|
|
115
|
+
const {
|
|
116
|
+
protectedRoutes = [],
|
|
117
|
+
authRoutes = [],
|
|
118
|
+
loginUrl = '/login',
|
|
119
|
+
homeUrl = '/',
|
|
120
|
+
tokenCookieName = ACCESS_TOKEN_KEY,
|
|
121
|
+
onAuthenticated,
|
|
122
|
+
} = config;
|
|
123
|
+
|
|
124
|
+
return function authMiddleware(request: NextRequest): NextResponse {
|
|
125
|
+
const { pathname } = request.nextUrl;
|
|
126
|
+
|
|
127
|
+
// Get token from cookie
|
|
128
|
+
const token = request.cookies.get(tokenCookieName)?.value;
|
|
129
|
+
const isAuthenticated = token && !isTokenExpired(token);
|
|
130
|
+
|
|
131
|
+
// Check if route is protected
|
|
132
|
+
const isProtectedRoute = matchesAnyPattern(pathname, protectedRoutes);
|
|
133
|
+
|
|
134
|
+
// Check if route is an auth route (login, signup, etc.)
|
|
135
|
+
const isAuthRoute = matchesAnyPattern(pathname, authRoutes);
|
|
136
|
+
|
|
137
|
+
// Redirect unauthenticated users from protected routes
|
|
138
|
+
if (isProtectedRoute && !isAuthenticated) {
|
|
139
|
+
const url = new URL(loginUrl, request.url);
|
|
140
|
+
url.searchParams.set('returnTo', pathname);
|
|
141
|
+
return NextResponse.redirect(url);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Redirect authenticated users from auth routes
|
|
145
|
+
if (isAuthRoute && isAuthenticated) {
|
|
146
|
+
const returnTo = request.nextUrl.searchParams.get('returnTo');
|
|
147
|
+
const redirectUrl = returnTo || homeUrl;
|
|
148
|
+
return NextResponse.redirect(new URL(redirectUrl, request.url));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Run custom callback if provided
|
|
152
|
+
if (isAuthenticated && onAuthenticated) {
|
|
153
|
+
const customResponse = onAuthenticated(request);
|
|
154
|
+
if (customResponse) return customResponse;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return NextResponse.next();
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Helper to get auth token from request (for API routes)
|
|
163
|
+
*/
|
|
164
|
+
export function getTokenFromRequest(request: NextRequest): string | null {
|
|
165
|
+
// Try Authorization header first
|
|
166
|
+
const authHeader = request.headers.get('authorization');
|
|
167
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
168
|
+
return authHeader.substring(7);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Try cookie
|
|
172
|
+
const token = request.cookies.get(ACCESS_TOKEN_KEY)?.value;
|
|
173
|
+
return token || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Helper to check if request is authenticated (for API routes)
|
|
178
|
+
*/
|
|
179
|
+
export function isRequestAuthenticated(request: NextRequest): boolean {
|
|
180
|
+
const token = getTokenFromRequest(request);
|
|
181
|
+
if (!token) return false;
|
|
182
|
+
return !isTokenExpired(token);
|
|
183
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
|
|
3
|
+
import type { AuthSession, AuthUser, TokenPayload } from '../types';
|
|
4
|
+
|
|
5
|
+
const ACCESS_TOKEN_KEY = 'auth_access_token';
|
|
6
|
+
const REFRESH_TOKEN_KEY = 'auth_refresh_token';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decode JWT token (basic decode without verification)
|
|
10
|
+
* For server-side use, you should verify the token with your backend
|
|
11
|
+
*/
|
|
12
|
+
function decodeToken(token: string): TokenPayload | null {
|
|
13
|
+
try {
|
|
14
|
+
const parts = token.split('.');
|
|
15
|
+
if (parts.length !== 3) return null;
|
|
16
|
+
|
|
17
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8')) as TokenPayload;
|
|
18
|
+
return payload;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if token is expired
|
|
26
|
+
*/
|
|
27
|
+
function isTokenExpired(token: string): boolean {
|
|
28
|
+
const payload = decodeToken(token);
|
|
29
|
+
if (!payload || !payload.exp) return true;
|
|
30
|
+
return Date.now() >= payload.exp * 1000;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the access token from cookies (Server Components)
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* // app/dashboard/page.tsx
|
|
39
|
+
* import { getAccessToken } from '@repo/auth/next';
|
|
40
|
+
*
|
|
41
|
+
* export default async function DashboardPage() {
|
|
42
|
+
* const token = await getAccessToken();
|
|
43
|
+
*
|
|
44
|
+
* if (!token) {
|
|
45
|
+
* redirect('/login');
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* // Fetch data with token
|
|
49
|
+
* const data = await fetch('/api/data', {
|
|
50
|
+
* headers: { Authorization: `Bearer ${token}` },
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* return <Dashboard data={data} />;
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export async function getAccessToken(): Promise<string | null> {
|
|
58
|
+
const cookieStore = await cookies();
|
|
59
|
+
const token = cookieStore.get(ACCESS_TOKEN_KEY)?.value;
|
|
60
|
+
|
|
61
|
+
if (!token || isTokenExpired(token)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the refresh token from cookies (Server Components)
|
|
70
|
+
*/
|
|
71
|
+
export async function getRefreshToken(): Promise<string | null> {
|
|
72
|
+
const cookieStore = await cookies();
|
|
73
|
+
return cookieStore.get(REFRESH_TOKEN_KEY)?.value || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get session from cookies (Server Components)
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* // app/profile/page.tsx
|
|
82
|
+
* import { getSession } from '@repo/auth/next';
|
|
83
|
+
* import { redirect } from 'next/navigation';
|
|
84
|
+
*
|
|
85
|
+
* export default async function ProfilePage() {
|
|
86
|
+
* const session = await getSession();
|
|
87
|
+
*
|
|
88
|
+
* if (!session) {
|
|
89
|
+
* redirect('/login');
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* return <Profile user={session.user} />;
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export async function getSession(): Promise<AuthSession | null> {
|
|
97
|
+
const token = await getAccessToken();
|
|
98
|
+
if (!token) return null;
|
|
99
|
+
|
|
100
|
+
const payload = decodeToken(token);
|
|
101
|
+
if (!payload) return null;
|
|
102
|
+
|
|
103
|
+
const refreshToken = await getRefreshToken();
|
|
104
|
+
|
|
105
|
+
// Build user from token payload
|
|
106
|
+
const user: AuthUser = {
|
|
107
|
+
id: payload.sub,
|
|
108
|
+
email: payload.email || '',
|
|
109
|
+
name: payload.name,
|
|
110
|
+
metadata: {},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Copy additional fields from payload to metadata
|
|
114
|
+
const knownFields = ['sub', 'email', 'name', 'exp', 'iat', 'aud', 'iss'];
|
|
115
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
116
|
+
if (!knownFields.includes(key)) {
|
|
117
|
+
user.metadata![key] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
user,
|
|
123
|
+
accessToken: token,
|
|
124
|
+
refreshToken: refreshToken || undefined,
|
|
125
|
+
expiresAt: payload.exp * 1000,
|
|
126
|
+
provider: 'email', // Will be overwritten if available in token
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get user from session (Server Components)
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* // app/layout.tsx
|
|
136
|
+
* import { getUser } from '@repo/auth/next';
|
|
137
|
+
*
|
|
138
|
+
* export default async function RootLayout({ children }) {
|
|
139
|
+
* const user = await getUser();
|
|
140
|
+
*
|
|
141
|
+
* return (
|
|
142
|
+
* <html>
|
|
143
|
+
* <body>
|
|
144
|
+
* <Header user={user} />
|
|
145
|
+
* {children}
|
|
146
|
+
* </body>
|
|
147
|
+
* </html>
|
|
148
|
+
* );
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export async function getUser(): Promise<AuthUser | null> {
|
|
153
|
+
const session = await getSession();
|
|
154
|
+
return session?.user || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if user is authenticated (Server Components)
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```tsx
|
|
162
|
+
* // app/admin/layout.tsx
|
|
163
|
+
* import { isAuthenticated } from '@repo/auth/next';
|
|
164
|
+
* import { redirect } from 'next/navigation';
|
|
165
|
+
*
|
|
166
|
+
* export default async function AdminLayout({ children }) {
|
|
167
|
+
* const authenticated = await isAuthenticated();
|
|
168
|
+
*
|
|
169
|
+
* if (!authenticated) {
|
|
170
|
+
* redirect('/login?returnTo=/admin');
|
|
171
|
+
* }
|
|
172
|
+
*
|
|
173
|
+
* return <>{children}</>;
|
|
174
|
+
* }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export async function isAuthenticated(): Promise<boolean> {
|
|
178
|
+
const token = await getAccessToken();
|
|
179
|
+
return token !== null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if user has specific roles (Server Components)
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```tsx
|
|
187
|
+
* // app/admin/page.tsx
|
|
188
|
+
* import { hasRole } from '@repo/auth/next';
|
|
189
|
+
* import { redirect } from 'next/navigation';
|
|
190
|
+
*
|
|
191
|
+
* export default async function AdminPage() {
|
|
192
|
+
* const isAdmin = await hasRole(['admin', 'superadmin']);
|
|
193
|
+
*
|
|
194
|
+
* if (!isAdmin) {
|
|
195
|
+
* redirect('/unauthorized');
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* return <AdminDashboard />;
|
|
199
|
+
* }
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export async function hasRole(roles: string[]): Promise<boolean> {
|
|
203
|
+
const session = await getSession();
|
|
204
|
+
if (!session) return false;
|
|
205
|
+
|
|
206
|
+
const userRoles = (session.user.metadata?.roles as string[]) || [];
|
|
207
|
+
return roles.some(role => userRoles.includes(role));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if user has specific permissions (Server Components)
|
|
212
|
+
*/
|
|
213
|
+
export async function hasPermission(permissions: string[]): Promise<boolean> {
|
|
214
|
+
const session = await getSession();
|
|
215
|
+
if (!session) return false;
|
|
216
|
+
|
|
217
|
+
const userPermissions = (session.user.metadata?.permissions as string[]) || [];
|
|
218
|
+
return permissions.every(perm => userPermissions.includes(perm));
|
|
219
|
+
}
|