Skip to content
This repository was archived by the owner on Jul 6, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 54 additions & 11 deletions packages/snjs/lib/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ import { RemoteSession } from '.';
import { SNWebSocketsService } from './services/api/websockets_service';
import { SettingName } from '@standardnotes/settings';
import { SNSettingsService } from './services/settings_service';
import { SNMfaService } from './services/mfa_service';
import { SensitiveSettingName } from './services/settings_service/SensitiveSettingName';

/** How often to automatically sync, in milliseconds */
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000;
Expand Down Expand Up @@ -146,6 +148,7 @@ export class SNApplication {
private credentialService!: SNCredentialService;
private webSocketsService!: SNWebSocketsService;
private settingsService!: SNSettingsService;
private mfaService!: SNMfaService;

private eventHandlers: ApplicationObserver[] = [];
private services: PureService<any, any>[] = [];
Expand Down Expand Up @@ -455,7 +458,7 @@ export class SNApplication {
/**
* Finds an item by predicate.
*/
public getAll(uuids: UuidString[]): (SNItem | undefined)[] {
public getAll(uuids: UuidString[]): (SNItem | PurePayload | undefined)[] {
return this.itemManager.findItems(uuids);
}

Expand Down Expand Up @@ -1446,22 +1449,54 @@ export class SNApplication {
}

public async listSettings() {
return this.settingsService.settings().listSettings();
return this.settingsService.listSettings();
}

public async getSetting(name: SettingName) {
return this.settingsService.settings().getSetting(name);
return this.settingsService.getSetting(name);
}

public async updateSetting(name: SettingName, payload: string) {
return this.settingsService.settings().updateSetting(name, payload);
public async getSensitiveSetting(
name: SensitiveSettingName
): Promise<boolean> {
return this.settingsService.getSensitiveSetting(name);
}

public async updateSetting(
name: SettingName,
payload: string,
sensitive = false
) {
return this.settingsService.updateSetting(name, payload, sensitive);
}

public async deleteSetting(name: SettingName) {
return this.settingsService.settings().deleteSetting(name);
return this.settingsService.deleteSetting(name);
}

public downloadExternalFeature(url: string): Promise<SNComponent | undefined> {
public async isMfaActivated() {
return this.mfaService.isMfaActivated();
}

public async generateMfaSecret() {
return this.mfaService.generateMfaSecret();
}

public async getOtpToken(secret: string) {
return this.mfaService.getOtpToken(secret);
}

public async enableMfa(secret: string, otpToken: string) {
return this.mfaService.enableMfa(secret, otpToken);
}

public async disableMfa() {
return this.mfaService.disableMfa();
}

public downloadExternalFeature(
url: string
): Promise<SNComponent | undefined> {
return this.featuresService.downloadExternalFeature(url);
}

Expand Down Expand Up @@ -1496,6 +1531,7 @@ export class SNApplication {
this.createSettingsService();
this.createFeaturesService();
this.createMigrationService();
this.createMfaService();
}

private clearServices() {
Expand All @@ -1521,6 +1557,7 @@ export class SNApplication {
(this.credentialService as unknown) = undefined;
(this.webSocketsService as unknown) = undefined;
(this.settingsService as unknown) = undefined;
(this.mfaService as unknown) = undefined;

this.services = [];
}
Expand All @@ -1532,15 +1569,15 @@ export class SNApplication {
this.itemManager,
this.componentManager,
this.webSocketsService,
this.settingsService,
this.settingsService
);
this.services.push(this.featuresService);
}

private createWebSocketsService() {
this.webSocketsService = new SNWebSocketsService(
this.storageService,
this.webSocketUrl,
this.webSocketUrl
);
this.services.push(this.webSocketsService);
}
Expand Down Expand Up @@ -1579,7 +1616,7 @@ export class SNApplication {
this.apiService = new SNApiService(
this.httpService,
this.storageService,
this.defaultHost,
this.defaultHost
);
this.services.push(this.apiService);
}
Expand Down Expand Up @@ -1669,7 +1706,7 @@ export class SNApplication {
this.alertService,
this.protocolService,
this.challengeService,
this.webSocketsService,
this.webSocketsService
);
this.serviceObservers.push(
this.sessionManager.addEventObserver(async (event) => {
Expand Down Expand Up @@ -1809,6 +1846,12 @@ export class SNApplication {
this.sessionManager,
this.apiService
);
this.services.push(this.settingsService);
}

private createMfaService() {
this.mfaService = new SNMfaService(this.settingsService, this.crypto);
this.services.push(this.mfaService);
}

private getClass<T>(base: T) {
Expand Down
4 changes: 3 additions & 1 deletion packages/snjs/lib/services/api/api_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,13 @@ export class SNApiService extends PureService<
async updateSetting(
userUuid: UuidString,
settingName: string,
settingValue: string | null
settingValue: string | null,
sensitive: boolean
): Promise<UpdateSettingResponse> {
const params = {
name: settingName,
value: settingValue,
sensitive: sensitive,
};
return this.tokenRefreshableRequest<UpdateSettingResponse>({
verb: HttpVerb.Put,
Expand Down
4 changes: 4 additions & 0 deletions packages/snjs/lib/services/api/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum StatusCode {
HttpStatusInvalidSession = 401,
/** User's IP is rate-limited. */
HttpStatusForbidden = 403,
HttpBadRequest = 400,
}

type Error = {
Expand Down Expand Up @@ -210,9 +211,11 @@ type SettingData = {
uuid: string;
name: string;
value: string;
sensitive?: boolean;
};

export type MinimalHttpResponse = {
status?: StatusCode;
error?: Error;
};

Expand All @@ -223,6 +226,7 @@ export type ListSettingsResponse = MinimalHttpResponse & {
};
export type GetSettingResponse = MinimalHttpResponse & {
data?: {
success?: boolean;
setting?: SettingData;
};
};
Expand Down
6 changes: 2 additions & 4 deletions packages/snjs/lib/services/features_service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ describe('featuresService', () => {
webSocketsService.addEventObserver = jest.fn();

settingsService = {} as jest.Mocked<SNSettingsService>;
settingsService.settings = jest.fn().mockReturnValue({
updateSetting: jest.fn(),
});
settingsService.updateSetting = jest.fn();
});

describe('loadUserRoles()', () => {
Expand Down Expand Up @@ -302,7 +300,7 @@ describe('featuresService', () => {
}) as jest.Mocked<SNItem>;
const featuresService = createService();
await featuresService.updateExtensionKeySetting([extensionRepoItem]);
expect(settingsService.settings().updateSetting).toHaveBeenCalledWith(SettingName.ExtensionKey, extensionKey);
expect(settingsService.updateSetting).toHaveBeenCalledWith(SettingName.ExtensionKey, extensionKey, true);
});
})
});
3 changes: 1 addition & 2 deletions packages/snjs/lib/services/features_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ export class SNFeaturesService extends PureService<void> {
if (userKeyMatch && userKeyMatch.length > 0) {
const userKey = userKeyMatch[0];
await this.settingsService
.settings()
.updateSetting(SettingName.ExtensionKey, userKey);
.updateSetting(SettingName.ExtensionKey, userKey, true);
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions packages/snjs/lib/services/mfa_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SettingName } from '@standardnotes/settings';

import { SNSettingsService } from './settings_service';
import { PureService } from './pure_service';
import * as messages from './api/messages';
import { SNPureCrypto } from '@standardnotes/sncrypto-common';

export class SNMfaService extends PureService {
constructor(
private settingsService: SNSettingsService,
private crypto: SNPureCrypto
) {
super();
}

private async saveMfaSetting(secret: string): Promise<void> {
return await this.settingsService.updateSetting(
SettingName.MfaSecret,
secret,
true
);
}

async isMfaActivated() {
const mfaSetting = await this.settingsService.getSensitiveSetting(
SettingName.MfaSecret
);
return mfaSetting != null && mfaSetting != false;
}

async generateMfaSecret() {
return this.crypto.generateOtpSecret();
}

async getOtpToken(secret: string) {
return this.crypto.totpToken(secret, Date.now(), 6, 30);
}

async enableMfa(secret: string, otpToken: string) {
const otpTokenValid =
otpToken != null && otpToken === (await this.getOtpToken(secret));
if (!otpTokenValid) throw new Error(messages.SignInStrings.IncorrectMfa);

return this.saveMfaSetting(secret);
}

async disableMfa(): Promise<void> {
return await this.settingsService.deleteSetting(SettingName.MfaSecret);
}

deinit() {
(this.settingsService as unknown) = undefined;
(this.crypto as unknown) = undefined;
super.deinit();
}
}
27 changes: 22 additions & 5 deletions packages/snjs/lib/services/settings_service/SNSettingsService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { PureService } from '../pure_service';

import { SNApiService } from '../api/api_service';
import { SettingsProvider } from './SettingsProvider';
import { SettingsGateway } from './SettingsGateway';
import { SNSessionManager } from '../api/session_manager';
import { SettingName } from '@Lib/../../settings/dist';
import { SensitiveSettingName } from './SensitiveSettingName';

export class SNSettingsService extends PureService {
private _provider!: SettingsProvider;
private _provider!: SettingsGateway;

constructor(
private readonly sessionManager: SNSessionManager,
Expand All @@ -19,12 +20,28 @@ export class SNSettingsService extends PureService {
this._provider = new SettingsGateway(this.apiService, this.sessionManager);
}

settings(): SettingsProvider {
return this._provider;
async listSettings() {
return this._provider.listSettings();
}

async getSetting(name: SettingName) {
return this._provider.getSetting(name);
}

async updateSetting(name: SettingName, payload: string, sensitive: boolean) {
return this._provider.updateSetting(name, payload, sensitive);
}

async getSensitiveSetting(name: SensitiveSettingName) {
return this._provider.getSensitiveSetting(name);
}

async deleteSetting(name: SettingName) {
return this._provider.deleteSetting(name);
}

deinit(): void {
this._provider.deinit();
this._provider?.deinit();
(this._provider as unknown) = undefined;
(this.sessionManager as unknown) = undefined;
(this.apiService as unknown) = undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SettingName } from '@standardnotes/settings';

export type SensitiveSettingName =
| SettingName.MfaSecret
| SettingName.ExtensionKey;
Loading