mirror of
https://github.com/bitwarden/jslib
synced 2025-12-10 21:33:17 +00:00
Split jslib into multiple modules (#363)
* Split jslib into multiple modules
This commit is contained in:
1280
node/package-lock.json
generated
Normal file
1280
node/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
node/package.json
Normal file
40
node/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@bitwarden/jslib-node",
|
||||
"version": "0.0.0",
|
||||
"description": "Common code used across Bitwarden JavaScript projects.",
|
||||
"keywords": [
|
||||
"bitwarden"
|
||||
],
|
||||
"author": "Bitwarden Inc.",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/jslib"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch",
|
||||
"lint": "tslint 'src/**/*.ts' 'spec/**/*.ts'",
|
||||
"lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/inquirer": "^7.3.1",
|
||||
"@types/lowdb": "^1.0.10",
|
||||
"@types/node": "^14.17.1",
|
||||
"@types/node-fetch": "^2.5.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bitwarden/jslib-common": "file:../common",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "7.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"inquirer": "8.0.0",
|
||||
"lowdb": "1.0.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
108
node/src/cli/baseProgram.ts
Normal file
108
node/src/cli/baseProgram.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as chalk from 'chalk';
|
||||
|
||||
import { Response } from './models/response';
|
||||
import { ListResponse } from './models/response/listResponse';
|
||||
import { MessageResponse } from './models/response/messageResponse';
|
||||
import { StringResponse } from './models/response/stringResponse';
|
||||
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
|
||||
export abstract class BaseProgram {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private writeLn: (s: string, finalLine: boolean, error: boolean) => void) { }
|
||||
|
||||
protected processResponse(response: Response, exitImmediately = false, dataProcessor: () => string = null) {
|
||||
if (!response.success) {
|
||||
if (process.env.BW_QUIET !== 'true') {
|
||||
if (process.env.BW_RESPONSE === 'true') {
|
||||
this.writeLn(this.getJson(response), true, false);
|
||||
} else {
|
||||
this.writeLn(chalk.redBright(response.message), true, true);
|
||||
}
|
||||
}
|
||||
const exitCode = process.env.BW_CLEANEXIT ? 0 : 1;
|
||||
if (exitImmediately) {
|
||||
process.exit(exitCode);
|
||||
} else {
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.BW_RESPONSE === 'true') {
|
||||
this.writeLn(this.getJson(response), true, false);
|
||||
} else if (response.data != null) {
|
||||
let out: string = dataProcessor != null ? dataProcessor() : null;
|
||||
if (out == null) {
|
||||
if (response.data.object === 'string') {
|
||||
const data = (response.data as StringResponse).data;
|
||||
if (data != null) {
|
||||
out = data;
|
||||
}
|
||||
} else if (response.data.object === 'list') {
|
||||
out = this.getJson((response.data as ListResponse).data);
|
||||
} else if (response.data.object === 'message') {
|
||||
out = this.getMessage(response);
|
||||
} else {
|
||||
out = this.getJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (out != null && process.env.BW_QUIET !== 'true') {
|
||||
this.writeLn(out, true, false);
|
||||
}
|
||||
}
|
||||
if (exitImmediately) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exitCode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected getJson(obj: any): string {
|
||||
if (process.env.BW_PRETTY === 'true') {
|
||||
return JSON.stringify(obj, null, ' ');
|
||||
} else {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMessage(response: Response): string {
|
||||
const message = (response.data as MessageResponse);
|
||||
if (process.env.BW_RAW === 'true') {
|
||||
return message.raw;
|
||||
}
|
||||
|
||||
let out: string = '';
|
||||
if (message.title != null) {
|
||||
if (message.noColor) {
|
||||
out = message.title;
|
||||
} else {
|
||||
out = chalk.greenBright(message.title);
|
||||
}
|
||||
}
|
||||
if (message.message != null) {
|
||||
if (message.title != null) {
|
||||
out += '\n';
|
||||
}
|
||||
out += message.message;
|
||||
}
|
||||
return out.trim() === '' ? null : out;
|
||||
}
|
||||
|
||||
protected async exitIfAuthed() {
|
||||
const authed = await this.userService.isAuthenticated();
|
||||
if (authed) {
|
||||
const email = await this.userService.getEmail();
|
||||
this.processResponse(Response.error('You are already logged in as ' + email + '.'), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected async exitIfNotAuthed() {
|
||||
const authed = await this.userService.isAuthenticated();
|
||||
if (!authed) {
|
||||
this.processResponse(Response.error('You are not logged in.'), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
316
node/src/cli/commands/login.command.ts
Normal file
316
node/src/cli/commands/login.command.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import * as program from 'commander';
|
||||
import * as http from 'http';
|
||||
import * as inquirer from 'inquirer';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
|
||||
|
||||
import { AuthResult } from 'jslib-common/models/domain/authResult';
|
||||
import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest';
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
|
||||
import { Response } from '../models/response';
|
||||
|
||||
import { MessageResponse } from '../models/response/messageResponse';
|
||||
|
||||
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
|
||||
// tslint:disable-next-line
|
||||
const open = require('open');
|
||||
|
||||
export class LoginCommand {
|
||||
protected validatedParams: () => Promise<any>;
|
||||
protected success: () => Promise<MessageResponse>;
|
||||
protected canInteract: boolean;
|
||||
protected clientId: string;
|
||||
|
||||
private ssoRedirectUri: string = null;
|
||||
|
||||
constructor(protected authService: AuthService, protected apiService: ApiService,
|
||||
protected i18nService: I18nService, protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationService,
|
||||
protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService,
|
||||
clientId: string) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
async run(email: string, password: string, options: program.OptionValues) {
|
||||
this.canInteract = process.env.BW_NOINTERACTION !== 'true';
|
||||
|
||||
let ssoCodeVerifier: string = null;
|
||||
let ssoCode: string = null;
|
||||
|
||||
let clientId: string = null;
|
||||
let clientSecret: string = null;
|
||||
|
||||
if (options.apikey != null) {
|
||||
const storedClientId: string = process.env.BW_CLIENTID;
|
||||
const storedClientSecret: string = process.env.BW_CLIENTSECRET;
|
||||
if (storedClientId == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'input',
|
||||
name: 'clientId',
|
||||
message: 'client_id:',
|
||||
});
|
||||
clientId = answer.clientId;
|
||||
} else {
|
||||
clientId = null;
|
||||
}
|
||||
} else {
|
||||
clientId = storedClientId;
|
||||
}
|
||||
if (this.canInteract && storedClientSecret == null) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'input',
|
||||
name: 'clientSecret',
|
||||
message: 'client_secret:',
|
||||
});
|
||||
clientSecret = answer.clientSecret;
|
||||
} else {
|
||||
clientSecret = storedClientSecret;
|
||||
}
|
||||
} else if (options.sso != null && this.canInteract) {
|
||||
const passwordOptions: any = {
|
||||
type: 'password',
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256');
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
try {
|
||||
ssoCode = await this.getSsoCode(codeChallenge, state);
|
||||
} catch {
|
||||
return Response.badRequest('Something went wrong. Try again.');
|
||||
}
|
||||
} else {
|
||||
if ((email == null || email === '') && this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'input',
|
||||
name: 'email',
|
||||
message: 'Email address:',
|
||||
});
|
||||
email = answer.email;
|
||||
}
|
||||
if (email == null || email.trim() === '') {
|
||||
return Response.badRequest('Email address is required.');
|
||||
}
|
||||
if (email.indexOf('@') === -1) {
|
||||
return Response.badRequest('Email address is invalid.');
|
||||
}
|
||||
|
||||
if (password == null || password === '') {
|
||||
if (options.passwordfile) {
|
||||
password = await NodeUtils.readFirstLine(options.passwordfile);
|
||||
} else if (options.passwordenv && process.env[options.passwordenv]) {
|
||||
password = process.env[options.passwordenv];
|
||||
} else if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Master password:',
|
||||
});
|
||||
password = answer.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (password == null || password === '') {
|
||||
return Response.badRequest('Master password is required.');
|
||||
}
|
||||
}
|
||||
|
||||
let twoFactorToken: string = options.code;
|
||||
let twoFactorMethod: TwoFactorProviderType = null;
|
||||
try {
|
||||
if (options.method != null) {
|
||||
twoFactorMethod = parseInt(options.method, null);
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error('Invalid two-step login method.');
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.validatedParams != null) {
|
||||
await this.validatedParams();
|
||||
}
|
||||
|
||||
let response: AuthResult = null;
|
||||
if (twoFactorToken != null && twoFactorMethod != null) {
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logInApiKeyComplete(clientId, clientSecret, twoFactorMethod,
|
||||
twoFactorToken, false);
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logInSsoComplete(ssoCode, ssoCodeVerifier, this.ssoRedirectUri,
|
||||
twoFactorMethod, twoFactorToken, false);
|
||||
} else {
|
||||
response = await this.authService.logInComplete(email, password, twoFactorMethod,
|
||||
twoFactorToken, false);
|
||||
}
|
||||
} else {
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logInApiKey(clientId, clientSecret);
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri);
|
||||
} else {
|
||||
response = await this.authService.logIn(email, password);
|
||||
}
|
||||
if (response.twoFactor) {
|
||||
let selectedProvider: any = null;
|
||||
const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest('No providers available for this client.');
|
||||
}
|
||||
|
||||
if (twoFactorMethod != null) {
|
||||
try {
|
||||
selectedProvider = twoFactorProviders.filter(p => p.type === twoFactorMethod)[0];
|
||||
} catch (e) {
|
||||
return Response.error('Invalid two-step login method.');
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider == null) {
|
||||
if (twoFactorProviders.length === 1) {
|
||||
selectedProvider = twoFactorProviders[0];
|
||||
} else if (this.canInteract) {
|
||||
const twoFactorOptions = twoFactorProviders.map(p => p.name);
|
||||
twoFactorOptions.push(new inquirer.Separator());
|
||||
twoFactorOptions.push('Cancel');
|
||||
const answer: inquirer.Answers =
|
||||
await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'list',
|
||||
name: 'method',
|
||||
message: 'Two-step login method:',
|
||||
choices: twoFactorOptions,
|
||||
});
|
||||
const i = twoFactorOptions.indexOf(answer.method);
|
||||
if (i === (twoFactorOptions.length - 1)) {
|
||||
return Response.error('Login failed.');
|
||||
}
|
||||
selectedProvider = twoFactorProviders[i];
|
||||
}
|
||||
if (selectedProvider == null) {
|
||||
return Response.error('Login failed. No provider selected.');
|
||||
}
|
||||
}
|
||||
|
||||
if (twoFactorToken == null && response.twoFactorProviders.size > 1 &&
|
||||
selectedProvider.type === TwoFactorProviderType.Email) {
|
||||
const emailReq = new TwoFactorEmailRequest(this.authService.email,
|
||||
this.authService.masterPasswordHash);
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
|
||||
if (twoFactorToken == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers =
|
||||
await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'input',
|
||||
name: 'token',
|
||||
message: 'Two-step login code:',
|
||||
});
|
||||
twoFactorToken = answer.token;
|
||||
}
|
||||
if (twoFactorToken == null || twoFactorToken === '') {
|
||||
return Response.badRequest('Code is required.');
|
||||
}
|
||||
}
|
||||
|
||||
response = await this.authService.logInTwoFactor(selectedProvider.type,
|
||||
twoFactorToken, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.twoFactor) {
|
||||
return Response.error('Login failed.');
|
||||
}
|
||||
|
||||
if (response.resetMasterPassword) {
|
||||
return Response.error('In order to log in with SSO from the CLI, you must first log in' +
|
||||
' through the web vault to set your master password.');
|
||||
}
|
||||
|
||||
if (this.success != null) {
|
||||
const res = await this.success();
|
||||
return Response.success(res);
|
||||
} else {
|
||||
const res = new MessageResponse('You are logged in!', null);
|
||||
return Response.success(res);
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getSsoCode(codeChallenge: string, state: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callbackServer = http.createServer((req, res) => {
|
||||
const urlString = 'http://localhost' + req.url;
|
||||
const url = new URL(urlString);
|
||||
const code = url.searchParams.get('code');
|
||||
const receivedState = url.searchParams.get('state');
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
|
||||
res.writeHead(200);
|
||||
res.end('<html><head><title>Success | Bitwarden CLI</title></head><body>' +
|
||||
'<h1>Successfully authenticated with the Bitwarden CLI</h1>' +
|
||||
'<p>You may now close this tab and return to the terminal.</p>' +
|
||||
'</body></html>');
|
||||
callbackServer.close(() => resolve(code));
|
||||
} else {
|
||||
res.writeHead(400);
|
||||
res.end('<html><head><title>Failed | Bitwarden CLI</title></head><body>' +
|
||||
'<h1>Something went wrong logging into the Bitwarden CLI</h1>' +
|
||||
'<p>You may now close this tab and return to the terminal.</p>' +
|
||||
'</body></html>');
|
||||
callbackServer.close(() => reject());
|
||||
}
|
||||
});
|
||||
let foundPort = false;
|
||||
let webUrl = this.environmentService.getWebVaultUrl();
|
||||
if (webUrl == null) {
|
||||
webUrl = 'https://vault.bitwarden.com';
|
||||
}
|
||||
for (let port = 8065; port <= 8070; port++) {
|
||||
try {
|
||||
this.ssoRedirectUri = 'http://localhost:' + port;
|
||||
callbackServer.listen(port, () => {
|
||||
this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + this.clientId +
|
||||
'&redirectUri=' + encodeURIComponent(this.ssoRedirectUri) +
|
||||
'&state=' + state + '&codeChallenge=' + codeChallenge);
|
||||
});
|
||||
foundPort = true;
|
||||
break;
|
||||
} catch { }
|
||||
}
|
||||
if (!foundPort) {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkState(state: string, checkState: string): boolean {
|
||||
if (state === null || state === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (checkState === null || checkState === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateSplit = state.split('_identifier=');
|
||||
const checkStateSplit = checkState.split('_identifier=');
|
||||
return stateSplit[0] === checkStateSplit[0];
|
||||
}
|
||||
}
|
||||
19
node/src/cli/commands/logout.command.ts
Normal file
19
node/src/cli/commands/logout.command.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
|
||||
import { Response } from '../models/response';
|
||||
import { MessageResponse } from '../models/response/messageResponse';
|
||||
|
||||
export class LogoutCommand {
|
||||
constructor(private authService: AuthService, private i18nService: I18nService,
|
||||
private logoutCallback: () => Promise<void>) { }
|
||||
|
||||
async run() {
|
||||
await this.logoutCallback();
|
||||
this.authService.logOut(() => { /* Do nothing */ });
|
||||
const res = new MessageResponse('You have logged out.', null);
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
86
node/src/cli/commands/update.command.ts
Normal file
86
node/src/cli/commands/update.command.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as program from 'commander';
|
||||
import * as fetch from 'node-fetch';
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
|
||||
import { Response } from '../models/response';
|
||||
import { MessageResponse } from '../models/response/messageResponse';
|
||||
|
||||
export class UpdateCommand {
|
||||
inPkg: boolean = false;
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private repoName: string, private executableName: string, private showExtendedMessage: boolean) {
|
||||
this.inPkg = !!(process as any).pkg;
|
||||
}
|
||||
|
||||
async run(): Promise<Response> {
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
|
||||
const response = await fetch.default('https://api.github.com/repos/bitwarden/' +
|
||||
this.repoName + '/releases/latest');
|
||||
if (response.status === 200) {
|
||||
const responseJson = await response.json();
|
||||
const res = new MessageResponse(null, null);
|
||||
|
||||
const tagName: string = responseJson.tag_name;
|
||||
if (tagName === ('v' + currentVersion)) {
|
||||
res.title = 'No update available.';
|
||||
res.noColor = true;
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
let downloadUrl: string = null;
|
||||
if (responseJson.assets != null) {
|
||||
for (const a of responseJson.assets) {
|
||||
const download: string = a.browser_download_url;
|
||||
if (download == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (download.indexOf('.zip') === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && download.indexOf(this.executableName + '-windows') > -1) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
} else if (process.platform === 'darwin' && download.indexOf(this.executableName + '-macos') > -1) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
} else if (process.platform === 'linux' && download.indexOf(this.executableName + '-linux') > -1) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.title = 'A new version is available: ' + tagName;
|
||||
if (downloadUrl == null) {
|
||||
downloadUrl = 'https://github.com/bitwarden/' + this.repoName + '/releases';
|
||||
} else {
|
||||
res.raw = downloadUrl;
|
||||
}
|
||||
res.message = '';
|
||||
if (responseJson.body != null && responseJson.body !== '') {
|
||||
res.message = responseJson.body + '\n\n';
|
||||
}
|
||||
|
||||
res.message += 'You can download this update at ' + downloadUrl;
|
||||
|
||||
if (this.showExtendedMessage) {
|
||||
if (this.inPkg) {
|
||||
res.message += '\n\nIf you installed this CLI through a package manager ' +
|
||||
'you should probably update using its update command instead.';
|
||||
} else {
|
||||
res.message += '\n\nIf you installed this CLI through NPM ' +
|
||||
'you should update using `npm install -g @bitwarden/' + this.repoName + '`';
|
||||
}
|
||||
}
|
||||
return Response.success(res);
|
||||
} else {
|
||||
return Response.error('Error contacting update API: ' + response.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
node/src/cli/models/response.ts
Normal file
45
node/src/cli/models/response.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BaseResponse } from './response/baseResponse';
|
||||
|
||||
export class Response {
|
||||
static error(error: any, data?: any): Response {
|
||||
const res = new Response();
|
||||
res.success = false;
|
||||
if (typeof (error) === 'string') {
|
||||
res.message = error;
|
||||
} else {
|
||||
res.message = error.message != null ? error.message :
|
||||
error.toString() === '[object Object]' ? JSON.stringify(error) : error.toString();
|
||||
}
|
||||
res.data = data;
|
||||
return res;
|
||||
}
|
||||
|
||||
static notFound(): Response {
|
||||
return Response.error('Not found.');
|
||||
}
|
||||
|
||||
static badRequest(message: string): Response {
|
||||
return Response.error(message);
|
||||
}
|
||||
|
||||
static multipleResults(ids: string[]): Response {
|
||||
let msg = 'More than one result was found. Try getting a specific object by `id` instead. ' +
|
||||
'The following objects were found:';
|
||||
ids.forEach(id => {
|
||||
msg += '\n' + id;
|
||||
});
|
||||
return Response.error(msg, ids);
|
||||
}
|
||||
|
||||
static success(data?: BaseResponse): Response {
|
||||
const res = new Response();
|
||||
res.success = true;
|
||||
res.data = data;
|
||||
return res;
|
||||
}
|
||||
|
||||
success: boolean;
|
||||
message: string;
|
||||
errorCode: number;
|
||||
data: BaseResponse;
|
||||
}
|
||||
3
node/src/cli/models/response/baseResponse.ts
Normal file
3
node/src/cli/models/response/baseResponse.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface BaseResponse {
|
||||
object: string;
|
||||
}
|
||||
11
node/src/cli/models/response/listResponse.ts
Normal file
11
node/src/cli/models/response/listResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseResponse } from './baseResponse';
|
||||
|
||||
export class ListResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: BaseResponse[];
|
||||
|
||||
constructor(data: BaseResponse[]) {
|
||||
this.object = 'list';
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
15
node/src/cli/models/response/messageResponse.ts
Normal file
15
node/src/cli/models/response/messageResponse.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { BaseResponse } from './baseResponse';
|
||||
|
||||
export class MessageResponse implements BaseResponse {
|
||||
object: string;
|
||||
title: string;
|
||||
message: string;
|
||||
raw: string;
|
||||
noColor = false;
|
||||
|
||||
constructor(title: string, message: string) {
|
||||
this.object = 'message';
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
11
node/src/cli/models/response/stringResponse.ts
Normal file
11
node/src/cli/models/response/stringResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseResponse } from './baseResponse';
|
||||
|
||||
export class StringResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
this.object = 'string';
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
157
node/src/cli/services/cliPlatformUtils.service.ts
Normal file
157
node/src/cli/services/cliPlatformUtils.service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import * as child_process from 'child_process';
|
||||
|
||||
import { DeviceType } from 'jslib-common/enums/deviceType';
|
||||
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
|
||||
// tslint:disable-next-line
|
||||
const open = require('open');
|
||||
|
||||
export class CliPlatformUtilsService implements PlatformUtilsService {
|
||||
identityClientId: string;
|
||||
|
||||
private deviceCache: DeviceType = null;
|
||||
|
||||
constructor(identityClientId: string, private packageJson: any) {
|
||||
this.identityClientId = identityClientId;
|
||||
}
|
||||
|
||||
getDevice(): DeviceType {
|
||||
if (!this.deviceCache) {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
this.deviceCache = DeviceType.WindowsDesktop;
|
||||
break;
|
||||
case 'darwin':
|
||||
this.deviceCache = DeviceType.MacOsDesktop;
|
||||
break;
|
||||
case 'linux':
|
||||
default:
|
||||
this.deviceCache = DeviceType.LinuxDesktop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.deviceCache;
|
||||
}
|
||||
|
||||
getDeviceString(): string {
|
||||
const device = DeviceType[this.getDevice()].toLowerCase();
|
||||
return device.replace('desktop', '');
|
||||
}
|
||||
|
||||
isFirefox() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isChrome() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isEdge() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isOpera() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isVivaldi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSafari() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isIE() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMacAppStore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isViewOpen() {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
lockTimeout(): number {
|
||||
return null;
|
||||
}
|
||||
|
||||
launchUri(uri: string, options?: any): void {
|
||||
if (process.platform === 'linux') {
|
||||
child_process.spawnSync('xdg-open', [uri]);
|
||||
} else {
|
||||
open(uri);
|
||||
}
|
||||
}
|
||||
|
||||
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
getApplicationVersion(): Promise<string> {
|
||||
return Promise.resolve(this.packageJson.version);
|
||||
}
|
||||
|
||||
supportsWebAuthn(win: Window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsDuo(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[],
|
||||
options?: any): void {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string):
|
||||
Promise<boolean> {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise<boolean>):
|
||||
Promise<boolean> {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
isDev(): boolean {
|
||||
return process.env.BWCLI_ENV === 'development';
|
||||
}
|
||||
|
||||
isSelfHost(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
readFromClipboard(options?: any): Promise<string> {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
supportsBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
getDefaultSystemTheme() {
|
||||
return Promise.resolve('light' as 'light' | 'dark');
|
||||
}
|
||||
|
||||
onDefaultSystemThemeChange() {
|
||||
/* noop */
|
||||
}
|
||||
|
||||
supportsSecureStorage(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
23
node/src/cli/services/consoleLog.service.ts
Normal file
23
node/src/cli/services/consoleLog.service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { LogLevelType } from 'jslib-common/enums/logLevelType';
|
||||
|
||||
import { ConsoleLogService as BaseConsoleLogService } from 'jslib-common/services/consoleLog.service';
|
||||
|
||||
export class ConsoleLogService extends BaseConsoleLogService {
|
||||
constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) {
|
||||
super(isDev, filter);
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message: string) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.BW_RESPONSE === 'true') {
|
||||
// tslint:disable-next-line
|
||||
console.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
super.write(level, message);
|
||||
}
|
||||
}
|
||||
0
node/src/globals.d.ts
vendored
Normal file
0
node/src/globals.d.ts
vendored
Normal file
115
node/src/services/lowdbStorage.service.ts
Normal file
115
node/src/services/lowdbStorage.service.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as fs from 'fs';
|
||||
import * as lowdb from 'lowdb';
|
||||
import * as FileSync from 'lowdb/adapters/FileSync';
|
||||
import * as path from 'path';
|
||||
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
|
||||
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
|
||||
export class LowdbStorageService implements StorageService {
|
||||
protected dataFilePath: string;
|
||||
private db: lowdb.LowdbSync<any>;
|
||||
private defaults: any;
|
||||
|
||||
constructor(protected logService: LogService, defaults?: any, private dir?: string, private allowCache = false) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.logService.info('Initializing lowdb storage service.');
|
||||
let adapter: lowdb.AdapterSync<any>;
|
||||
if (Utils.isNode && this.dir != null) {
|
||||
if (!fs.existsSync(this.dir)) {
|
||||
this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`);
|
||||
NodeUtils.mkdirpSync(this.dir, '700');
|
||||
this.logService.info(`Created dir "${this.dir}".`);
|
||||
}
|
||||
this.dataFilePath = path.join(this.dir, 'data.json');
|
||||
if (!fs.existsSync(this.dataFilePath)) {
|
||||
this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`);
|
||||
fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 });
|
||||
fs.chmodSync(this.dataFilePath, 0o600);
|
||||
this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`);
|
||||
} else {
|
||||
this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`);
|
||||
}
|
||||
await this.lockDbFile(() => {
|
||||
adapter = new FileSync(this.dataFilePath);
|
||||
});
|
||||
}
|
||||
try {
|
||||
this.logService.info('Attempting to create lowdb storage adapter.');
|
||||
this.db = lowdb(adapter);
|
||||
this.logService.info('Successfully created lowdb storage adapter.');
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.logService.warning(`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`);
|
||||
if (fs.existsSync(this.dataFilePath)) {
|
||||
const backupPath = this.dataFilePath + '.bak';
|
||||
this.logService.warning(`Writing backup of data file to ${backupPath}`);
|
||||
await fs.copyFile(this.dataFilePath, backupPath, err => {
|
||||
this.logService.warning(`Error while creating data file backup, "${e.message}". No backup may have been created.`);
|
||||
});
|
||||
}
|
||||
adapter.write({});
|
||||
this.db = lowdb(adapter);
|
||||
} else {
|
||||
this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.defaults != null) {
|
||||
this.lockDbFile(() => {
|
||||
this.logService.info('Writing defaults.');
|
||||
this.readForNoCache();
|
||||
this.db.defaults(this.defaults).write();
|
||||
this.logService.info('Successfully wrote defaults to db.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
const val = this.db.get(key).value();
|
||||
this.logService.debug(`Successfully read ${key} from db`);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
return val as T;
|
||||
});
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.set(key, obj).write();
|
||||
this.logService.debug(`Successfully wrote ${key} to db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.unset(key).write();
|
||||
this.logService.debug(`Successfully removed ${key} from db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
protected async lockDbFile<T>(action: () => T): Promise<T> {
|
||||
// Lock methods implemented in clients
|
||||
return Promise.resolve(action());
|
||||
}
|
||||
|
||||
private readForNoCache() {
|
||||
if (!this.allowCache) {
|
||||
this.db.read();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
node/src/services/nodeApi.service.ts
Normal file
29
node/src/services/nodeApi.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as FormData from 'form-data';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import * as fe from 'node-fetch';
|
||||
|
||||
import { ApiService } from 'jslib-common/services/api.service';
|
||||
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
||||
|
||||
(global as any).fetch = fe.default;
|
||||
(global as any).Request = fe.Request;
|
||||
(global as any).Response = fe.Response;
|
||||
(global as any).Headers = fe.Headers;
|
||||
(global as any).FormData = FormData;
|
||||
|
||||
export class NodeApiService extends ApiService {
|
||||
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService,
|
||||
logoutCallback: (expired: boolean) => Promise<void>, customUserAgent: string = null) {
|
||||
super(tokenService, platformUtilsService, logoutCallback, customUserAgent);
|
||||
}
|
||||
|
||||
nativeFetch(request: Request): Promise<Response> {
|
||||
const proxy = process.env.http_proxy || process.env.https_proxy;
|
||||
if (proxy) {
|
||||
(request as any).agent = new HttpsProxyAgent(proxy);
|
||||
}
|
||||
return fetch(request);
|
||||
}
|
||||
}
|
||||
31
node/tsconfig.json
Normal file
31
node/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"lib": ["es5", "es6", "es7", "dom"],
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"declarationDir": "dist/types",
|
||||
"outDir": "dist",
|
||||
"types": [],
|
||||
"paths": {
|
||||
"jslib-common/*": [
|
||||
"../common/src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"spec"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user