Skip to content

Commit

Permalink
Re-architecture project (#139)
Browse files Browse the repository at this point in the history
Simplify the project structure and make it more coherent.

Fix #138
  • Loading branch information
Mikescops authored Aug 2, 2023
1 parent 86ac92b commit c593796
Show file tree
Hide file tree
Showing 48 changed files with 438 additions and 395 deletions.
40 changes: 18 additions & 22 deletions src/middleware/configure.ts → src/command-handlers/configure.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import Database from 'better-sqlite3';
import winston from 'winston';
import { encryptAES } from '../crypto/encrypt';
import { deleteLocalKey, setLocalKey, warnUnreachableKeychainDisabled } from '../crypto/keychainManager';
import { Secrets } from '../types';

interface ConfigureSaveMasterPassword {
db: Database.Database;
secrets: Secrets;
shouldNotSaveMasterPassword: boolean;
}

interface ConfigureDisableAutoSync {
db: Database.Database;
secrets: Secrets;
disableAutoSync: boolean;
}

export const configureSaveMasterPassword = (params: ConfigureSaveMasterPassword) => {
const { db, secrets } = params;
let shouldNotSaveMasterPassword = params.shouldNotSaveMasterPassword;
import { encryptAES } from '../modules/crypto/encrypt';
import { deleteLocalKey, setLocalKey, warnUnreachableKeychainDisabled } from '../modules/crypto/keychainManager';
import { connectAndPrepare } from '../modules/database';
import { parseBooleanString } from '../utils';

export const configureSaveMasterPassword = async (boolean: string) => {
let shouldNotSaveMasterPassword = !parseBooleanString(boolean);
const { db, secrets } = await connectAndPrepare({
autoSync: false,
shouldNotSaveMasterPasswordIfNoDeviceKeys: shouldNotSaveMasterPassword,
});

if (shouldNotSaveMasterPassword) {
// Forget the local key stored in the OS keychain because the master password and the DB are enough to retrieve the
Expand Down Expand Up @@ -54,12 +45,17 @@ export const configureSaveMasterPassword = (params: ConfigureSaveMasterPassword)
db.prepare('UPDATE device SET masterPasswordEncrypted = ?, shouldNotSaveMasterPassword = ? WHERE login = ?')
.bind(masterPasswordEncrypted, shouldNotSaveMasterPassword ? 1 : 0, secrets.login)
.run();

db.close();
};

export const configureDisableAutoSync = (params: ConfigureDisableAutoSync) => {
const { db, secrets, disableAutoSync } = params;
export const configureDisableAutoSync = async (boolean: string) => {
const disableAutoSync = parseBooleanString(boolean);
const { db, secrets } = await connectAndPrepare({ autoSync: false });

db.prepare('UPDATE device SET autoSync = ? WHERE login = ?')
.bind(disableAutoSync ? 0 : 1, secrets.login)
.run();

db.close();
};
3 changes: 1 addition & 2 deletions src/command-handlers/devices.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { connectAndPrepare } from '../database';
import { connectAndPrepare, reset } from '../modules/database';
import { deactivateDevices, listDevices, ListDevicesOutput } from '../endpoints';
import { reset } from '../middleware';
import { askConfirmReset, unixTimestampToHumanReadable } from '../utils';

type OutputDevice = ListDevicesOutput['devices'][number] & {
Expand Down
8 changes: 8 additions & 0 deletions src/command-handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
export * from './configure';
export * from './devices';
export * from './logout';
export * from './passwords';
export * from './secureNotes';
export * from './sync';
export * from './teamDevices';
export * from './teamLogs';
export * from './teamMembers';
export * from './teamReport';
42 changes: 42 additions & 0 deletions src/command-handlers/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Database } from 'better-sqlite3';
import winston from 'winston';
import { deactivateDevices } from '../endpoints';
import { connectAndPrepare, connect, reset } from '../modules/database';
import { Secrets, DeviceConfiguration } from '../types';
import { askConfirmReset } from '../utils';

export const runLogout = async () => {
const resetConfirmation = await askConfirmReset();
if (!resetConfirmation) {
return;
}

let db: Database;
let secrets: Secrets | undefined;
let deviceConfiguration: DeviceConfiguration | null | undefined;
try {
({ db, secrets, deviceConfiguration } = await connectAndPrepare({
autoSync: false,
failIfNoDB: true,
}));
} catch (error) {
let errorMessage = 'unknown error';
if (error instanceof Error) {
errorMessage = error.message;
}
winston.debug(`Unable to read device configuration during logout: ${errorMessage}`);

db = connect();
db.serialize();
}
if (secrets && deviceConfiguration) {
await deactivateDevices({
deviceIds: [deviceConfiguration.accessKey],
login: deviceConfiguration.login,
secrets,
});
}
reset({ db, secrets });
console.log('The local Dashlane local storage has been reset and you have been logged out');
db.close();
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,47 @@ import { Clipboard } from '@napi-rs/clipboard';
import { authenticator } from 'otplib';
import winston from 'winston';
import { AuthentifiantTransactionContent, BackupEditTransaction, Secrets, VaultCredential } from '../types';
import { decryptTransaction } from '../crypto';
import { decryptTransaction } from '../modules/crypto';
import { askCredentialChoice } from '../utils';
import { connectAndPrepare } from '../modules/database';

export const runPassword = async (filters: string[] | null, options: { output: string | null }) => {
const { db, secrets } = await connectAndPrepare({});

if (options.output === 'json') {
console.log(
JSON.stringify(
await selectCredentials({
filters,
secrets,
output: options.output,
db,
}),
null,
4
)
);
} else {
await getPassword({
filters,
secrets,
output: options.output,
db,
});
}
db.close();
};

export const runOtp = async (filters: string[] | null, options: { print: boolean }) => {
const { db, secrets } = await connectAndPrepare({});
await getOtp({
filters,
secrets,
output: options.print ? 'otp' : 'clipboard',
db,
});
db.close();
};

interface GetCredential {
filters: string[] | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import Database from 'better-sqlite3';
import winston from 'winston';
import { BackupEditTransaction, Secrets, SecureNoteTransactionContent, VaultNote } from '../types';
import { decryptTransaction } from '../crypto';
import { decryptTransaction } from '../modules/crypto';
import { askSecureNoteChoice } from '../utils';
import { connectAndPrepare } from '../modules/database';

export const runSecureNote = async (filter: string | null) => {
const { db, secrets } = await connectAndPrepare({});
await getNote({
titleFilter: filter,
secrets,
db,
});
db.close();
};

interface GetSecureNote {
titleFilter: string | null;
Expand Down
14 changes: 11 additions & 3 deletions src/middleware/sync.ts → src/command-handlers/sync.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import Database from 'better-sqlite3';
import winston from 'winston';
import { decrypt } from '../crypto/decrypt';
import { encryptAES } from '../crypto/encrypt';
import { replaceMasterPassword } from '../crypto/keychainManager';
import { connectAndPrepare } from '../modules/database';
import { decrypt } from '../modules/crypto/decrypt';
import { encryptAES } from '../modules/crypto/encrypt';
import { replaceMasterPassword } from '../modules/crypto/keychainManager';
import { getLatestContent } from '../endpoints';
import type { DeviceConfiguration, Secrets } from '../types';
import { notEmpty } from '../utils';
import { askReplaceIncorrectMasterPassword } from '../utils/dialogs';

export const runSync = async () => {
const { db, secrets, deviceConfiguration } = await connectAndPrepare({ autoSync: false });
await sync({ db, secrets, deviceConfiguration });
winston.info('Successfully synced');
db.close();
};

interface Sync {
db: Database.Database;
secrets: Secrets;
Expand Down
3 changes: 1 addition & 2 deletions src/command-handlers/teamDevices.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { connectAndPrepare } from '../database';
import { connectAndPrepare } from '../modules/database';
import { listTeamDevices } from '../endpoints';
import { unixTimestampToHumanReadable } from '../utils';

Expand Down Expand Up @@ -36,7 +36,6 @@ export async function listAllTeamDevices(options: { json: boolean }) {
};
});

// print results
console.table(result);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import winston from 'winston';
import { getAuditLogQueryResults, startAuditLogsQuery, StartAuditLogsQueryParams } from '../endpoints';
import { connectAndPrepare } from '../modules/database';
import { StartAuditLogsQueryParams, startAuditLogsQuery, getAuditLogQueryResults } from '../endpoints';
import { getTeamDeviceCredentials } from '../utils';

export const runTeamLogs = async (options: { start: string; end: string; type: string; category: string }) => {
const teamDeviceCredentials = getTeamDeviceCredentials();

const { start, type, category } = options;
const end = options.end === 'now' ? Date.now().toString() : options.end;

const { db } = await connectAndPrepare({ autoSync: false });
await getAuditLogs({
teamDeviceCredentials,
startDateRangeUnix: parseInt(start),
endDateRangeUnix: parseInt(end),
logType: type,
category,
});
db.close();
};

const MAX_RESULT = 1000;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { getTeamMembers as getTeamMembersRequest } from '../endpoints';
import { TeamDeviceCredentials } from '../types';
import { getTeamDeviceCredentials } from '../utils';

interface GetTeamMembersParams {
teamDeviceCredentials: TeamDeviceCredentials;
page: number;
limit: number;
}

export const getTeamMembers = async (params: GetTeamMembersParams) => {
const { teamDeviceCredentials, page, limit } = params;
export const runTeamMembers = async (params: GetTeamMembersParams) => {
const { page, limit } = params;
const teamDeviceCredentials = getTeamDeviceCredentials();

const response = await getTeamMembersRequest({
teamDeviceCredentials,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { getTeamReport as getTeamReportRequest } from '../endpoints';
import { TeamDeviceCredentials } from '../types';
import { getTeamDeviceCredentials } from '../utils';

interface GetTeamMembersParams {
teamDeviceCredentials: TeamDeviceCredentials;
days: number;
}

export const getTeamReport = async (params: GetTeamMembersParams) => {
const { teamDeviceCredentials, days } = params;
export const runTeamReport = async (params: GetTeamMembersParams) => {
const { days } = params;
const teamDeviceCredentials = getTeamDeviceCredentials();

const response = await getTeamReportRequest({
teamDeviceCredentials,
Expand Down
18 changes: 18 additions & 0 deletions src/commands/configure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Command } from 'commander';
import { configureDisableAutoSync, configureSaveMasterPassword } from '../command-handlers';

export const configureCommands = (params: { program: Command }) => {
const { program } = params;

const configureGroup = program.command('configure').alias('c').description('Configure the CLI');

configureGroup
.command('disable-auto-sync <boolean>')
.description('Disable automatic synchronization which is done once per hour (default: false)')
.action(configureDisableAutoSync);

configureGroup
.command('save-master-password <boolean>')
.description('Should the encrypted master password be saved and the OS keychain be used (default: true)')
.action(configureSaveMasterPassword);
};
22 changes: 22 additions & 0 deletions src/commands/devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Command } from 'commander';
import { listAllDevices, removeAllDevices } from '../command-handlers';

export const devicesCommands = (params: { program: Command }) => {
const { program } = params;

const devicesGroup = program.command('devices').alias('d').description('Operations on devices');

devicesGroup
.command('list')
.option('--json', 'Output in JSON format')
.description('Lists all registered devices that can access your account')
.action(listAllDevices);

devicesGroup
.command('remove')
.option('--all', 'remove all devices including this one (dangerous)')
.option('--others', 'remove all other devices')
.argument('[device ids...]', 'ids of the devices to remove')
.description('De-registers a list of devices. De-registering the CLI will implies doing a "dcli logout"')
.action(removeAllDevices);
};
56 changes: 56 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Command } from 'commander';
import { devicesCommands } from './devices';
import { teamCommands } from './team';
import { configureCommands } from './configure';
import { runSync, runOtp, runPassword, runSecureNote, runLogout } from '../command-handlers';

export const rootCommands = (params: { program: Command }) => {
const { program } = params;

program
.command('sync')
.alias('s')
.description('Manually synchronize the local vault with Dashlane')
.action(runSync);

program
.command('password')
.alias('p')
.description('Retrieve a password from the local vault and copy it to the clipboard')
.option(
'-o, --output <type>',
'How to print the passwords among `clipboard, password, json`. The JSON option outputs all the matching credentials',
'clipboard'
)
.argument(
'[filters...]',
'Filter credentials based on any parameter using <param>=<value>; if <param> is not specified in the filter, will default to url and title'
)
.action(runPassword);

program
.command('otp')
.alias('o')
.description('Retrieve an OTP code from local vault and copy it to the clipboard')
.option('--print', 'Prints just the OTP code, instead of copying it to the clipboard')
.argument(
'[filters...]',
'Filter credentials based on any parameter using <param>=<value>; if <param> is not specified in the filter, will default to url and title'
)
.action(runOtp);

program
.command('note')
.alias('n')
.description('Retrieve a secure note from the local vault and open it')
.argument('[filter]', 'Filter notes based on their title')
.action(runSecureNote);

devicesCommands({ program });

teamCommands({ program });

configureCommands({ program });

program.command('logout').description('Logout and clean your local database and OS keychain').action(runLogout);
};
Loading

0 comments on commit c593796

Please sign in to comment.