Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(devices): add list and remove devices commands #96

Merged
merged 4 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
},
{
"name": "Léos Julien"
},
{
"name": "Tristan Parisot"
}
],
"license": "Apache-2.0",
Expand Down
91 changes: 91 additions & 0 deletions src/command-handlers/devices.ts
quadristan marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { connectAndPrepare } from '../database';
import { deactivateDevices, listDevices, ListDevicesOutput } from '../endpoints';
import { reset } from '../middleware';
import { askConfirmReset } from '../utils';

type OutputDevice = ListDevicesOutput['devices'][number] & {
isCurrentDevice: boolean;
};

export async function listAllDevices(options: { json: boolean }) {
const { secrets, deviceConfiguration } = await connectAndPrepare({ autoSync: false });
if (!deviceConfiguration) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think you need this, the connectAndPrepare will register the device if it doesn't exist

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to fix the typings of connectAndPrepare then because right now it returns a nullable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok my bad, then leave it, i will take a look

throw new Error('Require to be connected');
}
const listDevicesResponse = await listDevices({ secrets, login: deviceConfiguration.login });
const result: OutputDevice[] = listDevicesResponse.devices.map(
(c) => <OutputDevice>{ ...c, isCurrentDevice: c.deviceId === secrets.accessKey }
quadristan marked this conversation as resolved.
Show resolved Hide resolved
);

if (options.json) {
console.log(JSON.stringify(result));
} else {
// order by last activity, ascending.
// we sort it only on non-json because it is likely that it will be used
// for human consumption
result.sort((a, b) => a.lastActivityDateUnix - b.lastActivityDateUnix);

// print results
for (const device of result) {
console.log(
[
device.deviceId,
device.deviceName,
device.devicePlatform,
device.isCurrentDevice ? 'current' : 'other',
].join('\t')
);
}
}
}

export async function removeAllDevices(devices: string[] | null, options: { all: boolean; others: boolean }) {
if (devices && devices.length > 0 && (options.all || options.others)) {
throw new Error('devices cannot be specified when you use --all or --others');
}
if (options.all && options.others) {
throw new Error('Please use either --all, either --other, but not both');
}

const { secrets, deviceConfiguration, db } = await connectAndPrepare({ autoSync: false });
if (!deviceConfiguration) {
throw new Error('Requires to be connected');
}
const listDevicesResponse = await listDevices({ secrets, login: deviceConfiguration.login });
const existingDevices = listDevicesResponse.devices.map((d) => d.deviceId);
quadristan marked this conversation as resolved.
Show resolved Hide resolved

if (options.all) {
devices = existingDevices;
} else if (options.others) {
devices = existingDevices.filter((d) => d != secrets.accessKey);
} else if (!devices) {
// if there is no devices provided, well we will have an easy job
// let's not fail
devices = [];
}

const shouldReset = devices.includes(secrets.accessKey);

if (shouldReset) {
const confirmation = await askConfirmReset();
if (!confirmation) {
return;
}
}
const notFoundDevices = devices.filter((d) => !existingDevices.includes(d));
if (notFoundDevices.length > 0) {
throw new Error(`These devices do not exist: ${notFoundDevices.join('\t')}`);
}

await deactivateDevices({
deviceIds: devices,
login: deviceConfiguration.login,
secrets,
pairingGroupIds: [],
quadristan marked this conversation as resolved.
Show resolved Hide resolved
quadristan marked this conversation as resolved.
Show resolved Hide resolved
});

if (shouldReset) {
reset({ db, secrets });
}
db.close();
}
1 change: 1 addition & 0 deletions src/command-handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './devices';
32 changes: 32 additions & 0 deletions src/endpoints/deactivateDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { requestUserApi } from '../requestApi';
import { Secrets } from '../types';

interface DeactivateDevicesParams {
quadristan marked this conversation as resolved.
Show resolved Hide resolved
secrets: Secrets;
login: string;

/**
* List of deviceIds to deactivate
*/
deviceIds?: string[];
/**
* List of pairingGroupIds to deactivate
*/
pairingGroupIds?: string[];
}

export interface DeactivateDevicesOutput {}

export const deactivateDevices = (params: DeactivateDevicesParams) =>
requestUserApi<DeactivateDevicesOutput>({
path: 'devices/DeactivateDevices',
payload: {
deviceIds: params.deviceIds,
pairingGroupIds: params.pairingGroupIds,
},
login: params.login,
deviceKeys: {
accessKey: params.secrets.accessKey,
secretKey: params.secrets.secretKey,
},
});
2 changes: 2 additions & 0 deletions src/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './performTotpVerification';
export * from './requestDeviceRegistration';
export * from './getAuditLogs';
export * from './getTeamReport';
export * from './listDevices';
export * from './deactivateDevices';
50 changes: 50 additions & 0 deletions src/endpoints/listDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { requestUserApi } from '../requestApi';
import { Secrets } from '../types';

interface ListDeviceParams {
secrets: Secrets;
login: string;
}

export interface ListDevicesOutput {
pairingGroups: {
pairingGroupUUID: string;
/**
* A computed name for the pairing group null if we don't manage to compute one.
*/
name: string;
/**
* A computed platform for the pairing group null if we don't manage to compute one.
*/
platform: string;
devices: string[];
isBucketOwner?: boolean;
}[];
/**
* @minItems 1
*/
devices: {
deviceId: string;
deviceName: null | string;
devicePlatform: null | string;
creationDateUnix: number;
lastUpdateDateUnix: number;
lastActivityDateUnix: number;
temporary: boolean;
/**
* FALSE if the device is in a pairing group with isBucketOwner = true
*/
isBucketOwner?: boolean;
}[];
}

export const listDevices = (params: ListDeviceParams) =>
requestUserApi<ListDevicesOutput>({
path: 'devices/ListDevices',
payload: {},
login: params.login,
deviceKeys: {
accessKey: params.secrets.accessKey,
secretKey: params.secrets.secretKey,
},
});
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from './middleware';
import { cliVersionToString, CLI_VERSION } from './cliVersion';
import { registerTeamDevice } from './endpoints/registerTeamDevice';
import { listAllDevices, removeAllDevices } from './command-handlers';

const teamDeviceCredentials = getTeamDeviceCredentialsFromEnv();

Expand Down Expand Up @@ -258,6 +259,21 @@ program
}
});

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 (dangerous)')
.option('--other', 'remove all other devices')
.argument('[device ids...]', 'ids of the devices to remove')
.description('Unregisters a list of devices. Unregistering the CLI will implies a reset')
quadristan marked this conversation as resolved.
Show resolved Hide resolved
.action(removeAllDevices);

program.parseAsync().catch((error: Error) => {
console.error(`ERROR: ${error.message}`);
process.exit(1);
Expand Down