academe-kit 0.5.8 → 0.6.1

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.
package/dist/index.cjs CHANGED
@@ -5772,6 +5772,54 @@ function createCourseService(apiClient) {
5772
5772
  };
5773
5773
  }
5774
5774
 
5775
+ function createStorageFileService(apiClient) {
5776
+ return {
5777
+ /**
5778
+ * Upload a new file
5779
+ * Note: This method requires FormData with a 'file' field
5780
+ */
5781
+ upload(file) {
5782
+ const formData = new FormData();
5783
+ formData.append("file", file);
5784
+ // Note: Route not typed in OpenAPI yet, using type assertion
5785
+ return apiClient.POST("/storage-files", {
5786
+ body: formData,
5787
+ bodySerializer: (body) => body,
5788
+ });
5789
+ },
5790
+ /**
5791
+ * List all storage files
5792
+ */
5793
+ getAll() {
5794
+ // Note: Route not typed in OpenAPI yet, using type assertion
5795
+ return apiClient.GET("/storage-files", {});
5796
+ },
5797
+ /**
5798
+ * Get storage file by ID
5799
+ */
5800
+ getById(id) {
5801
+ // Note: Route not typed in OpenAPI yet, using type assertion
5802
+ return apiClient.GET(`/storage-files/${id}`, {});
5803
+ },
5804
+ /**
5805
+ * Update storage file metadata
5806
+ */
5807
+ update(id, data) {
5808
+ // Note: Route not typed in OpenAPI yet, using type assertion
5809
+ return apiClient.PATCH(`/storage-files/${id}`, {
5810
+ body: data,
5811
+ });
5812
+ },
5813
+ /**
5814
+ * Delete storage file
5815
+ */
5816
+ delete(id) {
5817
+ // Note: Route not typed in OpenAPI yet, using type assertion
5818
+ return apiClient.DELETE(`/storage-files/${id}`, {});
5819
+ },
5820
+ };
5821
+ }
5822
+
5775
5823
  function createAcademeApiClient(baseUrl) {
5776
5824
  return createClient({ baseUrl });
5777
5825
  }
@@ -5793,6 +5841,7 @@ function createAcademeServices(apiClient) {
5793
5841
  certificateTemplate: createCertificateTemplateService(apiClient),
5794
5842
  seatCode: createSeatCodeService(apiClient),
5795
5843
  product: createProductService(apiClient),
5844
+ storageFile: createStorageFileService(apiClient),
5796
5845
  };
5797
5846
  }
5798
5847
 
