Files
playzer/apps/frontend/src/lib/auth.tsx

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;
}