>(
+ "/users/reset-password",
+ data,
+ );
+ return response.data;
+ },
};
diff --git a/lib/auth/ProtectedRoute.tsx b/lib/auth/ProtectedRoute.tsx
new file mode 100644
index 0000000..11f2b43
--- /dev/null
+++ b/lib/auth/ProtectedRoute.tsx
@@ -0,0 +1,196 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter, usePathname } from "next/navigation";
+import { useAuthStore } from "@/store/authStore";
+import type { User } from "@/types";
+
+// Role type for access control
+export type UserRole = "admin" | "user" | "moderator";
+
+// Protected route props
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+ requiredRole?: UserRole;
+ allowedRoles?: UserRole[];
+ fallbackPath?: string;
+ loadingComponent?: React.ReactNode;
+}
+
+// JWT token validation utility
+const isTokenValid = (token: string): boolean => {
+ try {
+ // Decode JWT payload (without verification for simplicity)
+ const payload = JSON.parse(atob(token.split(".")[1]));
+ const currentTime = Date.now() / 1000;
+
+ // Check if token is expired
+ return payload.exp > currentTime;
+ } catch (error) {
+ console.error("Invalid token format:", error);
+ return false;
+ }
+};
+
+// Check if user has required role
+const hasRequiredRole = (
+ user: User | null,
+ requiredRole?: UserRole,
+ allowedRoles?: UserRole[],
+): boolean => {
+ if (!user) return false;
+ if (!requiredRole && !allowedRoles) return true;
+
+ const userRole = (user as any).role as UserRole;
+
+ if (allowedRoles) {
+ return allowedRoles.includes(userRole);
+ }
+
+ if (requiredRole) {
+ return userRole === requiredRole;
+ }
+
+ return false;
+};
+
+// Loading spinner component
+const DefaultLoadingSpinner = () => (
+
+);
+
+// Main ProtectedRoute HOC component
+export function ProtectedRoute({
+ children,
+ requiredRole,
+ allowedRoles,
+ fallbackPath = "/auth/login",
+ loadingComponent,
+}: ProtectedRouteProps) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const { user, token, isAuthenticated, isLoading } = useAuthStore();
+ const [isCheckingAuth, setIsCheckingAuth] = useState(true);
+ const [isAuthorized, setIsAuthorized] = useState(false);
+
+ useEffect(() => {
+ const checkAuthentication = async () => {
+ setIsCheckingAuth(true);
+
+ // If auth store is still loading, wait
+ if (isLoading) {
+ return;
+ }
+
+ // Check if user is authenticated and token is valid
+ const isTokenValidValue = token ? isTokenValid(token) : false;
+ const isUserAuthenticated = isAuthenticated && isTokenValidValue;
+
+ if (!isUserAuthenticated) {
+ // Store intended destination and redirect to login
+ if (
+ typeof window !== "undefined" &&
+ pathname !== fallbackPath &&
+ pathname !== "/auth/login"
+ ) {
+ sessionStorage.setItem("intended-destination", pathname);
+ }
+ router.replace(fallbackPath);
+ setIsAuthorized(false);
+ setIsCheckingAuth(false);
+ return;
+ }
+
+ // Check role-based access
+ const hasRoleAccess = hasRequiredRole(user, requiredRole, allowedRoles);
+
+ if (!hasRoleAccess) {
+ // Redirect to unauthorized page or dashboard
+ router.replace("/unauthorized");
+ setIsAuthorized(false);
+ setIsCheckingAuth(false);
+ return;
+ }
+
+ // User is authenticated and authorized
+ setIsAuthorized(true);
+ setIsCheckingAuth(false);
+
+ // Clear intended destination if we're on the intended page
+ if (typeof window !== "undefined") {
+ const intendedDest = sessionStorage.getItem("intended-destination");
+ if (pathname === intendedDest) {
+ sessionStorage.removeItem("intended-destination");
+ }
+ }
+ };
+
+ checkAuthentication();
+ }, [
+ isAuthenticated,
+ token,
+ user,
+ isLoading,
+ requiredRole,
+ allowedRoles,
+ router,
+ pathname,
+ fallbackPath,
+ ]);
+
+ // Show loading state during auth check
+ if (isCheckingAuth || isLoading) {
+ return loadingComponent || ;
+ }
+
+ // Only render children if authorized
+ if (!isAuthorized) {
+ return null;
+ }
+
+ return <>{children}>;
+}
+
+// Higher-order component wrapper
+export function withAuth(
+ WrappedComponent: React.ComponentType
,
+ options?: Omit,
+) {
+ return function AuthenticatedComponent(props: P) {
+ return (
+
+
+
+ );
+ };
+}
+
+// Get intended destination from session storage
+const getIntendedDestination = (): string => {
+ if (typeof window === "undefined") return "/dashboard";
+ return sessionStorage.getItem("intended-destination") || "/dashboard";
+};
+
+// Hook for getting intended destination
+export function useIntendedDestination(): string {
+ return getIntendedDestination();
+}
+
+// Hook for redirecting to intended destination
+export function useRedirectToIntended() {
+ const router = useRouter();
+ const intendedDestination = useIntendedDestination();
+
+ const redirect = () => {
+ if (typeof window !== "undefined") {
+ sessionStorage.removeItem("intended-destination");
+ }
+ router.replace(intendedDestination);
+ };
+
+ return { redirect, intendedDestination };
+}
+
+export default ProtectedRoute;
diff --git a/lib/errorTracking.ts b/lib/errorTracking.ts
index 615d292..1864299 100644
--- a/lib/errorTracking.ts
+++ b/lib/errorTracking.ts
@@ -32,7 +32,9 @@ let config: ErrorTrackingConfig = {
};
// Initialize error tracking (call this in app initialization)
-export function initErrorTracking(customConfig?: Partial): void {
+export function initErrorTracking(
+ customConfig?: Partial,
+): void {
if (customConfig) {
config = { ...config, ...customConfig };
}
@@ -40,7 +42,7 @@ export function initErrorTracking(customConfig?: Partial):
// Setup global error handlers if enabled
if (config.enabled) {
setupGlobalErrorHandlers();
-
+
// Initialize Sentry if available
initSentry();
}
@@ -58,10 +60,10 @@ function setupGlobalErrorHandlers(): void {
source?: string,
lineno?: number,
colno?: number,
- error?: Error
+ error?: Error,
) => {
const errorObj = error || new Error(String(message));
-
+
captureException(errorObj, {
type: "uncaught-error",
source,
@@ -75,9 +77,10 @@ function setupGlobalErrorHandlers(): void {
// Handle unhandled promise rejections
window.onunhandledrejection = (event: PromiseRejectionEvent) => {
- const error = event.reason instanceof Error
- ? event.reason
- : new Error(String(event.reason));
+ const error =
+ event.reason instanceof Error
+ ? event.reason
+ : new Error(String(event.reason));
captureException(error, {
type: "unhandled-promise-rejection",
@@ -94,7 +97,9 @@ async function initSentry(): Promise {
const sentryDsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
if (!sentryDsn) {
- console.log("[ErrorTracking] Sentry DSN not configured, skipping Sentry init");
+ console.log(
+ "[ErrorTracking] Sentry DSN not configured, skipping Sentry init",
+ );
return;
}
@@ -106,38 +111,49 @@ async function initSentry(): Promise {
// eslint-disable-next-line
sentryModule = require("@sentry/nextjs");
} catch {
- console.log("[ErrorTracking] @sentry/nextjs not installed, skipping Sentry initialization");
+ console.log(
+ "[ErrorTracking] @sentry/nextjs not installed, skipping Sentry initialization",
+ );
return;
}
-
- if (!sentryModule || typeof sentryModule !== 'object') {
- console.log("[ErrorTracking] Sentry module invalid, skipping initialization");
+
+ if (!sentryModule || typeof sentryModule !== "object") {
+ console.log(
+ "[ErrorTracking] Sentry module invalid, skipping initialization",
+ );
return;
}
-
+
const Sentry = sentryModule as {
init?: (config: Record) => Promise;
browserTracingIntegration?: () => unknown;
- replayIntegration?: (options: { maskAllText: boolean; blockAllMedia: boolean }) => unknown;
+ replayIntegration?: (options: {
+ maskAllText: boolean;
+ blockAllMedia: boolean;
+ }) => unknown;
};
-
+
if (!Sentry.init) {
- console.log("[ErrorTracking] Sentry.init not found, skipping initialization");
+ console.log(
+ "[ErrorTracking] Sentry.init not found, skipping initialization",
+ );
return;
}
-
+
const integrations: unknown[] = [];
-
+
if (Sentry.browserTracingIntegration) {
integrations.push(Sentry.browserTracingIntegration());
}
if (Sentry.replayIntegration) {
- integrations.push(Sentry.replayIntegration({
- maskAllText: true,
- blockAllMedia: true,
- }));
+ integrations.push(
+ Sentry.replayIntegration({
+ maskAllText: true,
+ blockAllMedia: true,
+ }),
+ );
}
-
+
await Sentry.init({
dsn: sentryDsn,
environment: config.environment,
@@ -151,17 +167,16 @@ async function initSentry(): Promise {
console.log("[ErrorTracking] Sentry initialized successfully");
} catch (error) {
- console.log("[ErrorTracking] Sentry not available, skipping initialization");
+ console.log(
+ "[ErrorTracking] Sentry not available, skipping initialization",
+ );
}
}
/**
* Capture and report an exception
*/
-export function captureException(
- error: Error,
- context?: ErrorContext
-): void {
+export function captureException(error: Error, context?: ErrorContext): void {
if (!config.enabled) {
console.error("[ErrorTracking] Error (tracking disabled):", error, context);
return;
@@ -190,10 +205,13 @@ export function captureException(
export function captureMessage(
message: string,
level: "info" | "warning" | "error" = "info",
- context?: ErrorContext
+ context?: ErrorContext,
): void {
if (!config.enabled) {
- console.log(`[ErrorTracking] Message (tracking disabled): ${message}`, context);
+ console.log(
+ `[ErrorTracking] Message (tracking disabled): ${message}`,
+ context,
+ );
return;
}
@@ -231,7 +249,9 @@ export function addBreadcrumb(breadcrumb: Breadcrumb): void {
/**
* Set user context for error tracking
*/
-export function setUserContext(user: { id: string; email?: string; username?: string } | null): void {
+export function setUserContext(
+ user: { id: string; email?: string; username?: string } | null,
+): void {
if (!config.enabled) return;
if (typeof window !== "undefined") {
@@ -263,7 +283,9 @@ export function setExtraContext(context: ErrorContext): void {
/**
* Create a retry wrapper for async functions with exponential backoff
*/
-export function createRetryableFunction Promise>(
+export function createRetryableFunction<
+ T extends (...args: unknown[]) => Promise,
+>(
fn: T,
options: {
maxRetries?: number;
@@ -271,7 +293,7 @@ export function createRetryableFunction Promis
maxDelay?: number;
backoffMultiplier?: number;
onRetry?: (attempt: number, error: Error) => void;
- } = {}
+ } = {},
): T {
const {
maxRetries = 3,
@@ -287,7 +309,7 @@ export function createRetryableFunction Promis
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
- return await fn(...args) as ReturnType;
+ return (await fn(...args)) as ReturnType;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
@@ -308,7 +330,7 @@ export function createRetryableFunction Promis
// Wait with exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay));
-
+
// Increase delay for next attempt
delay = Math.min(delay * backoffMultiplier, maxDelay);
}
@@ -345,17 +367,26 @@ declare global {
interface Window {
Sentry?: {
captureException: (error: Error, context?: ErrorContext) => void;
- captureMessage: (message: string, level?: string, context?: ErrorContext) => void;
+ captureMessage: (
+ message: string,
+ level?: string,
+ context?: ErrorContext,
+ ) => void;
addBreadcrumb: (breadcrumb: Breadcrumb) => void;
- setUser: (user: { id: string; email?: string; username?: string } | null) => void;
+ setUser: (
+ user: { id: string; email?: string; username?: string } | null,
+ ) => void;
setExtra: (key: string, value: unknown) => void;
browserTracingIntegration?: () => unknown;
- replayIntegration?: (options: { maskAllText: boolean; blockAllMedia: boolean }) => unknown;
+ replayIntegration?: (options: {
+ maskAllText: boolean;
+ blockAllMedia: boolean;
+ }) => unknown;
};
}
}
-const errorTrackingModule = {
+const errorTracking = {
initErrorTracking,
captureException,
captureMessage,
@@ -366,4 +397,4 @@ const errorTrackingModule = {
isRetryableError,
};
-export default errorTrackingModule;
+export default errorTracking;
diff --git a/types/index.ts b/types/index.ts
index b7138ff..0417c93 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -1,9 +1,12 @@
// User Types
+export type UserRole = "admin" | "user" | "moderator";
+
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
+ role?: UserRole;
}
// Auth Store Types
@@ -43,11 +46,11 @@ export interface WalletActions {
export type WalletStore = WalletState & WalletActions;
// UI Store Types
-export type ModalType = 'login' | 'wallet' | 'settings' | null;
+export type ModalType = "login" | "wallet" | "settings" | null;
export interface Notification {
id: string;
- type: 'success' | 'error' | 'warning' | 'info';
+ type: "success" | "error" | "warning" | "info";
message: string;
duration?: number;
}
@@ -56,24 +59,24 @@ export interface UIState {
activeModal: ModalType;
notifications: Notification[];
isSidebarOpen: boolean;
- theme: 'light' | 'dark';
+ theme: "light" | "dark";
isGlobalLoading: boolean;
}
export interface UIActions {
openModal: (modal: ModalType) => void;
closeModal: () => void;
- addNotification: (notification: Omit) => void;
+ addNotification: (notification: Omit) => void;
removeNotification: (id: string) => void;
toggleSidebar: () => void;
- setTheme: (theme: 'light' | 'dark') => void;
+ setTheme: (theme: "light" | "dark") => void;
setGlobalLoading: (isLoading: boolean) => void;
}
export type UIStore = UIState & UIActions;
// Stellar Types
-export type StellarNetworkType = 'testnet' | 'public' | 'futurenet';
+export type StellarNetworkType = "testnet" | "public" | "futurenet";
export interface StellarAccount {
address: string;
@@ -102,7 +105,7 @@ export interface StellarTransaction {
asset: string;
createdAt: string;
memo?: string;
- status: 'pending' | 'completed' | 'failed';
+ status: "pending" | "completed" | "failed";
}
export interface ConnectionStatus {