@@ -5823,6 +5872,7 @@ const AcademeAuthProvider = ({ realm, hubUrl, children, clientId, keycloakUrl, a
5823
5872
  const SecurityContext = React2.createContext({
5824
5873
  isInitialized: false,
5825
5874
  isTokenReady: false,
5875
+ isMobileAuth: false,
5826
5876
  user: null,
5827
5877
  refreshUserData: async () => { },
5828
5878
  signOut: () => null,
@@ -5852,119 +5902,70 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
5852
5902
  const [currentUser, setCurrentUser] = React2.useState(null);
5853
5903
  const hasTriedSignInSilent = React2.useRef(false);
5854
5904
  const [isTokenReady, setIsTokenReady] = React2.useState(false);
5905
+ const [isRefreshing, setIsRefreshing] = React2.useState(false);
5906
+ // --- Estado para autenticação mobile ---
5907
+ const [isMobileAuth, setIsMobileAuth] = React2.useState(false);
5908
+ const [mobileToken, setMobileToken] = React2.useState(undefined);
5909
+ const mobileInitializedRef = React2.useRef(false);
5855
5910
  // Ref para armazenar o resolver da Promise de token
5856
5911
  const tokenReadyResolverRef = React2.useRef(null);
5857
5912
  const tokenReadyPromiseRef = React2.useRef(null);
5858
- // Estados para controle de token injetado pelo mobile
5859
- const [mobileToken, setMobileToken] = React2.useState(undefined);
5860
- const [isMobileWebView, setIsMobileWebView] = React2.useState(false);
5861
- const [isMobileDetectionComplete, setIsMobileDetectionComplete] = React2.useState(false);
5862
5913
  // Extrair valores primitivos do auth para usar como dependências estáveis
5863
5914
  const isAuthenticated = auth.isAuthenticated;
5864
5915
  const isLoading = auth.isLoading;
5865
5916
  const activeNavigator = auth.activeNavigator;
5866
- const oidcAccessToken = auth.user?.access_token;
5917
+ const accessToken = auth.user?.access_token;
5867
5918
  const userProfile = auth.user?.profile;
5868
5919
  const userProfileSub = auth.user?.profile?.sub;
5869
- // Token a ser usado: prioriza mobile, depois OIDC
5870
- const accessToken = isMobileWebView ? mobileToken : oidcAccessToken;
5871
- // --- 0. Detectar Mobile WebView e escutar eventos de token ---
5920
+ // Ref para armazenar o token atual (acessível no middleware)
5921
+ const currentTokenRef = React2.useRef(undefined);
5922
+ // --- 0. Detecção de Token Mobile (ANTES do SSO check) ---
5872
5923
  React2.useEffect(() => {
5924
+ // Só executa uma vez na inicialização
5925
+ if (mobileInitializedRef.current)
5926
+ return;
5927
+ mobileInitializedRef.current = true;
5873
5928
  if (typeof window === "undefined")
5874
5929
  return;
5875
- let pollingInterval = null;
5876
- let pollingAttempts = 0;
5877
- // Detectar se estamos em um contexto de Mobile WebView
5878
- // Verificamos múltiplos indicadores porque no iOS o ReactNativeWebView pode ser injetado depois
5879
- const urlParams = new URLSearchParams(window.location.search);
5880
- const isEmbedded = urlParams.get('embedded') === 'true';
5881
- const hasReactNativeWebView = !!window.ReactNativeWebView;
5882
- const hasKeycloakConfig = !!window.keycloakConfig?.fromMobile;
5883
- const shouldCheckForMobileToken = isEmbedded || hasReactNativeWebView || hasKeycloakConfig;
5884
- console.log('[SecurityProvider] Verificação mobile:', {
5885
- isEmbedded,
5886
- hasReactNativeWebView,
5887
- hasKeycloakConfig,
5888
- shouldCheckForMobileToken
5889
- });
5890
- // Se não há indicadores de mobile, completa imediatamente
5891
- if (!shouldCheckForMobileToken) {
5892
- console.log('[SecurityProvider] Não é contexto mobile - pulando detecção');
5893
- setIsMobileDetectionComplete(true);
5930
+ const keycloakConfig = window.keycloakConfig;
5931
+ if (!keycloakConfig?.fromMobile || !keycloakConfig?.token) {
5932
+ console.debug("[SecurityProvider] Modo web normal - sem token mobile");
5894
5933
  return;
5895
5934
  }
5896
- console.log('[SecurityProvider] Contexto mobile detectado - iniciando detecção de token...');
5897
- // Polling para mobile: 30 tentativas de 100ms = 3 segundos max
5898
- const MAX_POLLING_ATTEMPTS = 30;
5899
- const POLLING_INTERVAL_MS = 100;
5900
- // Função para verificar e configurar o token mobile
5901
- const checkAndSetMobileToken = () => {
5902
- if (window.keycloakConfig?.fromMobile && window.keycloakConfig?.token) {
5903
- console.log('[SecurityProvider] Token mobile detectado');
5904
- setIsMobileWebView(true);
5905
- setMobileToken(window.keycloakConfig.token);
5906
- setIsMobileDetectionComplete(true);
5907
- return true;
5908
- }
5909
- return false;
5910
- };
5911
- // 1. Verificação imediata
5912
- if (checkAndSetMobileToken()) {
5913
- console.log('[SecurityProvider] Token encontrado na verificação inicial');
5935
+ // Validar e decodificar o token mobile
5936
+ const decoded = decodeAccessToken(keycloakConfig.token);
5937
+ if (!decoded) {
5938
+ console.error("[SecurityProvider] Token mobile inválido - ignorando");
5939
+ return;
5914
5940
  }
5915
- else {
5916
- // 2. Polling até encontrar o token ou atingir o limite
5917
- pollingInterval = setInterval(() => {
5918
- pollingAttempts++;
5919
- if (checkAndSetMobileToken()) {
5920
- console.log(`[SecurityProvider] Token encontrado após ${pollingAttempts} tentativas (${pollingAttempts * POLLING_INTERVAL_MS}ms)`);
5921
- if (pollingInterval) {
5922
- clearInterval(pollingInterval);
5923
- pollingInterval = null;
5924
- }
5925
- }
5926
- else if (pollingAttempts >= MAX_POLLING_ATTEMPTS) {
5927
- console.log('[SecurityProvider] Polling encerrado - token não encontrado após 3 segundos');
5928
- setIsMobileDetectionComplete(true);
5929
- if (pollingInterval) {
5930
- clearInterval(pollingInterval);
5931
- pollingInterval = null;
5932
- }
5933
- }
5934
- }, POLLING_INTERVAL_MS);
5941
+ // Verificar se o token não está expirado
5942
+ const now = Math.floor(Date.now() / 1000);
5943
+ if (decoded.exp && decoded.exp < now) {
5944
+ console.error("[SecurityProvider] Token mobile expirado - ignorando");
5945
+ return;
5935
5946
  }
5936
- // 3. Escutar evento de injeção (fallback)
5937
- const handleInjection = (event) => {
5938
- console.log('[SecurityProvider] Token recebido via evento keycloakConfigInjected');
5939
- setIsMobileWebView(true);
5940
- setMobileToken(event.detail.token);
5941
- setIsMobileDetectionComplete(true);
5942
- // Parar polling se ainda estiver rodando
5943
- if (pollingInterval) {
5944
- clearInterval(pollingInterval);
5945
- pollingInterval = null;
5946
- }
5947
- };
5948
- // 4. Escutar evento de refresh do token
5949
- const handleTokenRefresh = (event) => {
5950
- console.log('[SecurityProvider] Token atualizado via evento keycloakTokenRefreshed');
5951
- setMobileToken(event.detail.token);
5952
- };
5953
- window.addEventListener("keycloakConfigInjected", handleInjection);
5954
- window.addEventListener("keycloakTokenRefreshed", handleTokenRefresh);
5955
- return () => {
5956
- if (pollingInterval) {
5957
- clearInterval(pollingInterval);
5958
- }
5959
- window.removeEventListener("keycloakConfigInjected", handleInjection);
5960
- window.removeEventListener("keycloakTokenRefreshed", handleTokenRefresh);
5961
- };
5947
+ console.log("[SecurityProvider] Token mobile detectado e válido");
5948
+ // Ativar modo mobile
5949
+ setMobileToken(keycloakConfig.token);
5950
+ setIsMobileAuth(true);
5951
+ // Sincronizar com window.accessToken e currentTokenRef
5952
+ window.accessToken = keycloakConfig.token;
5953
+ currentTokenRef.current = keycloakConfig.token;
5954
+ // Marcar que já tentamos SSO (para pular o check)
5955
+ hasTriedSignInSilent.current = true;
5956
+ // Resolver promise de token imediatamente
5957
+ if (tokenReadyResolverRef.current) {
5958
+ tokenReadyResolverRef.current();
5959
+ }
5960
+ setIsTokenReady(true);
5962
5961
  }, []);
5963
- // --- 1. Silent Check Inicial (Check SSO) ---
5962
+ // --- 1. Silent Check Inicial (Check SSO) - Pulado se mobile ---
5964
5963
  React2.useEffect(() => {
5965
- // Não tentar silent login no mobile WebView
5966
- if (isMobileWebView)
5964
+ // Pular SSO check se estamos em modo mobile
5965
+ if (isMobileAuth) {
5966
+ console.debug("[SecurityProvider] Modo mobile - pulando SSO check");
5967
5967
  return;
5968
+ }
5968
5969
  if (!isAuthenticated &&
5969
5970
  !isLoading &&
5970
5971
  !activeNavigator &&
@@ -5975,10 +5976,8 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
5975
5976
  });
5976
5977
  }
5977
5978
  // eslint-disable-next-line react-hooks/exhaustive-deps
5978
- }, [isAuthenticated, isLoading, activeNavigator, isMobileWebView]);
5979
+ }, [isAuthenticated, isLoading, activeNavigator, isMobileAuth]);
5979
5980
  // --- 2. Configuração de API e Services ---
5980
- // Ref para armazenar o token atual (acessível no middleware)
5981
- const currentTokenRef = React2.useRef(undefined);
5982
5981
  const apiClient = React2.useMemo(() => {
5983
5982
  const client = createAcademeApiClient(apiBaseUrl);
5984
5983
  // Inicializa a Promise de token ready
@@ -6008,11 +6007,18 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6008
6007
  const services = React2.useMemo(() => {
6009
6008
  return createAcademeServices(apiClient);
6010
6009
  }, [apiClient]);
6010
+ // Token efetivo: prioriza mobile, depois OIDC
6011
+ const effectiveToken = mobileToken || accessToken;
6011
6012
  const decodedAccessToken = React2.useMemo(() => {
6012
- return accessToken ? decodeAccessToken(accessToken) : null;
6013
- }, [accessToken]);
6013
+ return effectiveToken ? decodeAccessToken(effectiveToken) : null;
6014
+ }, [effectiveToken]);
6014
6015
  // Atualização do Token e resolução da Promise
6016
+ // Não sobrescreve se estamos em modo mobile
6015
6017
  React2.useEffect(() => {
6018
+ // Se estamos em modo mobile, o token já foi configurado
6019
+ if (isMobileAuth) {
6020
+ return;
6021
+ }
6016
6022
  currentTokenRef.current = accessToken;
6017
6023
  if (typeof window !== "undefined") {
6018
6024
  window.accessToken = accessToken;
@@ -6031,16 +6037,26 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6031
6037
  setIsTokenReady(true);
6032
6038
  }
6033
6039
  }
6034
- }, [accessToken, isLoading, isAuthenticated]);
6040
+ }, [accessToken, isLoading, isAuthenticated, isMobileAuth]);
6035
6041
  // --- 3. Helpers de Usuário e Roles ---
