@igstack/app-catalog-frontend-core 6.10.2 → 6.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ interface AuthContextType {
13
13
  isLoading: boolean;
14
14
  isAuthenticated: boolean;
15
15
  }
16
+ export declare const AUTH_USER_STORAGE_KEY = "ac_auth_user";
16
17
  /**
17
18
  * Provider component for authentication state
18
19
  * Fetches and manages session from the backend
@@ -1,13 +1,13 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { use, createContext, useState, useEffect, useMemo } from "react";
3
3
  const AuthContext = createContext(void 0);
4
- const STORAGE_KEY = "ac_auth_user";
4
+ const AUTH_USER_STORAGE_KEY = "ac_auth_user";
5
5
  function AuthProvider({ children }) {
6
6
  const [session, setSession] = useState(null);
7
7
  const [user, setUser] = useState(() => {
8
8
  if (typeof window === "undefined") return null;
9
9
  try {
10
- const stored = localStorage.getItem(STORAGE_KEY);
10
+ const stored = localStorage.getItem(AUTH_USER_STORAGE_KEY);
11
11
  return stored ? JSON.parse(stored) : null;
12
12
  } catch {
13
13
  return null;
@@ -28,14 +28,17 @@ function AuthProvider({ children }) {
28
28
  setSession(data);
29
29
  setUser((data == null ? void 0 : data.user) || null);
30
30
  if (data == null ? void 0 : data.user) {
31
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data.user));
31
+ localStorage.setItem(
32
+ AUTH_USER_STORAGE_KEY,
33
+ JSON.stringify(data.user)
34
+ );
32
35
  } else {
33
- localStorage.removeItem(STORAGE_KEY);
36
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY);
34
37
  }
35
38
  } else {
36
39
  setSession(null);
37
40
  setUser(null);
38
- localStorage.removeItem(STORAGE_KEY);
41
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY);
39
42
  }
40
43
  } catch (error) {
41
44
  if (error instanceof Error && error.name === "AbortError") {
@@ -44,7 +47,7 @@ function AuthProvider({ children }) {
44
47
  console.error("Failed to fetch session:", error);
45
48
  setSession(null);
46
49
  setUser(null);
47
- localStorage.removeItem(STORAGE_KEY);
50
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY);
48
51
  } finally {
49
52
  setIsLoading(false);
50
53
  }
@@ -85,6 +88,7 @@ function useOptionalUser() {
85
88
  return ((_a = use(AuthContext)) == null ? void 0 : _a.user) ?? null;
86
89
  }
87
90
  export {
91
+ AUTH_USER_STORAGE_KEY,
88
92
  AuthProvider,
89
93
  useAuth,
90
94
  useIsAuthenticated,
@@ -1 +1 @@
1
- {"version":3,"file":"AuthContext.js","sources":["../../../../src/modules/auth/AuthContext.tsx"],"sourcesContent":["import type { Session } from 'better-auth/types'\nimport { createContext, use, useEffect, useMemo, useState } from 'react'\n\ninterface User {\n id: string\n email?: string | null\n name?: string | null\n image?: string | null\n emailVerified?: boolean\n isAdmin?: boolean\n}\n\ninterface AuthContextType {\n session: Session | null\n user: User | null\n isLoading: boolean\n isAuthenticated: boolean\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nconst STORAGE_KEY = 'ac_auth_user'\n\n/**\n * Provider component for authentication state\n * Fetches and manages session from the backend\n * Uses localStorage for optimistic rendering to avoid flickering\n */\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n const [session, setSession] = useState<Session | null>(null)\n const [user, setUser] = useState<User | null>(() => {\n // Hydrate from localStorage on mount to avoid flickering\n if (typeof window === 'undefined') return null\n try {\n const stored = localStorage.getItem(STORAGE_KEY)\n return stored ? JSON.parse(stored) : null\n } catch {\n return null\n }\n })\n const [isLoading, setIsLoading] = useState(true)\n\n useEffect(() => {\n // Use AbortController to cancel fetch on unmount (e.g., React StrictMode double-mount)\n const abortController = new AbortController()\n\n // Fetch session from backend\n async function fetchSession() {\n try {\n const baseUrl =\n typeof window !== 'undefined' ? window.location.origin : ''\n const response = await fetch(`${baseUrl}/api/auth/session`, {\n credentials: 'include',\n signal: abortController.signal,\n })\n if (response.ok) {\n const data = await response.json()\n setSession(data)\n setUser(data?.user || null)\n if (data?.user) {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data.user))\n } else {\n localStorage.removeItem(STORAGE_KEY)\n }\n } else {\n setSession(null)\n setUser(null)\n localStorage.removeItem(STORAGE_KEY)\n }\n } catch (error) {\n // Ignore abort errors (happens on cleanup)\n if (error instanceof Error && error.name === 'AbortError') {\n return\n }\n console.error('Failed to fetch session:', error)\n setSession(null)\n setUser(null)\n localStorage.removeItem(STORAGE_KEY)\n } finally {\n setIsLoading(false)\n }\n }\n\n fetchSession()\n\n // Cleanup: abort fetch on unmount\n return () => {\n abortController.abort()\n }\n }, [])\n\n const value: AuthContextType = useMemo(\n () => ({\n session,\n user,\n isLoading,\n isAuthenticated: !!user,\n }),\n [session, user, isLoading],\n )\n\n return <AuthContext value={value}>{children}</AuthContext>\n}\n\n/**\n * Hook to access authentication state\n * @returns Authentication context containing session, user, and auth state\n */\nexport function useAuth() {\n const context = use(AuthContext)\n if (!context) {\n throw new Error('useAuth must be used within AuthProvider')\n }\n return context\n}\n\n/**\n * Hook to check if user is authenticated\n * @returns Boolean indicating if user is authenticated\n */\nexport function useIsAuthenticated() {\n const { isAuthenticated } = useAuth()\n return isAuthenticated\n}\n\n/**\n * Hook to get the current user\n * @returns Current user or null\n */\nexport function useUser() {\n const { user } = useAuth()\n return user\n}\n\n/**\n * The current user, or null when there is no `AuthProvider` above.\n *\n * `useUser()` throws without a provider, which is correct for UI that only ever\n * renders inside the app but wrong for anything a test may mount bare. Several\n * components are rendered bare on purpose (`AppDetailPanel`, `AppCatalogGrid`),\n * so assembling a plugin slot payload with `useUser()` there turns every one of\n * those tests red — \"useAuth must be used within AuthProvider\".\n *\n * Signed-out and no-provider are the same answer for a slot payload: null.\n */\nexport function useOptionalUser() {\n return use(AuthContext)?.user ?? null\n}\n"],"names":[],"mappings":";;AAmBA,MAAM,cAAc,cAA2C,MAAS;AAExE,MAAM,cAAc;AAOb,SAAS,aAAa,EAAE,YAA2C;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,MAAM;AAElD,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAI;AACF,YAAM,SAAS,aAAa,QAAQ,WAAW;AAC/C,aAAO,SAAS,KAAK,MAAM,MAAM,IAAI;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MAAM;AAEd,UAAM,kBAAkB,IAAI,gBAAA;AAG5B,mBAAe,eAAe;AAC5B,UAAI;AACF,cAAM,UACJ,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AAC3D,cAAM,WAAW,MAAM,MAAM,GAAG,OAAO,qBAAqB;AAAA,UAC1D,aAAa;AAAA,UACb,QAAQ,gBAAgB;AAAA,QAAA,CACzB;AACD,YAAI,SAAS,IAAI;AACf,gBAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,qBAAW,IAAI;AACf,mBAAQ,6BAAM,SAAQ,IAAI;AAC1B,cAAI,6BAAM,MAAM;AACd,yBAAa,QAAQ,aAAa,KAAK,UAAU,KAAK,IAAI,CAAC;AAAA,UAC7D,OAAO;AACL,yBAAa,WAAW,WAAW;AAAA,UACrC;AAAA,QACF,OAAO;AACL,qBAAW,IAAI;AACf,kBAAQ,IAAI;AACZ,uBAAa,WAAW,WAAW;AAAA,QACrC;AAAA,MACF,SAAS,OAAO;AAEd,YAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD;AAAA,QACF;AACA,gBAAQ,MAAM,4BAA4B,KAAK;AAC/C,mBAAW,IAAI;AACf,gBAAQ,IAAI;AACZ,qBAAa,WAAW,WAAW;AAAA,MACrC,UAAA;AACE,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,iBAAA;AAGA,WAAO,MAAM;AACX,sBAAgB,MAAA;AAAA,IAClB;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAyB;AAAA,IAC7B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB,CAAC,CAAC;AAAA,IAAA;AAAA,IAErB,CAAC,SAAS,MAAM,SAAS;AAAA,EAAA;AAG3B,SAAO,oBAAC,aAAA,EAAY,OAAe,SAAA,CAAS;AAC9C;AAMO,SAAS,UAAU;AACxB,QAAM,UAAU,IAAI,WAAW;AAC/B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,SAAO;AACT;AAMO,SAAS,qBAAqB;AACnC,QAAM,EAAE,gBAAA,IAAoB,QAAA;AAC5B,SAAO;AACT;AAMO,SAAS,UAAU;AACxB,QAAM,EAAE,KAAA,IAAS,QAAA;AACjB,SAAO;AACT;AAaO,SAAS,kBAAkB;;AAChC,WAAO,SAAI,WAAW,MAAf,mBAAkB,SAAQ;AACnC;"}
1
+ {"version":3,"file":"AuthContext.js","sources":["../../../../src/modules/auth/AuthContext.tsx"],"sourcesContent":["import type { Session } from 'better-auth/types'\nimport { createContext, use, useEffect, useMemo, useState } from 'react'\n\ninterface User {\n id: string\n email?: string | null\n name?: string | null\n image?: string | null\n emailVerified?: boolean\n isAdmin?: boolean\n}\n\ninterface AuthContextType {\n session: Session | null\n user: User | null\n isLoading: boolean\n isAuthenticated: boolean\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nexport const AUTH_USER_STORAGE_KEY = 'ac_auth_user'\n\n/**\n * Provider component for authentication state\n * Fetches and manages session from the backend\n * Uses localStorage for optimistic rendering to avoid flickering\n */\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n const [session, setSession] = useState<Session | null>(null)\n const [user, setUser] = useState<User | null>(() => {\n // Hydrate from localStorage on mount to avoid flickering\n if (typeof window === 'undefined') return null\n try {\n const stored = localStorage.getItem(AUTH_USER_STORAGE_KEY)\n return stored ? JSON.parse(stored) : null\n } catch {\n return null\n }\n })\n const [isLoading, setIsLoading] = useState(true)\n\n useEffect(() => {\n // Use AbortController to cancel fetch on unmount (e.g., React StrictMode double-mount)\n const abortController = new AbortController()\n\n // Fetch session from backend\n async function fetchSession() {\n try {\n const baseUrl =\n typeof window !== 'undefined' ? window.location.origin : ''\n const response = await fetch(`${baseUrl}/api/auth/session`, {\n credentials: 'include',\n signal: abortController.signal,\n })\n if (response.ok) {\n const data = await response.json()\n setSession(data)\n setUser(data?.user || null)\n if (data?.user) {\n localStorage.setItem(\n AUTH_USER_STORAGE_KEY,\n JSON.stringify(data.user),\n )\n } else {\n localStorage.removeItem(AUTH_USER_STORAGE_KEY)\n }\n } else {\n setSession(null)\n setUser(null)\n localStorage.removeItem(AUTH_USER_STORAGE_KEY)\n }\n } catch (error) {\n // Ignore abort errors (happens on cleanup)\n if (error instanceof Error && error.name === 'AbortError') {\n return\n }\n console.error('Failed to fetch session:', error)\n setSession(null)\n setUser(null)\n localStorage.removeItem(AUTH_USER_STORAGE_KEY)\n } finally {\n setIsLoading(false)\n }\n }\n\n fetchSession()\n\n // Cleanup: abort fetch on unmount\n return () => {\n abortController.abort()\n }\n }, [])\n\n const value: AuthContextType = useMemo(\n () => ({\n session,\n user,\n isLoading,\n isAuthenticated: !!user,\n }),\n [session, user, isLoading],\n )\n\n return <AuthContext value={value}>{children}</AuthContext>\n}\n\n/**\n * Hook to access authentication state\n * @returns Authentication context containing session, user, and auth state\n */\nexport function useAuth() {\n const context = use(AuthContext)\n if (!context) {\n throw new Error('useAuth must be used within AuthProvider')\n }\n return context\n}\n\n/**\n * Hook to check if user is authenticated\n * @returns Boolean indicating if user is authenticated\n */\nexport function useIsAuthenticated() {\n const { isAuthenticated } = useAuth()\n return isAuthenticated\n}\n\n/**\n * Hook to get the current user\n * @returns Current user or null\n */\nexport function useUser() {\n const { user } = useAuth()\n return user\n}\n\n/**\n * The current user, or null when there is no `AuthProvider` above.\n *\n * `useUser()` throws without a provider, which is correct for UI that only ever\n * renders inside the app but wrong for anything a test may mount bare. Several\n * components are rendered bare on purpose (`AppDetailPanel`, `AppCatalogGrid`),\n * so assembling a plugin slot payload with `useUser()` there turns every one of\n * those tests red — \"useAuth must be used within AuthProvider\".\n *\n * Signed-out and no-provider are the same answer for a slot payload: null.\n */\nexport function useOptionalUser() {\n return use(AuthContext)?.user ?? null\n}\n"],"names":[],"mappings":";;AAmBA,MAAM,cAAc,cAA2C,MAAS;AAEjE,MAAM,wBAAwB;AAO9B,SAAS,aAAa,EAAE,YAA2C;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,MAAM;AAElD,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAI;AACF,YAAM,SAAS,aAAa,QAAQ,qBAAqB;AACzD,aAAO,SAAS,KAAK,MAAM,MAAM,IAAI;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MAAM;AAEd,UAAM,kBAAkB,IAAI,gBAAA;AAG5B,mBAAe,eAAe;AAC5B,UAAI;AACF,cAAM,UACJ,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AAC3D,cAAM,WAAW,MAAM,MAAM,GAAG,OAAO,qBAAqB;AAAA,UAC1D,aAAa;AAAA,UACb,QAAQ,gBAAgB;AAAA,QAAA,CACzB;AACD,YAAI,SAAS,IAAI;AACf,gBAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,qBAAW,IAAI;AACf,mBAAQ,6BAAM,SAAQ,IAAI;AAC1B,cAAI,6BAAM,MAAM;AACd,yBAAa;AAAA,cACX;AAAA,cACA,KAAK,UAAU,KAAK,IAAI;AAAA,YAAA;AAAA,UAE5B,OAAO;AACL,yBAAa,WAAW,qBAAqB;AAAA,UAC/C;AAAA,QACF,OAAO;AACL,qBAAW,IAAI;AACf,kBAAQ,IAAI;AACZ,uBAAa,WAAW,qBAAqB;AAAA,QAC/C;AAAA,MACF,SAAS,OAAO;AAEd,YAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD;AAAA,QACF;AACA,gBAAQ,MAAM,4BAA4B,KAAK;AAC/C,mBAAW,IAAI;AACf,gBAAQ,IAAI;AACZ,qBAAa,WAAW,qBAAqB;AAAA,MAC/C,UAAA;AACE,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,iBAAA;AAGA,WAAO,MAAM;AACX,sBAAgB,MAAA;AAAA,IAClB;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAyB;AAAA,IAC7B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB,CAAC,CAAC;AAAA,IAAA;AAAA,IAErB,CAAC,SAAS,MAAM,SAAS;AAAA,EAAA;AAG3B,SAAO,oBAAC,aAAA,EAAY,OAAe,SAAA,CAAS;AAC9C;AAMO,SAAS,UAAU;AACxB,QAAM,UAAU,IAAI,WAAW;AAC/B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,SAAO;AACT;AAMO,SAAS,qBAAqB;AACnC,QAAM,EAAE,gBAAA,IAAoB,QAAA;AAC5B,SAAO;AACT;AAMO,SAAS,UAAU;AACxB,QAAM,EAAE,KAAA,IAAS,QAAA;AACjB,SAAO;AACT;AAaO,SAAS,kBAAkB;;AAChC,WAAO,SAAI,WAAW,MAAf,mBAAkB,SAAQ;AACnC;"}
@@ -1,4 +1,5 @@
1
1
  import { useCallback } from "react";
2
+ import { AUTH_USER_STORAGE_KEY } from "./AuthContext.js";
2
3
  function useAuthActions() {
3
4
  const login = useCallback(async (credentials) => {
4
5
  const response = await fetch("/api/auth/sign-in/email", {
@@ -37,7 +38,8 @@ function useAuthActions() {
37
38
  credentials: "include"
38
39
  });
39
40
  if (!response.ok) ;
40
- window.location.href = "/";
41
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY);
42
+ window.location.reload();
41
43
  }, []);
42
44
  const devLogin = useCallback(async () => {
43
45
  const response = await fetch("/api/auth/dev-login", {
@@ -1 +1 @@
1
- {"version":3,"file":"useAuthActions.js","sources":["../../../../src/modules/auth/useAuthActions.ts"],"sourcesContent":["import { useCallback } from 'react'\n\ninterface LoginCredentials {\n email: string\n password: string\n}\n\ninterface SignupCredentials extends LoginCredentials {\n name: string\n}\n\n/**\n * Hook for authentication actions (login, signup, logout)\n */\nexport function useAuthActions() {\n const login = useCallback(async (credentials: LoginCredentials) => {\n const response = await fetch('/api/auth/sign-in/email', {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(credentials),\n })\n\n if (!response.ok) {\n const error = await response.json()\n throw new Error(error.message || 'Login failed')\n }\n\n return response.json()\n }, [])\n\n const signup = useCallback(async (credentials: SignupCredentials) => {\n const response = await fetch('/api/auth/sign-up/email', {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(credentials),\n })\n\n if (!response.ok) {\n const error = await response.json()\n throw new Error(error.message || 'Signup failed')\n }\n\n return response.json()\n }, [])\n\n const logout = useCallback(async () => {\n // Clear dev session cookie if present\n await fetch('/api/auth/dev-logout', {\n method: 'POST',\n credentials: 'include',\n }).catch(() => {})\n\n const response = await fetch('/api/auth/sign-out', {\n method: 'POST',\n credentials: 'include',\n })\n\n if (!response.ok) {\n // Sign-out may fail if there's no real session (dev-only login)\n // Still reload to clear UI state\n }\n\n // Clear session and reload to update UI\n window.location.href = '/'\n }, [])\n\n const devLogin = useCallback(async () => {\n const response = await fetch('/api/auth/dev-login', {\n method: 'POST',\n credentials: 'include',\n })\n if (!response.ok) {\n throw new Error('Dev login failed')\n }\n window.location.reload()\n }, [])\n\n const socialLogin = useCallback(async (provider: 'github' | 'google') => {\n const response = await fetch(`/api/auth/signin/${provider}`, {\n method: 'POST',\n credentials: 'include',\n })\n\n if (!response.ok) {\n throw new Error(`${provider} login failed`)\n }\n\n const data = await response.json()\n // Redirect to provider's OAuth flow\n if (data.redirectUrl) {\n window.location.href = data.redirectUrl\n }\n }, [])\n\n return {\n login,\n signup,\n logout,\n socialLogin,\n devLogin,\n }\n}\n"],"names":[],"mappings":";AAcO,SAAS,iBAAiB;AAC/B,QAAM,QAAQ,YAAY,OAAO,gBAAkC;AACjE,UAAM,WAAW,MAAM,MAAM,2BAA2B;AAAA,MACtD,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,WAAW;AAAA,IAAA,CACjC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAA;AAC7B,YAAM,IAAI,MAAM,MAAM,WAAW,cAAc;AAAA,IACjD;AAEA,WAAO,SAAS,KAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,SAAS,YAAY,OAAO,gBAAmC;AACnE,UAAM,WAAW,MAAM,MAAM,2BAA2B;AAAA,MACtD,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,WAAW;AAAA,IAAA,CACjC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAA;AAC7B,YAAM,IAAI,MAAM,MAAM,WAAW,eAAe;AAAA,IAClD;AAEA,WAAO,SAAS,KAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,SAAS,YAAY,YAAY;AAErC,UAAM,MAAM,wBAAwB;AAAA,MAClC,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAEjB,UAAM,WAAW,MAAM,MAAM,sBAAsB;AAAA,MACjD,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd;AAED,QAAI,CAAC,SAAS,GAAI;AAMlB,WAAO,SAAS,OAAO;AAAA,EACzB,GAAG,CAAA,CAAE;AAEL,QAAM,WAAW,YAAY,YAAY;AACvC,UAAM,WAAW,MAAM,MAAM,uBAAuB;AAAA,MAClD,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,kBAAkB;AAAA,IACpC;AACA,WAAO,SAAS,OAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,cAAc,YAAY,OAAO,aAAkC;AACvE,UAAM,WAAW,MAAM,MAAM,oBAAoB,QAAQ,IAAI;AAAA,MAC3D,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,GAAG,QAAQ,eAAe;AAAA,IAC5C;AAEA,UAAM,OAAO,MAAM,SAAS,KAAA;AAE5B,QAAI,KAAK,aAAa;AACpB,aAAO,SAAS,OAAO,KAAK;AAAA,IAC9B;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"useAuthActions.js","sources":["../../../../src/modules/auth/useAuthActions.ts"],"sourcesContent":["import { useCallback } from 'react'\nimport { AUTH_USER_STORAGE_KEY } from './AuthContext'\n\ninterface LoginCredentials {\n email: string\n password: string\n}\n\ninterface SignupCredentials extends LoginCredentials {\n name: string\n}\n\n/**\n * Hook for authentication actions (login, signup, logout)\n */\nexport function useAuthActions() {\n const login = useCallback(async (credentials: LoginCredentials) => {\n const response = await fetch('/api/auth/sign-in/email', {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(credentials),\n })\n\n if (!response.ok) {\n const error = await response.json()\n throw new Error(error.message || 'Login failed')\n }\n\n return response.json()\n }, [])\n\n const signup = useCallback(async (credentials: SignupCredentials) => {\n const response = await fetch('/api/auth/sign-up/email', {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(credentials),\n })\n\n if (!response.ok) {\n const error = await response.json()\n throw new Error(error.message || 'Signup failed')\n }\n\n return response.json()\n }, [])\n\n const logout = useCallback(async () => {\n // Clear dev session cookie if present\n await fetch('/api/auth/dev-logout', {\n method: 'POST',\n credentials: 'include',\n }).catch(() => {})\n\n const response = await fetch('/api/auth/sign-out', {\n method: 'POST',\n credentials: 'include',\n })\n\n if (!response.ok) {\n // Sign-out may fail if there's no real session (dev-only login)\n // Still reload to clear UI state\n }\n\n // Stay on the current page: drop the optimistic user the provider hydrates\n // from, so the reload renders signed out without flashing the old user.\n localStorage.removeItem(AUTH_USER_STORAGE_KEY)\n window.location.reload()\n }, [])\n\n const devLogin = useCallback(async () => {\n const response = await fetch('/api/auth/dev-login', {\n method: 'POST',\n credentials: 'include',\n })\n if (!response.ok) {\n throw new Error('Dev login failed')\n }\n window.location.reload()\n }, [])\n\n const socialLogin = useCallback(async (provider: 'github' | 'google') => {\n const response = await fetch(`/api/auth/signin/${provider}`, {\n method: 'POST',\n credentials: 'include',\n })\n\n if (!response.ok) {\n throw new Error(`${provider} login failed`)\n }\n\n const data = await response.json()\n // Redirect to provider's OAuth flow\n if (data.redirectUrl) {\n window.location.href = data.redirectUrl\n }\n }, [])\n\n return {\n login,\n signup,\n logout,\n socialLogin,\n devLogin,\n }\n}\n"],"names":[],"mappings":";;AAeO,SAAS,iBAAiB;AAC/B,QAAM,QAAQ,YAAY,OAAO,gBAAkC;AACjE,UAAM,WAAW,MAAM,MAAM,2BAA2B;AAAA,MACtD,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,WAAW;AAAA,IAAA,CACjC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAA;AAC7B,YAAM,IAAI,MAAM,MAAM,WAAW,cAAc;AAAA,IACjD;AAEA,WAAO,SAAS,KAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,SAAS,YAAY,OAAO,gBAAmC;AACnE,UAAM,WAAW,MAAM,MAAM,2BAA2B;AAAA,MACtD,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,WAAW;AAAA,IAAA,CACjC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAA;AAC7B,YAAM,IAAI,MAAM,MAAM,WAAW,eAAe;AAAA,IAClD;AAEA,WAAO,SAAS,KAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,SAAS,YAAY,YAAY;AAErC,UAAM,MAAM,wBAAwB;AAAA,MAClC,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAEjB,UAAM,WAAW,MAAM,MAAM,sBAAsB;AAAA,MACjD,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd;AAED,QAAI,CAAC,SAAS,GAAI;AAOlB,iBAAa,WAAW,qBAAqB;AAC7C,WAAO,SAAS,OAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,WAAW,YAAY,YAAY;AACvC,UAAM,WAAW,MAAM,MAAM,uBAAuB;AAAA,MAClD,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,kBAAkB;AAAA,IACpC;AACA,WAAO,SAAS,OAAA;AAAA,EAClB,GAAG,CAAA,CAAE;AAEL,QAAM,cAAc,YAAY,OAAO,aAAkC;AACvE,UAAM,WAAW,MAAM,MAAM,oBAAoB,QAAQ,IAAI;AAAA,MAC3D,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA,CACd;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,GAAG,QAAQ,eAAe;AAAA,IAC5C;AAEA,UAAM,OAAO,MAAM,SAAS,KAAA;AAE5B,QAAI,KAAK,aAAa;AACpB,aAAO,SAAS,OAAO,KAAK;AAAA,IAC9B;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "6.10.2",
3
+ "version": "6.10.3",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -118,7 +118,7 @@
118
118
  "tailwindcss": "^4.1.7",
119
119
  "tiptap-markdown": "^0.8.0",
120
120
  "tslib": "^2.3.0",
121
- "@igstack/app-catalog-shared-core": "6.10.2"
121
+ "@igstack/app-catalog-shared-core": "6.10.3"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@tailwindcss/vite": "^4.0.5",
@@ -146,7 +146,7 @@
146
146
  "vite-plugin-static-copy": "^3.1.4",
147
147
  "vite-plugin-svgr": "^4.2.0",
148
148
  "vitest": "^4.1.2",
149
- "@igstack/app-catalog-backend-core": "6.10.2"
149
+ "@igstack/app-catalog-backend-core": "6.10.3"
150
150
  },
151
151
  "peerDependencies": {
152
152
  "@tanstack/react-router": "1.170.41",
@@ -156,7 +156,7 @@
156
156
  "vite": "^6.3.5",
157
157
  "vite-plugin-svgr": "^4.2.0"
158
158
  },
159
- "gitHead": "f3e5b024e83e55773503820fdb5bfd858319b6d1",
159
+ "gitHead": "980bbf4c4901132d351291df3522fa6b6eaaa123",
160
160
  "scripts": {
161
161
  "build": "vite build",
162
162
  "build:lenient": "vite build --mode lenient",
@@ -0,0 +1,49 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { AUTH_USER_STORAGE_KEY } from '~/modules/auth/AuthContext'
4
+ import { useAuthActions } from '~/modules/auth/useAuthActions'
5
+
6
+ const originalLocation = window.location
7
+ const reload = vi.fn()
8
+
9
+ beforeEach(() => {
10
+ vi.stubGlobal(
11
+ 'fetch',
12
+ vi.fn(async () => new Response('{}', { status: 200 })),
13
+ )
14
+ // jsdom's location cannot be spied on; swap in one whose reload is observable.
15
+ Object.defineProperty(window, 'location', {
16
+ configurable: true,
17
+ value: { ...originalLocation, href: originalLocation.href, reload },
18
+ })
19
+ })
20
+
21
+ afterEach(() => {
22
+ Object.defineProperty(window, 'location', {
23
+ configurable: true,
24
+ value: originalLocation,
25
+ })
26
+ vi.unstubAllGlobals()
27
+ reload.mockClear()
28
+ localStorage.clear()
29
+ })
30
+
31
+ describe('useAuthActions.logout', () => {
32
+ it('reloads the current page signed out instead of going home', async () => {
33
+ const page = 'http://localhost:3000/app/sample-review-app?tab=access'
34
+ window.location.href = page
35
+ localStorage.setItem(
36
+ AUTH_USER_STORAGE_KEY,
37
+ JSON.stringify({ id: 'u1', name: 'Signed In' }),
38
+ )
39
+ const { result } = renderHook(() => useAuthActions())
40
+
41
+ await result.current.logout()
42
+
43
+ expect(reload).toHaveBeenCalledOnce()
44
+ expect(window.location.href).toBe(page)
45
+ // The provider hydrates from this before the session fetch returns, so a
46
+ // leftover entry would flash the old user on the reloaded page.
47
+ expect(localStorage.getItem(AUTH_USER_STORAGE_KEY)).toBeNull()
48
+ })
49
+ })
@@ -19,7 +19,7 @@ interface AuthContextType {
19
19
 
20
20
  const AuthContext = createContext<AuthContextType | undefined>(undefined)
21
21
 
22
- const STORAGE_KEY = 'ac_auth_user'
22
+ export const AUTH_USER_STORAGE_KEY = 'ac_auth_user'
23
23
 
24
24
  /**
25
25
  * Provider component for authentication state
@@ -32,7 +32,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
32
32
  // Hydrate from localStorage on mount to avoid flickering
33
33
  if (typeof window === 'undefined') return null
34
34
  try {
35
- const stored = localStorage.getItem(STORAGE_KEY)
35
+ const stored = localStorage.getItem(AUTH_USER_STORAGE_KEY)
36
36
  return stored ? JSON.parse(stored) : null
37
37
  } catch {
38
38
  return null
@@ -58,14 +58,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
58
58
  setSession(data)
59
59
  setUser(data?.user || null)
60
60
  if (data?.user) {
61
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data.user))
61
+ localStorage.setItem(
62
+ AUTH_USER_STORAGE_KEY,
63
+ JSON.stringify(data.user),
64
+ )
62
65
  } else {
63
- localStorage.removeItem(STORAGE_KEY)
66
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY)
64
67
  }
65
68
  } else {
66
69
  setSession(null)
67
70
  setUser(null)
68
- localStorage.removeItem(STORAGE_KEY)
71
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY)
69
72
  }
70
73
  } catch (error) {
71
74
  // Ignore abort errors (happens on cleanup)
@@ -75,7 +78,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
75
78
  console.error('Failed to fetch session:', error)
76
79
  setSession(null)
77
80
  setUser(null)
78
- localStorage.removeItem(STORAGE_KEY)
81
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY)
79
82
  } finally {
80
83
  setIsLoading(false)
81
84
  }
@@ -1,4 +1,5 @@
1
1
  import { useCallback } from 'react'
2
+ import { AUTH_USER_STORAGE_KEY } from './AuthContext'
2
3
 
3
4
  interface LoginCredentials {
4
5
  email: string
@@ -62,8 +63,10 @@ export function useAuthActions() {
62
63
  // Still reload to clear UI state
63
64
  }
64
65
 
65
- // Clear session and reload to update UI
66
- window.location.href = '/'
66
+ // Stay on the current page: drop the optimistic user the provider hydrates
67
+ // from, so the reload renders signed out without flashing the old user.
68
+ localStorage.removeItem(AUTH_USER_STORAGE_KEY)
69
+ window.location.reload()
67
70
  }, [])
68
71
 
69
72
  const devLogin = useCallback(async () => {