Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/lib/data/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use server';

import jsonwebtoken from 'jsonwebtoken';
import { sdk } from "@lib/config"
import { setAuthToken } from './cookies';

interface DecodedJWT {
exp?: number;
sub?: string;
[key: string]: unknown;
}

/**
* Check if a JWT token is expired or will expire soon
* @param token - The JWT token to validate
* @param bufferSeconds - Number of seconds before expiry to consider token as expired (default: 60)
* @returns true if token is expired or will expire within buffer time
*/
export async function isTokenExpired(token: string, bufferSeconds = 60): Promise<boolean> {
try {
const decoded = jsonwebtoken.decode(token) as DecodedJWT | null;

if (!decoded || !decoded.exp) {
console.info('No expiration claim means we should treat as expired');
return true; // No expiration claim means we should treat as expired
}

const currentTime = Math.floor(Date.now() / 1000);
const expirationTime = decoded.exp - bufferSeconds;

return currentTime >= expirationTime;
} catch {
return true; // If we can't decode, treat as expired
}
}

/**
* Refresh the Medusa authentication token using the SDK
* This function attempts to refresh the token by calling the Medusa auth refresh endpoint
* @param currentToken - The current JWT token
* @returns The new token if successful, null otherwise
*/
export async function refreshMedusaToken(currentToken: string): Promise<string | null> {
try {
// Attempt to refresh the token using the Medusa SDK
// The SDK should handle the refresh token logic internally
const result = await sdk.client.fetch<{ token?: string }>('/auth/token/refresh', {
method: 'POST',
headers: {
Authorization: `Bearer ${currentToken}`,
},
});

if (result.token) {
// Update the token in the cookie
await setAuthToken(result.token);
// Update the SDK's internal token
await sdk.client.setToken(result.token);
return result.token;
}

return null;
} catch (error) {
console.error('Token refresh failed:', error);
return null;
}
}

/**
* Validate and refresh token if needed
* @param token - The current JWT token
* @returns Valid token (either the original or refreshed), null if refresh fails
*/
export async function validateAndRefreshToken(token: string): Promise<string | null> {
// Check if token is expired or will expire soon
const isExpired = await isTokenExpired(token);
if (!isExpired) {
return token; // Token is still valid
}

// Attempt to refresh the token
const newToken = await refreshMedusaToken(token);

if (!newToken) {
console.warn('Failed to refresh expired token, user may need to re-authenticate');
return null;
}

return newToken;
}
67 changes: 57 additions & 10 deletions src/lib/data/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,69 @@
import "server-only"
import { cookies as nextCookies } from "next/headers"

export const getAuthHeaders = async (): Promise<
{ authorization: string } | {}
> => {
import { headers, cookies as nextCookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { validateAndRefreshToken } from './auth';


/**
* Get auth headers from cookie for server-side requests
* Automatically validates and refreshes expired tokens
*/
export const getAuthHeaders = async (): Promise<{authorization: string; } | undefined> => {
try {
const cookies = await nextCookies()
const token = cookies.get("_medusa_jwt")?.value
const cookies = await nextCookies();
const token = cookies.get('_medusa_jwt')?.value;

// If there's no valid token available, we want to destroy the supabase cookies and force reauthentication
if (!token) {
return {}
const redirectUrl = await getLoginRedirectUrl();
redirect(redirectUrl);
}

// Validate and refresh token if needed
const validToken = await validateAndRefreshToken(token);

// If token validation/refreshing failed, clear
if (!validToken) {
await removeAuthToken();
const redirectUrl = await getLoginRedirectUrl();
redirect(redirectUrl);
}

return { authorization: `Bearer ${token}` }
return { authorization: `Bearer ${validToken}` };
} catch {
return {}
await removeAuthToken();
const redirectUrl = await getLoginRedirectUrl();
redirect(redirectUrl);
}
}
};

/**
* Helper to build login redirect URL with current page as ext parameter
*/
const getLoginRedirectUrl = async (): Promise<string> => {
try {
const headersList = await headers();
// Try to get the current page from various headers
const referer = headersList.get('referer');
const pathname = headersList.get('x-pathname') || headersList.get('x-invoke-path');

let currentPage = '/';

if (referer) {
// Extract pathname and search params from referer URL
const url = new URL(referer);
currentPage = url.pathname + url.search;
} else if (pathname) {
const searchParams = headersList.get('x-search-params') || '';
currentPage = searchParams ? `${pathname}?${searchParams}` : pathname;
}

return `/login?next=${encodeURIComponent(currentPage)}`;
} catch {
return '/login';
}
};

export const getCacheTag = async (tag: string): Promise<string> => {
try {
Expand Down