6036
6042
  const getKeycloakUser = React2.useCallback(() => {
6043
+ // Se estamos em modo mobile, extrair do token decodificado
6044
+ if (isMobileAuth && decodedAccessToken) {
6045
+ return {
6046
+ email: decodedAccessToken.email || "",
6047
+ name: decodedAccessToken.given_name || "",
6048
+ lastName: decodedAccessToken.family_name || "",
6049
+ };
6050
+ }
6051
+ // Modo OIDC normal
6037
6052
  const profile = userProfile;
6038
6053
  return {
6039
6054
  email: profile?.email || "",
6040
6055
  name: profile?.given_name || "",
6041
6056
  lastName: profile?.family_name || "",
6057
+ avatar_url: decodedAccessToken?.avatar_url
6042
6058
  };
6043
- }, [userProfile]);
6059
+ }, [userProfile, isMobileAuth, decodedAccessToken]);
6044
6060
  const hasRealmRole = React2.useCallback((role) => {
6045
6061
  return decodedAccessToken?.realm_access?.roles?.includes(role) ?? false;
6046
6062
  }, [decodedAccessToken]);
@@ -6055,17 +6071,20 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6055
6071
  }
6056
6072
  return Object.values(decodedAccessToken.resource_access).some((resource) => resource.roles?.includes(role));
