codalware-auth
Version:
Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.
256 lines (227 loc) • 7.72 kB
text/typescript
/**
* React Hooks for Auth Provider Abstraction
*
* This module provides React hooks that work with both NextAuth and better-auth.
* The hooks dynamically detect which provider is configured and use the appropriate implementation.
*/
"use client";
import { useEffect, useState } from 'react';
import type { AuthSession } from './types';
import * as nextAuthReact from 'next-auth/react';
import * as betterAuthReact from 'better-auth/react';
// Lightweight typed shapes for the provider modules to avoid explicit `any` usage
type NextAuthReactModule = {
getSession?: () => Promise<unknown>;
useSession?: () => {
data?: {
user?: {
id?: string;
email?: string;
name?: string;
image?: string;
role?: string;
tenantId?: string;
status?: string;
} | null;
expires?: string;
} | null;
status?: 'loading' | 'authenticated' | 'unauthenticated';
update?: (d?: unknown) => Promise<unknown>;
isPending?: boolean;
};
signIn?: (...args: unknown[]) => Promise<unknown> | unknown;
signOut?: (...args: unknown[]) => Promise<unknown> | unknown;
};
type BetterAuthReactModule = {
getSession?: () => Promise<unknown>;
useSession?: () => {
data?: {
user?: {
id?: string;
email?: string;
name?: string;
image?: string;
role?: string;
tenantId?: string;
status?: string;
} | null;
session?: { expiresAt?: string } | null;
} | null;
isPending?: boolean;
};
signIn?: {
email: (opts: { email: string; password: string; callbackURL?: string }) => Promise<{ data?: unknown; error?: { message?: string } }>;
social: (opts: { provider: string; callbackURL?: string }) => Promise<{ data?: unknown; error?: { message?: string } }>;
};
signOut?: (opts?: { fetchOptions?: { onSuccess?: () => void } }) => Promise<unknown>;
};
const nextAuth = (nextAuthReact as unknown) as NextAuthReactModule;
const betterAuth = (betterAuthReact as unknown) as BetterAuthReactModule;
// Unified session hook return type
export interface UseSessionResult {
data: AuthSession | null;
status: 'loading' | 'authenticated' | 'unauthenticated';
update?: (data?: unknown) => Promise<AuthSession | null>;
}
// Unified signIn function type
export type SignInOptions = {
email?: string;
password?: string;
redirect?: boolean;
callbackUrl?: string;
[key: string]: unknown;
};
export type SignInResult = {
ok?: boolean;
error?: string;
url?: string;
} | undefined;
// Check which provider is configured
const isUsingBetterAuth = () => {
if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_AUTH_PROVIDER === 'better-auth') {
return true;
}
return false;
};
/**
* Universal useSession hook
* Automatically uses the correct provider based on NEXT_PUBLIC_AUTH_PROVIDER
*/
export function useSession(): UseSessionResult {
const [state, setState] = useState<UseSessionResult>({ data: null, status: 'loading' });
useEffect(() => {
let mounted = true;
type SessionLike = {
user?: { id?: string; email?: string; name?: string; image?: string; role?: string; tenantId?: string; status?: string };
session?: { expiresAt?: string };
expires?: string;
} | null | undefined;
async function resolveSession() {
try {
if (isUsingBetterAuth() && betterAuth && betterAuth.getSession) {
const s = await betterAuth.getSession();
if (!mounted) return;
// Map better-auth session shape to AuthSession
// We do a best-effort mapping; missing fields defaulted
const data = s as SessionLike;
setState({
data: data
? {
user: {
id: data.user?.id ?? '',
email: data.user?.email ?? '',
name: data.user?.name,
image: data.user?.image,
role: data.user?.role ?? 'USER',
tenantId: data.user?.tenantId,
status: data.user?.status ?? 'APPROVED',
},
expires: data.session?.expiresAt ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
}
: null,
status: data ? 'authenticated' : 'unauthenticated',
});
return;
}
if (nextAuth && nextAuth.getSession) {
const s = await nextAuth.getSession();
if (!mounted) return;
const data = s as SessionLike;
setState({
data: data
? {
user: {
id: data.user?.id ?? '',
email: data.user?.email ?? '',
name: data.user?.name,
image: data.user?.image,
role: data.user?.role ?? 'USER',
tenantId: data.user?.tenantId,
status: data.user?.status ?? 'APPROVED',
},
expires: data.expires ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
}
: null,
status: data ? 'authenticated' : 'unauthenticated',
});
return;
}
setState({ data: null, status: 'unauthenticated' });
} catch {
if (!mounted) return;
setState({ data: null, status: 'unauthenticated' });
}
}
resolveSession();
return () => {
mounted = false;
};
}, []);
return state;
}
/**
* Get signIn function for the configured provider
*/
export function useSignIn() {
if (isUsingBetterAuth()) {
const betterSignIn = betterAuth?.signIn;
if (betterSignIn) {
return async (provider: string, options: SignInOptions = {}): Promise<SignInResult> => {
if (provider === 'credentials') {
const result = await betterSignIn.email({
email: options.email!,
password: options.password!,
callbackURL: options.callbackUrl,
});
return {
ok: !!result.data,
error: result.error?.message,
};
} else {
// Social provider
const result = await betterSignIn.social({
provider,
callbackURL: options.callbackUrl,
});
return {
ok: !!result.data,
error: result.error?.message,
};
}
};
}
}
// Fall back to NextAuth
if (nextAuth && nextAuth.signIn) {
return nextAuth.signIn;
}
// Return no-op if no provider available
return async () => ({ ok: false, error: 'No auth provider configured' });
}
/**
* Get signOut function for the configured provider
*/
export function useSignOut() {
if (isUsingBetterAuth()) {
const betterSignOutLocal = betterAuth?.signOut;
if (betterSignOutLocal) {
return async (options: { redirect?: boolean; callbackUrl?: string } = {}) => {
await betterSignOutLocal({
fetchOptions: {
onSuccess: () => {
if (options.redirect !== false) {
window.location.href = options.callbackUrl || '/';
}
},
},
});
};
}
}
// Fall back to NextAuth
if (nextAuth && nextAuth.signOut) {
return nextAuth.signOut;
}
// Return no-op if no provider available
return async () => {};
}