mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: enhance login handling by introducing local hash derivation and updating session management
This commit is contained in:
+57
-11
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createAuthedFetch,
|
||||
deriveLoginHash,
|
||||
deriveLoginHashLocally,
|
||||
getProfile,
|
||||
loadSession,
|
||||
loginWithPassword,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
unlockVaultKey,
|
||||
} from '@/lib/api/auth';
|
||||
import { readInviteCodeFromUrl } from '@/lib/app-support';
|
||||
import type { AppPhase, Profile, SessionState, WebBootstrapResponse } from '@/lib/types';
|
||||
import type { AppPhase, Profile, SessionState, TokenSuccess, WebBootstrapResponse } from '@/lib/types';
|
||||
|
||||
export interface PendingTotp {
|
||||
email: string;
|
||||
@@ -38,6 +39,7 @@ export interface InitialAppBootstrapState {
|
||||
export interface CompletedLogin {
|
||||
session: SessionState;
|
||||
profile: Profile;
|
||||
profilePromise: Promise<Profile>;
|
||||
}
|
||||
|
||||
export type PasswordLoginResult =
|
||||
@@ -91,6 +93,42 @@ function readWindowBootstrap(): WebBootstrapResponse {
|
||||
return raw && typeof raw === 'object' ? raw : {};
|
||||
}
|
||||
|
||||
interface AccessTokenClaims {
|
||||
sub?: string;
|
||||
email?: string;
|
||||
name?: string | null;
|
||||
premium?: boolean;
|
||||
}
|
||||
|
||||
function decodeAccessTokenClaims(accessToken: string): AccessTokenClaims {
|
||||
try {
|
||||
const parts = accessToken.split('.');
|
||||
if (parts.length < 2) return {};
|
||||
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = payload.padEnd(Math.ceil(payload.length / 4) * 4, '=');
|
||||
return (JSON.parse(atob(padded)) as AccessTokenClaims) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function buildTransientProfile(token: TokenSuccess, email: string): Profile {
|
||||
const claims = decodeAccessTokenClaims(token.access_token);
|
||||
const normalizedEmail = String(claims.email || email || '').trim().toLowerCase();
|
||||
const accountKeys = token.accountKeys ?? token.AccountKeys ?? null;
|
||||
return {
|
||||
id: String(claims.sub || ''),
|
||||
email: normalizedEmail,
|
||||
name: String(claims.name || normalizedEmail || ''),
|
||||
key: String(token.Key || ''),
|
||||
privateKey: token.PrivateKey ?? null,
|
||||
role: 'user',
|
||||
premium: !!claims.premium,
|
||||
accountKeys,
|
||||
object: 'profile',
|
||||
};
|
||||
}
|
||||
|
||||
export function readInitialAppBootstrapState(): InitialAppBootstrapState {
|
||||
const boot = readWindowBootstrap();
|
||||
const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000);
|
||||
@@ -168,21 +206,29 @@ export async function bootstrapAppSession(): Promise<BootstrapAppResult> {
|
||||
}
|
||||
|
||||
export async function completeLogin(
|
||||
tokenAccess: string,
|
||||
tokenRefresh: string,
|
||||
token: TokenSuccess,
|
||||
email: string,
|
||||
masterKey: Uint8Array
|
||||
): Promise<CompletedLogin> {
|
||||
const baseSession: SessionState = { accessToken: tokenAccess, refreshToken: tokenRefresh, email };
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const baseSession: SessionState = {
|
||||
accessToken: token.access_token,
|
||||
refreshToken: token.refresh_token,
|
||||
email: normalizedEmail,
|
||||
};
|
||||
const tempFetch = createAuthedFetch(
|
||||
() => baseSession,
|
||||
() => {}
|
||||
);
|
||||
const profile = await getProfile(tempFetch);
|
||||
const profile = buildTransientProfile(token, normalizedEmail);
|
||||
if (!profile.key) {
|
||||
throw new Error('Missing profile key');
|
||||
}
|
||||
const keys = await unlockVaultKey(profile.key, masterKey);
|
||||
return {
|
||||
session: { ...baseSession, ...keys },
|
||||
profile,
|
||||
profilePromise: getProfile(tempFetch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,13 +238,13 @@ export async function performPasswordLogin(
|
||||
fallbackIterations: number
|
||||
): Promise<PasswordLoginResult> {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const derived = await deriveLoginHash(normalizedEmail, password, fallbackIterations);
|
||||
const derived = await deriveLoginHashLocally(normalizedEmail, password, fallbackIterations);
|
||||
const token = await loginWithPassword(normalizedEmail, derived.hash, { useRememberToken: true });
|
||||
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return {
|
||||
kind: 'success',
|
||||
login: await completeLogin(token.access_token, token.refresh_token, normalizedEmail, derived.masterKey),
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,7 +276,7 @@ export async function performTotpLogin(
|
||||
rememberDevice,
|
||||
});
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return completeLogin(token.access_token, token.refresh_token, pendingTotp.email, pendingTotp.masterKey);
|
||||
return completeLogin(token, pendingTotp.email, pendingTotp.masterKey);
|
||||
}
|
||||
const tokenError = token as { error_description?: string; error?: string };
|
||||
throw new Error(tokenError.error_description || tokenError.error || 'TOTP verify failed');
|
||||
@@ -243,13 +289,13 @@ export async function performRecoverTwoFactorLogin(
|
||||
fallbackIterations: number
|
||||
): Promise<RecoverTwoFactorResult> {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const derived = await deriveLoginHash(normalizedEmail, password, fallbackIterations);
|
||||
const derived = await deriveLoginHashLocally(normalizedEmail, password, fallbackIterations);
|
||||
const recovered = await recoverTwoFactor(normalizedEmail, derived.hash, recoveryCode.trim());
|
||||
const token = await loginWithPassword(normalizedEmail, derived.hash, { useRememberToken: false });
|
||||
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return {
|
||||
login: await completeLogin(token.access_token, token.refresh_token, normalizedEmail, derived.masterKey),
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey),
|
||||
newRecoveryCode: recovered.newRecoveryCode || null,
|
||||
};
|
||||
}
|
||||
@@ -282,7 +328,7 @@ export async function performUnlock(
|
||||
password: string,
|
||||
fallbackIterations: number
|
||||
): Promise<SessionState> {
|
||||
const derived = await deriveLoginHash(profile.email || session.email, password, fallbackIterations);
|
||||
const derived = await deriveLoginHashLocally(profile.email || session.email, password, fallbackIterations);
|
||||
const keys = await unlockVaultKey(profile.key, derived.masterKey);
|
||||
const refreshedSession = await maybeRefreshSession(session);
|
||||
if (!refreshedSession) {
|
||||
|
||||
Reference in New Issue
Block a user