6057
6073
  }, [decodedAccessToken]);
6074
+ // Autenticação efetiva: mobile ou OIDC
6075
+ const effectiveIsAuthenticated = isMobileAuth || isAuthenticated;
6076
+ const effectiveUserSub = isMobileAuth ? decodedAccessToken?.sub : userProfileSub;
6058
6077
  // --- 4. Fetch de Dados do Usuário (Backend) ---
6059
6078
  React2.useEffect(() => {
6060
6079
  let isMounted = true;
6061
6080
  const fetchUserData = async () => {
6062
- if (isAuthenticated) {
6081
+ if (effectiveIsAuthenticated) {
6063
6082
  if (skipApiUserFetch) {
6064
6083
  if (isMounted) {
6065
6084
  const academeUser = {
6066
6085
  keycloakUser: getKeycloakUser(),
6067
6086
  };
6068
- setCurrentUser({ ...academeUser, id: userProfileSub || "" });
6087
+ setCurrentUser({ ...academeUser, id: effectiveUserSub || "" });
6069
6088
  }
6070
6089
  return;
6071
6090
  }
@@ -6083,7 +6102,7 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6083
6102
  console.error("[SecurityProvider] Error fetching user data:", error);
6084
6103
  }
6085
6104
  }
6086
- else if (!isAuthenticated && !isLoading) {
6105
+ else if (!effectiveIsAuthenticated && !isLoading && !isMobileAuth) {
6087
6106
  if (isMounted) {
6088
6107
  setCurrentUser(null);
6089
6108
  }
@@ -6094,16 +6113,19 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6094
6113
  isMounted = false;
6095
6114
  };
6096
6115
  }, [
6097
- isAuthenticated,
6116
+ effectiveIsAuthenticated,
6098
6117
  isLoading,
6099
6118
  getKeycloakUser,
6100
6119
  services,
6101
6120
  skipApiUserFetch,
6102
- userProfileSub,
6121
+ effectiveUserSub,
6122
+ isMobileAuth,
6103
6123
  ]);
6104
6124
  const refreshUserData = React2.useCallback(async () => {
6105
- if (isAuthenticated) {
6125
+ setIsRefreshing(true);
6126
+ if (effectiveIsAuthenticated) {
6106
6127
  if (skipApiUserFetch) {
6128
+ await auth.signinSilent();
6107
6129
  const academeUser = {
6108
6130
  keycloakUser: getKeycloakUser(),
6109
6131
  };
@@ -6124,10 +6146,12 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6124
6146
  console.error("[SecurityProvider] Error refreshing user data:", error);
6125
6147
  }
6126
6148
  }
6127
- }, [isAuthenticated, getKeycloakUser, services, skipApiUserFetch]);
6149
+ setIsRefreshing(false);
6150
+ }, [effectiveIsAuthenticated, getKeycloakUser, services, skipApiUserFetch]);
6128
6151
  // --- 5. Ações de Auth ---
6129
- const signOut = React2.useCallback(() => {
6130
- console.log("[KC LOGOUT!]");
6152
+ // SignOut para modo OIDC (web normal)
6153
+ const signOutOidc = React2.useCallback(() => {
6154
+ console.log("[KC LOGOUT - OIDC]");
6131
6155
  setCurrentUser(null);
6132
6156
  auth.removeUser();
6133
6157
  auth.signoutRedirect({
@@ -6135,6 +6159,32 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6135
6159
  });
6136
6160
  auth.clearStaleState();
6137
6161
  }, [auth]);
6162
+ // SignOut para modo mobile
6163
+ const signOutMobile = React2.useCallback(() => {
6164
+ console.log("[KC LOGOUT - Mobile]");
6165
+ setCurrentUser(null);
6166
+ setMobileToken(undefined);
6167
+ setIsMobileAuth(false);
6168
+ // Limpar tokens globais
6169
+ if (typeof window !== "undefined") {
6170
+ window.accessToken = undefined;
6171
+ window.keycloakConfig = undefined;
6172
+ }
6173
+ currentTokenRef.current = undefined;
6174
+ // Notificar o app mobile para fazer logout
6175
+ if (window.ReactNativeWebView) {
6176
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: "LOGOUT_REQUESTED" }));
6177
+ }
6178
+ }, []);
6179
+ // SignOut unificado: escolhe automaticamente baseado no modo
6180
+ const signOut = React2.useCallback(() => {
6181
+ if (isMobileAuth) {
6182
+ signOutMobile();
6183
+ }
6184
+ else {
6185
+ signOutOidc();
6186
+ }
6187
+ }, [isMobileAuth, signOutMobile, signOutOidc]);
6138
6188
  const hasSchool = React2.useCallback((schoolId) => {
6139
6189
  if (hasRealmRole(exports.GLOBAL_ROLES.ADMIN_ACADEME)) {
6140
6190
  return true;
@@ -6142,44 +6192,45 @@ const SecurityProvider = ({ apiBaseUrl = "https://stg-api.academe.com.br", skipA
6142
6192
  return (currentUser?.institutionRegistrations?.some((registration) => registration.institutionId === schoolId) ?? false);
6143
6193
  }, [hasRealmRole, currentUser?.institutionRegistrations]);
6144
6194
  const goToLogin = React2.useCallback(() => {
6195
+ // Em modo mobile, notificar o app para fazer login
6196
+ if (isMobileAuth && window.ReactNativeWebView) {
6197
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: "LOGIN_REQUESTED" }));
6198
+ return Promise.resolve();
6199
+ }
6145
6200
  return auth.signinRedirect();
6146
6201
  // eslint-disable-next-line react-hooks/exhaustive-deps
6147
- }, []);
6202
+ }, [isMobileAuth]);
6148
6203
  // Memoizar o value do context para evitar re-renders desnecessários
