Skip to content

Commit

Permalink
Merge pull request #1139 from jetstreamapp/feat/add-billing-and-pricing
Browse files Browse the repository at this point in the history
Prepare for billing
  • Loading branch information
paustint authored Jan 20, 2025
2 parents 5c0c3d4 + ad48614 commit 79ce78d
Show file tree
Hide file tree
Showing 269 changed files with 5,404 additions and 1,256 deletions.
10 changes: 8 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ JETSTREAM_POSTGRES_DBURI='postgres://postgres@localhost:5432/postgres'

# Used in landing page to redirect to the correct URL
# If running locally but not developing the platform, use port `:3333` for all of these
NEXT_PUBLIC_CLIENT_URL='http://localhost:4200/app'
NEXT_PUBLIC_SERVER_URL='http://localhost:3333'
NX_PUBLIC_CLIENT_URL='http://localhost:4200/app'
NX_PUBLIC_SERVER_URL='http://localhost:3333'

# OAUTH FOR LOGGING IN TO THE APP
# You can provide your own keys by creating a connected app in your dev or production org.
Expand Down Expand Up @@ -100,3 +100,9 @@ HONEYCOMB_API_KEY=''
# This is disabled for existing workspaces to maintain compatibility
# For more info, see: https://nx.dev/concepts/inferred-tasks
NX_ADD_PLUGINS=false

# Billing related keys
NX_PUBLIC_BILLING_ENABLED=''
NX_PUBLIC_STRIPE_PUBLIC_KEY=''
NX_PUBLIC_STRIPE_PRO_ANNUAL_PRICE_ID=''
NX_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID=''
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ env:
NX_CLOUD_DISTRIBUTED_EXECUTION: false
NX_PUBLIC_AMPLITUDE_KEY: ${{ secrets.NX_PUBLIC_AMPLITUDE_KEY }}
NX_PUBLIC_ROLLBAR_KEY: ${{ secrets.NX_PUBLIC_ROLLBAR_KEY }}
NEXT_PUBLIC_CLIENT_URL: 'http://localhost:3333/app'
NEXT_PUBLIC_SERVER_URL: 'http://localhost:3333'
NX_PUBLIC_CLIENT_URL: 'http://localhost:3333/app'
NX_PUBLIC_SERVER_URL: 'http://localhost:3333'

