useAuth.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { computed, ref } from "vue";
  2. import { loginRequest, logoutRequest, refreshTokenRequest } from "../api/auth.js";
  3. import { setAccessToken, setRefreshCallback, setAuthErrorCallback } from "../api/client.js";
  4. import { resetChatSingleton } from "./useChat.js";
  5. import { resetConversasSingleton } from "./useConversas.js";
  6. import { resetUsersSingleton } from "./useUsers.js";
  7. const LOCAL_STORAGE_KEY = "auth.session";
  8. const SESSION_STORAGE_KEY = "auth.session.temp";
  9. function getStorage(kind) {
  10. if (typeof window === "undefined") return null;
  11. return kind === "local" ? window.localStorage : window.sessionStorage;
  12. }
  13. function loadStoredSession() {
  14. for (const kind of ["local", "session"]) {
  15. try {
  16. const storage = getStorage(kind);
  17. const raw = storage?.getItem(kind === "local" ? LOCAL_STORAGE_KEY : SESSION_STORAGE_KEY);
  18. if (!raw) continue;
  19. const parsed = JSON.parse(raw);
  20. if (parsed?.usuario) return parsed;
  21. } catch {}
  22. }
  23. return null;
  24. }
  25. function persistSession(sess) {
  26. try {
  27. getStorage("local")?.removeItem(LOCAL_STORAGE_KEY);
  28. getStorage("session")?.removeItem(SESSION_STORAGE_KEY);
  29. if (sess?.rememberMe) {
  30. getStorage("local")?.setItem(LOCAL_STORAGE_KEY, JSON.stringify(sess));
  31. return;
  32. }
  33. if (sess) {
  34. getStorage("session")?.setItem(SESSION_STORAGE_KEY, JSON.stringify(sess));
  35. }
  36. } catch {}
  37. }
  38. export const NIVEL_ADMIN = 3;
  39. const session = ref(loadStoredSession());
  40. const user = computed(() => session.value?.usuario ?? null);
  41. const isAuthenticated = computed(() => Boolean(user.value?.Id));
  42. const nivel = computed(() => Number(user.value?.Nivel ?? 0));
  43. const isAdmin = computed(() => nivel.value >= NIVEL_ADMIN);
  44. let _proactiveRefreshTimer = null;
  45. let _refreshInFlight = null;
  46. let _authEpoch = 0;
  47. function doRefresh() {
  48. if (_refreshInFlight) return _refreshInFlight;
  49. const epoch = _authEpoch;
  50. _refreshInFlight = (async () => {
  51. const rt = session.value?.refreshToken;
  52. if (!rt) throw new Error("no_refresh_token");
  53. const result = await refreshTokenRequest({ refreshToken: rt });
  54. // logout()/authErrorCallback podem ter rodado enquanto o refresh estava em
  55. // voo — não reaplicar sessão/token de um ciclo de auth que já terminou
  56. if (epoch !== _authEpoch) throw new Error("auth_epoch_stale");
  57. session.value = {
  58. ...session.value,
  59. accessToken: result.accessToken,
  60. refreshToken: result.refreshToken ?? session.value?.refreshToken ?? null
  61. };
  62. persistSession(session.value);
  63. setAccessToken(result.accessToken);
  64. scheduleProactiveRefresh(result.accessToken);
  65. return result.accessToken;
  66. })().finally(() => {
  67. _refreshInFlight = null;
  68. });
  69. return _refreshInFlight;
  70. }
  71. function scheduleProactiveRefresh(token) {
  72. clearTimeout(_proactiveRefreshTimer);
  73. if (!token) return;
  74. try {
  75. const payload = JSON.parse(atob(token.split(".")[1]));
  76. const delay = payload.exp * 1000 - Date.now() - 60_000;
  77. if (delay <= 0) return;
  78. _proactiveRefreshTimer = setTimeout(() => {
  79. doRefresh().catch(() => {
  80. });
  81. }, delay);
  82. } catch {
  83. }
  84. }
  85. function setupAuth() {
  86. const token = session.value?.accessToken ?? null;
  87. setAccessToken(token);
  88. if (!token) return;
  89. scheduleProactiveRefresh(token);
  90. setRefreshCallback(doRefresh);
  91. setAuthErrorCallback(() => {
  92. _authEpoch++;
  93. clearTimeout(_proactiveRefreshTimer);
  94. resetChatSingleton();
  95. resetConversasSingleton();
  96. resetUsersSingleton();
  97. session.value = null;
  98. persistSession(null);
  99. setAccessToken(null);
  100. setRefreshCallback(null);
  101. setAuthErrorCallback(null);
  102. window.location.href = "/login?redirect=" + encodeURIComponent(window.location.pathname + window.location.search);
  103. });
  104. }
  105. setupAuth();
  106. async function login({ login, senha, rememberMe = false }) {
  107. const payload = await loginRequest({ login, senha, rememberMe });
  108. session.value = {
  109. usuario: payload.usuario,
  110. accessToken: payload.accessToken ?? null,
  111. refreshToken: payload.refreshToken ?? null,
  112. rememberMe,
  113. savedAt: new Date().toISOString()
  114. };
  115. persistSession(session.value);
  116. setupAuth();
  117. return payload;
  118. }
  119. async function logout() {
  120. _authEpoch++;
  121. clearTimeout(_proactiveRefreshTimer);
  122. const rt = session.value?.refreshToken;
  123. try {
  124. await logoutRequest({ refreshToken: rt });
  125. } finally {
  126. resetChatSingleton();
  127. resetConversasSingleton();
  128. resetUsersSingleton();
  129. session.value = null;
  130. persistSession(null);
  131. setAccessToken(null);
  132. setRefreshCallback(null);
  133. setAuthErrorCallback(null);
  134. }
  135. }
  136. export function useAuth() {
  137. return {
  138. user,
  139. nivel,
  140. isAdmin,
  141. isAuthenticated,
  142. login,
  143. logout
  144. };
  145. }