222 lines
5.9 KiB
TypeScript
222 lines
5.9 KiB
TypeScript
import { authClient } from '@/lib/api-client';
|
|
import { type PropsWithChildren, createContext, useContext, useMemo } from 'react';
|
|
|
|
export type User = typeof authClient.$Infer.Session.user;
|
|
export type Session = typeof authClient.$Infer.Session.session;
|
|
|
|
type SignInParams = {
|
|
email: string;
|
|
password: string;
|
|
};
|
|
|
|
type SignUpParams = {
|
|
email: string;
|
|
password: string;
|
|
fullName: string;
|
|
};
|
|
|
|
type UpdateProfileParams = {
|
|
fullName: string;
|
|
};
|
|
|
|
type UpdatePasswordParams = {
|
|
currentPassword?: string;
|
|
password: string;
|
|
};
|
|
|
|
export type AuthContextValue = {
|
|
user: User | null;
|
|
session: Session | null;
|
|
loading: boolean;
|
|
isAuthenticated: boolean;
|
|
displayName: string;
|
|
initials: string;
|
|
avatarUrl: string | null;
|
|
signInWithPassword: (params: SignInParams) => Promise<void>;
|
|
signUp: (params: SignUpParams) => Promise<{ requiresEmailConfirmation: boolean }>;
|
|
updateProfile: (params: UpdateProfileParams) => Promise<void>;
|
|
updatePassword: (params: UpdatePasswordParams) => Promise<void>;
|
|
signOut: () => Promise<void>;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
|
|
/**
|
|
* Standard MD5 implementation for Gravatar support
|
|
* (Self-contained to avoid dependency issues)
|
|
*/
|
|
function md5(string: string) {
|
|
const k = Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32));
|
|
let a = 0x67452301;
|
|
let b = 0xefcdab89;
|
|
let c = 0x98badcfe;
|
|
let d = 0x10325476;
|
|
const s = [
|
|
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
|
|
14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21,
|
|
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
|
];
|
|
|
|
const str = unescape(encodeURIComponent(string));
|
|
const msg = new Uint8Array(str.length);
|
|
for (let i = 0; i < str.length; i++) msg[i] = str.charCodeAt(i);
|
|
|
|
const len = msg.length;
|
|
const wordLen = ((len + 8) >> 6) + 1;
|
|
const words = new Uint32Array(wordLen * 16);
|
|
for (let i = 0; i < len; i++) words[i >> 2] |= msg[i] << ((i % 4) * 8);
|
|
words[len >> 2] |= 0x80 << ((len % 4) * 8);
|
|
words[wordLen * 16 - 2] = len * 8;
|
|
|
|
for (let i = 0; i < words.length; i += 16) {
|
|
const [aa, bb, cc, dd] = [a, b, c, d];
|
|
for (let j = 0; j < 64; j++) {
|
|
let f: number;
|
|
let g: number;
|
|
if (j < 16) {
|
|
f = (b & c) | (~b & d);
|
|
g = j;
|
|
} else if (j < 32) {
|
|
f = (d & b) | (~d & c);
|
|
g = (5 * j + 1) % 16;
|
|
} else if (j < 48) {
|
|
f = b ^ c ^ d;
|
|
g = (3 * j + 5) % 16;
|
|
} else {
|
|
f = c ^ (b | ~d);
|
|
g = (7 * j) % 16;
|
|
}
|
|
const temp = d;
|
|
d = c;
|
|
c = b;
|
|
b =
|
|
(b +
|
|
(((a + f + k[j] + words[i + g]) << s[j]) |
|
|
((a + f + k[j] + words[i + g]) >>> (32 - s[j])))) |
|
|
0;
|
|
a = temp;
|
|
}
|
|
a = (a + aa) | 0;
|
|
b = (b + bb) | 0;
|
|
c = (c + cc) | 0;
|
|
d = (d + dd) | 0;
|
|
}
|
|
|
|
return [a, b, c, d]
|
|
.map((v) =>
|
|
Array.from({ length: 4 }, (_, i) =>
|
|
((v >> (i * 8)) & 0xff).toString(16).padStart(2, '0')
|
|
).join('')
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
function getGravatarUrl(email: string): string {
|
|
const hash = md5(email.trim().toLowerCase());
|
|
return `https://www.gravatar.com/avatar/${hash}?d=identicon`;
|
|
}
|
|
|
|
function getDisplayName(user: User | null): string {
|
|
if (!user) return 'Usuario';
|
|
if (user.name?.trim()) return user.name.trim();
|
|
return user.email ?? 'Usuario';
|
|
}
|
|
|
|
function getInitials(name: string): string {
|
|
const words = name.trim().split(/\s+/).slice(0, 2);
|
|
const initials = words.map((word) => word[0]?.toUpperCase() ?? '').join('');
|
|
return initials || 'U';
|
|
}
|
|
|
|
function getAvatarUrl(user: User | null): string | null {
|
|
if (!user) return null;
|
|
return user.image || getGravatarUrl(user.email);
|
|
}
|
|
|
|
export function AuthProvider({ children }: PropsWithChildren) {
|
|
const { data, isPending } = authClient.useSession();
|
|
|
|
const user = data?.user ?? null;
|
|
const session = data?.session ?? null;
|
|
|
|
const value = useMemo<AuthContextValue>(() => {
|
|
const displayName = getDisplayName(user);
|
|
const initials = getInitials(displayName);
|
|
const avatarUrl = getAvatarUrl(user);
|
|
|
|
return {
|
|
user,
|
|
session,
|
|
loading: isPending,
|
|
isAuthenticated: Boolean(user),
|
|
displayName,
|
|
initials,
|
|
avatarUrl,
|
|
signInWithPassword: async ({ email, password }) => {
|
|
const { error } = await authClient.signIn.email({
|
|
email,
|
|
password,
|
|
});
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
},
|
|
signUp: async ({ email, password, fullName }) => {
|
|
const { data: signUpData, error } = await authClient.signUp.email({
|
|
email,
|
|
password,
|
|
name: fullName,
|
|
});
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
requiresEmailConfirmation: !signUpData || !('session' in signUpData),
|
|
};
|
|
},
|
|
updateProfile: async ({ fullName }) => {
|
|
const { error } = await authClient.updateUser({
|
|
name: fullName,
|
|
});
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
},
|
|
updatePassword: async ({ currentPassword, password }) => {
|
|
// Better Auth uses changePassword for authenticated users
|
|
const { error } = await authClient.changePassword({
|
|
newPassword: password,
|
|
currentPassword: currentPassword || '',
|
|
revokeOtherSessions: true,
|
|
});
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
},
|
|
signOut: async () => {
|
|
const { error } = await authClient.signOut();
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
}, [isPending, session, user]);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
|
|
if (!context) {
|
|
throw new Error('useAuth must be used within AuthProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|