memos/web/src/auth-state.ts
Johnny 50606a850e fix(auth): resolve token refresh and persistence issues
- Fix cookie expiration timezone to use GMT (RFC 6265 compliance)
- Use Connect RPC client for token refresh instead of fetch
- Fix error code checking (numeric Code.Unauthenticated instead of string)
- Prevent infinite redirect loop when already on /auth page
- Fix protobuf Timestamp conversion using timestampDate helper
- Store access token in sessionStorage to avoid unnecessary refreshes on page reload
- Add refresh token cookie fallback for attachment authentication
- Improve error handling with proper type checking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 20:56:54 +08:00

73 lines
2.5 KiB
TypeScript

// Access token storage using sessionStorage for persistence across page refreshes
// sessionStorage is cleared when the tab/window is closed, providing reasonable security
// while avoiding unnecessary token refreshes on page reload
let accessToken: string | null = null;
let tokenExpiresAt: Date | null = null;
const SESSION_TOKEN_KEY = "memos_access_token";
const SESSION_EXPIRES_KEY = "memos_token_expires_at";
export const getAccessToken = (): string | null => {
// If not in memory, try to restore from sessionStorage
if (!accessToken) {
try {
const storedToken = sessionStorage.getItem(SESSION_TOKEN_KEY);
const storedExpires = sessionStorage.getItem(SESSION_EXPIRES_KEY);
if (storedToken && storedExpires) {
const expiresAt = new Date(storedExpires);
// Only restore if token hasn't expired
if (expiresAt > new Date()) {
accessToken = storedToken;
tokenExpiresAt = expiresAt;
} else {
// Token expired, clean up sessionStorage
sessionStorage.removeItem(SESSION_TOKEN_KEY);
sessionStorage.removeItem(SESSION_EXPIRES_KEY);
}
}
} catch (e) {
// sessionStorage might not be available (e.g., in some privacy modes)
console.warn("Failed to access sessionStorage:", e);
}
}
return accessToken;
};
export const setAccessToken = (token: string | null, expiresAt?: Date): void => {
accessToken = token;
tokenExpiresAt = expiresAt || null;
try {
if (token && expiresAt) {
// Store in sessionStorage for persistence across page refreshes
sessionStorage.setItem(SESSION_TOKEN_KEY, token);
sessionStorage.setItem(SESSION_EXPIRES_KEY, expiresAt.toISOString());
} else {
// Clear sessionStorage if token is being cleared
sessionStorage.removeItem(SESSION_TOKEN_KEY);
sessionStorage.removeItem(SESSION_EXPIRES_KEY);
}
} catch (e) {
// sessionStorage might not be available (e.g., in some privacy modes)
console.warn("Failed to write to sessionStorage:", e);
}
};
export const isTokenExpired = (): boolean => {
if (!tokenExpiresAt) return true;
// Consider expired 30 seconds before actual expiry for safety
return new Date() >= new Date(tokenExpiresAt.getTime() - 30000);
};
export const clearAccessToken = (): void => {
accessToken = null;
tokenExpiresAt = null;
try {
sessionStorage.removeItem(SESSION_TOKEN_KEY);
sessionStorage.removeItem(SESSION_EXPIRES_KEY);
} catch (e) {
console.warn("Failed to clear sessionStorage:", e);
}
};