jobs:
# Build application
Expand Down
5 changes: 2 additions & 3 deletions .release-it-web-ext.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@
"github": {
"release": true,
"releaseName": "Jetstream Web Extension ${version}",
"assets": ["dist/jetstream-web-extension-${version}.zip"]
"assets": []
},
"hooks": {
"after:bump": [
"npx prettier --write apps/jetstream-web-extension/src/manifest.json",
"yarn build:web-extension",
"rm -f dist/apps/jetstream-web-extension-${version}.zip *",
"cd dist/apps/jetstream-web-extension && zip -r -9 ../../jetstream-web-extension-${version}.zip *"
"version=v${version} yarn build:web-extension:zip"
],
"after:release": "echo Successfully released ${name} v${version}."
}
Expand Down
5 changes: 5 additions & 0 deletions apps/api/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
"input": "apps/api/src/assets",
"output": "assets",
"ignore": [".gitkeep"]
},
{
"glob": "**/*",
"input": "apps/api/src/assets-web-extension",
"output": "assets-web-extension"
}
],
"generatePackageJson": true,
Expand Down
192 changes: 110 additions & 82 deletions apps/api/src/app/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ const callback = createRoute(routeDefinition.callback.validators, async ({ body,
linkIdentity: linkIdentityCookie,
returnUrl,
rememberDevice,
redirectUrl: redirectUrlCookie,
} = getCookieConfig(ENV.USE_SECURE_COOKIES);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cookies = parseCookie(req.headers.cookie!);
Expand Down Expand Up @@ -469,15 +470,21 @@ const callback = createRoute(routeDefinition.callback.validators, async ({ body,
if (isNewUser) {
await sendWelcomeEmail(req.session.user.email);
}

let redirectUrl = ENV.JETSTREAM_CLIENT_URL;

if (cookies[redirectUrlCookie.name]) {
const redirectValue = cookies[redirectUrlCookie.name];
redirectUrl = redirectValue.startsWith('/') ? `${ENV.JETSTREAM_CLIENT_URL}${redirectValue}` : redirectValue;
clearCookie(redirectUrlCookie.name, redirectUrlCookie.options);
}

// No verification required
if (provider.type === 'oauth') {
redirect(res, ENV.JETSTREAM_CLIENT_URL);
redirect(res, redirectUrl);
} else {
// this was an API call, client will handle redirect
sendJson(res, {
error: false,
redirect: ENV.JETSTREAM_CLIENT_URL,
});
sendJson(res, { error: false, redirect: redirectUrl });
}
}

Expand All @@ -497,94 +504,105 @@ const callback = createRoute(routeDefinition.callback.validators, async ({ body,
}
});

const verification = createRoute(routeDefinition.verification.validators, async ({ body, user, setCookie }, req, res, next) => {
try {
if (!req.session.user || !req.session.pendingVerification) {
throw new InvalidSession('Missing user or pending verification');
}
const verification = createRoute(
routeDefinition.verification.validators,
async ({ body, user, setCookie, clearCookie }, req, res, next) => {
try {
if (!req.session.user || !req.session.pendingVerification) {
throw new InvalidSession('Missing user or pending verification');
}

const { csrfToken, code, type, rememberDevice } = body;
const pendingVerification = req.session.pendingVerification.find((item) => item.type === type);
let rememberDeviceId: string | undefined;
const { csrfToken, code, type, rememberDevice } = body;
const pendingVerification = req.session.pendingVerification.find((item) => item.type === type);
let rememberDeviceId: string | undefined;

const cookieConfig = getCookieConfig(ENV.USE_SECURE_COOKIES);
const cookieConfig = getCookieConfig(ENV.USE_SECURE_COOKIES);

if (!pendingVerification) {
throw new InvalidSession('Missing pending verification');
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cookies = parseCookie(req.headers.cookie!);

await verifyCSRFFromRequestOrThrow(csrfToken, req.headers.cookie || '');
if (!pendingVerification) {
throw new InvalidSession('Missing pending verification');
}

if (pendingVerification.exp <= new Date().getTime()) {
throw new ExpiredVerificationToken(`Pending verification is expired: ${pendingVerification.exp}`);
}
await verifyCSRFFromRequestOrThrow(csrfToken, req.headers.cookie || '');

switch (pendingVerification.type) {
case 'email': {
const { token } = pendingVerification;
if (token !== code) {
throw new InvalidVerificationToken('Provided code does not match');
}
req.session.user = (await setUserEmailVerified(req.session.user.id)) as UserProfileSession;
break;
if (pendingVerification.exp <= new Date().getTime()) {
throw new ExpiredVerificationToken(`Pending verification is expired: ${pendingVerification.exp}`);
}
case '2fa-email': {
const { token } = pendingVerification;
if (token !== code) {
throw new InvalidVerificationToken('Provided code does not match');

switch (pendingVerification.type) {
case 'email': {
const { token } = pendingVerification;
if (token !== code) {
throw new InvalidVerificationToken('Provided code does not match');
}
req.session.user = (await setUserEmailVerified(req.session.user.id)) as UserProfileSession;
break;
}
case '2fa-email': {
const { token } = pendingVerification;
if (token !== code) {
throw new InvalidVerificationToken('Provided code does not match');
}
rememberDeviceId = rememberDevice ? generateRandomString(32) : undefined;
break;
}
case '2fa-otp': {
const { secret } = await getTotpAuthenticationFactor(req.session.user.id);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await verify2faTotpOrThrow(secret!, code);
rememberDeviceId = rememberDevice ? generateRandomString(32) : undefined;
break;
}
default: {
throw new InvalidVerificationToken(`Invalid verification type`);
}
rememberDeviceId = rememberDevice ? generateRandomString(32) : undefined;
break;
}
case '2fa-otp': {
const { secret } = await getTotpAuthenticationFactor(req.session.user.id);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await verify2faTotpOrThrow(secret!, code);
rememberDeviceId = rememberDevice ? generateRandomString(32) : undefined;
break;
}
default: {
throw new InvalidVerificationToken(`Invalid verification type`);

if (rememberDeviceId) {
await createRememberDevice({
userId: user.id,
deviceId: rememberDeviceId,
ipAddress: res.locals.ipAddress || getApiAddressFromReq(req),
userAgent: req.get('User-Agent'),
});
setCookie(cookieConfig.rememberDevice.name, rememberDeviceId, cookieConfig.rememberDevice.options);
}
}

if (rememberDeviceId) {
await createRememberDevice({
userId: user.id,
deviceId: rememberDeviceId,
ipAddress: res.locals.ipAddress || getApiAddressFromReq(req),
userAgent: req.get('User-Agent'),
});
setCookie(cookieConfig.rememberDevice.name, rememberDeviceId, cookieConfig.rememberDevice.options);
}
req.session.pendingVerification = null;

req.session.pendingVerification = null;
if (req.session.sendNewUserEmailAfterVerify && req.session.user) {
req.session.sendNewUserEmailAfterVerify = undefined;
await sendWelcomeEmail(req.session.user.email);
}

if (req.session.sendNewUserEmailAfterVerify && req.session.user) {
req.session.sendNewUserEmailAfterVerify = undefined;
await sendWelcomeEmail(req.session.user.email);
}
let redirectUrl = ENV.JETSTREAM_CLIENT_URL;

createUserActivityFromReq(req, res, {
action: '2FA_VERIFICATION',
method: type.toUpperCase(),
success: true,
});
if (cookies[cookieConfig.redirectUrl.name]) {
const redirectValue = cookies[cookieConfig.redirectUrl.name];
redirectUrl = redirectValue.startsWith('/') ? `${ENV.JETSTREAM_CLIENT_URL}${redirectValue}` : redirectValue;
clearCookie(cookieConfig.redirectUrl.name, cookieConfig.redirectUrl.options);
}

sendJson(res, {
error: false,
redirect: ENV.JETSTREAM_CLIENT_URL,
});
} catch (ex) {
createUserActivityFromReqWithError(req, res, ex, {
action: '2FA_VERIFICATION',
method: body?.type?.toUpperCase(),
success: false,
});
createUserActivityFromReq(req, res, {
action: '2FA_VERIFICATION',
method: type.toUpperCase(),
success: true,
});

next(ensureAuthError(ex));
sendJson(res, { error: false, redirect: redirectUrl });
} catch (ex) {
createUserActivityFromReqWithError(req, res, ex, {
action: '2FA_VERIFICATION',
method: body?.type?.toUpperCase(),
success: false,
});

next(ensureAuthError(ex));
}
}
});
);

const resendVerification = createRoute(routeDefinition.resendVerification.validators, async ({ body }, req, res, next) => {
try {
Expand Down Expand Up @@ -723,17 +741,27 @@ const validatePasswordReset = createRoute(routeDefinition.validatePasswordReset.
}
});

const verifyEmailViaLink = createRoute(routeDefinition.verification.validators, async ({ query }, req, res, next) => {
const verifyEmailViaLink = createRoute(routeDefinition.verification.validators, async ({ query, clearCookie }, req, res, next) => {
try {
if (!req.session.user) {
throw new InvalidSession('User not set on session');
}

const cookieConfig = getCookieConfig(ENV.USE_SECURE_COOKIES);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cookies = parseCookie(req.headers.cookie!);

let redirectUrl = ENV.JETSTREAM_CLIENT_URL;

if (cookies[cookieConfig.redirectUrl.name]) {
const redirectValue = cookies[cookieConfig.redirectUrl.name];
redirectUrl = redirectValue.startsWith('/') ? `${ENV.JETSTREAM_CLIENT_URL}${redirectValue}` : redirectValue;
clearCookie(cookieConfig.redirectUrl.name, cookieConfig.redirectUrl.options);
}

if (!req.session.pendingVerification?.length) {
sendJson(res, {
error: false,
redirect: ENV.JETSTREAM_CLIENT_URL,
});
sendJson(res, { error: false, redirect: redirectUrl });
return;
}

Expand Down Expand Up @@ -769,7 +797,7 @@ const verifyEmailViaLink = createRoute(routeDefinition.verification.validators,
});

req.session.pendingVerification = null;
redirect(res, ENV.JETSTREAM_CLIENT_URL);
redirect(res, redirectUrl);
} catch (ex) {
createUserActivityFromReqWithError(req, res, ex, {
action: 'EMAIL_VERIFICATION',
Expand Down
Loading

0 comments on commit 79ce78d

Please sign in to comment.