-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add auth method verifyUserIsAuthorizedToAccessPaidContent
- Loading branch information
1 parent
e152ba2
commit bd59a3d
Showing
2 changed files
with
148 additions
and
0 deletions.
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
src/services/AuthService/verifyUserIsAuthorizedToAccessPaidContent.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { MOCK_USERS, MOCK_USER_SCAs } from "@/tests/staticMockItems"; | ||
import { verifyUserIsAuthorizedToAccessPaidContent } from "./verifyUserIsAuthorizedToAccessPaidContent.js"; | ||
import type { UserSubscription } from "@/types/graphql.js"; | ||
import type { AuthTokenPayload } from "@/types/open-api.js"; | ||
|
||
describe("verifyUserIsAuthorizedToAccessPaidContent()", () => { | ||
const YEAR_2000 = new Date(2000, 0); | ||
const YEAR_9999 = new Date(9999, 0); | ||
|
||
const mockUserWithSubFields = ( | ||
subFieldsToTest: Pick<UserSubscription, "status" | "currentPeriodEnd"> | ||
): AuthTokenPayload => ({ | ||
...MOCK_USERS.USER_A, | ||
stripeConnectAccount: MOCK_USER_SCAs.SCA_A, | ||
subscription: { id: "", ...subFieldsToTest }, | ||
}); | ||
|
||
test(`does not throw when called with a valid "active" subscription`, () => { | ||
expect(() => { | ||
verifyUserIsAuthorizedToAccessPaidContent({ | ||
authenticatedUser: mockUserWithSubFields({ | ||
status: "active", | ||
currentPeriodEnd: YEAR_9999, | ||
}), | ||
}); | ||
}).not.toThrow(); | ||
}); | ||
|
||
test(`does not throw when called with a valid "trialing" subscription`, () => { | ||
expect(() => { | ||
verifyUserIsAuthorizedToAccessPaidContent({ | ||
authenticatedUser: mockUserWithSubFields({ | ||
status: "trialing", | ||
currentPeriodEnd: YEAR_9999, | ||
}), | ||
}); | ||
}).not.toThrow(); | ||
}); | ||
|
||
test(`throws an error when called with a subscription with an invalid status`, () => { | ||
expect(() => { | ||
verifyUserIsAuthorizedToAccessPaidContent({ | ||
authenticatedUser: mockUserWithSubFields({ | ||
status: "past_due", | ||
currentPeriodEnd: YEAR_9999, | ||
}), | ||
}); | ||
}).toThrow("past due"); | ||
}); | ||
|
||
test(`throws an error when called with an expired subscription`, () => { | ||
expect(() => { | ||
verifyUserIsAuthorizedToAccessPaidContent({ | ||
authenticatedUser: mockUserWithSubFields({ | ||
status: "active", | ||
currentPeriodEnd: YEAR_2000, | ||
}), | ||
}); | ||
}).toThrow("expired"); | ||
}); | ||
}); |
87 changes: 87 additions & 0 deletions
87
src/services/AuthService/verifyUserIsAuthorizedToAccessPaidContent.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { hasKey } from "@nerdware/ts-type-safety-utils"; | ||
import dayjs from "dayjs"; | ||
import { PaymentRequiredError } from "@/utils/httpErrors.js"; | ||
import type { SubscriptionStatus } from "@/types/graphql.js"; | ||
import type { AuthTokenPayload } from "@/types/open-api.js"; | ||
|
||
/** | ||
* ### AuthService: Verify User Is Authorized To Access Paid Content | ||
* | ||
* Verify whether a User's existing subscription is valid for service-access. | ||
* | ||
* A UserSubscription is only considered valid for service-access if all of the following are true: | ||
* | ||
* 1. The subscription's status is `active` or `trialing`. | ||
* 2. The subscription's `currentPeriodEnd` timestamp is in the future. | ||
*/ | ||
export const verifyUserIsAuthorizedToAccessPaidContent = ({ | ||
authenticatedUser, | ||
}: { | ||
authenticatedUser: AuthTokenPayload; | ||
}) => { | ||
// Destructure necessary fields | ||
const { status, currentPeriodEnd } = authenticatedUser.subscription ?? {}; | ||
|
||
if ( | ||
!status || | ||
!hasKey(SUB_STATUS_AUTH_METADATA, status) || | ||
SUB_STATUS_AUTH_METADATA[status]?.isValid !== true || | ||
!currentPeriodEnd || | ||
!dayjs(currentPeriodEnd).isValid() | ||
) { | ||
throw new Error( | ||
!!status && hasKey(SUB_STATUS_AUTH_METADATA, status) | ||
? SUB_STATUS_AUTH_METADATA[status]?.reason ?? "Invalid subscription." | ||
: "Invalid subscription." | ||
); | ||
} | ||
|
||
// Coerce to unix timestamp in seconds and compare | ||
if (dayjs().unix() >= dayjs(currentPeriodEnd).unix()) { | ||
throw new PaymentRequiredError( | ||
"This subscription has expired — please update your payment settings to re-activate your subscription." | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* Subscription-Status authorization metadata objects: | ||
* | ||
* - Subscription statuses which indicate a sub IS VALID for Fixit service usage | ||
* contain `isValid: true`. | ||
* - Subscription statuses which indicate a sub IS NOT VALID for Fixit service | ||
* usage contain a `reason` plainly explaining to end users _why_ their sub is | ||
* invalid, and what their course of action should be to resolve the issue. | ||
* | ||
* @see https://stripe.com/docs/api/subscriptions/object#subscription_object-status | ||
*/ | ||
export const SUB_STATUS_AUTH_METADATA: Readonly< | ||
Record< | ||
SubscriptionStatus, | ||
{ isValid: true; reason?: never } | { isValid?: false; reason: string } | ||
> | ||
> = { | ||
// Statuses which indicate a subscription IS VALID for service usage: | ||
active: { | ||
isValid: true, | ||
}, | ||
trialing: { | ||
isValid: true, | ||
}, | ||
// Statuses which indicate a subscription IS NOT VALID for service usage: | ||
incomplete: { | ||
reason: "Sorry, your subscription payment is incomplete.", | ||
}, | ||
incomplete_expired: { | ||
reason: "Sorry, please try again.", | ||
}, | ||
past_due: { | ||
reason: "Sorry, payment for your subscription is past due. Please submit payment and try again.", // prettier-ignore | ||
}, | ||
canceled: { | ||
reason: "Sorry, this subscription was canceled.", | ||
}, | ||
unpaid: { | ||
reason: "Sorry, payment for your subscription is past due. Please submit payment and try again.", // prettier-ignore | ||
}, | ||
}; |