AuthContext.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
  2. import { api, getAuthToken, setAuthToken } from '../api/client';
  3. import type { Permission, UserResponse } from '../api/client';
  4. interface AuthContextType {
  5. user: UserResponse | null;
  6. authEnabled: boolean;
  7. requiresSetup: boolean;
  8. loading: boolean;
  9. isAdmin: boolean;
  10. login: (username: string, password: string) => Promise<void>;
  11. logout: () => void;
  12. refreshUser: () => Promise<void>;
  13. refreshAuth: () => Promise<void>;
  14. hasPermission: (permission: Permission) => boolean;
  15. hasAnyPermission: (...permissions: Permission[]) => boolean;
  16. hasAllPermissions: (...permissions: Permission[]) => boolean;
  17. }
  18. const AuthContext = createContext<AuthContextType | undefined>(undefined);
  19. export function AuthProvider({ children }: { children: React.ReactNode }) {
  20. const [user, setUser] = useState<UserResponse | null>(null);
  21. const [authEnabled, setAuthEnabled] = useState(false);
  22. const [requiresSetup, setRequiresSetup] = useState(false);
  23. const [loading, setLoading] = useState(true);
  24. const hasRedirectedRef = useRef(false);
  25. const mountedRef = useRef(true);
  26. const checkAuthStatus = async () => {
  27. try {
  28. const status = await api.getAuthStatus();
  29. if (!mountedRef.current) return;
  30. setAuthEnabled(status.auth_enabled);
  31. setRequiresSetup(status.requires_setup);
  32. if (status.auth_enabled) {
  33. const token = getAuthToken();
  34. if (token) {
  35. try {
  36. const currentUser = await api.getCurrentUser();
  37. if (!mountedRef.current) return;
  38. setUser(currentUser);
  39. } catch {
  40. // Token invalid, clear it
  41. setAuthToken(null);
  42. if (!mountedRef.current) return;
  43. setUser(null);
  44. }
  45. } else {
  46. setUser(null);
  47. }
  48. } else {
  49. // Auth not enabled, allow access
  50. setUser(null);
  51. }
  52. } catch {
  53. if (!mountedRef.current) return;
  54. setAuthEnabled(false);
  55. setUser(null);
  56. } finally {
  57. if (mountedRef.current) {
  58. setLoading(false);
  59. }
  60. }
  61. };
  62. useEffect(() => {
  63. mountedRef.current = true;
  64. // Check auth status on mount
  65. checkAuthStatus();
  66. return () => {
  67. mountedRef.current = false;
  68. };
  69. }, []);
  70. // Separate effect to handle redirect only when setup is required
  71. useEffect(() => {
  72. // Only redirect if setup is truly required (first time setup)
  73. // Don't redirect if user manually navigated to /setup or is on camera page
  74. if (!loading && requiresSetup && !authEnabled) {
  75. const currentPath = window.location.pathname;
  76. // Only redirect if not already on setup page or camera page, and haven't redirected yet
  77. if (currentPath !== '/setup' && !currentPath.startsWith('/camera/') && !hasRedirectedRef.current) {
  78. hasRedirectedRef.current = true;
  79. window.location.href = '/setup';
  80. }
  81. } else if (!requiresSetup) {
  82. // Reset redirect flag when setup is no longer required
  83. hasRedirectedRef.current = false;
  84. }
  85. }, [loading, requiresSetup, authEnabled]);
  86. const login = async (username: string, password: string) => {
  87. const response = await api.login({ username, password });
  88. setAuthToken(response.access_token);
  89. setUser(response.user);
  90. };
  91. const logout = () => {
  92. setAuthToken(null);
  93. setUser(null);
  94. api.logout().catch(() => {
  95. // Ignore logout errors
  96. });
  97. window.location.href = '/login';
  98. };
  99. const refreshUser = async () => {
  100. if (authEnabled && getAuthToken()) {
  101. try {
  102. const currentUser = await api.getCurrentUser();
  103. if (mountedRef.current) {
  104. setUser(currentUser);
  105. }
  106. } catch {
  107. setAuthToken(null);
  108. if (mountedRef.current) {
  109. setUser(null);
  110. }
  111. }
  112. }
  113. };
  114. const refreshAuth = async () => {
  115. await checkAuthStatus();
  116. };
  117. // Memoize permission set for efficient lookups
  118. const permissionSet = useMemo(() => {
  119. return new Set(user?.permissions ?? []);
  120. }, [user?.permissions]);
  121. // Computed admin status
  122. const isAdmin = useMemo(() => {
  123. if (!authEnabled) return true; // Auth disabled = admin access
  124. return user?.is_admin ?? false;
  125. }, [authEnabled, user?.is_admin]);
  126. // Permission check functions
  127. const hasPermission = useCallback((permission: Permission): boolean => {
  128. if (!authEnabled) return true; // Auth disabled = allow all
  129. if (isAdmin) return true; // Admins have all permissions
  130. return permissionSet.has(permission);
  131. }, [authEnabled, isAdmin, permissionSet]);
  132. const hasAnyPermission = useCallback((...permissions: Permission[]): boolean => {
  133. if (!authEnabled) return true;
  134. if (isAdmin) return true;
  135. return permissions.some(p => permissionSet.has(p));
  136. }, [authEnabled, isAdmin, permissionSet]);
  137. const hasAllPermissions = useCallback((...permissions: Permission[]): boolean => {
  138. if (!authEnabled) return true;
  139. if (isAdmin) return true;
  140. return permissions.every(p => permissionSet.has(p));
  141. }, [authEnabled, isAdmin, permissionSet]);
  142. return (
  143. <AuthContext.Provider
  144. value={{
  145. user,
  146. authEnabled,
  147. requiresSetup,
  148. loading,
  149. isAdmin,
  150. login,
  151. logout,
  152. refreshUser,
  153. refreshAuth,
  154. hasPermission,
  155. hasAnyPermission,
  156. hasAllPermissions,
  157. }}
  158. >
  159. {children}
  160. </AuthContext.Provider>
  161. );
  162. }
  163. export function useAuth() {
  164. const context = useContext(AuthContext);
  165. if (context === undefined) {
  166. throw new Error('useAuth must be used within an AuthProvider');
  167. }
  168. return context;
  169. }