diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 71ccaab7dd..d1b51b611f 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -24,7 +24,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { isCardExpired } from "@bitwarden/common/vault/utils"; import { DialogService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -123,7 +123,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), ); - this.cardIsExpired = extensionRefreshEnabled && this.isCardExpiryInThePast(); + this.cardIsExpired = extensionRefreshEnabled && isCardExpired(this.cipher.card); } ngOnDestroy() { @@ -235,24 +235,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.viewingPasswordHistory = !this.viewingPasswordHistory; } - isCardExpiryInThePast() { - if (this.cipher.card) { - const { expMonth, expYear }: CardView = this.cipher.card; - - if (expYear && expMonth) { - // `Date` months are zero-indexed - const parsedMonth = parseInt(expMonth) - 1; - const parsedYear = parseInt(expYear); - - // First day of the next month minus one, to get last day of the card month - const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); - const now = new Date(); - - return cardExpiry < now; - } - } - } - protected cleanUp() { if (this.totpInterval) { window.clearInterval(this.totpInterval); diff --git a/libs/common/src/vault/utils.spec.ts b/libs/common/src/vault/utils.spec.ts index 1cb185cffd..54ec66984e 100644 --- a/libs/common/src/vault/utils.spec.ts +++ b/libs/common/src/vault/utils.spec.ts @@ -1,4 +1,5 @@ -import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { normalizeExpiryYearFormat, isCardExpired } from "@bitwarden/common/vault/utils"; function getExpiryYearValueFormats(currentCentury: string) { return [ @@ -72,3 +73,50 @@ describe("normalizeExpiryYearFormat", () => { jest.clearAllTimers(); }); }); + +function getCardExpiryDateValues() { + const currentDate = new Date(); + + const currentYear = currentDate.getFullYear(); + + // `Date` months are zero-indexed, our expiry date month inputs are one-indexed + const currentMonth = currentDate.getMonth() + 1; + + return [ + [null, null, false], // no month, no year + [undefined, undefined, false], // no month, no year, invalid values + ["", "", false], // no month, no year, invalid values + ["12", "agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", false], // invalid values + ["0", `${currentYear - 1}`, true], // invalid 0 month + ["00", `${currentYear + 1}`, false], // invalid 0 month + [`${currentMonth}`, "0000", true], // current month, in the year 2000 + [null, `${currentYear}`.slice(-2), false], // no month, this year + [null, `${currentYear - 1}`.slice(-2), true], // no month, last year + ["1", null, false], // no year, January + ["1", `${currentYear - 1}`, true], // January last year + ["13", `${currentYear}`, false], // 12 + 1 is Feb. in the next year (Date is zero-indexed) + [`${currentMonth + 36}`, `${currentYear - 1}`, true], // even though the month value would put the date 3 years into the future when calculated with `Date`, an explicit year in the past indicates the card is expired + [`${currentMonth}`, `${currentYear}`, false], // this year, this month (not expired until the month is over) + [`${currentMonth}`, `${currentYear}`.slice(-2), false], // This month, this year (not expired until the month is over) + [`${currentMonth - 1}`, `${currentYear}`, true], // last month + [`${currentMonth - 1}`, `${currentYear + 1}`, false], // 11 months from now + ]; +} + +describe("isCardExpired", () => { + const expiryYearValueFormats = getCardExpiryDateValues(); + + expiryYearValueFormats.forEach( + ([inputMonth, inputYear, expectedValue]: [string | null, string | null, boolean]) => { + it(`should return ${expectedValue} when the card expiry month is ${inputMonth} and the card expiry year is ${inputYear}`, () => { + const testCardView = new CardView(); + testCardView.expMonth = inputMonth; + testCardView.expYear = inputYear; + + const cardIsExpired = isCardExpired(testCardView); + + expect(cardIsExpired).toBe(expectedValue); + }); + }, + ); +}); diff --git a/libs/common/src/vault/utils.ts b/libs/common/src/vault/utils.ts index 7fed4abc12..7d8784eda7 100644 --- a/libs/common/src/vault/utils.ts +++ b/libs/common/src/vault/utils.ts @@ -1,3 +1,5 @@ +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; + type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`; @@ -40,3 +42,42 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu return expirationYear as Year | null; } + +/** + * Takes a cipher card view and returns "true" if the month and year affirmativey indicate + * the card is expired. + * + * @export + * @param {CardView} cipherCard + * @return {*} {boolean} + */ +export function isCardExpired(cipherCard: CardView): boolean { + if (cipherCard) { + const { expMonth = null, expYear = null } = cipherCard; + + const now = new Date(); + const normalizedYear = normalizeExpiryYearFormat(expYear); + + // If the card year is before the current year, don't bother checking the month + if (normalizedYear && parseInt(normalizedYear) < now.getFullYear()) { + return true; + } + + if (normalizedYear && expMonth) { + // `Date` months are zero-indexed + const parsedMonth = + parseInt(expMonth) - 1 || + // Add a month floor of 0 to protect against an invalid low month value of "0" + 0; + + const parsedYear = parseInt(normalizedYear); + + // First day of the next month minus one, to get last day of the card month + const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); + + return cardExpiry < now; + } + } + + return false; +} diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index e737e43f35..10701083b7 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -8,10 +8,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CollectionId } from "@bitwarden/common/types/guid"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { isCardExpired } from "@bitwarden/common/vault/utils"; import { SearchModule, CalloutModule } from "@bitwarden/components"; import { AdditionalOptionsComponent } from "./additional-options/additional-options.component"; @@ -61,7 +61,7 @@ export class CipherViewComponent implements OnInit, OnDestroy { async ngOnInit() { await this.loadCipherData(); - this.cardIsExpired = this.isCardExpiryInThePast(); + this.cardIsExpired = isCardExpired(this.cipher.card); } ngOnDestroy(): void { @@ -102,24 +102,4 @@ export class CipherViewComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroyed$)); } } - - isCardExpiryInThePast() { - if (this.cipher.card) { - const { expMonth, expYear }: CardView = this.cipher.card; - - if (expYear && expMonth) { - // `Date` months are zero-indexed - const parsedMonth = parseInt(expMonth) - 1; - const parsedYear = parseInt(expYear); - - // First day of the next month minus one, to get last day of the card month - const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); - const now = new Date(); - - return cardExpiry < now; - } - } - - return false; - } }