From c5937962b63d90b32881b39b2500ce949ace3808 Mon Sep 17 00:00:00 2001 From: Corentin Mors Date: Wed, 2 Aug 2023 17:06:22 +0200 Subject: [PATCH] Re-architecture project (#139) Simplify the project structure and make it more coherent. Fix #138 --- .../configure.ts | 40 ++- src/command-handlers/devices.ts | 3 +- src/command-handlers/index.ts | 8 + src/command-handlers/logout.ts | 42 +++ .../passwords.ts} | 41 ++- .../secureNotes.ts} | 13 +- src/{middleware => command-handlers}/sync.ts | 14 +- src/command-handlers/teamDevices.ts | 3 +- .../teamLogs.ts} | 21 +- .../teamMembers.ts} | 8 +- .../teamReport.ts} | 8 +- src/commands/configure.ts | 18 ++ src/commands/devices.ts | 22 ++ src/commands/index.ts | 56 ++++ src/commands/team/credentials.ts | 52 +++ src/commands/team/index.ts | 57 ++++ src/database/index.ts | 1 - src/endpoints/index.ts | 13 +- src/index.ts | 301 +----------------- src/middleware/index.ts | 9 - src/{ => modules}/api-connect/index.ts | 0 .../api-connect/makeCanonicalRequest.ts | 0 .../api-connect/postRequestAPI.ts | 0 src/{ => modules}/api-connect/signRequest.ts | 0 src/{ => modules}/api-connect/types.ts | 0 src/modules/auth/index.ts | 2 + .../auth}/perform2FAVerification.ts | 8 +- .../auth}/registerDevice.ts | 10 +- src/{ => modules}/crypto/README.md | 0 src/{ => modules}/crypto/decrypt.ts | 2 +- src/{ => modules}/crypto/encrypt.ts | 0 .../crypto/encryptedDataDeserialization.ts | 0 .../crypto/encryptedDataSerialization.ts | 0 src/{ => modules}/crypto/hash.ts | 0 src/{ => modules}/crypto/index.ts | 0 src/{ => modules}/crypto/keychainManager.ts | 11 +- src/{ => modules}/crypto/test.ts | 0 src/{ => modules}/crypto/types.ts | 0 src/{ => modules}/database/connect.ts | 0 .../database/connectAndPrepare.ts | 10 +- src/modules/database/index.ts | 3 + src/{ => modules}/database/prepare.ts | 2 +- src/{middleware => modules/database}/reset.ts | 2 +- src/requestApi.ts | 6 +- src/utils/gotImplementation.ts | 6 +- src/utils/index.ts | 1 + src/utils/strings.ts | 16 - src/utils/teamDeviceCredentials.ts | 24 ++ 48 files changed, 438 insertions(+), 395 deletions(-) rename src/{middleware => command-handlers}/configure.ts (69%) create mode 100644 src/command-handlers/logout.ts rename src/{middleware/getPasswords.ts => command-handlers/passwords.ts} (84%) rename src/{middleware/getSecureNotes.ts => command-handlers/secureNotes.ts} (88%) rename src/{middleware => command-handlers}/sync.ts (89%) rename src/{middleware/getAuditLogs.ts => command-handlers/teamLogs.ts} (61%) rename src/{middleware/getTeamMembers.ts => command-handlers/teamMembers.ts} (59%) rename src/{middleware/getTeamReport.ts => command-handlers/teamReport.ts} (56%) create mode 100644 src/commands/configure.ts create mode 100644 src/commands/devices.ts create mode 100644 src/commands/index.ts create mode 100644 src/commands/team/credentials.ts create mode 100644 src/commands/team/index.ts delete mode 100644 src/database/index.ts delete mode 100644 src/middleware/index.ts rename src/{ => modules}/api-connect/index.ts (100%) rename src/{ => modules}/api-connect/makeCanonicalRequest.ts (100%) rename src/{ => modules}/api-connect/postRequestAPI.ts (100%) rename src/{ => modules}/api-connect/signRequest.ts (100%) rename src/{ => modules}/api-connect/types.ts (100%) create mode 100644 src/modules/auth/index.ts rename src/{middleware => modules/auth}/perform2FAVerification.ts (84%) rename src/{middleware => modules/auth}/registerDevice.ts (86%) rename src/{ => modules}/crypto/README.md (100%) rename src/{ => modules}/crypto/decrypt.ts (99%) rename src/{ => modules}/crypto/encrypt.ts (100%) rename src/{ => modules}/crypto/encryptedDataDeserialization.ts (100%) rename src/{ => modules}/crypto/encryptedDataSerialization.ts (100%) rename src/{ => modules}/crypto/hash.ts (100%) rename src/{ => modules}/crypto/index.ts (100%) rename src/{ => modules}/crypto/keychainManager.ts (95%) rename src/{ => modules}/crypto/test.ts (100%) rename src/{ => modules}/crypto/types.ts (100%) rename src/{ => modules}/database/connect.ts (100%) rename src/{ => modules}/database/connectAndPrepare.ts (93%) create mode 100644 src/modules/database/index.ts rename src/{ => modules}/database/prepare.ts (96%) rename src/{middleware => modules/database}/reset.ts (96%) create mode 100644 src/utils/teamDeviceCredentials.ts diff --git a/src/middleware/configure.ts b/src/command-handlers/configure.ts similarity index 69% rename from src/middleware/configure.ts rename to src/command-handlers/configure.ts index 2a5c6449..6eaa56dc 100644 --- a/src/middleware/configure.ts +++ b/src/command-handlers/configure.ts @@ -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 @@ -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(); }; diff --git a/src/command-handlers/devices.ts b/src/command-handlers/devices.ts index 5a26e77a..b6316652 100644 --- a/src/command-handlers/devices.ts +++ b/src/command-handlers/devices.ts @@ -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] & { diff --git a/src/command-handlers/index.ts b/src/command-handlers/index.ts index dbaf1383..960e8b7f 100644 --- a/src/command-handlers/index.ts +++ b/src/command-handlers/index.ts @@ -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'; diff --git a/src/command-handlers/logout.ts b/src/command-handlers/logout.ts new file mode 100644 index 00000000..b2e76c06 --- /dev/null +++ b/src/command-handlers/logout.ts @@ -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(); +}; diff --git a/src/middleware/getPasswords.ts b/src/command-handlers/passwords.ts similarity index 84% rename from src/middleware/getPasswords.ts rename to src/command-handlers/passwords.ts index 26f7e2d7..6ca90137 100644 --- a/src/middleware/getPasswords.ts +++ b/src/command-handlers/passwords.ts @@ -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; diff --git a/src/middleware/getSecureNotes.ts b/src/command-handlers/secureNotes.ts similarity index 88% rename from src/middleware/getSecureNotes.ts rename to src/command-handlers/secureNotes.ts index b06cd309..51eb1b78 100644 --- a/src/middleware/getSecureNotes.ts +++ b/src/command-handlers/secureNotes.ts @@ -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; diff --git a/src/middleware/sync.ts b/src/command-handlers/sync.ts similarity index 89% rename from src/middleware/sync.ts rename to src/command-handlers/sync.ts index 5101ae2e..9d4d8ed6 100644 --- a/src/middleware/sync.ts +++ b/src/command-handlers/sync.ts @@ -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; diff --git a/src/command-handlers/teamDevices.ts b/src/command-handlers/teamDevices.ts index ed75db60..bd0fc537 100644 --- a/src/command-handlers/teamDevices.ts +++ b/src/command-handlers/teamDevices.ts @@ -1,4 +1,4 @@ -import { connectAndPrepare } from '../database'; +import { connectAndPrepare } from '../modules/database'; import { listTeamDevices } from '../endpoints'; import { unixTimestampToHumanReadable } from '../utils'; @@ -36,7 +36,6 @@ export async function listAllTeamDevices(options: { json: boolean }) { }; }); - // print results console.table(result); } } diff --git a/src/middleware/getAuditLogs.ts b/src/command-handlers/teamLogs.ts similarity index 61% rename from src/middleware/getAuditLogs.ts rename to src/command-handlers/teamLogs.ts index a1d2c795..a45c13fb 100644 --- a/src/middleware/getAuditLogs.ts +++ b/src/command-handlers/teamLogs.ts @@ -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; diff --git a/src/middleware/getTeamMembers.ts b/src/command-handlers/teamMembers.ts similarity index 59% rename from src/middleware/getTeamMembers.ts rename to src/command-handlers/teamMembers.ts index 1bd87cb9..62a22b30 100644 --- a/src/middleware/getTeamMembers.ts +++ b/src/command-handlers/teamMembers.ts @@ -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, diff --git a/src/middleware/getTeamReport.ts b/src/command-handlers/teamReport.ts similarity index 56% rename from src/middleware/getTeamReport.ts rename to src/command-handlers/teamReport.ts index 1c3c5c10..3d80135f 100644 --- a/src/middleware/getTeamReport.ts +++ b/src/command-handlers/teamReport.ts @@ -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, diff --git a/src/commands/configure.ts b/src/commands/configure.ts new file mode 100644 index 00000000..0febf4e4 --- /dev/null +++ b/src/commands/configure.ts @@ -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 ') + .description('Disable automatic synchronization which is done once per hour (default: false)') + .action(configureDisableAutoSync); + + configureGroup + .command('save-master-password ') + .description('Should the encrypted master password be saved and the OS keychain be used (default: true)') + .action(configureSaveMasterPassword); +}; diff --git a/src/commands/devices.ts b/src/commands/devices.ts new file mode 100644 index 00000000..a31a9e23 --- /dev/null +++ b/src/commands/devices.ts @@ -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); +}; diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 00000000..28487b95 --- /dev/null +++ b/src/commands/index.ts @@ -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 ', + '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 =; if 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 =; if 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); +}; diff --git a/src/commands/team/credentials.ts b/src/commands/team/credentials.ts new file mode 100644 index 00000000..bd6c2922 --- /dev/null +++ b/src/commands/team/credentials.ts @@ -0,0 +1,52 @@ +import { Command } from 'commander'; +import { listAllTeamDevices } from '../../command-handlers'; +import { connectAndPrepare } from '../../modules/database'; +import { deactivateTeamDevice, registerTeamDevice } from '../../endpoints'; + +export const teamCredentialsCommands = (params: { teamGroup: Command }) => { + const { teamGroup } = params; + + const teamCredentialsGroup = teamGroup.command('credentials').alias('c').description('Team credentials operations'); + + teamCredentialsGroup + .command('generate') + .option('--json', 'Output in JSON format') + .description('Generate new team credentials') + .action(async (options: { json: boolean }) => { + const { db, secrets } = await connectAndPrepare({ autoSync: false }); + const credentials = await registerTeamDevice({ secrets, deviceName: 'Dashlane CLI' }); + db.close(); + + if (options.json) { + console.log( + JSON.stringify({ + DASHLANE_TEAM_UUID: credentials.teamUuid, + DASHLANE_TEAM_ACCESS_KEY: credentials.deviceAccessKey, + DASHLANE_TEAM_SECRET_KEY: credentials.deviceSecretKey, + }) + ); + } else { + console.log(`export DASHLANE_TEAM_UUID=${credentials.teamUuid}`); + console.log(`export DASHLANE_TEAM_ACCESS_KEY=${credentials.deviceAccessKey}`); + console.log(`export DASHLANE_TEAM_SECRET_KEY=${credentials.deviceSecretKey}`); + } + }); + + teamCredentialsGroup + .command('list') + .option('--json', 'Output in JSON format') + .description('List all team credentials') + .action(listAllTeamDevices); + + teamCredentialsGroup + .command('revoke') + .description('Revoke credentials by access key') + .argument('', 'Access key of the credentials to revoke') + .action(async (accessKey: string) => { + const { db, secrets } = await connectAndPrepare({ autoSync: false }); + await deactivateTeamDevice({ secrets, teamDeviceAccessKey: accessKey }); + db.close(); + + console.log('The credentials have been revoked'); + }); +}; diff --git a/src/commands/team/index.ts b/src/commands/team/index.ts new file mode 100644 index 00000000..f6693621 --- /dev/null +++ b/src/commands/team/index.ts @@ -0,0 +1,57 @@ +import { Command } from 'commander'; +import { teamCredentialsCommands } from './credentials'; +import { CouldNotFindTeamCredentialsError } from '../../errors'; +import { runTeamLogs, runTeamMembers, runTeamReport } from '../../command-handlers'; +import { getTeamDeviceCredentials } from '../../utils'; + +export const teamCommands = (params: { program: Command }) => { + const { program } = params; + + const teamGroup = program.command('team').alias('t').description('Team related commands'); + + try { + getTeamDeviceCredentials(); + } catch (error) { + if (error instanceof CouldNotFindTeamCredentialsError) { + teamGroup.addHelpText( + 'before', + '/!\\ Commands in this section (except credentials) require team credentials to be set in the environment.\n' + + 'Use generate-credentials to generate some team credentials (requires to be a team administrator).\n' + ); + } + } + + teamCredentialsCommands({ teamGroup }); + + teamGroup + .command('members') + .alias('m') + .description('List team members') + .argument('[page]', 'Page number', '0') + .argument('[limit]', 'Limit of members per page', '0') + .action(async (page: string, limit: string) => { + await runTeamMembers({ + page: parseInt(page), + limit: parseInt(limit), + }); + }); + + teamGroup + .command('logs') + .alias('l') + .description('List audit logs') + .option('--start ', 'start timestamp in ms', '0') + .option('--end ', 'end timestamp in ms (use "now" to get the current timestamp)', 'now') + .option('--type ', 'log type') + .option('--category ', 'log category') + .action(runTeamLogs); + + teamGroup + .command('report') + .alias('r') + .description('Get team report') + .argument('[days]', 'Number of days in history', '0') + .action(async (days: string) => { + await runTeamReport({ days: parseInt(days) }); + }); +}; diff --git a/src/database/index.ts b/src/database/index.ts deleted file mode 100644 index 340754b3..00000000 --- a/src/database/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './connectAndPrepare'; diff --git a/src/endpoints/index.ts b/src/endpoints/index.ts index e5aff542..d356f819 100644 --- a/src/endpoints/index.ts +++ b/src/endpoints/index.ts @@ -1,14 +1,17 @@ export * from './completeDeviceRegistration'; +export * from './completeLoginWithAuthTicket'; +export * from './deactivateDevices'; +export * from './deactivateTeamDevice'; +export * from './getAuditLogs'; export * from './getLatestContent'; export * from './getStatus'; export * from './getTeamMembers'; +export * from './getTeamReport'; +export * from './listDevices'; +export * from './listTeamDevices'; export * from './performDashlaneAuthenticatorVerification'; export * from './performDuoPushVerification'; export * from './performEmailTokenVerification'; export * from './performTotpVerification'; export * from './requestDeviceRegistration'; -export * from './getAuditLogs'; -export * from './getTeamReport'; -export * from './listDevices'; -export * from './listTeamDevices'; -export * from './deactivateDevices'; +export * from './registerTeamDevice'; diff --git a/src/index.ts b/src/index.ts index f985ad58..47320434 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,10 @@ #!/usr/bin/env node import { Command } from 'commander'; import winston from 'winston'; -import { Database } from 'better-sqlite3'; -import { connectAndPrepare } from './database/index'; -import { askConfirmReset } from './utils/dialogs'; -import { getTeamDeviceCredentialsFromEnv, parseBooleanString } from './utils'; -import { DeviceConfiguration, Secrets } from './types'; -import { connect } from './database/connect'; -import { - sync, - selectCredentials, - getOtp, - getNote, - getPassword, - getTeamMembers, - configureDisableAutoSync, - configureSaveMasterPassword, - reset, - getAuditLogs, - getTeamReport, -} from './middleware'; -import { cliVersionToString, CLI_VERSION } from './cliVersion'; -import { registerTeamDevice } from './endpoints/registerTeamDevice'; -import { listAllDevices, removeAllDevices, listAllTeamDevices } from './command-handlers'; -import { deactivateTeamDevice } from './endpoints/deactivateTeamDevice'; -import { CouldNotFindTeamCredentialsError } from './errors'; -import { deactivateDevices } from './endpoints'; -const teamDeviceCredentials = getTeamDeviceCredentialsFromEnv(); +import { cliVersionToString, CLI_VERSION } from './cliVersion'; +import { rootCommands } from './commands'; +import { initTeamDeviceCredentials } from './utils'; const debugLevel = process.argv.indexOf('--debug') !== -1 ? 'debug' : 'info'; @@ -37,281 +14,15 @@ winston.configure({ transports: [new winston.transports.Console({ stderrLevels: ['error', 'debug', 'info'] })], }); +initTeamDeviceCredentials(); + const program = new Command(); program.name('dcli').description('Dashlane CLI').version(cliVersionToString(CLI_VERSION)); program.option('--debug', 'Print debug messages'); -program - .command('sync') - .alias('s') - .description('Manually synchronize the local vault with Dashlane') - .action(async () => { - const { db, secrets, deviceConfiguration } = await connectAndPrepare({ autoSync: false }); - await sync({ db, secrets, deviceConfiguration }); - console.log('Successfully synced'); - db.close(); - }); - -program - .command('password') - .alias('p') - .description('Retrieve a password from the local vault and copy it to the clipboard') - .option( - '-o, --output ', - '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 =; if is not specified in the filter, will default to url and title' - ) - .action(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(); - }); - -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 =; if is not specified in the filter, will default to url and title' - ) - .action(async (filters: string[] | null, options: { print: boolean }) => { - const { db, secrets } = await connectAndPrepare({}); - await getOtp({ - filters, - secrets, - output: options.print ? 'otp' : 'clipboard', - db, - }); - db.close(); - }); - -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(async (filter: string | null) => { - const { db, secrets } = await connectAndPrepare({}); - await getNote({ - titleFilter: filter, - secrets, - db, - }); - db.close(); - }); - -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 to 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); - -const teamGroup = program.command('team').alias('t').description('Team related commands'); - -if (!teamDeviceCredentials) { - teamGroup.addHelpText( - 'before', - `/!\\ Commands in this section (except generate-credentials) require team credentials to be set in the environment. -Use generate-credentials to generate some team credentials (requires to be a team administrator). -` - ); -} - -const teamCredentialsGroup = teamGroup.command('credentials').alias('c').description('Team credentials operations'); - -teamCredentialsGroup - .command('generate') - .option('--json', 'Output in JSON format') - .description('Generate new team credentials') - .action(async (options: { json: boolean }) => { - const { db, secrets } = await connectAndPrepare({ autoSync: false }); - const credentials = await registerTeamDevice({ secrets, deviceName: 'Dashlane CLI' }); - db.close(); - - if (options.json) { - console.log( - JSON.stringify({ - DASHLANE_TEAM_UUID: credentials.teamUuid, - DASHLANE_TEAM_ACCESS_KEY: credentials.deviceAccessKey, - DASHLANE_TEAM_SECRET_KEY: credentials.deviceSecretKey, - }) - ); - } else { - console.log(`export DASHLANE_TEAM_UUID=${credentials.teamUuid}`); - console.log(`export DASHLANE_TEAM_ACCESS_KEY=${credentials.deviceAccessKey}`); - console.log(`export DASHLANE_TEAM_SECRET_KEY=${credentials.deviceSecretKey}`); - } - }); - -teamCredentialsGroup - .command('list') - .option('--json', 'Output in JSON format') - .description('List all team credentials') - .action(listAllTeamDevices); - -teamCredentialsGroup - .command('revoke') - .description('Revoke credentials by access key') - .argument('', 'Access key of the credentials to revoke') - .action(async (accessKey: string) => { - const { db, secrets } = await connectAndPrepare({ autoSync: false }); - await deactivateTeamDevice({ secrets, teamDeviceAccessKey: accessKey }); - db.close(); - - console.log('The credentials have been revoked'); - }); - -teamGroup - .command('members') - .alias('m') - .description('List team members') - .argument('[page]', 'Page number', '0') - .argument('[limit]', 'Limit of members per page', '0') - .action(async (page: string, limit: string) => { - if (!teamDeviceCredentials) { - throw new CouldNotFindTeamCredentialsError(); - } - await getTeamMembers({ teamDeviceCredentials, page: parseInt(page), limit: parseInt(limit) }); - }); - -teamGroup - .command('logs') - .alias('l') - .description('List audit logs') - .option('--start ', 'start timestamp in ms', '0') - .option('--end ', 'end timestamp in ms (use "now" to get the current timestamp)', 'now') - .option('--type ', 'log type') - .option('--category ', 'log category') - .action(async (options: { start: string; end: string; type: string; category: string }) => { - if (!teamDeviceCredentials) { - throw new CouldNotFindTeamCredentialsError(); - } - - 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(); - }); - -teamGroup - .command('report') - .alias('r') - .description('Get team report') - .argument('[days]', 'Number of days in history', '0') - .action(async (days: string) => { - if (!teamDeviceCredentials) { - throw new CouldNotFindTeamCredentialsError(); - } - - const { db } = await connectAndPrepare({ autoSync: false }); - await getTeamReport({ teamDeviceCredentials, days: parseInt(days) }); - db.close(); - }); - -const configureGroup = program.command('configure').alias('c').description('Configure the CLI'); - -configureGroup - .command('disable-auto-sync ') - .description('Disable automatic synchronization which is done once per hour (default: false)') - .action(async (boolean: string) => { - const disableAutoSync = parseBooleanString(boolean); - const { db, secrets } = await connectAndPrepare({ autoSync: false }); - configureDisableAutoSync({ db, secrets, disableAutoSync }); - db.close(); - }); - -configureGroup - .command('save-master-password ') - .description('Should the encrypted master password be saved and the OS keychain be used (default: true)') - .action(async (boolean: string) => { - const shouldNotSaveMasterPassword = !parseBooleanString(boolean); - const { db, secrets } = await connectAndPrepare({ - autoSync: false, - shouldNotSaveMasterPasswordIfNoDeviceKeys: shouldNotSaveMasterPassword, - }); - configureSaveMasterPassword({ db, secrets, shouldNotSaveMasterPassword }); - db.close(); - }); - -program - .command('logout') - .description('Logout and clean your local database and OS keychain') - .action(async () => { - const resetConfirmation = await askConfirmReset(); - if (resetConfirmation) { - 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(); - } - }); +rootCommands({ program }); program.parseAsync().catch((error: Error) => { console.error(`ERROR:`, error.message); diff --git a/src/middleware/index.ts b/src/middleware/index.ts deleted file mode 100644 index 200f5d95..00000000 --- a/src/middleware/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './configure'; -export * from './getAuditLogs'; -export * from './getPasswords'; -export * from './getSecureNotes'; -export * from './getTeamMembers'; -export * from './getTeamReport'; -export * from './registerDevice'; -export * from './reset'; -export * from './sync'; diff --git a/src/api-connect/index.ts b/src/modules/api-connect/index.ts similarity index 100% rename from src/api-connect/index.ts rename to src/modules/api-connect/index.ts diff --git a/src/api-connect/makeCanonicalRequest.ts b/src/modules/api-connect/makeCanonicalRequest.ts similarity index 100% rename from src/api-connect/makeCanonicalRequest.ts rename to src/modules/api-connect/makeCanonicalRequest.ts diff --git a/src/api-connect/postRequestAPI.ts b/src/modules/api-connect/postRequestAPI.ts similarity index 100% rename from src/api-connect/postRequestAPI.ts rename to src/modules/api-connect/postRequestAPI.ts diff --git a/src/api-connect/signRequest.ts b/src/modules/api-connect/signRequest.ts similarity index 100% rename from src/api-connect/signRequest.ts rename to src/modules/api-connect/signRequest.ts diff --git a/src/api-connect/types.ts b/src/modules/api-connect/types.ts similarity index 100% rename from src/api-connect/types.ts rename to src/modules/api-connect/types.ts diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts new file mode 100644 index 00000000..a1d30b09 --- /dev/null +++ b/src/modules/auth/index.ts @@ -0,0 +1,2 @@ +export * from './perform2FAVerification'; +export * from './registerDevice'; diff --git a/src/middleware/perform2FAVerification.ts b/src/modules/auth/perform2FAVerification.ts similarity index 84% rename from src/middleware/perform2FAVerification.ts rename to src/modules/auth/perform2FAVerification.ts index 8b85fbf3..00337599 100644 --- a/src/middleware/perform2FAVerification.ts +++ b/src/modules/auth/perform2FAVerification.ts @@ -1,7 +1,7 @@ -import { performTotpVerification } from '../endpoints'; -import { completeLoginWithAuthTicket } from '../endpoints/completeLoginWithAuthTicket'; -import { Get2FAStatusOutput, get2FAStatusUnauthenticated } from '../endpoints/get2FAStatusUnauthenticated'; -import { askOtp } from '../utils'; +import { performTotpVerification } from '../../endpoints'; +import { completeLoginWithAuthTicket } from '../../endpoints/completeLoginWithAuthTicket'; +import { Get2FAStatusOutput, get2FAStatusUnauthenticated } from '../../endpoints/get2FAStatusUnauthenticated'; +import { askOtp } from '../../utils'; interface Params { login: string; diff --git a/src/middleware/registerDevice.ts b/src/modules/auth/registerDevice.ts similarity index 86% rename from src/middleware/registerDevice.ts rename to src/modules/auth/registerDevice.ts index 33daa05c..4efdc1f4 100644 --- a/src/middleware/registerDevice.ts +++ b/src/modules/auth/registerDevice.ts @@ -6,11 +6,11 @@ import { performDuoPushVerification, performEmailTokenVerification, performTotpVerification, -} from '../endpoints'; -import { askOtp, askToken, askVerificationMethod } from '../utils'; -import { getAuthenticationMethodsForDevice } from '../endpoints/getAuthenticationMethodsForDevice'; -import { requestEmailTokenVerification } from '../endpoints/requestEmailTokenVerification'; -import { SupportedAuthenticationMethod } from '../types'; +} from '../../endpoints'; +import { askOtp, askToken, askVerificationMethod } from '../../utils'; +import { getAuthenticationMethodsForDevice } from '../../endpoints/getAuthenticationMethodsForDevice'; +import { requestEmailTokenVerification } from '../../endpoints/requestEmailTokenVerification'; +import type { SupportedAuthenticationMethod } from '../../types'; interface RegisterDevice { login: string; diff --git a/src/crypto/README.md b/src/modules/crypto/README.md similarity index 100% rename from src/crypto/README.md rename to src/modules/crypto/README.md diff --git a/src/crypto/decrypt.ts b/src/modules/crypto/decrypt.ts similarity index 99% rename from src/crypto/decrypt.ts rename to src/modules/crypto/decrypt.ts index 326b7744..ea563580 100644 --- a/src/crypto/decrypt.ts +++ b/src/modules/crypto/decrypt.ts @@ -7,7 +7,7 @@ import zlib from 'zlib'; import { CipherData, EncryptedData } from './types'; import { hmacSha256, sha512 } from './hash'; import { deserializeEncryptedData } from './encryptedDataDeserialization'; -import { BackupEditTransaction, Secrets, SymmetricKeyGetter, TransactionContent } from '../types'; +import { BackupEditTransaction, Secrets, SymmetricKeyGetter, TransactionContent } from '../../types'; const decryptCipherData = (cipherData: CipherData, originalKey: Buffer): Buffer => { const combinedKey = sha512(originalKey); diff --git a/src/crypto/encrypt.ts b/src/modules/crypto/encrypt.ts similarity index 100% rename from src/crypto/encrypt.ts rename to src/modules/crypto/encrypt.ts diff --git a/src/crypto/encryptedDataDeserialization.ts b/src/modules/crypto/encryptedDataDeserialization.ts similarity index 100% rename from src/crypto/encryptedDataDeserialization.ts rename to src/modules/crypto/encryptedDataDeserialization.ts diff --git a/src/crypto/encryptedDataSerialization.ts b/src/modules/crypto/encryptedDataSerialization.ts similarity index 100% rename from src/crypto/encryptedDataSerialization.ts rename to src/modules/crypto/encryptedDataSerialization.ts diff --git a/src/crypto/hash.ts b/src/modules/crypto/hash.ts similarity index 100% rename from src/crypto/hash.ts rename to src/modules/crypto/hash.ts diff --git a/src/crypto/index.ts b/src/modules/crypto/index.ts similarity index 100% rename from src/crypto/index.ts rename to src/modules/crypto/index.ts diff --git a/src/crypto/keychainManager.ts b/src/modules/crypto/keychainManager.ts similarity index 95% rename from src/crypto/keychainManager.ts rename to src/modules/crypto/keychainManager.ts index 748941bd..6945a168 100644 --- a/src/crypto/keychainManager.ts +++ b/src/modules/crypto/keychainManager.ts @@ -6,12 +6,11 @@ import { decrypt, getDerivateUsingParametersFromEncryptedData } from './decrypt' import { encryptAES } from './encrypt'; import { sha512 } from './hash'; import { EncryptedData } from './types'; -import { CLI_VERSION, cliVersionToString } from '../cliVersion'; -import { registerDevice } from '../middleware/registerDevice'; -import { DeviceConfiguration, Secrets } from '../types'; -import { askEmailAddress, askMasterPassword } from '../utils/dialogs'; -import { get2FAStatusUnauthenticated } from '../endpoints/get2FAStatusUnauthenticated'; -import { perform2FAVerification } from '../middleware/perform2FAVerification'; +import { CLI_VERSION, cliVersionToString } from '../../cliVersion'; +import { perform2FAVerification, registerDevice } from '../auth'; +import { DeviceConfiguration, Secrets } from '../../types'; +import { askEmailAddress, askMasterPassword } from '../../utils/dialogs'; +import { get2FAStatusUnauthenticated } from '../../endpoints/get2FAStatusUnauthenticated'; const SERVICE = 'dashlane-cli'; diff --git a/src/crypto/test.ts b/src/modules/crypto/test.ts similarity index 100% rename from src/crypto/test.ts rename to src/modules/crypto/test.ts diff --git a/src/crypto/types.ts b/src/modules/crypto/types.ts similarity index 100% rename from src/crypto/types.ts rename to src/modules/crypto/types.ts diff --git a/src/database/connect.ts b/src/modules/database/connect.ts similarity index 100% rename from src/database/connect.ts rename to src/modules/database/connect.ts diff --git a/src/database/connectAndPrepare.ts b/src/modules/database/connectAndPrepare.ts similarity index 93% rename from src/database/connectAndPrepare.ts rename to src/modules/database/connectAndPrepare.ts index 8c5dba86..1d337075 100644 --- a/src/database/connectAndPrepare.ts +++ b/src/modules/database/connectAndPrepare.ts @@ -2,18 +2,18 @@ import Database from 'better-sqlite3'; import winston from 'winston'; import { connect } from './connect'; import { prepareDB } from './prepare'; +import { reset } from './reset'; import { CLI_VERSION, breakingChangesVersions, cliVersionLessThan, cliVersionToString, stringToCliVersion, -} from '../cliVersion'; +} from '../../cliVersion'; import { getSecrets } from '../crypto'; -import { reset } from '../middleware/reset'; -import { sync } from '../middleware/sync'; -import { DeviceConfiguration, Secrets } from '../types'; -import { askIgnoreBreakingChanges } from '../utils/dialogs'; +import { DeviceConfiguration, Secrets } from '../../types'; +import { askIgnoreBreakingChanges } from '../../utils/dialogs'; +import { sync } from '../../command-handlers'; export interface ConnectAndPrepareParams { /* Is the vault automatically synchronized every hour */ diff --git a/src/modules/database/index.ts b/src/modules/database/index.ts new file mode 100644 index 00000000..cbdd0e63 --- /dev/null +++ b/src/modules/database/index.ts @@ -0,0 +1,3 @@ +export * from './connect'; +export * from './connectAndPrepare'; +export * from './reset'; diff --git a/src/database/prepare.ts b/src/modules/database/prepare.ts similarity index 96% rename from src/database/prepare.ts rename to src/modules/database/prepare.ts index cf8068f0..d0005b00 100644 --- a/src/database/prepare.ts +++ b/src/modules/database/prepare.ts @@ -1,5 +1,5 @@ import Database from 'better-sqlite3'; -import { DeviceConfiguration } from '../types'; +import { DeviceConfiguration } from '../../types'; interface PrepareDB { db: Database.Database; diff --git a/src/middleware/reset.ts b/src/modules/database/reset.ts similarity index 96% rename from src/middleware/reset.ts rename to src/modules/database/reset.ts index dcafd9ab..9ea35f38 100644 --- a/src/middleware/reset.ts +++ b/src/modules/database/reset.ts @@ -1,7 +1,7 @@ import Database from 'better-sqlite3'; import winston from 'winston'; import { deleteLocalKey } from '../crypto/keychainManager'; -import { Secrets } from '../types'; +import { Secrets } from '../../types'; interface ResetDB { db: Database.Database; diff --git a/src/requestApi.ts b/src/requestApi.ts index 15be8e96..5deadbb1 100644 --- a/src/requestApi.ts +++ b/src/requestApi.ts @@ -1,12 +1,12 @@ import * as got from 'got'; -import * as apiconnect from './api-connect'; +import * as apiConnect from './modules/api-connect'; import { CLI_VERSION, cliVersionToString } from './cliVersion'; import { gotImplementation } from './utils/'; interface RequestApi { payload: Dictionary; path: string; - authentication: apiconnect.Authentication; + authentication: apiConnect.Authentication; } interface DashlaneApiErrorResponse { @@ -37,7 +37,7 @@ const requestApi = async (params: RequestApi): Promise => { let response; try { - response = await apiconnect.postRequestAPI>({ + response = await apiConnect.postRequestAPI>({ requestFunction: gotImplementation, authentication, path: 'v1/' + path, diff --git a/src/utils/gotImplementation.ts b/src/utils/gotImplementation.ts index 2bb26e44..d40a4e12 100644 --- a/src/utils/gotImplementation.ts +++ b/src/utils/gotImplementation.ts @@ -1,8 +1,8 @@ import * as got from 'got'; -import * as apiconnect from '../api-connect'; +import * as apiConnect from '../modules/api-connect'; -export const gotImplementation: apiconnect.RequestFunction> = ( - options: apiconnect.RequestFunctionOptions +export const gotImplementation: apiConnect.RequestFunction> = ( + options: apiConnect.RequestFunctionOptions ) => { const { headers, json, url } = options; diff --git a/src/utils/index.ts b/src/utils/index.ts index a21d9fa8..0087b347 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './dialogs'; export * from './gotImplementation'; export * from './strings'; +export * from './teamDeviceCredentials'; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 3923cc87..2352888d 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1,5 +1,3 @@ -import { TeamDeviceCredentials } from '../types'; - export const notEmpty = (value: TValue | null | undefined): value is TValue => { return value !== null && value !== undefined; }; @@ -13,20 +11,6 @@ export const parseBooleanString = (booleanString: string): boolean => { throw new Error("The provided boolean variable should be either 'true' or 'false'"); }; -// TODO move this somewhere else -export const getTeamDeviceCredentialsFromEnv = (): TeamDeviceCredentials | null => { - const { DASHLANE_TEAM_UUID, DASHLANE_TEAM_ACCESS_KEY, DASHLANE_TEAM_SECRET_KEY } = process.env; - if (DASHLANE_TEAM_UUID && DASHLANE_TEAM_ACCESS_KEY && DASHLANE_TEAM_SECRET_KEY) { - return { - uuid: DASHLANE_TEAM_UUID, - accessKey: DASHLANE_TEAM_ACCESS_KEY, - secretKey: DASHLANE_TEAM_SECRET_KEY, - }; - } else { - return null; - } -}; - /** Remove underscores and capitalize string */ export const removeUnderscoresAndCapitalize = (string: string): string => { return string diff --git a/src/utils/teamDeviceCredentials.ts b/src/utils/teamDeviceCredentials.ts new file mode 100644 index 00000000..48f49ff4 --- /dev/null +++ b/src/utils/teamDeviceCredentials.ts @@ -0,0 +1,24 @@ +import { CouldNotFindTeamCredentialsError } from '../errors'; +import { TeamDeviceCredentials } from '../types'; + +let teamDeviceCredentials: TeamDeviceCredentials | null = null; + +export const initTeamDeviceCredentials = (): TeamDeviceCredentials | null => { + const { DASHLANE_TEAM_UUID, DASHLANE_TEAM_ACCESS_KEY, DASHLANE_TEAM_SECRET_KEY } = process.env; + if (DASHLANE_TEAM_UUID && DASHLANE_TEAM_ACCESS_KEY && DASHLANE_TEAM_SECRET_KEY) { + teamDeviceCredentials = { + uuid: DASHLANE_TEAM_UUID, + accessKey: DASHLANE_TEAM_ACCESS_KEY, + secretKey: DASHLANE_TEAM_SECRET_KEY, + }; + } + return teamDeviceCredentials; +}; + +export const getTeamDeviceCredentials = (): TeamDeviceCredentials => { + if (!teamDeviceCredentials) { + throw new CouldNotFindTeamCredentialsError(); + } + + return teamDeviceCredentials; +};