Skip to content

Commit

Permalink
feat(newDeviceverification) : adding state structure to track verify …
Browse files Browse the repository at this point in the history
…devices for active user; added API call to server.
  • Loading branch information
ike-kottlowski committed Jan 10, 2025
1 parent 707675b commit d900890
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 2 deletions.
16 changes: 14 additions & 2 deletions libs/common/src/auth/abstractions/account-api.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RegisterFinishRequest } from "../models/request/registration/register-finish.request";
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request";
import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request";
import { Verification } from "../types/verification";

export abstract class AccountApiService {
Expand All @@ -18,7 +19,7 @@ export abstract class AccountApiService {
*
* @param request - The request object containing
* information needed to send the verification email, such as the user's email address.
* @returns A promise that resolves to a string tokencontaining the user's encrypted
* @returns A promise that resolves to a string token containing the user's encrypted
* information which must be submitted to complete registration or `null` if
* email verification is enabled (users must get the token by clicking a
* link in the email that will be sent to them).
Expand All @@ -33,7 +34,7 @@ export abstract class AccountApiService {
*
* @param request - The request object containing the email verification token and the
* user's email address (which is required to validate the token)
* @returns A promise that resolves when the event is logged on the server succcessfully or a bad
* @returns A promise that resolves when the event is logged on the server successfully or a bad
* request if the token is invalid for any reason.
*/
abstract registerVerificationEmailClicked(
Expand All @@ -50,4 +51,15 @@ export abstract class AccountApiService {
* registration process is successfully completed.
*/
abstract registerFinish(request: RegisterFinishRequest): Promise<string>;

/**
* Sets the [dbo].[User].[VerifyDevices] flag to true or false.
*
* @param request - The request object is a SecretVerificationRequest extension
* that also contains the boolean value that the VerifyDevices property is being
* set to.
* @returns A promise that resolves when the process is successfully completed or
* a bad request if secret verification fails.
*/
abstract setVerifyDevices(request: SetVerifyDevicesRequest): Promise<string>;
}
8 changes: 8 additions & 0 deletions libs/common/src/auth/abstractions/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export abstract class AccountService {
* Observable of the last activity time for each account.
*/
accountActivity$: Observable<Record<UserId, Date>>;
/** Observable of the new device login property for each account. */
accountVerifyDevices$: Observable<boolean>;
/** Account list in order of descending recency */
sortedUserIds$: Observable<UserId[]>;
/** Next account that is not the current active account */
Expand Down Expand Up @@ -77,6 +79,12 @@ export abstract class AccountService {
* Updates the `activeAccount$` observable with the new active account.
* @param userId
*/
/**
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
* @param userId
* @param VerifyNewDeviceLogin
*/
abstract setAccountVerifyDevices(userId: UserId, verifyNewDeviceLogin: boolean): Promise<void>;
abstract switchAccount(userId: UserId | null): Promise<void>;
/**
* Cleans personal information for the given account from the `accounts$` observable. Does not remove the userId from the observable.
Expand Down
18 changes: 18 additions & 0 deletions libs/common/src/auth/services/account-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UserVerificationService } from "../abstractions/user-verification/user-
import { RegisterFinishRequest } from "../models/request/registration/register-finish.request";
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request";
import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request";
import { Verification } from "../types/verification";

export class AccountApiServiceImplementation implements AccountApiService {
Expand Down Expand Up @@ -102,4 +103,21 @@ export class AccountApiServiceImplementation implements AccountApiService {
throw e;
}
}

async setVerifyDevices(request: SetVerifyDevicesRequest): Promise<string> {
try {
const response = await this.apiService.send(
"POST",
"/accounts/verify-devices",
request,
true,
true,
);

return response;
} catch (e: unknown) {
this.logService.error(e);
throw e;
}
}
}
34 changes: 34 additions & 0 deletions libs/common/src/auth/services/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { Utils } from "../../platform/misc/utils";
import {
ACCOUNT_DISK,
ActiveUserStateProvider,
GlobalState,
GlobalStateProvider,
KeyDefinition,
UserKeyDefinition,
} from "../../platform/state";
import { UserId } from "../../types/guid";

Expand All @@ -42,6 +44,15 @@ export const ACCOUNT_ACTIVITY = KeyDefinition.record<Date, UserId>(ACCOUNT_DISK,
deserializer: (activity) => new Date(activity),
});

export const ACCOUNT_VERIFY_NEW_DEVICE_LOGIN = new UserKeyDefinition<boolean>(
ACCOUNT_DISK,
"verifyNewDeviceLogin",
{
deserializer: (verifyDevices) => verifyDevices,
clearOn: ["logout"],
},
);

const LOGGED_OUT_INFO: AccountInfo = {
email: "",
emailVerified: false,
Expand Down Expand Up @@ -73,13 +84,15 @@ export class AccountServiceImplementation implements InternalAccountService {
accounts$: Observable<Record<UserId, AccountInfo>>;
activeAccount$: Observable<Account | null>;
accountActivity$: Observable<Record<UserId, Date>>;
accountVerifyDevices$: Observable<boolean>;
sortedUserIds$: Observable<UserId[]>;
nextUpAccount$: Observable<Account>;

constructor(
private messagingService: MessagingService,
private logService: LogService,
private globalStateProvider: GlobalStateProvider,
private activeUserStateProvider: ActiveUserStateProvider,
) {
this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS);
this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID);
Expand Down Expand Up @@ -114,6 +127,10 @@ export class AccountServiceImplementation implements InternalAccountService {
return nextId ? { id: nextId, ...accounts[nextId] } : null;
}),
);

this.accountVerifyDevices$ = this.activeUserStateProvider
.get(ACCOUNT_VERIFY_NEW_DEVICE_LOGIN)
.state$.pipe(map((verifyDevices) => verifyDevices ?? true));
}

async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
Expand Down Expand Up @@ -193,6 +210,23 @@ export class AccountServiceImplementation implements InternalAccountService {
);
}

async setAccountVerifyDevices(userId: UserId, setVerifyNewDeviceLogin: boolean): Promise<void> {
if (!Utils.isGuid(userId)) {
// only store for valid userIds
return;
}

await this.activeUserStateProvider.get(ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).update(
() => {
return setVerifyNewDeviceLogin;
},
{
shouldUpdate: (oldNewDeviceVerification) =>
oldNewDeviceVerification !== setVerifyNewDeviceLogin,
},
);
}

async removeAccountActivity(userId: UserId): Promise<void> {
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
(activity) => {
Expand Down
2 changes: 2 additions & 0 deletions libs/common/src/enums/feature-flag.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum FeatureFlag {
PM12443RemovePagingLogic = "pm-12443-remove-paging-logic",
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
NewDeviceVerification = "new-device-verification",
}

export type AllowedFeatureFlagTypes = boolean | number | string;
Expand Down Expand Up @@ -100,6 +101,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM12443RemovePagingLogic]: FALSE,
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
[FeatureFlag.NewDeviceVerification]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
Expand Down
2 changes: 2 additions & 0 deletions libs/common/src/models/response/profile.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class ProfileResponse extends BaseResponse {
securityStamp: string;
forcePasswordReset: boolean;
usesKeyConnector: boolean;
verifyDevices: boolean;
organizations: ProfileOrganizationResponse[] = [];
providers: ProfileProviderResponse[] = [];
providerOrganizations: ProfileProviderOrganizationResponse[] = [];
Expand All @@ -42,6 +43,7 @@ export class ProfileResponse extends BaseResponse {
this.securityStamp = this.getResponseProperty("SecurityStamp");
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
this.verifyDevices = this.getResponseProperty("VerifyDevices") ?? true;

const organizations = this.getResponseProperty("Organizations");
if (organizations != null) {
Expand Down
1 change: 1 addition & 0 deletions libs/common/src/platform/sync/default-sync.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export class DefaultSyncService extends CoreSyncService {
await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor);
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
await this.accountService.setAccountVerifyDevices(response.id, response.verifyDevices);

await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,
Expand Down

0 comments on commit d900890

Please sign in to comment.