Skip to content

Commit

Permalink
fix: 3898/passwordless schema changes (#3902)
Browse files Browse the repository at this point in the history
* feat: mfaCode -> singleUseCode

* feat: adding new field to jurisdiction
  • Loading branch information
YazeedLoonat authored Feb 29, 2024
1 parent 16772d9 commit c514748
Show file tree
Hide file tree
Showing 15 changed files with 92 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- AlterTable

ALTER TABLE "user_accounts" RENAME COLUMN "mfa_code" TO "single_use_code";


ALTER TABLE "user_accounts" RENAME COLUMN "mfa_code_updated_at" TO "single_use_code_updated_at";

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "jurisdictions" ADD COLUMN "allow_single_use_code_login" BOOLEAN NOT NULL DEFAULT false;
18 changes: 9 additions & 9 deletions api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ model Jurisdictions {
enableAccessibilityFeatures Boolean @default(false) @map("enable_accessibility_features")
enableUtilitiesIncluded Boolean @default(false) @map("enable_utilities_included")
enableGeocodingPreferences Boolean @default(false) @map("enable_geocoding_preferences")
allowSingleUseCodeLogin Boolean @default(false) @map("allow_single_use_code_login")
amiChart AmiChart[]
multiselectQuestions MultiselectQuestions[]
listings Listings[]
Expand Down Expand Up @@ -571,10 +572,10 @@ model Listings {
}

model MapLayers {
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
name String
jurisdictionId String @map("jurisdiction_id")
featureCollection Json @map("feature_collection") @default("{}")
jurisdictionId String @map("jurisdiction_id")
featureCollection Json @default("{}") @map("feature_collection")
@@map("map_layers")
}
Expand Down Expand Up @@ -650,9 +651,9 @@ model Translations {

// Note: [name] formerly max length 256
model UnitAccessibilityPriorityTypes {
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6)
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6)
name String
units Units[]
unitGroup UnitGroup[]
Expand Down Expand Up @@ -779,8 +780,8 @@ model UserAccounts {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6)
language LanguagesEnum?
mfaEnabled Boolean @default(false) @map("mfa_enabled")
mfaCode String? @map("mfa_code") @db.VarChar
mfaCodeUpdatedAt DateTime? @map("mfa_code_updated_at") @db.Timestamptz(6)
singleUseCode String? @map("single_use_code") @db.VarChar
singleUseCodeUpdatedAt DateTime? @map("single_use_code_updated_at") @db.Timestamptz(6)
lastLoginAt DateTime @default(now()) @map("last_login_at") @db.Timestamp(6)
failedLoginAttemptsCount Int @default(0) @map("failed_login_attempts_count")
phoneNumberVerified Boolean? @default(false) @map("phone_number_verified")
Expand Down Expand Up @@ -809,7 +810,6 @@ model UserRoles {
@@map("user_roles")
}


// START DETROIT SPECIFIC
model ListingNeighborhoodAmenities {
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
Expand Down
6 changes: 3 additions & 3 deletions api/prisma/seed-helpers/user-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const userFactory = async (optionalParams?: {
firstName?: string;
lastName?: string;
email?: string;
mfaCode?: string;
singleUseCode?: string;
mfaEnabled?: boolean;
confirmedAt?: Date;
phoneNumber?: string;
Expand All @@ -30,10 +30,10 @@ export const userFactory = async (optionalParams?: {
isPartner: optionalParams?.roles?.isPartner || false,
},
},
mfaCode: optionalParams?.mfaCode || null,
singleUseCode: optionalParams?.singleUseCode || null,
mfaEnabled: optionalParams?.mfaEnabled || false,
confirmedAt: optionalParams?.confirmedAt || null,
mfaCodeUpdatedAt: optionalParams?.mfaEnabled ? new Date() : undefined,
singleUseCodeUpdatedAt: optionalParams?.mfaEnabled ? new Date() : undefined,
phoneNumber: optionalParams?.phoneNumber || null,
phoneNumberVerified: optionalParams?.phoneNumberVerified || null,
agreedToTermsOfService: optionalParams?.acceptedTerms || false,
Expand Down
2 changes: 1 addition & 1 deletion api/prisma/seed-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const stagingSeed = async (
jurisdictionIds: [jurisdiction.id],
acceptedTerms: true,
mfaEnabled: true,
mfaCode: '12345',
singleUseCode: '12345',
}),
});
// add jurisdiction specific translations and default ones
Expand Down
6 changes: 6 additions & 0 deletions api/src/dtos/jurisdictions/jurisdiction.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ export class Jurisdiction extends AbstractDTO {
@ApiProperty()
enableUtilitiesIncluded: boolean;

@Expose()
@IsBoolean({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
allowSingleUseCodeLogin: boolean;

@Expose()
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@IsEnum(UserRoleEnum, {
Expand Down
33 changes: 19 additions & 14 deletions api/src/passports/mfa.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,33 +113,38 @@ export class MfaStrategy extends PassportStrategy(Strategy, 'mfa') {
}

let authSuccess = true;
if (!dto.mfaCode || !rawUser.mfaCode || !rawUser.mfaCodeUpdatedAt) {
// if an mfaCode was not sent, and an mfaCode wasn't stored in the db for the user
if (
!dto.mfaCode ||
!rawUser.singleUseCode ||
!rawUser.singleUseCodeUpdatedAt
) {
// if an mfaCode was not sent, and a singleUseCode wasn't stored in the db for the user
// signal to the front end to request an mfa code
await this.updateFailedLoginCount(0, rawUser.id);
throw new UnauthorizedException({
name: 'mfaCodeIsMissing',
});
} else if (
new Date(
rawUser.mfaCodeUpdatedAt.getTime() + Number(process.env.MFA_CODE_VALID),
rawUser.singleUseCodeUpdatedAt.getTime() +
Number(process.env.MFA_CODE_VALID),
) < new Date() ||
rawUser.mfaCode !== dto.mfaCode
rawUser.singleUseCode !== dto.mfaCode
) {
// if mfaCode TTL has expired, or if the mfa code input was incorrect
authSuccess = false;
} else {
// if mfaCode login was a success
rawUser.mfaCode = null;
rawUser.mfaCodeUpdatedAt = new Date();
rawUser.singleUseCode = null;
rawUser.singleUseCodeUpdatedAt = new Date();
}

if (!authSuccess) {
// if we failed login validation
rawUser.failedLoginAttemptsCount += 1;
await this.updateStoredUser(
rawUser.mfaCode,
rawUser.mfaCodeUpdatedAt,
rawUser.singleUseCode,
rawUser.singleUseCodeUpdatedAt,
rawUser.phoneNumberVerified,
rawUser.failedLoginAttemptsCount,
rawUser.id,
Expand All @@ -161,8 +166,8 @@ export class MfaStrategy extends PassportStrategy(Strategy, 'mfa') {
}

await this.updateStoredUser(
rawUser.mfaCode,
rawUser.mfaCodeUpdatedAt,
rawUser.singleUseCode,
rawUser.singleUseCodeUpdatedAt,
rawUser.phoneNumberVerified,
rawUser.failedLoginAttemptsCount,
rawUser.id,
Expand All @@ -188,16 +193,16 @@ export class MfaStrategy extends PassportStrategy(Strategy, 'mfa') {
}

async updateStoredUser(
mfaCode: string,
mfaCodeUpdatedAt: Date,
singleUseCode: string,
singleUseCodeUpdatedAt: Date,
phoneNumberVerified: boolean,
failedLoginAttemptsCount: number,
userId: string,
): Promise<void> {
await this.prisma.userAccounts.update({
data: {
mfaCode,
mfaCodeUpdatedAt,
singleUseCode,
singleUseCodeUpdatedAt,
phoneNumberVerified,
failedLoginAttemptsCount,
lastLoginAt: new Date(),
Expand Down
12 changes: 6 additions & 6 deletions api/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,11 @@ export class AuthService {
}
}

const mfaCode = this.generateMfaCode();
const singleUseCode = this.generateSingleUseCode();
await this.prisma.userAccounts.update({
data: {
mfaCode,
mfaCodeUpdatedAt: new Date(),
singleUseCode,
singleUseCodeUpdatedAt: new Date(),
phoneNumber: user.phoneNumber,
},
where: {
Expand All @@ -232,9 +232,9 @@ export class AuthService {
});

if (dto.mfaType === MfaType.email) {
await this.emailsService.sendMfaCode(mapTo(User, user), mfaCode);
await this.emailsService.sendMfaCode(mapTo(User, user), singleUseCode);
} else if (dto.mfaType === MfaType.sms) {
await this.smsService.sendMfaCode(user.phoneNumber, mfaCode);
await this.smsService.sendMfaCode(user.phoneNumber, singleUseCode);
}

return dto.mfaType === MfaType.email
Expand Down Expand Up @@ -328,7 +328,7 @@ export class AuthService {
/*
generates a numeric mfa code
*/
generateMfaCode() {
generateSingleUseCode() {
let out = '';
const characters = '0123456789';
for (let i = 0; i < Number(process.env.MFA_CODE_LENGTH); i++) {
Expand Down
4 changes: 2 additions & 2 deletions api/src/services/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export class EmailService {
);
}

public async sendMfaCode(user: User, mfaCode: string) {
public async sendMfaCode(user: User, singleUseCode: string) {
const jurisdiction = await this.getJurisdiction(user.jurisdictions);
void (await this.loadTranslations(jurisdiction, user.language));
const emailFromAddress = await this.getEmailToSendFrom(
Expand All @@ -312,7 +312,7 @@ export class EmailService {
'Partners Portal account access token',
this.template('mfa-code')({
user: user,
mfaCodeOptions: { mfaCode },
mfaCodeOptions: { singleUseCode },
}),
);
}
Expand Down
4 changes: 2 additions & 2 deletions api/src/services/sms.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ export class SmsService {
}
public async sendMfaCode(
phoneNumber: string,
mfaCode: string,
singleUseCode: string,
): Promise<void> {
if (!this.client) {
return;
}
await this.client.messages.create({
body: `Your Partners Portal account access token: ${mfaCode}`,
body: `Your Partners Portal account access token: ${singleUseCode}`,
from: process.env.TWILIO_PHONE_NUMBER,
to: phoneNumber,
});
Expand Down
14 changes: 7 additions & 7 deletions api/test/integration/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Auth Controller Tests', () => {
const storedUser = await prisma.userAccounts.create({
data: await userFactory({
roles: { isAdmin: true },
mfaCode: 'abcdef',
singleUseCode: 'abcdef',
mfaEnabled: true,
confirmedAt: new Date(),
}),
Expand All @@ -54,7 +54,7 @@ describe('Auth Controller Tests', () => {
.send({
email: storedUser.email,
password: 'abcdef',
mfaCode: storedUser.mfaCode,
mfaCode: storedUser.singleUseCode,
mfaType: MfaType.email,
} as Login)
.expect(201);
Expand All @@ -78,7 +78,7 @@ describe('Auth Controller Tests', () => {
});

expect(loggedInUser.lastLoginAt).not.toBeNull();
expect(loggedInUser.mfaCode).toBeNull();
expect(loggedInUser.singleUseCode).toBeNull();
expect(loggedInUser.activeAccessToken).not.toBeNull();
expect(loggedInUser.activeRefreshToken).not.toBeNull();
});
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('Auth Controller Tests', () => {
});

expect(loggedInUser.lastLoginAt).not.toBeNull();
expect(loggedInUser.mfaCode).toBeNull();
expect(loggedInUser.singleUseCode).toBeNull();
expect(loggedInUser.activeAccessToken).not.toBeNull();
expect(loggedInUser.activeRefreshToken).not.toBeNull();
});
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('Auth Controller Tests', () => {
});

expect(loggedInUser.lastLoginAt).not.toBeNull();
expect(loggedInUser.mfaCode).toBeNull();
expect(loggedInUser.singleUseCode).toBeNull();
expect(loggedInUser.activeAccessToken).toBeNull();
expect(loggedInUser.activeRefreshToken).toBeNull();
});
Expand Down Expand Up @@ -208,8 +208,8 @@ describe('Auth Controller Tests', () => {
},
});

expect(user.mfaCode).not.toBeNull();
expect(user.mfaCodeUpdatedAt).not.toBeNull();
expect(user.singleUseCode).not.toBeNull();
expect(user.singleUseCodeUpdatedAt).not.toBeNull();
});

it('should update password', async () => {
Expand Down
3 changes: 3 additions & 0 deletions api/test/integration/jurisdiction.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe('Jurisdiction Controller Tests', () => {
enablePartnerSettings: true,
enableAccessibilityFeatures: true,
enableUtilitiesIncluded: true,
allowSingleUseCodeLogin: true,
listingApprovalPermissions: [],
};
const res = await request(app.getHttpServer())
Expand All @@ -149,6 +150,7 @@ describe('Jurisdiction Controller Tests', () => {
enablePartnerSettings: true,
enableAccessibilityFeatures: true,
enableUtilitiesIncluded: true,
allowSingleUseCodeLogin: true,
listingApprovalPermissions: [],
};
const res = await request(app.getHttpServer())
Expand Down Expand Up @@ -178,6 +180,7 @@ describe('Jurisdiction Controller Tests', () => {
enablePartnerSettings: true,
enableAccessibilityFeatures: true,
enableUtilitiesIncluded: true,
allowSingleUseCodeLogin: true,
listingApprovalPermissions: [],
};

Expand Down
2 changes: 2 additions & 0 deletions api/test/integration/permission-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const buildJurisdictionCreateMock = (
enablePartnerSettings: true,
enableAccessibilityFeatures: true,
enableUtilitiesIncluded: true,
allowSingleUseCodeLogin: true,
listingApprovalPermissions: [],
};
};
Expand All @@ -127,6 +128,7 @@ export const buildJurisdictionUpdateMock = (
enablePartnerSettings: true,
enableAccessibilityFeatures: true,
enableUtilitiesIncluded: true,
allowSingleUseCodeLogin: true,
listingApprovalPermissions: [],
};
};
Expand Down
Loading

0 comments on commit c514748

Please sign in to comment.