mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:35:23 +02:00
Auth: Implement automatic session refresh logic to prevent 'JWT expired' errors. The app now handles token renewal in the background, allowing users to save changes without interruption.
This commit is contained in:
+21
-12
@@ -50,6 +50,17 @@ export default function App() {
|
||||
setSession(null);
|
||||
};
|
||||
|
||||
// Sync session state when localStorage is updated (e.g. by token refresh)
|
||||
useEffect(() => {
|
||||
const handleStorage = (e: StorageEvent) => {
|
||||
if (e.key === 'craze_auth_session') {
|
||||
setSession(getStoredSession());
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefaultData = async () => {
|
||||
setIsLoadingDefault(true);
|
||||
@@ -122,7 +133,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
console.log('Fetching synced data from Supabase...');
|
||||
const syncedData = await getAllSyncedRows(session?.access_token);
|
||||
const syncedData = await getAllSyncedRows();
|
||||
|
||||
const resolvedCols = resolveColumnIndices(headers);
|
||||
const editableColumns = new Set([
|
||||
@@ -316,7 +327,7 @@ export default function App() {
|
||||
|
||||
// 3. Post-export: Reset pending statuses in Supabase
|
||||
console.log('Resetting pending statuses in Supabase...');
|
||||
resetAllPendingRows(session?.access_token).then(success => {
|
||||
resetAllPendingRows().then(success => {
|
||||
if (success) {
|
||||
console.log('Successfully reset all pending statuses');
|
||||
setRowStatuses({}); // Clear local statuses
|
||||
@@ -414,18 +425,17 @@ export default function App() {
|
||||
|
||||
setIsSavingAll(true);
|
||||
let failedArticles: string[] = [];
|
||||
let jwtExpired = false;
|
||||
const token = session?.access_token;
|
||||
let sessionIssue = false;
|
||||
|
||||
try {
|
||||
for (const [articleNo, { newData, originalData, articleName }] of entries) {
|
||||
console.log('[handleSaveAll] Saving article:', articleNo);
|
||||
const result = await saveRowToSupabase(articleNo, newData, token);
|
||||
const result = await saveRowToSupabase(articleNo, newData);
|
||||
console.log('[handleSaveAll] Save result for', articleNo, ':', result);
|
||||
|
||||
if (result.success) {
|
||||
// Also save to history
|
||||
await saveHistoryEntry(articleNo, articleName, originalData, newData, session?.user?.email || 'unknown', token);
|
||||
await saveHistoryEntry(articleNo, articleName, originalData, newData, session?.user?.email || 'unknown');
|
||||
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' }));
|
||||
setPendingRows(prev => {
|
||||
@@ -436,16 +446,16 @@ export default function App() {
|
||||
} else {
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'error' }));
|
||||
failedArticles.push(`${articleNo} [${result.error || 'Unknown error'}]`);
|
||||
if (result.error?.includes('401') || result.error?.includes('JWT expired')) {
|
||||
jwtExpired = true;
|
||||
if (result.error?.includes('401') || result.error?.includes('expired') || result.error?.includes('Session expired')) {
|
||||
sessionIssue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[handleSaveAll] Finished loop. Failed:', failedArticles.length);
|
||||
|
||||
if (jwtExpired) {
|
||||
alert("Tu sesión ha caducado (JWT expired). Por favor, haz clic en el botón de Cerrar Sesión (Sign out) arriba a la derecha y vuelve a iniciar sesión para guardar tus cambios.");
|
||||
if (sessionIssue) {
|
||||
alert("Tu sesión ha caducado definitivamente. Por favor, cierra sesión e inicia sesión de nuevo para continuar. No refresques la página para no perder tus cambios pendientes en pantalla.");
|
||||
} else if (failedArticles.length > 0) {
|
||||
alert(`Failed to save items:\n\n${failedArticles.join('\n')}\n\nPlease try again.`);
|
||||
} else {
|
||||
@@ -667,7 +677,6 @@ export default function App() {
|
||||
<HistoryView
|
||||
headers={appState.headers}
|
||||
data={appState.data}
|
||||
sessionToken={session?.access_token}
|
||||
onEdit={(index) => setEditingRowIndex(index)}
|
||||
onRevert={async (articleNo, revertedData, historyId) => {
|
||||
// Find the row in appState.data and update it
|
||||
@@ -691,7 +700,7 @@ export default function App() {
|
||||
}));
|
||||
// Delete the history entry after revert
|
||||
if (historyId) {
|
||||
await deleteHistoryEntry(String(historyId), session?.access_token);
|
||||
await deleteHistoryEntry(String(historyId));
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -10,10 +10,9 @@ interface HistoryViewProps {
|
||||
data: ExcelRow[];
|
||||
onRevert: (articleNo: string, oldData: ExcelRow, historyId?: number) => void;
|
||||
onEdit?: (rowIndex: number) => void;
|
||||
sessionToken?: string;
|
||||
}
|
||||
|
||||
export function HistoryView({ headers, data, onRevert, onEdit, sessionToken }: HistoryViewProps) {
|
||||
export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProps) {
|
||||
const COLUMNS = useColumns();
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -26,7 +25,7 @@ export function HistoryView({ headers, data, onRevert, onEdit, sessionToken }: H
|
||||
|
||||
const loadHistory = async () => {
|
||||
setLoading(true);
|
||||
const data = await getHistory(sessionToken);
|
||||
const data = await getHistory();
|
||||
setHistory(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ const SESSION_KEY = 'craze_auth_session';
|
||||
|
||||
export interface AuthSession {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
user: { id: string; email: string };
|
||||
}
|
||||
|
||||
@@ -41,6 +42,33 @@ export async function signIn(email: string, password: string): Promise<AuthSessi
|
||||
const data = await response.json();
|
||||
const session: AuthSession = {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
user: { id: data.user.id, email: data.user.email },
|
||||
};
|
||||
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function refreshSession(refreshToken: string): Promise<AuthSession> {
|
||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': SUPABASE_ANON_KEY,
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
throw new Error('Session expired. Please sign in again.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const session: AuthSession = {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
user: { id: data.user.id, email: data.user.email },
|
||||
};
|
||||
|
||||
|
||||
+43
-31
@@ -1,11 +1,36 @@
|
||||
import { refreshSession, getStoredSession } from './auth';
|
||||
|
||||
const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
|
||||
|
||||
function authHeaders(token?: string): Record<string, string> {
|
||||
return {
|
||||
async function safeFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const session = getStoredSession();
|
||||
const token = session?.access_token || SUPABASE_ANON_KEY;
|
||||
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'apikey': SUPABASE_ANON_KEY,
|
||||
'Authorization': `Bearer ${token ?? SUPABASE_ANON_KEY}`,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
};
|
||||
|
||||
let response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401 && session?.refresh_token) {
|
||||
console.log('JWT expired, attempting refresh...');
|
||||
try {
|
||||
const newSession = await refreshSession(session.refresh_token);
|
||||
const newHeaders = {
|
||||
...options.headers,
|
||||
'apikey': SUPABASE_ANON_KEY,
|
||||
'Authorization': `Bearer ${newSession.access_token}`,
|
||||
};
|
||||
response = await fetch(url, { ...options, headers: newHeaders });
|
||||
} catch (refreshError) {
|
||||
console.error('Session refresh failed:', refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export interface ExcelRow extends Array<any> {}
|
||||
@@ -15,14 +40,11 @@ export interface SyncedRow {
|
||||
status?: 'pending' | 'synced';
|
||||
}
|
||||
|
||||
export async function getAllSyncedRows(token?: string): Promise<Record<string, SyncedRow>> {
|
||||
export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await safeFetch(
|
||||
`${SUPABASE_URL}/rest/v1/products?select=product_id,data,status&order=updated_at.desc`,
|
||||
{
|
||||
cache: 'no-store',
|
||||
headers: authHeaders(token),
|
||||
}
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -42,15 +64,14 @@ export async function getAllSyncedRows(token?: string): Promise<Record<string, S
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveRowToSupabase(articleNo: string, rowData: ExcelRow, token?: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function saveRowToSupabase(articleNo: string, rowData: ExcelRow): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await safeFetch(
|
||||
`${SUPABASE_URL}/rest/v1/products`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders(token),
|
||||
'Prefer': 'resolution=merge-duplicates',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -92,17 +113,15 @@ export async function saveHistoryEntry(
|
||||
articleName: string,
|
||||
oldData: ExcelRow,
|
||||
newData: ExcelRow,
|
||||
changedBy: string,
|
||||
token?: string
|
||||
changedBy: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await safeFetch(
|
||||
`${SUPABASE_URL}/rest/v1/products_history`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders(token),
|
||||
'Prefer': 'return=minimal',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -131,14 +150,11 @@ export async function saveHistoryEntry(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHistory(token?: string): Promise<HistoryEntry[]> {
|
||||
export async function getHistory(): Promise<HistoryEntry[]> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await safeFetch(
|
||||
`${SUPABASE_URL}/rest/v1/products_history?select=*&order=changed_at.desc&limit=100`,
|
||||
{
|
||||
cache: 'no-store',
|
||||
headers: authHeaders(token),
|
||||
}
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -168,14 +184,11 @@ export async function getHistory(token?: string): Promise<HistoryEntry[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteHistoryEntry(id: string, token?: string): Promise<boolean> {
|
||||
export async function deleteHistoryEntry(id: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await safeFetch(
|
||||
`${SUPABASE_URL}/rest/v1/products_history?id=eq.${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(token),
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
return response.ok;
|
||||
@@ -185,15 +198,14 @@ export async function deleteHistoryEntry(id: string, token?: string): Promise<bo
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetAllPendingRows(token?: string): Promise<boolean> {
|
||||
export async function resetAllPendingRows(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await safeFetch(
|
||||
`${SUPABASE_URL}/rest/v1/products?status=eq.pending`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders(token),
|
||||
'Prefer': 'return=minimal',
|
||||
},
|
||||
body: JSON.stringify({ status: 'synced' })
|
||||
|
||||
Reference in New Issue
Block a user