Skip to content

Commit 8cbc1f0

Browse files
committed
fix(auth-resets): secure logout session invalidation, cookie-based token refresh lifecycle, explicit cookie path configuration, and UTC monthly reset timezone alignment
1 parent 578f7ef commit 8cbc1f0

6 files changed

Lines changed: 50 additions & 17 deletions

File tree

src/__tests__/commission-monthly-reset.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ describe('Commission Tier Monthly Reset Flow', () => {
7878
it('should identify when monthly reset is needed', () => {
7979
// Current month's reset should not need reset
8080
const now = new Date()
81-
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1)
81+
const currentMonthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0))
8282
expect(shouldResetMonthlyVolume(currentMonthStart.toISOString())).toBe(false)
8383

8484
// Last month's reset should need reset
85-
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
85+
const lastMonthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0, 0))
8686
expect(shouldResetMonthlyVolume(lastMonthStart.toISOString())).toBe(true)
8787

8888
// Null should need reset

src/app/api/auth/login/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ export async function POST(request: NextRequest) {
5151
},
5252
});
5353

54-
// Set httpOnly cookies for secure session persistence
54+
// Set httpOnly cookies for secure session persistence with explicit path
5555
response.cookies.set({
5656
name: 'sb-access-token',
5757
value: data.session.access_token,
5858
httpOnly: true,
5959
secure: process.env.NODE_ENV === 'production',
6060
sameSite: 'lax',
61+
path: '/',
6162
maxAge: data.session.expires_in,
6263
});
6364

@@ -67,6 +68,7 @@ export async function POST(request: NextRequest) {
6768
httpOnly: true,
6869
secure: process.env.NODE_ENV === 'production',
6970
sameSite: 'lax',
71+
path: '/',
7072
maxAge: 60 * 60 * 24 * 7, // 7 days
7173
});
7274

src/app/api/auth/logout/route.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,40 @@ import { NextRequest, NextResponse } from 'next/server';
22
import { getSupabase } from '@/lib/db';
33
import * as Sentry from '@sentry/nextjs';
44

5-
export async function POST(_request: NextRequest) {
5+
export async function POST(request: NextRequest) {
66
try {
77
const supabase = getSupabase();
8-
const { error } = await supabase.auth.signOut();
8+
9+
// Retrieve cookies to populate session state
10+
const accessToken = request.cookies.get('sb-access-token')?.value;
11+
const refreshToken = request.cookies.get('sb-refresh-token')?.value;
12+
13+
if (accessToken && refreshToken) {
14+
await supabase.auth.setSession({
15+
access_token: accessToken,
16+
refresh_token: refreshToken,
17+
});
18+
}
19+
20+
// Call signOut with global scope to invalidate session in Supabase DB
21+
const { error } = await supabase.auth.signOut({ scope: 'global' });
922

1023
if (error) {
1124
Sentry.captureException(error);
12-
return NextResponse.json(
13-
{ error: error.message },
25+
const errorResponse = NextResponse.json(
26+
{ error: 'Logout failed' },
1427
{ status: 400 }
1528
);
29+
errorResponse.cookies.set('sb-access-token', '', { path: '/', maxAge: 0 });
30+
errorResponse.cookies.set('sb-refresh-token', '', { path: '/', maxAge: 0 });
31+
return errorResponse;
1632
}
1733

1834
const response = NextResponse.json({ message: 'Logout successful' });
1935

20-
// Clear session cookies
21-
response.cookies.delete('sb-access-token');
22-
response.cookies.delete('sb-refresh-token');
36+
// Clear session cookies using explicit path
37+
response.cookies.set('sb-access-token', '', { path: '/', maxAge: 0 });
38+
response.cookies.set('sb-refresh-token', '', { path: '/', maxAge: 0 });
2339

2440
return response;
2541
} catch (error) {

src/app/api/auth/refresh/route.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import * as Sentry from '@sentry/nextjs';
44

55
export async function POST(request: NextRequest) {
66
try {
7-
const { refreshToken } = await request.json();
7+
let refreshToken = request.cookies.get('sb-refresh-token')?.value;
8+
9+
if (!refreshToken) {
10+
try {
11+
const body = await request.json();
12+
refreshToken = body.refreshToken;
13+
} catch {
14+
// Ignore JSON parsing errors if cookie was expected
15+
}
16+
}
817

918
if (!refreshToken) {
1019
return NextResponse.json(
@@ -24,40 +33,46 @@ export async function POST(request: NextRequest) {
2433
if (!error && !data.session) {
2534
Sentry.captureException(new Error('Token refresh: no error but no session'));
2635
}
27-
return NextResponse.json(
36+
const errorResponse = NextResponse.json(
2837
{ error: 'Token refresh failed' },
2938
{ status: 401 }
3039
);
40+
errorResponse.cookies.set('sb-access-token', '', { path: '/', maxAge: 0 });
41+
errorResponse.cookies.set('sb-refresh-token', '', { path: '/', maxAge: 0 });
42+
return errorResponse;
3143
}
3244

3345
const response = NextResponse.json({
3446
message: 'Token refreshed successfully',
3547
session: {
3648
access_token: data.session.access_token,
49+
refresh_token: data.session.refresh_token,
3750
expires_in: data.session.expires_in,
3851
expires_at: data.session.expires_at,
3952
token_type: 'Bearer',
4053
},
4154
});
4255

43-
// Update access token cookie
56+
// Update access token cookie with explicit path
4457
response.cookies.set({
4558
name: 'sb-access-token',
4659
value: data.session.access_token,
4760
httpOnly: true,
4861
secure: process.env.NODE_ENV === 'production',
4962
sameSite: 'lax',
63+
path: '/',
5064
maxAge: data.session.expires_in,
5165
});
5266

53-
// Update refresh token cookie if provided
67+
// Update refresh token cookie if provided with explicit path
5468
if (data.session.refresh_token) {
5569
response.cookies.set({
5670
name: 'sb-refresh-token',
5771
value: data.session.refresh_token,
5872
httpOnly: true,
5973
secure: process.env.NODE_ENV === 'production',
6074
sameSite: 'lax',
75+
path: '/',
6176
maxAge: 60 * 60 * 24 * 7, // 7 days
6277
});
6378
}

src/lib/commissions/monthlyResetScheduler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export function shouldResetMonthlyVolume(resetAt: string | null): boolean {
3232
const lastReset = new Date(resetAt);
3333
const now = new Date();
3434

35-
// Get start of current month (00:00:00 on the 1st)
36-
const startOfCurrentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
35+
// Get start of current month in UTC (00:00:00 on the 1st) to align timezone logic
36+
const startOfCurrentMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0));
3737

3838
return lastReset < startOfCurrentMonth;
3939
}

tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)