feat: improve account security
Support TOTP 2fa, allow for changing password, add session management and account removal.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { BetterAuthClientOptions, InferSessionFromClient, InferUserFromClient, User } from 'better-auth/client';
|
||||
import { twoFactorClient } from 'better-auth/client/plugins';
|
||||
import { createAuthClient } from 'better-auth/vue';
|
||||
import { Err, Ok, type Result } from '~~/types/result';
|
||||
import { assert } from '~~/utils/assert';
|
||||
@@ -12,15 +13,27 @@ export enum AuthError {
|
||||
NetworkError = 'NETWORK_ERROR',
|
||||
}
|
||||
|
||||
type sessionData = {
|
||||
session: InferSessionFromClient<BetterAuthClientOptions> | null;
|
||||
user: InferUserFromClient<BetterAuthClientOptions> | null;
|
||||
export type AuthUser = InferUserFromClient<BetterAuthClientOptions> & {
|
||||
twoFactorEnabled?: boolean | null;
|
||||
};
|
||||
|
||||
type sessionData = {
|
||||
session: InferSessionFromClient<BetterAuthClientOptions> | null;
|
||||
user: AuthUser | null;
|
||||
};
|
||||
|
||||
// Signing in either finishes right away, or stops at the second factor when
|
||||
// the account has two-factor authentication turned on.
|
||||
export type SignInResult =
|
||||
| { twoFactorRequired: false; user: User; token: string }
|
||||
| { twoFactorRequired: true };
|
||||
|
||||
export const useAuth = () => {
|
||||
const authClient = createAuthClient({});
|
||||
const authClient = createAuthClient({
|
||||
plugins: [twoFactorClient()],
|
||||
});
|
||||
const session = useState<InferSessionFromClient<BetterAuthClientOptions> | null>('auth:session', () => null);
|
||||
const user = useState<InferUserFromClient<BetterAuthClientOptions> | null>('auth:user', () => null);
|
||||
const user = useState<AuthUser | null>('auth:user', () => null);
|
||||
const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false);
|
||||
|
||||
let sessionPromise: Promise<Result<sessionData, AuthError>> | null = null;
|
||||
@@ -36,7 +49,7 @@ export const useAuth = () => {
|
||||
sessionPromise = promise;
|
||||
let data: {
|
||||
session: InferSessionFromClient<BetterAuthClientOptions>;
|
||||
user: InferUserFromClient<BetterAuthClientOptions>;
|
||||
user: AuthUser;
|
||||
} | null = null;
|
||||
|
||||
try {
|
||||
@@ -45,7 +58,7 @@ export const useAuth = () => {
|
||||
(
|
||||
await useFetch<{
|
||||
session: InferSessionFromClient<BetterAuthClientOptions>;
|
||||
user: InferUserFromClient<BetterAuthClientOptions>;
|
||||
user: AuthUser;
|
||||
}>('/api/auth/get-session')
|
||||
).data.value ?? null;
|
||||
} else {
|
||||
@@ -75,7 +88,7 @@ export const useAuth = () => {
|
||||
const signIn = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<Result<{ user: User; token: string }, { error: AuthError, data?: any }>> => {
|
||||
): Promise<Result<SignInResult, { error: AuthError, data?: any }>> => {
|
||||
sessionFetching.value = true;
|
||||
|
||||
try {
|
||||
@@ -93,11 +106,16 @@ export const useAuth = () => {
|
||||
return Err({ error: AuthError.SignInFailed });
|
||||
}
|
||||
|
||||
// no session exists yet, verifyTwoFactor() finishes the sign in
|
||||
if ('twoFactorRedirect' in data && data.twoFactorRedirect) {
|
||||
return Ok({ twoFactorRequired: true });
|
||||
}
|
||||
|
||||
user.value = data.user;
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok({ user: data.user, token: data.token });
|
||||
return Ok({ twoFactorRequired: false, user: data.user, token: data.token });
|
||||
} catch (err) {
|
||||
console.error('Sign in error:', err);
|
||||
return Err({ error: AuthError.NetworkError });
|
||||
@@ -106,6 +124,39 @@ export const useAuth = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const verifyTwoFactor = async (
|
||||
code: string,
|
||||
options: { backupCode?: boolean; trustDevice?: boolean } = {}
|
||||
): Promise<Result<{ user: User; token: string }, { error: AuthError, data?: any }>> => {
|
||||
sessionFetching.value = true;
|
||||
|
||||
try {
|
||||
const body = { code, trustDevice: options.trustDevice ?? false };
|
||||
const { data, error } = options.backupCode
|
||||
? await authClient.twoFactor.verifyBackupCode(body)
|
||||
: await authClient.twoFactor.verifyTotp(body);
|
||||
|
||||
if (error) {
|
||||
return Err({ error: AuthError.SignInFailed, data: error });
|
||||
}
|
||||
|
||||
if (!data || !data.token) {
|
||||
return Err({ error: AuthError.SignInFailed });
|
||||
}
|
||||
|
||||
user.value = data.user as AuthUser;
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok({ user: data.user as User, token: data.token });
|
||||
} catch (err) {
|
||||
console.error('Two-factor verification error:', err);
|
||||
return Err({ error: AuthError.NetworkError });
|
||||
} finally {
|
||||
sessionFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -195,6 +246,7 @@ export const useAuth = () => {
|
||||
user,
|
||||
loggedIn: computed(() => !!session.value),
|
||||
signIn,
|
||||
verifyTwoFactor,
|
||||
signUp,
|
||||
signOut,
|
||||
updateProfile,
|
||||
|
||||
Reference in New Issue
Block a user