Skip to content

Commit

Permalink
Merge pull request #1077 from jetstreamapp/bug/email-version-bugfixes
Browse files Browse the repository at this point in the history
Bug/email version bugfixes
  • Loading branch information
paustint authored Nov 17, 2024
2 parents 8f31d0f + ebed60a commit d5cf388
Show file tree
Hide file tree
Showing 29 changed files with 178 additions and 246 deletions.
10 changes: 6 additions & 4 deletions apps/api/src/app/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createRememberDevice,
createUserActivityFromReq,
createUserActivityFromReqWithError,
EMAIL_VERIFICATION_TOKEN_DURATION_HOURS,
ensureAuthError,
ExpiredVerificationToken,
generatePasswordResetToken,
Expand Down Expand Up @@ -41,7 +42,7 @@ import {
} from '@jetstream/email';
import { ensureBoolean } from '@jetstream/shared/utils';
import { parse as parseCookie } from 'cookie';
import { addMinutes } from 'date-fns';
import { addHours, addMinutes } from 'date-fns';
import { z } from 'zod';
import { Request } from '../types/types';
import { redirect, sendJson, setCsrfCookie } from '../utils/response.handlers';
Expand Down Expand Up @@ -180,15 +181,16 @@ function initSession(
req.session.pendingVerification = null;

if (verificationRequired) {
const exp = addMinutes(new Date(), TOKEN_DURATION_MINUTES).getTime();
const token = generateRandomCode(6);
if (isNewUser) {
req.session.sendNewUserEmailAfterVerify = true;
}
if (verificationRequired.email) {
const exp = addHours(new Date(), EMAIL_VERIFICATION_TOKEN_DURATION_HOURS).getTime();
// If email verification is required, we can consider that as 2fa as well, so do not need to combine with other 2fa factors
req.session.pendingVerification = [{ type: 'email', exp, token }];
} else if (verificationRequired.twoFactor?.length > 0) {
const exp = addMinutes(new Date(), TOKEN_DURATION_MINUTES).getTime();
req.session.pendingVerification = verificationRequired.twoFactor.map((factor) => {
switch (factor.type) {
case '2fa-otp':
Expand Down Expand Up @@ -451,7 +453,7 @@ const callback = createRoute(routeDefinition.callback.validators, async ({ body,
const initialVerification = req.session.pendingVerification[0];

if (initialVerification.type === 'email') {
await sendEmailVerification(req.session.user.email, initialVerification.token, TOKEN_DURATION_MINUTES);
await sendEmailVerification(req.session.user.email, initialVerification.token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
} else if (initialVerification.type === '2fa-email') {
await sendVerificationCode(req.session.user.email, initialVerification.token, TOKEN_DURATION_MINUTES);
}
Expand Down Expand Up @@ -621,7 +623,7 @@ const resendVerification = createRoute(routeDefinition.resendVerification.valida

switch (type) {
case 'email': {
await sendEmailVerification(req.session.user.email, token, TOKEN_DURATION_MINUTES);
await sendEmailVerification(req.session.user.email, token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
break;
}
case '2fa-email': {
Expand Down
24 changes: 0 additions & 24 deletions apps/api/src/app/db/transactions.db.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
import { getExceptionLog, logger, prisma } from '@jetstream/api-config';
import { UserProfileSession } from '@jetstream/auth/types';
import { PrismaPromise } from '@prisma/client';

/**
* This file manages db operations as transactions that span multiple tables
*/

export async function deleteUserAndOrgs(user: UserProfileSession) {
if (!user?.id) {
throw new Error('A valid user must be provided');
}
try {
const deleteOrgs = prisma.salesforceOrg.deleteMany({
where: { jetstreamUserId: user.id },
});

const deleteUser = prisma.user.update({
where: { userId: user.id },
data: {
deletedAt: new Date(),
},
});

await prisma.$transaction([deleteOrgs, deleteUser]);
} catch (ex) {
logger.error({ userId: user?.id, ...getExceptionLog(ex) }, '[DB][TX][DEL_ORGS_AND_USER][ERROR] %o', ex);
throw ex;
}
}

/**
* Hard delete all orgs and users for a given org.
* If no user exists or no orgs exist, then no db transaction is submitted
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/routes/api.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ routes.use(addOrgsToLocal);
// used to make sure the user is authenticated and can communicate with the server
routes.get('/heartbeat', (req: express.Request, res: express.Response) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendJson(res as any, { version: ENV.GIT_VERSION || null, announcements: getAnnouncements() });
sendJson(res as any, { version: ENV.VERSION || null, announcements: getAnnouncements() });
});

/**
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ if (cluster.isPrimary) {
NODE_ENV=${ENV.NODE_ENV}
ENVIRONMENT=${ENV.ENVIRONMENT}
GIT_VERSION=${ENV.GIT_VERSION ?? '<unspecified>'}
VERSION=${ENV.VERSION ?? '<unspecified>'}
LOG_LEVEL=${ENV.LOG_LEVEL}
JETSTREAM_SERVER_URL=${ENV.JETSTREAM_SERVER_URL}
JETSTREAM_CLIENT_URL=${ENV.JETSTREAM_CLIENT_URL}
Expand Down Expand Up @@ -350,7 +350,6 @@ try {
name: user.name,
password: passwordHash,
passwordUpdatedAt: new Date(),
deletedAt: null,
lastLoggedIn: new Date(),
preferences: { create: { skipFrontdoorLogin: false } },
authFactors: { create: { type: '2fa-email', enabled: false } },
Expand Down
2 changes: 1 addition & 1 deletion apps/cron-tasks/src/config/env-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dotenv.config();
let VERSION;
try {
VERSION = readFileSync(join(__dirname, '../../VERSION'), 'utf-8').trim();
console.warn(`APP VERSION ${VERSION}`);
console.info(`APP VERSION ${VERSION}`);
} catch (ex) {
console.warn('COULD NOT READ VERSION FILE', getExceptionLog(ex));
}
Expand Down
29 changes: 0 additions & 29 deletions apps/cron-tasks/src/utils/cron-utils.ts

This file was deleted.

3 changes: 2 additions & 1 deletion apps/landing/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import Link from 'next/link';

const footerNavigation = {
support: [
{ name: 'Documentation', href: 'https://docs.getjetstream.app/', target: '_blank' },
{ name: 'Documentation', href: 'https://docs.getjetstream.app', target: '_blank' },
{ name: 'Status', href: 'https://status.getjetstream.app', target: '_blank' },
{ name: 'Ask a question', href: 'https://discord.gg/sfxd', target: '_blank' },
{ name: 'File an issue', href: 'https://github.com/jetstreamapp/jetstream/issues', target: '_blank' },
{ name: 'Contact Us', href: 'mailto:[email protected]', target: '_blank' },
Expand Down
15 changes: 11 additions & 4 deletions apps/landing/components/auth/Captcha.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Turnstile } from '@marsidev/react-turnstile';
import { useId } from 'react';
import { Maybe } from '@jetstream/types';
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
import { forwardRef, useId, useImperativeHandle, useRef } from 'react';
import { ENVIRONMENT } from '../../utils/environment';

interface CaptchaProps {
Expand All @@ -17,9 +18,13 @@ interface CaptchaProps {
onFinished: () => void;
}

export function Captcha({ action, formError, onLoad, onChange, onFinished }: CaptchaProps) {
// eslint-disable-next-line react/display-name
export const Captcha = forwardRef<Maybe<TurnstileInstance>, CaptchaProps>(({ action, formError, onLoad, onChange, onFinished }, ref) => {
const turnstileRef = useRef<TurnstileInstance>(null);
const id = useId();

useImperativeHandle<unknown, Maybe<TurnstileInstance>>(ref, () => turnstileRef.current, [turnstileRef]);

// Skip rendering the captcha if we're running in Playwright or if the key is not set
// In real environments the server will still validate and prevent access if there isn't a valid token
if (!ENVIRONMENT.CAPTCHA_KEY || (window as any)?.playwright) {
Expand All @@ -31,13 +36,15 @@ export function Captcha({ action, formError, onLoad, onChange, onFinished }: Cap
<>
<Turnstile
id={id}
ref={turnstileRef}
siteKey={ENVIRONMENT.CAPTCHA_KEY}
options={{
action,
theme: 'light',
appearance: 'always',
size: 'flexible',
refreshExpired: 'auto',
feedbackEnabled: true,
}}
onWidgetLoad={onLoad}
onSuccess={(token) => {
Expand All @@ -52,4 +59,4 @@ export function Captcha({ action, formError, onLoad, onChange, onFinished }: Cap
)}
</>
);
}
});
10 changes: 9 additions & 1 deletion apps/landing/components/auth/LoginOrSignUp.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-disable @next/next/no-img-element */
import { zodResolver } from '@hookform/resolvers/zod';
import { Providers } from '@jetstream/auth/types';
import { TurnstileInstance } from '@marsidev/react-turnstile';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useState } from 'react';
import { Fragment, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { AUTH_PATHS, ENVIRONMENT } from '../../utils/environment';
Expand Down Expand Up @@ -53,6 +54,7 @@ export function LoginOrSignUp({ action, providers, csrfToken }: LoginOrSignUpPro
const router = useRouter();
const [showPasswordActive, setShowPasswordActive] = useState(false);
const [finishedCaptcha, setFinishedCaptcha] = useState(false);
const captchaRef = useRef<TurnstileInstance>(null);

const {
register,
Expand Down Expand Up @@ -100,6 +102,11 @@ export function LoginOrSignUp({ action, providers, csrfToken }: LoginOrSignUpPro

if (!response.ok || error) {
router.push(`${router.pathname}?${new URLSearchParams({ error: errorType || 'UNKNOWN_ERROR' })}`);
try {
captchaRef?.current?.reset();
} catch (ex) {
console.error('Error resetting captcha', ex);
}
return;
}

Expand Down Expand Up @@ -258,6 +265,7 @@ export function LoginOrSignUp({ action, providers, csrfToken }: LoginOrSignUpPro
</div>

<Captcha
ref={captchaRef}
formError={errors?.captchaToken?.message}
action={action}
onChange={(token) => setValue('captchaToken', token)}
Expand Down
10 changes: 9 additions & 1 deletion apps/landing/components/auth/PasswordResetInit.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable @next/next/no-img-element */
import { zodResolver } from '@hookform/resolvers/zod';
import { TurnstileInstance } from '@marsidev/react-turnstile';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useState } from 'react';
import { Fragment, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { AUTH_PATHS } from '../../utils/environment';
Expand All @@ -29,6 +30,7 @@ export function PasswordResetInit({ csrfToken }: PasswordResetInitProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
const [finishedCaptcha, setFinishedCaptcha] = useState(false);
const [error, setError] = useState<string>();
const captchaRef = useRef<TurnstileInstance>(null);

const {
register,
Expand Down Expand Up @@ -58,6 +60,11 @@ export function PasswordResetInit({ csrfToken }: PasswordResetInitProps) {
});

if (!response.ok) {
try {
captchaRef?.current?.reset();
} catch (ex) {
console.error('Error resetting captcha', ex);
}
throw new Error('Unable to initialize the reset process');
}

Expand Down Expand Up @@ -127,6 +134,7 @@ export function PasswordResetInit({ csrfToken }: PasswordResetInitProps) {
/>

<Captcha
ref={captchaRef}
formError={errors?.captchaToken?.message}
action="password-reset-init"
onChange={(token) => setValue('captchaToken', token)}
Expand Down
4 changes: 3 additions & 1 deletion apps/landing/components/auth/VerifyEmailOr2fa.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { TwoFactorType } from '@jetstream/auth/types';
import { Maybe } from '@jetstream/types';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useRouter } from 'next/router';
import { FormEvent, Fragment, useState } from 'react';
import { useForm } from 'react-hook-form';
Expand Down Expand Up @@ -47,6 +48,7 @@ interface VerifyEmailOr2faProps {

export function VerifyEmailOr2fa({ csrfToken, email, pendingVerifications }: VerifyEmailOr2faProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [error, setError] = useState<string>();
const [hasResent, setHasResent] = useState(false);

Expand All @@ -61,7 +63,7 @@ export function VerifyEmailOr2fa({ csrfToken, email, pendingVerifications }: Ver
} = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
code: '',
code: searchParams.get('code') || '',
csrfToken,
captchaToken: '',
type: activeFactor,
Expand Down
4 changes: 2 additions & 2 deletions libs/api-config/src/lib/api-rollbar-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Rollbar from 'rollbar';
import { ENV } from './env-config';

export const rollbarServer = new Rollbar({
codeVersion: ENV.GIT_VERSION || '',
code_version: ENV.GIT_VERSION || '',
codeVersion: ENV.VERSION || '',
code_version: ENV.VERSION || '',
accessToken: ENV.ROLLBAR_SERVER_TOKEN || '',
environment: ENV.ENVIRONMENT,
captureUncaught: true,
Expand Down
14 changes: 6 additions & 8 deletions libs/api-config/src/lib/email.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ export async function sendEmail({
...rest,
});

if (results.id) {
prisma.emailActivity
.create({
data: { email: to, subject, status: `${results.status || ''}`, providerId: results.id },
select: { id: true },
})
.catch((err) => logger.error({ message: err?.message }, '[EMAIL][ERROR] Error logging email activity'));
}
prisma.emailActivity
.create({
data: { email: to, subject, status: `${results.status}` || null, providerId: results.id },
select: { id: true },
})
.catch((err) => logger.error({ message: err?.message }, '[EMAIL][ERROR] Error logging email activity'));
}
7 changes: 3 additions & 4 deletions libs/api-config/src/lib/env-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ import { readFileSync } from 'fs-extra';
import { isNumber } from 'lodash';
import { join } from 'path';
import { z } from 'zod';
import { getExceptionLog } from './api-logger';

dotenv.config();

let VERSION = 'unknown';
try {
VERSION = readFileSync(join(__dirname, '../../VERSION'), 'utf-8').trim();
console.warn(`APP VERSION ${VERSION}`);
} catch (ex) {
console.warn('COULD NOT READ VERSION FILE', getExceptionLog(ex));
// ignore errors
}

/**
Expand Down Expand Up @@ -103,7 +101,7 @@ const envSchema = z.object({
CAPTCHA_SECRET_KEY: z.string().optional(),
CAPTCHA_PROPERTY: z.literal('captchaToken').optional().default('captchaToken'),
IP_API_KEY: z.string().optional().describe('API Key used to get location information from IP address'),
GIT_VERSION: z.string().optional(),
VERSION: z.string().optional(),
ROLLBAR_SERVER_TOKEN: z.string().optional(),

// Legacy Auth0 - Used to allow JIT password migration
Expand Down Expand Up @@ -202,6 +200,7 @@ const parseResults = envSchema.safeParse({
EXAMPLE_USER_PASSWORD: ensureBoolean(process.env.EXAMPLE_USER_OVERRIDE) ? process.env.EXAMPLE_USER_PASSWORD : null,
EXAMPLE_USER_FULL_PROFILE: ensureBoolean(process.env.EXAMPLE_USER_OVERRIDE) ? EXAMPLE_USER_FULL_PROFILE : null,
SFDC_API_VERSION: process.env.NX_SFDC_API_VERSION || process.env.SFDC_API_VERSION,
VERSION,
});

if (!parseResults.success) {
Expand Down
1 change: 1 addition & 0 deletions libs/auth/server/src/lib/auth.constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const PASSWORD_RESET_DURATION_MINUTES = 30;
export const TOKEN_DURATION_MINUTES = 15;
export const EMAIL_VERIFICATION_TOKEN_DURATION_HOURS = 48;

export const DELETE_ACTIVITY_DAYS = 30;
export const DELETE_TOKEN_DAYS = 3;
Loading

0 comments on commit d5cf388

Please sign in to comment.