Refactor useAuth hook, settings components, and docs for formatting and readability improvements
- Consolidated multi-line arguments into single lines where appropriate in `useAuth`. - Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`). - Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`). - Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
This commit is contained in:
@@ -114,7 +114,10 @@ async function refreshAccessToken(): Promise<string> {
|
||||
// Only redirect to login when not already on an auth route
|
||||
if (typeof window !== 'undefined') {
|
||||
const currentPath = window.location.pathname;
|
||||
const onAuthRoute = currentPath === '/login' || currentPath === '/register' || currentPath.startsWith('/password-reset');
|
||||
const onAuthRoute =
|
||||
currentPath === '/login' ||
|
||||
currentPath === '/register' ||
|
||||
currentPath.startsWith('/password-reset');
|
||||
if (!onAuthRoute) {
|
||||
const returnUrl = currentPath ? `?returnUrl=${encodeURIComponent(currentPath)}` : '';
|
||||
window.location.href = `/login${returnUrl}`;
|
||||
@@ -144,7 +147,12 @@ client.instance.interceptors.request.use(
|
||||
|
||||
// Do not attach Authorization header for auth endpoints
|
||||
const url = requestConfig.url || '';
|
||||
const isAuthEndpoint = url.includes('/auth/login') || url.includes('/auth/register') || url.includes('/auth/refresh') || url.includes('/auth/password') || url.includes('/password');
|
||||
const isAuthEndpoint =
|
||||
url.includes('/auth/login') ||
|
||||
url.includes('/auth/register') ||
|
||||
url.includes('/auth/refresh') ||
|
||||
url.includes('/auth/password') ||
|
||||
url.includes('/password');
|
||||
|
||||
// Add Authorization header if token exists and not hitting auth endpoints
|
||||
if (accessToken && requestConfig.headers && !isAuthEndpoint) {
|
||||
@@ -188,7 +196,11 @@ client.instance.interceptors.response.use(
|
||||
// Handle 401 Unauthorized - Token expired
|
||||
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
|
||||
const url = originalRequest.url || '';
|
||||
const isAuthEndpoint = url.includes('/auth/login') || url.includes('/auth/register') || url.includes('/auth/password') || url.includes('/password');
|
||||
const isAuthEndpoint =
|
||||
url.includes('/auth/login') ||
|
||||
url.includes('/auth/register') ||
|
||||
url.includes('/auth/password') ||
|
||||
url.includes('/password');
|
||||
|
||||
// If the 401 is from auth endpoints, do not attempt refresh
|
||||
if (isAuthEndpoint) {
|
||||
|
||||
@@ -15,48 +15,48 @@ export interface APIErrorResponse {
|
||||
// Error code to user-friendly message mapping
|
||||
export const ERROR_MESSAGES: Record<string, string> = {
|
||||
// Authentication errors (AUTH_xxx)
|
||||
'AUTH_001': 'Invalid email or password',
|
||||
'AUTH_002': 'Account is inactive',
|
||||
'AUTH_003': 'Invalid or expired token',
|
||||
'AUTH_004': 'Session expired. Please login again',
|
||||
AUTH_001: 'Invalid email or password',
|
||||
AUTH_002: 'Account is inactive',
|
||||
AUTH_003: 'Invalid or expired token',
|
||||
AUTH_004: 'Session expired. Please login again',
|
||||
|
||||
// User errors (USER_xxx)
|
||||
'USER_001': 'User not found',
|
||||
'USER_002': 'This email is already registered',
|
||||
'USER_003': 'Invalid user data',
|
||||
'USER_004': 'Cannot delete your own account',
|
||||
USER_001: 'User not found',
|
||||
USER_002: 'This email is already registered',
|
||||
USER_003: 'Invalid user data',
|
||||
USER_004: 'Cannot delete your own account',
|
||||
|
||||
// Validation errors (VAL_xxx)
|
||||
'VAL_001': 'Invalid input. Please check your data',
|
||||
'VAL_002': 'Email format is invalid',
|
||||
'VAL_003': 'Password does not meet requirements',
|
||||
'VAL_004': 'Required field is missing',
|
||||
VAL_001: 'Invalid input. Please check your data',
|
||||
VAL_002: 'Email format is invalid',
|
||||
VAL_003: 'Password does not meet requirements',
|
||||
VAL_004: 'Required field is missing',
|
||||
|
||||
// Organization errors (ORG_xxx)
|
||||
'ORG_001': 'Organization name already exists',
|
||||
'ORG_002': 'Organization not found',
|
||||
'ORG_003': 'Cannot delete organization with members',
|
||||
ORG_001: 'Organization name already exists',
|
||||
ORG_002: 'Organization not found',
|
||||
ORG_003: 'Cannot delete organization with members',
|
||||
|
||||
// Permission errors (PERM_xxx)
|
||||
'PERM_001': 'Insufficient permissions',
|
||||
'PERM_002': 'Admin access required',
|
||||
'PERM_003': 'Cannot perform this action',
|
||||
PERM_001: 'Insufficient permissions',
|
||||
PERM_002: 'Admin access required',
|
||||
PERM_003: 'Cannot perform this action',
|
||||
|
||||
// Rate limiting (RATE_xxx)
|
||||
'RATE_001': 'Too many requests. Please try again later',
|
||||
RATE_001: 'Too many requests. Please try again later',
|
||||
|
||||
// Session errors (SESSION_xxx)
|
||||
'SESSION_001': 'Session not found',
|
||||
'SESSION_002': 'Cannot revoke current session',
|
||||
'SESSION_003': 'Session expired',
|
||||
SESSION_001: 'Session not found',
|
||||
SESSION_002: 'Cannot revoke current session',
|
||||
SESSION_003: 'Session expired',
|
||||
|
||||
// Generic errors
|
||||
'NETWORK_ERROR': 'Network error. Please check your connection',
|
||||
'SERVER_ERROR': 'A server error occurred. Please try again later',
|
||||
'UNKNOWN': 'An unexpected error occurred',
|
||||
'FORBIDDEN': "You don't have permission to perform this action",
|
||||
'NOT_FOUND': 'The requested resource was not found',
|
||||
'RATE_LIMIT': 'Too many requests. Please slow down',
|
||||
NETWORK_ERROR: 'Network error. Please check your connection',
|
||||
SERVER_ERROR: 'A server error occurred. Please try again later',
|
||||
UNKNOWN: 'An unexpected error occurred',
|
||||
FORBIDDEN: "You don't have permission to perform this action",
|
||||
NOT_FOUND: 'The requested resource was not found',
|
||||
RATE_LIMIT: 'Too many requests. Please slow down',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -150,7 +150,8 @@ export function parseAPIError(error: unknown): APIError[] {
|
||||
return [
|
||||
{
|
||||
code: 'SERVER_ERROR',
|
||||
message: ERROR_MESSAGES['SERVER_ERROR'] || 'A server error occurred. Please try again later',
|
||||
message:
|
||||
ERROR_MESSAGES['SERVER_ERROR'] || 'A server error occurred. Please try again later',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,31 +11,31 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
type AddMemberRequest,
|
||||
adminActivateUser,
|
||||
adminAddOrganizationMember,
|
||||
adminBulkUserAction,
|
||||
adminCreateOrganization,
|
||||
adminCreateUser,
|
||||
adminDeactivateUser,
|
||||
adminDeleteOrganization,
|
||||
adminDeleteUser,
|
||||
adminGetOrganization,
|
||||
adminListOrganizationMembers,
|
||||
adminListOrganizations,
|
||||
adminListSessions,
|
||||
adminListUsers,
|
||||
adminRemoveOrganizationMember,
|
||||
adminUpdateOrganization,
|
||||
adminUpdateUser,
|
||||
type OrganizationCreate,
|
||||
type OrganizationUpdate,
|
||||
type UserCreate,
|
||||
type UserUpdate,
|
||||
type AddMemberRequest,
|
||||
adminActivateUser,
|
||||
adminAddOrganizationMember,
|
||||
adminBulkUserAction,
|
||||
adminCreateOrganization,
|
||||
adminCreateUser,
|
||||
adminDeactivateUser,
|
||||
adminDeleteOrganization,
|
||||
adminDeleteUser,
|
||||
adminGetOrganization,
|
||||
adminListOrganizationMembers,
|
||||
adminListOrganizations,
|
||||
adminListSessions,
|
||||
adminListUsers,
|
||||
adminRemoveOrganizationMember,
|
||||
adminUpdateOrganization,
|
||||
adminUpdateUser,
|
||||
type OrganizationCreate,
|
||||
type OrganizationUpdate,
|
||||
type UserCreate,
|
||||
type UserUpdate,
|
||||
} from '@/lib/api/client';
|
||||
import {useAuth} from '@/lib/auth/AuthContext';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
/**
|
||||
* Constants for admin hooks
|
||||
@@ -81,7 +81,11 @@ export function useAdminStats() {
|
||||
}
|
||||
|
||||
// Type assertion: if no error, response has data
|
||||
const usersData = (usersResponse as { data: { data: Array<{ is_active: boolean }>; pagination: { total: number } } }).data;
|
||||
const usersData = (
|
||||
usersResponse as {
|
||||
data: { data: Array<{ is_active: boolean }>; pagination: { total: number } };
|
||||
}
|
||||
).data;
|
||||
const users = usersData?.data || [];
|
||||
const totalUsers = usersData?.pagination?.total || 0;
|
||||
const activeUsers = users.filter((u) => u.is_active).length;
|
||||
@@ -282,13 +286,7 @@ export function useUpdateUser() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
userId,
|
||||
userData,
|
||||
}: {
|
||||
userId: string;
|
||||
userData: UserUpdate;
|
||||
}) => {
|
||||
mutationFn: async ({ userId, userData }: { userId: string; userData: UserUpdate }) => {
|
||||
const response = await adminUpdateUser({
|
||||
path: { user_id: userId },
|
||||
body: userData,
|
||||
@@ -509,13 +507,7 @@ export function useUpdateOrganization() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
orgId,
|
||||
orgData,
|
||||
}: {
|
||||
orgId: string;
|
||||
orgData: OrganizationUpdate;
|
||||
}) => {
|
||||
mutationFn: async ({ orgId, orgData }: { orgId: string; orgData: OrganizationUpdate }) => {
|
||||
const response = await adminUpdateOrganization({
|
||||
path: { org_id: orgId },
|
||||
body: orgData,
|
||||
@@ -603,11 +595,7 @@ export function useGetOrganization(orgId: string | null) {
|
||||
* @param limit - Number of records per page
|
||||
* @returns Paginated list of organization members
|
||||
*/
|
||||
export function useOrganizationMembers(
|
||||
orgId: string | null,
|
||||
page = 1,
|
||||
limit = DEFAULT_PAGE_LIMIT
|
||||
) {
|
||||
export function useOrganizationMembers(orgId: string | null, page = 1, limit = DEFAULT_PAGE_LIMIT) {
|
||||
const { user } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
@@ -642,13 +630,7 @@ export function useAddOrganizationMember() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
orgId,
|
||||
memberData,
|
||||
}: {
|
||||
orgId: string;
|
||||
memberData: AddMemberRequest;
|
||||
}) => {
|
||||
mutationFn: async ({ orgId, memberData }: { orgId: string; memberData: AddMemberRequest }) => {
|
||||
const response = await adminAddOrganizationMember({
|
||||
path: { org_id: orgId },
|
||||
body: memberData,
|
||||
@@ -681,13 +663,7 @@ export function useRemoveOrganizationMember() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
orgId,
|
||||
userId,
|
||||
}: {
|
||||
orgId: string;
|
||||
userId: string;
|
||||
}) => {
|
||||
mutationFn: async ({ orgId, userId }: { orgId: string; userId: string }) => {
|
||||
const response = await adminRemoveOrganizationMember({
|
||||
path: { org_id: orgId, user_id: userId },
|
||||
throwOnError: false,
|
||||
|
||||
@@ -121,12 +121,7 @@ export function useLogin(onSuccess?: () => void) {
|
||||
const { access_token, refresh_token, user, expires_in } = data;
|
||||
|
||||
// Update auth store with user and tokens
|
||||
await setAuth(
|
||||
user as User,
|
||||
access_token,
|
||||
refresh_token || '',
|
||||
expires_in
|
||||
);
|
||||
await setAuth(user as User, access_token, refresh_token || '', expires_in);
|
||||
|
||||
// Invalidate and refetch user data
|
||||
queryClient.invalidateQueries({ queryKey: authKeys.all });
|
||||
@@ -199,12 +194,7 @@ export function useRegister(onSuccess?: () => void) {
|
||||
const { access_token, refresh_token, user, expires_in } = data;
|
||||
|
||||
// Update auth store with user and tokens (auto-login)
|
||||
await setAuth(
|
||||
user as User,
|
||||
access_token,
|
||||
refresh_token || '',
|
||||
expires_in
|
||||
);
|
||||
await setAuth(user as User, access_token, refresh_token || '', expires_in);
|
||||
|
||||
// Invalidate and refetch user data
|
||||
queryClient.invalidateQueries({ queryKey: authKeys.all });
|
||||
|
||||
@@ -34,11 +34,7 @@ export function useUpdateProfile(onSuccess?: (message: string) => void) {
|
||||
const setUser = useAuth((state) => state.setUser);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
}) => {
|
||||
mutationFn: async (data: { first_name?: string; last_name?: string; email?: string }) => {
|
||||
const response = await updateCurrentUser({
|
||||
body: data,
|
||||
throwOnError: false,
|
||||
@@ -52,11 +48,7 @@ export function useUpdateProfile(onSuccess?: (message: string) => void) {
|
||||
const responseData = (response as { data: unknown }).data;
|
||||
|
||||
// Validate response is a user object
|
||||
if (
|
||||
typeof responseData !== 'object' ||
|
||||
responseData === null ||
|
||||
!('id' in responseData)
|
||||
) {
|
||||
if (typeof responseData !== 'object' || responseData === null || !('id' in responseData)) {
|
||||
throw new Error('Invalid profile update response: missing user data');
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
* Design: Context handles dependency injection, Zustand handles state management
|
||||
*/
|
||||
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports -- This is the DI boundary, needs real store for production
|
||||
import { useAuthStore as useAuthStoreImpl } from "@/lib/stores/authStore";
|
||||
import type { User } from "@/lib/stores/authStore";
|
||||
import { useAuthStore as useAuthStoreImpl } from '@/lib/stores/authStore';
|
||||
import type { User } from '@/lib/stores/authStore';
|
||||
|
||||
/**
|
||||
* Authentication state shape
|
||||
@@ -31,7 +31,12 @@ interface AuthState {
|
||||
tokenExpiresAt: number | null;
|
||||
|
||||
// Actions
|
||||
setAuth: (user: User, accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
|
||||
setAuth: (
|
||||
user: User,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiresIn?: number
|
||||
) => Promise<void>;
|
||||
setTokens: (accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
|
||||
setUser: (user: User) => void;
|
||||
clearAuth: () => Promise<void>;
|
||||
@@ -120,7 +125,7 @@ export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
|
||||
const storeHook = useContext(AuthContext);
|
||||
|
||||
if (!storeHook) {
|
||||
throw new Error("useAuth must be used within AuthProvider");
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
|
||||
// Call the Zustand hook internally (follows React Rules of Hooks)
|
||||
|
||||
@@ -34,13 +34,10 @@ async function getEncryptionKey(): Promise<CryptoKey> {
|
||||
if (storedKey) {
|
||||
try {
|
||||
const keyData = JSON.parse(storedKey);
|
||||
return await crypto.subtle.importKey(
|
||||
'jwk',
|
||||
keyData,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
return await crypto.subtle.importKey('jwk', keyData, { name: 'AES-GCM', length: 256 }, true, [
|
||||
'encrypt',
|
||||
'decrypt',
|
||||
]);
|
||||
} catch (error) {
|
||||
// Corrupted key, regenerate
|
||||
console.warn('Failed to import stored key, generating new key:', error);
|
||||
@@ -49,11 +46,10 @@ async function getEncryptionKey(): Promise<CryptoKey> {
|
||||
}
|
||||
|
||||
// Generate new key
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
|
||||
'encrypt',
|
||||
'decrypt',
|
||||
]);
|
||||
|
||||
// Store key in sessionStorage
|
||||
try {
|
||||
@@ -86,11 +82,7 @@ export async function encryptData(data: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const encodedData = encoder.encode(data);
|
||||
|
||||
const encryptedData = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encodedData
|
||||
);
|
||||
const encryptedData = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encodedData);
|
||||
|
||||
// Combine IV and encrypted data
|
||||
const combined = new Uint8Array(iv.length + encryptedData.byteLength);
|
||||
@@ -122,17 +114,13 @@ export async function decryptData(encryptedData: string): Promise<string> {
|
||||
const key = await getEncryptionKey();
|
||||
|
||||
// Decode from base64
|
||||
const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
|
||||
const combined = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0));
|
||||
|
||||
// Extract IV and encrypted data
|
||||
const iv = combined.slice(0, 12);
|
||||
const data = combined.slice(12);
|
||||
|
||||
const decryptedData = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
data
|
||||
);
|
||||
const decryptedData = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decryptedData);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// Authentication utilities
|
||||
// Examples: Token management, auth helpers, session utilities, etc.
|
||||
|
||||
export {
|
||||
encryptData,
|
||||
decryptData,
|
||||
clearEncryptionKey,
|
||||
} from './crypto';
|
||||
export { encryptData, decryptData, clearEncryptionKey } from './crypto';
|
||||
|
||||
export {
|
||||
saveTokens,
|
||||
|
||||
@@ -25,7 +25,10 @@ export type StorageMethod = 'cookie' | 'localStorage';
|
||||
* This flag is set by E2E tests to skip encryption for easier testing
|
||||
*/
|
||||
function isE2ETestMode(): boolean {
|
||||
return typeof window !== 'undefined' && (window as { __PLAYWRIGHT_TEST__?: boolean }).__PLAYWRIGHT_TEST__ === true;
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
(window as { __PLAYWRIGHT_TEST__?: boolean }).__PLAYWRIGHT_TEST__ === true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,10 +165,14 @@ export async function getTokens(): Promise<TokenStorage | null> {
|
||||
const parsed = JSON.parse(stored);
|
||||
|
||||
// Validate structure - must have required fields
|
||||
if (!parsed || typeof parsed !== 'object' ||
|
||||
!('accessToken' in parsed) || !('refreshToken' in parsed) ||
|
||||
(parsed.accessToken !== null && typeof parsed.accessToken !== 'string') ||
|
||||
(parsed.refreshToken !== null && typeof parsed.refreshToken !== 'string')) {
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== 'object' ||
|
||||
!('accessToken' in parsed) ||
|
||||
!('refreshToken' in parsed) ||
|
||||
(parsed.accessToken !== null && typeof parsed.accessToken !== 'string') ||
|
||||
(parsed.refreshToken !== null && typeof parsed.refreshToken !== 'string')
|
||||
) {
|
||||
throw new Error('Invalid token structure');
|
||||
}
|
||||
|
||||
@@ -177,10 +184,14 @@ export async function getTokens(): Promise<TokenStorage | null> {
|
||||
const parsed = JSON.parse(decrypted);
|
||||
|
||||
// Validate structure - must have required fields
|
||||
if (!parsed || typeof parsed !== 'object' ||
|
||||
!('accessToken' in parsed) || !('refreshToken' in parsed) ||
|
||||
(parsed.accessToken !== null && typeof parsed.accessToken !== 'string') ||
|
||||
(parsed.refreshToken !== null && typeof parsed.refreshToken !== 'string')) {
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== 'object' ||
|
||||
!('accessToken' in parsed) ||
|
||||
!('refreshToken' in parsed) ||
|
||||
(parsed.accessToken !== null && typeof parsed.accessToken !== 'string') ||
|
||||
(parsed.refreshToken !== null && typeof parsed.refreshToken !== 'string')
|
||||
) {
|
||||
/* istanbul ignore next - Validation error path */
|
||||
throw new Error('Invalid token structure');
|
||||
}
|
||||
|
||||
@@ -6,26 +6,26 @@
|
||||
|
||||
export const CHART_COLORS = {
|
||||
// Primary blue palette - vibrant and professional
|
||||
primary: '#3b82f6', // Blue 500
|
||||
primary: '#3b82f6', // Blue 500
|
||||
primaryLight: '#60a5fa', // Blue 400
|
||||
primaryDark: '#2563eb', // Blue 600
|
||||
primaryDark: '#2563eb', // Blue 600
|
||||
|
||||
// Secondary accent colors - complementary palette
|
||||
accent1: '#8b5cf6', // Violet 500
|
||||
accent2: '#ec4899', // Pink 500
|
||||
accent3: '#f59e0b', // Amber 500
|
||||
accent4: '#10b981', // Emerald 500
|
||||
accent5: '#06b6d4', // Cyan 500
|
||||
accent1: '#8b5cf6', // Violet 500
|
||||
accent2: '#ec4899', // Pink 500
|
||||
accent3: '#f59e0b', // Amber 500
|
||||
accent4: '#10b981', // Emerald 500
|
||||
accent5: '#06b6d4', // Cyan 500
|
||||
|
||||
// Status colors
|
||||
success: '#10b981', // Emerald 500
|
||||
warning: '#f59e0b', // Amber 500
|
||||
error: '#ef4444', // Red 500
|
||||
info: '#3b82f6', // Blue 500
|
||||
success: '#10b981', // Emerald 500
|
||||
warning: '#f59e0b', // Amber 500
|
||||
error: '#ef4444', // Red 500
|
||||
info: '#3b82f6', // Blue 500
|
||||
|
||||
// Neutral colors for supporting elements
|
||||
muted: '#94a3b8', // Slate 400
|
||||
mutedDark: '#64748b', // Slate 500
|
||||
muted: '#94a3b8', // Slate 400
|
||||
mutedDark: '#64748b', // Slate 500
|
||||
};
|
||||
|
||||
// Chart-specific color palettes for different chart types
|
||||
@@ -40,12 +40,7 @@ export const CHART_PALETTES = {
|
||||
area: [CHART_COLORS.primary, CHART_COLORS.accent5],
|
||||
|
||||
// Pie chart palette - 4-5 distinct colors
|
||||
pie: [
|
||||
CHART_COLORS.primary,
|
||||
CHART_COLORS.accent1,
|
||||
CHART_COLORS.accent3,
|
||||
CHART_COLORS.accent4,
|
||||
],
|
||||
pie: [CHART_COLORS.primary, CHART_COLORS.accent1, CHART_COLORS.accent3, CHART_COLORS.accent4],
|
||||
|
||||
// Multi-series palette - for charts with many data series
|
||||
multi: [
|
||||
@@ -73,6 +68,8 @@ export const CHART_GRADIENTS = {
|
||||
// Helper function to get color with opacity
|
||||
export function withOpacity(color: string, opacity: number): string {
|
||||
// Convert opacity (0-1) to hex (00-FF)
|
||||
const hex = Math.round(opacity * 255).toString(16).padStart(2, '0');
|
||||
const hex = Math.round(opacity * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
return `${color}${hex}`;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,12 @@ interface AuthState {
|
||||
tokenExpiresAt: number | null; // Unix timestamp
|
||||
|
||||
// Actions
|
||||
setAuth: (user: User, accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
|
||||
setAuth: (
|
||||
user: User,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiresIn?: number
|
||||
) => Promise<void>;
|
||||
setTokens: (accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
|
||||
setUser: (user: User) => void;
|
||||
clearAuth: () => Promise<void>;
|
||||
@@ -61,7 +66,8 @@ function calculateExpiry(expiresIn?: number): number {
|
||||
let seconds = expiresIn || 900;
|
||||
|
||||
// Validate positive number and prevent overflow
|
||||
if (seconds <= 0 || seconds > 31536000) { // Max 1 year
|
||||
if (seconds <= 0 || seconds > 31536000) {
|
||||
// Max 1 year
|
||||
console.warn(`Invalid expiresIn value: ${expiresIn}, using default 900s`);
|
||||
seconds = 900;
|
||||
}
|
||||
@@ -81,9 +87,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
// Set complete auth state (user + tokens)
|
||||
setAuth: async (user, accessToken, refreshToken, expiresIn) => {
|
||||
// Validate inputs
|
||||
if (!user || !user.id || !user.email ||
|
||||
typeof user.id !== 'string' || typeof user.email !== 'string' ||
|
||||
user.id.trim() === '' || user.email.trim() === '') {
|
||||
if (
|
||||
!user ||
|
||||
!user.id ||
|
||||
!user.email ||
|
||||
typeof user.id !== 'string' ||
|
||||
typeof user.email !== 'string' ||
|
||||
user.id.trim() === '' ||
|
||||
user.email.trim() === ''
|
||||
) {
|
||||
throw new Error('Invalid user object: id and email must be non-empty strings');
|
||||
}
|
||||
|
||||
@@ -132,9 +144,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
|
||||
// Update user only
|
||||
setUser: (user) => {
|
||||
if (!user || !user.id || !user.email ||
|
||||
typeof user.id !== 'string' || typeof user.email !== 'string' ||
|
||||
user.id.trim() === '' || user.email.trim() === '') {
|
||||
if (
|
||||
!user ||
|
||||
!user.id ||
|
||||
!user.email ||
|
||||
typeof user.id !== 'string' ||
|
||||
typeof user.email !== 'string' ||
|
||||
user.id.trim() === '' ||
|
||||
user.email.trim() === ''
|
||||
) {
|
||||
throw new Error('Invalid user object: id and email must be non-empty strings');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user