Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Username validation refactor #1569

Merged
merged 12 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions waspc/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.12.0

### ⚠️ Breaking changes

- Auth field customization is no longer possible using the `_waspCustomValidations` on the `User` entity. This is a part of auth refactoring that we are doing to make it easier to customize auth. We will be adding more customization options in the future.

## 0.11.8

### 🎉 [New Feature] Serving the Client From a Subdirectory
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import { verifyPassword, throwInvalidCredentialsError } from "../../../core/auth.js";
import { findUserBy, createAuthToken, ensureValidEmailAndPassword } from "../../utils.js";
import { findUserBy, createAuthToken } from "../../utils.js";
import { ensureValidEmail, ensurePasswordIsPresent } from "../../validation.js";

export function getLoginRoute({
allowUnverifiedLogin,
Expand All @@ -11,20 +12,20 @@ export function getLoginRoute({
req: Request<{ email: string; password: string; }>,
res: Response,
): Promise<Response<{ token: string } | undefined>> {
const args = req.body || {}
ensureValidEmailAndPassword(args)
const userFields = req.body || {}
ensureValidArgs(userFields)

args.email = args.email.toLowerCase()
userFields.email = userFields.email.toLowerCase()

const user = await findUserBy({ email: args.email })
const user = await findUserBy({ email: userFields.email })
if (!user) {
throwInvalidCredentialsError()
}
if (!user.isEmailVerified && !allowUnverifiedLogin) {
throwInvalidCredentialsError()
}
try {
await verifyPassword(user.password, args.password);
await verifyPassword(user.password, userFields.password);
} catch(e) {
throwInvalidCredentialsError()
}
Expand All @@ -34,3 +35,8 @@ export function getLoginRoute({
return res.json({ token })
};
}

function ensureValidArgs(args: unknown): void {
ensureValidEmail(args);
ensurePasswordIsPresent(args);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Request, Response } from 'express';
import {
createPasswordResetLink,
findUserBy,
doFakeWork,
ensureValidEmail,
} from "../../utils.js";
import {
createPasswordResetLink,
sendPasswordResetEmail,
isEmailResendAllowed,
} from "../../utils.js";
} from "./utils.js";
import { ensureValidEmail } from "../../validation.js";
import type { EmailFromField } from '../../../email/core/types.js';
import { GetPasswordResetEmailContentFn } from './types.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Request, Response } from 'express';
import { ensureValidTokenAndNewPassword, findUserBy, updateUserPassword, verifyToken } from "../../utils.js";
import { findUserBy, verifyToken } from "../../utils.js";
import { updateUserPassword } from "./utils.js";
import { ensureTokenIsPresent, ensurePasswordIsPresent, ensureValidPassword } from "../../validation.js";
import { tokenVerificationErrors } from "./types.js";

export async function resetPassword(
req: Request<{ token: string; password: string; }>,
res: Response,
): Promise<Response<{ success: true } | { success: false; message: string }>> {
const args = req.body || {};
ensureValidTokenAndNewPassword(args);
ensureValidArgs(args);

const { token, password } = args;
try {
Expand All @@ -25,3 +27,9 @@ export async function resetPassword(
}
res.json({ success: true });
};

function ensureValidArgs(args: unknown): void {
ensureTokenIsPresent(args);
ensurePasswordIsPresent(args);
ensureValidPassword(args);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Request, Response } from 'express';
import { EmailFromField } from "../../../email/core/types.js";
import {
createEmailVerificationLink,
createUser,
findUserBy,
deleteUser,
doFakeWork,
ensureValidEmailAndPassword,
} from "../../utils.js";
import {
createEmailVerificationLink,
sendEmailVerificationEmail,
isEmailResendAllowed,
} from "../../utils.js";
} from "./utils.js";
import { ensureValidEmail, ensureValidPassword, ensurePasswordIsPresent } from "../../validation.js";
import { GetVerificationEmailContentFn } from './types.js';
import { validateAndGetAdditionalFields } from '../../utils.js'

Expand All @@ -27,7 +29,7 @@ export function getSignupRoute({
res: Response,
): Promise<Response<{ success: true } | { success: false; message: string }>> {
const userFields = req.body;
ensureValidEmailAndPassword(userFields);
ensureValidArgs(userFields);

userFields.email = userFields.email.toLowerCase();

Expand Down Expand Up @@ -69,3 +71,10 @@ export function getSignupRoute({
return res.json({ success: true });
};
}

function ensureValidArgs(args: unknown): void {
ensureValidEmail(args);
ensurePasswordIsPresent(args);
ensureValidPassword(args);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{{={= =}=}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving email utils from utils.ts to providers/email/utils.ts

import { sign } from '../../../core/auth.js'
import { emailSender } from '../../../email/index.js';
import { Email } from '../../../email/core/types.js';
import { rethrowPossiblePrismaError } from '../../utils.js'
import prisma from '../../../dbClient.js'
import waspServerConfig from '../../../config.js';
import { type {= userEntityUpper =} } from '../../../entities/index.js'


type {= userEntityUpper =}Id = {= userEntityUpper =}['id']

export async function updateUserEmailVerification(userId: {= userEntityUpper =}Id): Promise<void> {
try {
await prisma.{= userEntityLower =}.update({
where: { id: userId },
data: { isEmailVerified: true },
})
} catch (e) {
rethrowPossiblePrismaError(e);
}
}

export async function updateUserPassword(userId: {= userEntityUpper =}Id, password: string): Promise<void> {
try {
await prisma.{= userEntityLower =}.update({
where: { id: userId },
data: { password },
})
} catch (e) {
rethrowPossiblePrismaError(e);
}
}

export async function createEmailVerificationLink(user: {= userEntityUpper =}, clientRoute: string): Promise<string> {
const token = await createEmailVerificationToken(user);
return `${waspServerConfig.frontendUrl}${clientRoute}?token=${token}`;
}

export async function createPasswordResetLink(user: {= userEntityUpper =}, clientRoute: string): Promise<string> {
const token = await createPasswordResetToken(user);
return `${waspServerConfig.frontendUrl}${clientRoute}?token=${token}`;
}

async function createEmailVerificationToken(user: {= userEntityUpper =}): Promise<string> {
return sign(user.id, { expiresIn: '30m' });
}

async function createPasswordResetToken(user: {= userEntityUpper =}): Promise<string> {
return sign(user.id, { expiresIn: '30m' });
}

export async function sendPasswordResetEmail(
email: string,
content: Email,
): Promise<void> {
return sendEmailAndLogTimestamp(email, content, 'passwordResetSentAt');
}

export async function sendEmailVerificationEmail(
email: string,
content: Email,
): Promise<void> {
return sendEmailAndLogTimestamp(email, content, 'emailVerificationSentAt');
}

async function sendEmailAndLogTimestamp(
email: string,
content: Email,
field: 'emailVerificationSentAt' | 'passwordResetSentAt',
): Promise<void> {
// Set the timestamp first, and then send the email
// so the user can't send multiple requests while
// the email is being sent.
try {
await prisma.{= userEntityLower =}.update({
where: { email },
data: { [field]: new Date() },
})
} catch (e) {
rethrowPossiblePrismaError(e);
}
emailSender.send(content).catch((e) => {
console.error(`Failed to send email for ${field}`, e);
});
}

export function isEmailResendAllowed(
user: {= userEntityUpper =},
field: 'emailVerificationSentAt' | 'passwordResetSentAt',
resendInterval: number = 1000 * 60,
): boolean {
const sentAt = user[field];
if (!sentAt) {
return true;
}
const now = new Date();
const diff = now.getTime() - sentAt.getTime();
return diff > resendInterval;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import { updateUserEmailVerification, verifyToken } from '../../utils.js';
import { updateUserEmailVerification } from './utils.js';
import { verifyToken } from '../../utils.js';
import { tokenVerificationErrors } from './types.js';

export async function verifyEmail(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth
import { handleRejection } from '../../../utils.js'

import { findUserBy, createAuthToken } from '../../utils.js'
import { ensureValidUsername, ensurePasswordIsPresent } from '../../validation.js'

export default handleRejection(async (req, res) => {
const args = req.body || {}
const userFields = req.body || {}
ensureValidArgs(userFields)

const user = await findUserBy({ username: args.username })
const user = await findUserBy({ username: userFields.username })
if (!user) {
throwInvalidCredentialsError()
}

try {
await verifyPassword(user.password, args.password)
await verifyPassword(user.password, userFields.password)
} catch(e) {
throwInvalidCredentialsError()
}
Expand All @@ -28,3 +30,8 @@ export default handleRejection(async (req, res) => {

return res.json({ token })
})

function ensureValidArgs(args: unknown): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validating the username & password in the route

ensureValidUsername(args);
ensurePasswordIsPresent(args);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{{={= =}=}}
import { handleRejection } from '../../../utils.js'
import { createUser } from '../../utils.js'
import { ensureValidUsername, ensurePasswordIsPresent, ensureValidPassword } from '../../validation.js'
import { validateAndGetAdditionalFields } from '../../utils.js'

export default handleRejection(async (req, res) => {
const userFields = req.body || {}
ensureValidArgs(userFields)

const additionalFields = await validateAndGetAdditionalFields(userFields)

Expand All @@ -16,3 +18,9 @@ export default handleRejection(async (req, res) => {

return res.json({ success: true })
})

function ensureValidArgs(args: unknown): void {
ensureValidUsername(args);
ensurePasswordIsPresent(args);
ensureValidPassword(args);
}
Loading
Loading