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.
Files changed (106) hide show
  1. package/README.md +149 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1192 -0
  4. package/package.json +43 -0
  5. package/templates/default/.changeset/config.json +11 -0
  6. package/templates/default/.eslintignore +16 -0
  7. package/templates/default/.eslintrc.js +67 -0
  8. package/templates/default/.github/actions/build/action.yml +35 -0
  9. package/templates/default/.github/actions/quality/action.yml +53 -0
  10. package/templates/default/.github/dependabot.yml +51 -0
  11. package/templates/default/.github/workflows/deploy-dev.yml +83 -0
  12. package/templates/default/.github/workflows/deploy-prod.yml +83 -0
  13. package/templates/default/.github/workflows/deploy-rec.yml +83 -0
  14. package/templates/default/.husky/commit-msg +1 -0
  15. package/templates/default/.husky/pre-commit +1 -0
  16. package/templates/default/.nexu-version +1 -0
  17. package/templates/default/.prettierignore +7 -0
  18. package/templates/default/.prettierrc +19 -0
  19. package/templates/default/.vscode/extensions.json +14 -0
  20. package/templates/default/.vscode/settings.json +36 -0
  21. package/templates/default/apps/gitkeep +0 -0
  22. package/templates/default/commitlint.config.js +26 -0
  23. package/templates/default/docker/docker-compose.dev.yml +49 -0
  24. package/templates/default/docker/docker-compose.prod.yml +64 -0
  25. package/templates/default/docker/docker-compose.yml +6 -0
  26. package/templates/default/docs/architecture.md +452 -0
  27. package/templates/default/docs/cli.md +330 -0
  28. package/templates/default/docs/contributing.md +462 -0
  29. package/templates/default/docs/scripts.md +460 -0
  30. package/templates/default/gitignore +44 -0
  31. package/templates/default/lintstagedrc.cjs +4 -0
  32. package/templates/default/package.json +51 -0
  33. package/templates/default/packages/auth/package.json +61 -0
  34. package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
  35. package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
  36. package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
  37. package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
  38. package/templates/default/packages/auth/src/components/index.ts +4 -0
  39. package/templates/default/packages/auth/src/hooks/index.ts +4 -0
  40. package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
  41. package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
  42. package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
  43. package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
  44. package/templates/default/packages/auth/src/index.ts +45 -0
  45. package/templates/default/packages/auth/src/next/index.ts +18 -0
  46. package/templates/default/packages/auth/src/next/middleware.ts +183 -0
  47. package/templates/default/packages/auth/src/next/server.ts +219 -0
  48. package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
  49. package/templates/default/packages/auth/src/providers/index.ts +1 -0
  50. package/templates/default/packages/auth/src/types/index.ts +284 -0
  51. package/templates/default/packages/auth/src/utils/api.ts +228 -0
  52. package/templates/default/packages/auth/src/utils/index.ts +3 -0
  53. package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
  54. package/templates/default/packages/auth/src/utils/token.ts +204 -0
  55. package/templates/default/packages/auth/tsconfig.json +14 -0
  56. package/templates/default/packages/auth/tsup.config.ts +18 -0
  57. package/templates/default/packages/cache/package.json +26 -0
  58. package/templates/default/packages/cache/src/index.ts +137 -0
  59. package/templates/default/packages/cache/tsconfig.json +9 -0
  60. package/templates/default/packages/cache/tsup.config.ts +9 -0
  61. package/templates/default/packages/config/eslint/index.js +20 -0
  62. package/templates/default/packages/config/package.json +9 -0
  63. package/templates/default/packages/config/typescript/base.json +26 -0
  64. package/templates/default/packages/constants/package.json +26 -0
  65. package/templates/default/packages/constants/src/index.ts +121 -0
  66. package/templates/default/packages/constants/tsconfig.json +9 -0
  67. package/templates/default/packages/constants/tsup.config.ts +9 -0
  68. package/templates/default/packages/logger/package.json +27 -0
  69. package/templates/default/packages/logger/src/index.ts +197 -0
  70. package/templates/default/packages/logger/tsconfig.json +11 -0
  71. package/templates/default/packages/logger/tsup.config.ts +9 -0
  72. package/templates/default/packages/result/package.json +26 -0
  73. package/templates/default/packages/result/src/index.ts +142 -0
  74. package/templates/default/packages/result/tsconfig.json +9 -0
  75. package/templates/default/packages/result/tsup.config.ts +9 -0
  76. package/templates/default/packages/types/package.json +26 -0
  77. package/templates/default/packages/types/src/index.ts +78 -0
  78. package/templates/default/packages/types/tsconfig.json +9 -0
  79. package/templates/default/packages/types/tsup.config.ts +10 -0
  80. package/templates/default/packages/ui/package.json +38 -0
  81. package/templates/default/packages/ui/src/components/Button.tsx +58 -0
  82. package/templates/default/packages/ui/src/components/Card.tsx +85 -0
  83. package/templates/default/packages/ui/src/components/Input.tsx +45 -0
  84. package/templates/default/packages/ui/src/index.ts +15 -0
  85. package/templates/default/packages/ui/tsconfig.json +11 -0
  86. package/templates/default/packages/ui/tsup.config.ts +11 -0
  87. package/templates/default/packages/utils/package.json +30 -0
  88. package/templates/default/packages/utils/src/index.test.ts +130 -0
  89. package/templates/default/packages/utils/src/index.ts +154 -0
  90. package/templates/default/packages/utils/tsconfig.json +10 -0
  91. package/templates/default/packages/utils/tsup.config.ts +10 -0
  92. package/templates/default/pnpm-workspace.yaml +3 -0
  93. package/templates/default/scripts/audit.mjs +700 -0
  94. package/templates/default/scripts/deploy.mjs +40 -0
  95. package/templates/default/scripts/generate-app.mjs +808 -0
  96. package/templates/default/scripts/lib/package-manager.mjs +186 -0
  97. package/templates/default/scripts/setup.mjs +102 -0
  98. package/templates/default/services/.env.example +16 -0
  99. package/templates/default/services/docker-compose.yml +207 -0
  100. package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
  101. package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
  102. package/templates/default/services/postgres/init/gitkeep +2 -0
  103. package/templates/default/services/prometheus/prometheus.yml +13 -0
  104. package/templates/default/tsconfig.json +27 -0
  105. package/templates/default/turbo.json +40 -0
  106. 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
+ }