Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 1x 1x 3x 3x 3x 1x 1x 1x 2x 2x 3x 2x 1x 1x 1x 1x 1x 1x 1x 1x | import { inject, Injectable } from '@angular/core';
import {
applyActionCode,
Auth,
checkActionCode,
sendPasswordResetEmail,
} from '@angular/fire/auth';
import {
catchError,
delayWhen,
of,
switchMap,
timer,
} from 'rxjs';
import type { Observable } from 'rxjs';
import { getErrorCode } from '../error-code';
/** Combined model of email recovery results and sending password reset. */
export interface RecoverEmailResults extends ApplyResult {
/** Firebase response error code, if any. */
readonly errorCode?: string;
/** Indicates if the password reset email was sent on succesful email recovery. */
readonly passwordResetSent: boolean;
}
/** Results of email recovery. */
interface ApplyResult {
/** User original email address to be recovered, from Firebase oobCode. */
readonly restoredEmail: string | undefined;
/** Results of applying the oobCode to recover the account's original email address. */
readonly successful: boolean;
}
/** Sending the password reset email needs to wait until Firebase recognizes the email recovery. Milliseconds */
export const SEND_EMAIL_DELAY = 500;
/**
* Handles both recovering email oobCodes and sending the password reset email afterwards.
*/
@Injectable({ providedIn: 'root' })
export class RecoverEmailService {
private readonly _auth: Auth = inject(Auth);
/**
* Creates and Observable that when subscribed to will apply the action code to restore the user's
* original email address. And if present will automatically send a password reset email to the
* restored address in case of account compromise.
*
* @param delay - Not for production use! Only for use with testing.
*/
public recoverEmail$(code: string | undefined, delay: number = SEND_EMAIL_DELAY): Observable<RecoverEmailResults> {
return of(code).pipe(
switchMap(async (oobCode: string | undefined): Promise<ApplyResult> => this._doActionCode(oobCode)),
// Unfortunately it can take time for Firebase to recognize that the email has been restored
// so we can send the password reset email.
delayWhen((result: ApplyResult): Observable<number> => timer(result.restoredEmail ? delay : 0)),
// Give the user the option to reset their password in case the account was compromised:
switchMap(async (result: ApplyResult): Promise<RecoverEmailResults> => {
const passwordResetSent = await this._sendPasswordResetEmail(result.restoredEmail);
return { ...result, passwordResetSent };
}),
// Using `err` here trips promise/prefer-await-to-callbacks, but other names don't
catchError((problem: unknown): Observable<RecoverEmailResults> => {
console.error('RecoverEmailService', problem);
return of({
errorCode: getErrorCode(problem),
passwordResetSent: false,
restoredEmail: undefined,
successful: false,
});
}),
);
}
/**
* Check that the oobCode is still valid, and then apply it.
* @returns the restored email address and a success flag.
* @throws Error if the oobCode is falsy or the firebase methods fail.
*/
private async _doActionCode(oobCode: string | undefined): Promise<ApplyResult> {
if (oobCode) {
const info = await checkActionCode(this._auth, oobCode);
const { email: restoredEmail } = info.data;
await applyActionCode(this._auth, oobCode);
// Account email reverted to restoredEmail
// Problem with being pedantic with all types except undefined vs null is that sometimes you
// need to get rid of null from the type.
return { restoredEmail: restoredEmail ?? undefined, successful: true };
}
throw new Error('oobCode not found');
}
/**
* Firebase types indicate that the email may not always be returned (Accounts without email addresses?)
* If the email isn't truthy then just skip the reset.
* If the send email fails for some reason, just return false.
*/
private async _sendPasswordResetEmail(restoredEmail: string | undefined): Promise<boolean> {
if (restoredEmail) {
try {
await sendPasswordResetEmail(this._auth, restoredEmail);
return true;
} catch (err: unknown) {
console.error('RecoverEmailService#_sendPasswordResetEmail', err);
}
}
return false;
}
}
|