create-tigra 2.0.3 → 2.0.4

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": "create-tigra",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "type": "module",
5
5
  "description": "Create a production-ready full-stack app with Next.js 16 + Fastify 5 + Prisma + Redis",
6
6
  "bin": {
@@ -1,5 +1,14 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000";
4
+ const apiOrigin = (() => {
5
+ try {
6
+ return new URL(apiBaseUrl).origin;
7
+ } catch {
8
+ return "http://localhost:8000";
9
+ }
10
+ })();
11
+
3
12
  const nextConfig: NextConfig = {
4
13
  async headers() {
5
14
  return [
@@ -22,7 +31,7 @@ const nextConfig: NextConfig = {
22
31
  "base-uri 'self'",
23
32
  "form-action 'self'",
24
33
  "frame-ancestors 'none'",
25
- `connect-src 'self' ${process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"}`,
34
+ `connect-src 'self' ${apiOrigin}`,
26
35
  ].join("; "),
27
36
  },
28
37
  ],
@@ -80,12 +80,22 @@ apiClient.interceptors.response.use(
80
80
  } catch (refreshError) {
81
81
  processQueue(refreshError);
82
82
 
83
- const { logout } = await import('@/features/auth/store/authSlice');
84
- const { store } = await import('@/store');
85
- store.dispatch(logout());
86
-
87
- if (typeof window !== 'undefined') {
88
- window.location.href = ROUTES.LOGIN;
83
+ // Only logout on definitive auth failures (server responded with 4xx).
84
+ // Network errors (server unreachable, timeout) should NOT destroy the session.
85
+ const isAuthFailure =
86
+ axios.isAxiosError(refreshError) &&
87
+ refreshError.response != null &&
88
+ refreshError.response.status >= 400 &&
89
+ refreshError.response.status < 500;
90
+
91
+ if (isAuthFailure) {
92
+ const { logout } = await import('@/features/auth/store/authSlice');
93
+ const { store } = await import('@/store');
94
+ store.dispatch(logout());
95
+
96
+ if (typeof window !== 'undefined') {
97
+ window.location.href = ROUTES.LOGIN;
98
+ }
89
99
  }
90
100
 
91
101
  return Promise.reject(refreshError);
@@ -23,12 +23,21 @@ export function middleware(request: NextRequest): NextResponse {
23
23
  path === '/' ? pathname === '/' : pathname.startsWith(path)
24
24
  );
25
25
 
26
- if (isProtectedPath && (!token || isTokenExpired(token))) {
26
+ // No token at all — user is not logged in, redirect to login
27
+ if (isProtectedPath && !token) {
27
28
  const loginUrl = new URL('/login', request.url);
28
29
  loginUrl.searchParams.set('from', pathname);
29
30
  return NextResponse.redirect(loginUrl);
30
31
  }
31
32
 
33
+ // Expired token on protected path — allow through so client-side can attempt refresh.
34
+ // Delete the stale access_token so AuthInitializer starts fresh: getMe() → 401 → refresh.
35
+ if (isProtectedPath && token && isTokenExpired(token)) {
36
+ const response = NextResponse.next();
37
+ response.cookies.delete('access_token');
38
+ return response;
39
+ }
40
+
32
41
  const isAuthPath = authPaths.some((path) => pathname.startsWith(path));
33
42
 
34
43
  if (isAuthPath && token) {
@@ -1,6 +1,7 @@
1
1
  import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
  import { env } from '@config/env.js';
4
+ import { prisma } from '@libs/prisma.js';
4
5
  import { UnauthorizedError, ForbiddenError } from '@shared/errors/errors.js';
5
6
  import type { JwtPayload, UserRole } from '@shared/types/index.js';
6
7
 
@@ -58,6 +59,16 @@ export async function authenticate(
58
59
  } catch {
59
60
  throw new UnauthorizedError('Invalid or expired token');
60
61
  }
62
+
63
+ // Verify user still exists, is active, and not soft-deleted
64
+ const user = await prisma.user.findUnique({
65
+ where: { id: request.user.userId },
66
+ select: { isActive: true, deletedAt: true },
67
+ });
68
+
69
+ if (!user || !user.isActive || user.deletedAt) {
70
+ throw new UnauthorizedError('Account is deactivated or deleted');
71
+ }
61
72
  }
62
73
 
63
74
  export async function optionalAuth(
@@ -54,15 +54,17 @@ function sanitizeUser(user: {
54
54
  isActive: boolean;
55
55
  createdAt: Date;
56
56
  updatedAt: Date;
57
- password: string;
58
57
  }): SanitizedUser {
59
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
60
- const { password: _password, createdAt, updatedAt, avatarUrl, ...rest } = user;
61
58
  return {
62
- ...rest,
63
- avatarUrl: avatarUrl ?? null,
64
- createdAt: createdAt.toISOString(),
65
- updatedAt: updatedAt.toISOString(),
59
+ id: user.id,
60
+ email: user.email,
61
+ firstName: user.firstName,
62
+ lastName: user.lastName,
63
+ avatarUrl: user.avatarUrl ?? null,
64
+ role: user.role,
65
+ isActive: user.isActive,
66
+ createdAt: user.createdAt.toISOString(),
67
+ updatedAt: user.updatedAt.toISOString(),
66
68
  };
67
69
  }
68
70