6149
- // isInitialized só é true quando:
6150
- // 1. OIDC terminou de carregar (!isLoading)
6151
- // 2. E a detecção mobile completou (isMobileDetectionComplete)
6152
6204
  const contextValue = React2.useMemo(() => ({
6153
- isInitialized: !isLoading && isMobileDetectionComplete,
6205
+ isInitialized: isMobileAuth ? true : (!isLoading || isRefreshing),
6154
6206
  isTokenReady,
6207
+ isMobileAuth,
6155
6208
  user: currentUser,
6156
6209
  refreshUserData,
6157
6210
  signOut,
6158
- isAuthenticated: () => isMobileWebView ? !!mobileToken : isAuthenticated,
6211
+ isAuthenticated: () => effectiveIsAuthenticated,
6159
6212
  hasSchool,
6160
6213
  goToLogin,
6161
6214
  hasRealmRole,
6162
6215
  hasClientRole,
6163
- accessToken,
6216
+ accessToken: effectiveToken,
6164
6217
  apiClient,
6165
6218
  services,
6166
6219
  }), [
6220
+ isMobileAuth,
6167
6221
  isLoading,
6168
- isMobileDetectionComplete,
6169
6222
  isTokenReady,
6170
6223
  currentUser,
6171
6224
  refreshUserData,
6172
6225
  signOut,
6173
- isAuthenticated,
6226
+ effectiveIsAuthenticated,
6174
6227
  hasSchool,
6175
6228
  goToLogin,
6176
6229
  hasRealmRole,
6177
6230
  hasClientRole,
6178
- accessToken,
6231
+ effectiveToken,
6179
6232
  apiClient,
6180
6233
  services,
6181
- isMobileWebView,
6182
- mobileToken,
6183
6234
  ]);
6184
6235
  return (jsxRuntime.jsx(SecurityContext.Provider, { value: contextValue, children: children }));
6185
6236
  };