diff --git a/src/command/access/grant.ts b/src/command/access/grant.ts index 3aabc5b5..6f9682b8 100644 --- a/src/command/access/grant.ts +++ b/src/command/access/grant.ts @@ -60,6 +60,7 @@ export class Grant extends AccessCommand implements LeafCommand { granteeListRef: response.ref.toHex(), operation: AccessHistoryOperation.Grant, createdAt: Date.now(), + grantees: this.grantees, }) if (this.verbose) { diff --git a/src/command/access/history.ts b/src/command/access/history.ts new file mode 100644 index 00000000..02dbb755 --- /dev/null +++ b/src/command/access/history.ts @@ -0,0 +1,57 @@ +import { LeafCommand, Option } from 'furious-commander' +import { exit } from 'process' +import { AccessHistory } from '../../service/access' +import { AccessHistoryOperation } from '../../service/access/types/history-event' +import { createKeyValue, errorText, formatDate } from '../../utils/text' +import { AccessCommand } from './access-command' + +export class History extends AccessCommand implements LeafCommand { + public readonly name = 'history' + + public readonly description = 'Show the local history of operations on a grantee list' + + @Option({ + key: 'list-name', + alias: 'n', + description: 'Name of the grantee list', + required: true, + type: 'string', + }) + public listName!: string + + public run() { + super.init() + + const accessHistory = new AccessHistory(this.commandConfig, this.console) + const events = accessHistory.getEvents(this.listName).sort((a, b) => b.createdAt - a.createdAt) + + if (events.length === 0) { + this.console.error(errorText(`Grantee list with name '${this.listName}' does not exist!`)) + + exit(1) + } + + const lastEvent = events[0] + this.console.log(createKeyValue('Latest history address', lastEvent.historyAddress)) + this.console.log(createKeyValue('Latest grantee list reference', lastEvent.granteeListRef)) + + this.console.log('') + + for (const event of events) { + this.console.log(createKeyValue('Operation', event.operation)) + this.console.log(createKeyValue('Date', formatDate(new Date(event.createdAt)))) + + if ([AccessHistoryOperation.Grant, AccessHistoryOperation.Revoke].includes(event.operation) && event.grantees) { + this.console.log(createKeyValue('Grantees', event.grantees.join(', '))) + } + + if (this.verbose) { + this.console.log(createKeyValue('Stamp', event.stampId)) + this.console.log(createKeyValue('Grantee list reference', event.granteeListRef)) + this.console.log(createKeyValue('History address', event.historyAddress)) + } + + this.console.log('---') + } + } +} diff --git a/src/command/access/index.ts b/src/command/access/index.ts index f9444d77..c3e95d32 100644 --- a/src/command/access/index.ts +++ b/src/command/access/index.ts @@ -1,5 +1,6 @@ import { GroupCommand } from 'furious-commander' import { Grant } from './grant' +import { History } from './history' import { Init } from './init' import { List } from './list' import { Revoke } from './revoke' @@ -9,5 +10,5 @@ export class Access implements GroupCommand { public readonly description = 'Share access to your uploaded files/folders' - public subCommandClasses = [Init, Grant, Revoke, List] + public subCommandClasses = [Init, Grant, Revoke, List, History] } diff --git a/src/command/access/revoke.ts b/src/command/access/revoke.ts index 66023b32..1758dd5f 100644 --- a/src/command/access/revoke.ts +++ b/src/command/access/revoke.ts @@ -51,7 +51,7 @@ export class Revoke extends AccessCommand implements LeafCommand { const response = await this.bee.patchGrantees(stampId, granteeListRef, historyAddress, { revoke: this.grantees }) if (response.status === 200) { - this.console.log(successText(`Access revoked from ${this.grantees.join(', ')}!`)) + this.console.log(successText(`Access revoked from ${this.grantees.join(', ')}`)) } accessHistory.addEvent(this.listName, { @@ -60,6 +60,7 @@ export class Revoke extends AccessCommand implements LeafCommand { granteeListRef: response.ref.toHex(), operation: AccessHistoryOperation.Revoke, createdAt: Date.now(), + grantees: this.grantees, }) if (this.verbose) { diff --git a/src/service/access/index.ts b/src/service/access/index.ts index f68e6a99..548ce7b6 100644 --- a/src/service/access/index.ts +++ b/src/service/access/index.ts @@ -52,13 +52,18 @@ export class AccessHistory { history[granteeListName] = [] } - history[granteeListName].push({ + const newEvent = { stampId: event.stampId, historyAddress: event.historyAddress, granteeListRef: event.granteeListRef, operation: event.operation, createdAt: event.createdAt, - }) + } as AccessHistoryEvent + + if (event.grantees) { + newEvent.grantees = event.grantees + } + history[granteeListName].push(event) writeFileSync(this.commandConfig.getAccessHistoryFilePath(), JSON.stringify(history)) } diff --git a/src/service/access/types/history-event.ts b/src/service/access/types/history-event.ts index 3a6cd30a..8e1fca29 100644 --- a/src/service/access/types/history-event.ts +++ b/src/service/access/types/history-event.ts @@ -10,6 +10,7 @@ export type AccessHistoryEvent = { granteeListRef: string operation: AccessHistoryOperation createdAt: number + grantees?: string[] } export type AccessHistoryLog = { [name: string]: AccessHistoryEvent[] } diff --git a/src/service/stamp/index.ts b/src/service/stamp/index.ts index ffaa877d..188c64b1 100644 --- a/src/service/stamp/index.ts +++ b/src/service/stamp/index.ts @@ -2,7 +2,7 @@ import { Bee, PostageBatch } from '@ethersphere/bee-js' import { Dates } from 'cafe-utility' import { exit } from 'process' import { CommandLog } from '../../command/root-command/command-log' -import { createKeyValue } from '../../utils/text' +import { createKeyValue, formatDate } from '../../utils/text' /** * Displays an interactive stamp picker to select a Stamp ID. @@ -60,7 +60,7 @@ export function printStamp(stamp: PostageBatch, console: CommandLog, settings?: if (settings?.showTtl) { const ttl = Dates.secondsToHumanTime(stamp.duration.toSeconds()) - const expires = stamp.duration.toEndDate().toISOString().replace('T', ' ').slice(0, 19) + ' UTC' + const expires = formatDate(stamp.duration.toEndDate()) console.log(createKeyValue('TTL', `${ttl} (${expires})`)) } diff --git a/src/utils/text.ts b/src/utils/text.ts index a7f474cd..0fc46f50 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -37,6 +37,10 @@ export function ellipsis(value: string, startIndex: number, endIndex?: number | return `${value.slice(0, startIndex)}...${endIndex ? value.slice(endIndex) : ''}` } +export function formatDate(date: Date): string { + return date.toISOString().replace('T', ' ').slice(0, 19) + ' UTC' +} + export function printDivided( items: T[], printFn: (item: T, console: CommandLog) => void, diff --git a/test/command/access.spec.ts b/test/command/access.spec.ts index 66654e37..646b39cd 100644 --- a/test/command/access.spec.ts +++ b/test/command/access.spec.ts @@ -47,7 +47,7 @@ describeCommand( await System.sleepMillis(1000) const pubKey = await getPublicAddress('http://localhost:21633') await invokeTestCli(['access', 'grant', '--list-name', 'test-access', '--grantee', pubKey]) - expect(getLastMessage()).toContain(`Access granted to ${pubKey}!`) + expect(getLastMessage()).toContain(`Access granted to ${pubKey}`) }) describe('when grantee list does not exist', () => { @@ -88,7 +88,7 @@ describeCommand( await invokeTestCli(['access', 'init', ...getStampOption(), '-n', 'test-access', '--grantee', pubKey]) await System.sleepMillis(1000) await invokeTestCli(['access', 'revoke', '--list-name', 'test-access', '--grantee', pubKey]) - expect(getLastMessage()).toContain(`Access revoked from ${pubKey}!`) + expect(getLastMessage()).toContain(`Access revoked from ${pubKey}`) }) describe('when grantee list does not exist', () => { @@ -139,6 +139,40 @@ describeCommand( await invokeTestCli(['access', 'list', '--list-name', 'test-access']) expect(getLastMessage()).toContain("Grantee list 'test-access' has no grantees.") }) + + describe('when grantee list does not exist', () => { + it('should show error message', async () => { + await invokeTestCli(['access', 'list', '-n', 'nonexistent-list']) + expect(consoleMessages[0]).toContain("Grantee list with name 'nonexistent-list' does not exist!") + expect(consoleMessages[1]).toContain('process.exit() was called with code 1') + }) + }) + }) + + describe('history', () => { + it('should show the history of operations on a grantee list', async () => { + const granteePubKey = await getPublicAddress('http://localhost:21633') + await invokeTestCli(['access', 'init', ...getStampOption(), '-n', 'test-access', '--grantee', granteePubKey]) + await System.sleepMillis(1000) + await invokeTestCli(['access', 'revoke', '--list-name', 'test-access', '--grantee', granteePubKey]) + await System.sleepMillis(1000) + await invokeTestCli(['access', 'history', '--list-name', 'test-access']) + process.stderr.write(consoleMessages.join('\n')) + expect(getNthLastMessage(10)).toContain('Latest history address') + expect(getNthLastMessage(9)).toContain('Latest grantee list reference') + expect(getNthLastMessage(7)).toContain('revoke') + expect(getNthLastMessage(5)).toContain('Grantees:') + expect(getNthLastMessage(5)).toContain(granteePubKey) + expect(getNthLastMessage(3)).toContain('init') + }) + + describe('when grantee list does not exist', () => { + it('should show error message', async () => { + await invokeTestCli(['access', 'history', '-n', 'nonexistent-list']) + expect(consoleMessages[0]).toContain("Grantee list with name 'nonexistent-list' does not exist!") + expect(consoleMessages[1]).toContain('process.exit() was called with code 1') + }) + }) }) }, { configFileName: 'access' },