feat: integrate Better Auth and enhance authentication flow

- Added Better Auth integration to the web application, allowing for dual login support with both custom and Better Auth systems.
- Updated authentication client configuration to dynamically set the base URL based on the environment.
- Enhanced chat components to utilize user authentication status, improving user experience and security.
- Refactored various components to support Better Auth, including error handling and user identity management.
- Improved notification handling and user feedback mechanisms during authentication processes.
This commit is contained in:
2025-11-06 09:35:36 -03:00
parent 33f305220b
commit 06f03b53e5
28 changed files with 4109 additions and 436 deletions

View File

@@ -1,266 +1,266 @@
/**
* Solicita permissão para notificações desktop
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!("Notification" in window)) {
console.warn("Este navegador não suporta notificações desktop");
return "denied";
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission !== "denied") {
return await Notification.requestPermission();
}
return Notification.permission;
}
/**
* Mostra uma notificação desktop
*/
export function showNotification(title: string, options?: NotificationOptions): Notification | null {
if (!("Notification" in window)) {
return null;
}
if (Notification.permission !== "granted") {
return null;
}
try {
return new Notification(title, {
icon: "/favicon.png",
badge: "/favicon.png",
...options,
});
} catch (error) {
console.error("Erro ao exibir notificação:", error);
return null;
}
}
/**
* Toca o som de notificação
*/
export function playNotificationSound() {
try {
const audio = new Audio("/sounds/notification.mp3");
audio.volume = 0.5;
audio.play().catch((err) => {
console.warn("Não foi possível reproduzir o som de notificação:", err);
});
} catch (error) {
console.error("Erro ao tocar som de notificação:", error);
}
}
/**
* Verifica se o usuário está na aba ativa
*/
export function isTabActive(): boolean {
return !document.hidden;
}
/**
* Registrar service worker para push notifications
*/
export async function registrarServiceWorker(): Promise<ServiceWorkerRegistration | null> {
if (!("serviceWorker" in navigator)) {
console.warn("Service Workers não são suportados neste navegador");
return null;
}
try {
// Verificar se já existe um Service Worker ativo antes de registrar
const existingRegistration = await navigator.serviceWorker.getRegistration("/");
if (existingRegistration?.active) {
return existingRegistration;
}
// Registrar com timeout para evitar travamentos
const registerPromise = navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
const timeoutPromise = new Promise<ServiceWorkerRegistration | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const registration = await Promise.race([registerPromise, timeoutPromise]);
if (registration) {
// Log apenas em desenvolvimento
if (import.meta.env.DEV) {
console.log("Service Worker registrado:", registration);
}
}
return registration;
} catch (error) {
// Ignorar erros silenciosamente para evitar spam no console
// especialmente erros relacionados a message channel
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
!errorMessage.includes("message channel") &&
!errorMessage.includes("registration") &&
import.meta.env.DEV
) {
console.error("Erro ao registrar Service Worker:", error);
}
}
return null;
}
}
/**
* Solicitar subscription de push notification
*/
export async function solicitarPushSubscription(): Promise<PushSubscription | null> {
try {
// Registrar service worker primeiro com timeout
const registrationPromise = registrarServiceWorker();
const timeoutPromise = new Promise<null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const registration = await Promise.race([registrationPromise, timeoutPromise]);
if (!registration) {
return null;
}
// Verificar se push está disponível
if (!("PushManager" in window)) {
return null;
}
// Solicitar permissão com timeout
const permissionPromise = requestNotificationPermission();
const permissionTimeoutPromise = new Promise<NotificationPermission>((resolve) =>
setTimeout(() => resolve("denied"), 3000)
);
const permission = await Promise.race([permissionPromise, permissionTimeoutPromise]);
if (permission !== "granted") {
return null;
}
// Obter subscription existente ou criar nova com timeout
const getSubscriptionPromise = registration.pushManager.getSubscription();
const getSubscriptionTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
let subscription = await Promise.race([getSubscriptionPromise, getSubscriptionTimeoutPromise]);
if (!subscription) {
// VAPID public key deve vir do backend ou config
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY || "";
if (!vapidPublicKey) {
// Não logar warning para evitar spam no console
return null;
}
// Converter chave para formato Uint8Array
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
// Subscribe com timeout
const subscribePromise = registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
});
const subscribeTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 5000)
);
subscription = await Promise.race([subscribePromise, subscribeTimeoutPromise]);
}
return subscription;
} catch (error) {
// Ignorar erros relacionados a message channel ou service worker
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
errorMessage.includes("message channel") ||
errorMessage.includes("service worker") ||
errorMessage.includes("registration")
) {
return null;
}
}
return null;
}
}
/**
* Converter chave VAPID de base64 URL-safe para Uint8Array
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
/**
* Converter PushSubscription para formato serializável
*/
export function subscriptionToJSON(subscription: PushSubscription): {
endpoint: string;
keys: { p256dh: string; auth: string };
} {
const key = subscription.getKey("p256dh");
const auth = subscription.getKey("auth");
if (!key || !auth) {
throw new Error("Chaves de subscription não encontradas");
}
return {
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(key),
auth: arrayBufferToBase64(auth),
},
};
}
/**
* Converter ArrayBuffer para base64
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
/**
* Remover subscription de push notification
*/
export async function removerPushSubscription(): Promise<boolean> {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
return true;
}
return false;
}
/**
* Solicita permissão para notificações desktop
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!("Notification" in window)) {
console.warn("Este navegador não suporta notificações desktop");
return "denied";
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission !== "denied") {
return await Notification.requestPermission();
}
return Notification.permission;
}
/**
* Mostra uma notificação desktop
*/
export function showNotification(title: string, options?: NotificationOptions): Notification | null {
if (!("Notification" in window)) {
return null;
}
if (Notification.permission !== "granted") {
return null;
}
try {
return new Notification(title, {
icon: "/favicon.png",
badge: "/favicon.png",
...options,
});
} catch (error) {
console.error("Erro ao exibir notificação:", error);
return null;
}
}
/**
* Toca o som de notificação
*/
export function playNotificationSound() {
try {
const audio = new Audio("/sounds/notification.mp3");
audio.volume = 0.5;
audio.play().catch((err) => {
console.warn("Não foi possível reproduzir o som de notificação:", err);
});
} catch (error) {
console.error("Erro ao tocar som de notificação:", error);
}
}
/**
* Verifica se o usuário está na aba ativa
*/
export function isTabActive(): boolean {
return !document.hidden;
}
/**
* Registrar service worker para push notifications
*/
export async function registrarServiceWorker(): Promise<ServiceWorkerRegistration | null> {
if (!("serviceWorker" in navigator)) {
console.warn("Service Workers não são suportados neste navegador");
return null;
}
try {
// Verificar se já existe um Service Worker ativo antes de registrar
const existingRegistration = await navigator.serviceWorker.getRegistration("/");
if (existingRegistration?.active) {
return existingRegistration;
}
// Registrar com timeout para evitar travamentos
const registerPromise = navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
const timeoutPromise = new Promise<ServiceWorkerRegistration | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const registration = await Promise.race([registerPromise, timeoutPromise]);
if (registration) {
// Log apenas em desenvolvimento
if (import.meta.env.DEV) {
console.log("Service Worker registrado:", registration);
}
}
return registration;
} catch (error) {
// Ignorar erros silenciosamente para evitar spam no console
// especialmente erros relacionados a message channel
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
!errorMessage.includes("message channel") &&
!errorMessage.includes("registration") &&
import.meta.env.DEV
) {
console.error("Erro ao registrar Service Worker:", error);
}
}
return null;
}
}
/**
* Solicitar subscription de push notification
*/
export async function solicitarPushSubscription(): Promise<PushSubscription | null> {
try {
// Registrar service worker primeiro com timeout
const registrationPromise = registrarServiceWorker();
const timeoutPromise = new Promise<null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const registration = await Promise.race([registrationPromise, timeoutPromise]);
if (!registration) {
return null;
}
// Verificar se push está disponível
if (!("PushManager" in window)) {
return null;
}
// Solicitar permissão com timeout
const permissionPromise = requestNotificationPermission();
const permissionTimeoutPromise = new Promise<NotificationPermission>((resolve) =>
setTimeout(() => resolve("denied"), 3000)
);
const permission = await Promise.race([permissionPromise, permissionTimeoutPromise]);
if (permission !== "granted") {
return null;
}
// Obter subscription existente ou criar nova com timeout
const getSubscriptionPromise = registration.pushManager.getSubscription();
const getSubscriptionTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
let subscription = await Promise.race([getSubscriptionPromise, getSubscriptionTimeoutPromise]);
if (!subscription) {
// VAPID public key deve vir do backend ou config
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY || "";
if (!vapidPublicKey) {
// Não logar warning para evitar spam no console
return null;
}
// Converter chave para formato Uint8Array
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
// Subscribe com timeout
const subscribePromise = registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
});
const subscribeTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 5000)
);
subscription = await Promise.race([subscribePromise, subscribeTimeoutPromise]);
}
return subscription;
} catch (error) {
// Ignorar erros relacionados a message channel ou service worker
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
errorMessage.includes("message channel") ||
errorMessage.includes("service worker") ||
errorMessage.includes("registration")
) {
return null;
}
}
return null;
}
}
/**
* Converter chave VAPID de base64 URL-safe para Uint8Array
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
/**
* Converter PushSubscription para formato serializável
*/
export function subscriptionToJSON(subscription: PushSubscription): {
endpoint: string;
keys: { p256dh: string; auth: string };
} {
const key = subscription.getKey("p256dh");
const auth = subscription.getKey("auth");
if (!key || !auth) {
throw new Error("Chaves de subscription não encontradas");
}
return {
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(key),
auth: arrayBufferToBase64(auth),
},
};
}
/**
* Converter ArrayBuffer para base64
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
/**
* Remover subscription de push notification
*/
export async function removerPushSubscription(): Promise<boolean> {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
return true;
}
return false;
}