Skip to content

Commit e67ea52

Browse files
@W-20893693: Add AM - org topic
1 parent 2f6c0b3 commit e67ea52

31 files changed

Lines changed: 2872 additions & 136 deletions

packages/b2c-cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@
122122
"role": {
123123
"description": "Manage roles for Account Manager users"
124124
},
125+
"org": {
126+
"description": "Manage Account Manager organizations"
127+
},
125128
"slas": {
126129
"description": "Manage SLAS API clients and credentials",
127130
"subtopics": {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
import {Args, Flags} from '@oclif/core';
7+
import {OrgCommand, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
8+
import {getOrg, getOrgByName, getOrgAuditLogs} from '@salesforce/b2c-tooling-sdk/operations/orgs';
9+
import type {AuditLogRecord, AuditLogCollection} from '@salesforce/b2c-tooling-sdk';
10+
import {t} from '../../i18n/index.js';
11+
12+
function formatTimestamp(timestamp: string): string {
13+
const date = new Date(timestamp);
14+
const month = String(date.getMonth() + 1).padStart(2, '0');
15+
const day = String(date.getDate()).padStart(2, '0');
16+
const year = date.getFullYear();
17+
const hours = String(date.getHours()).padStart(2, '0');
18+
const minutes = String(date.getMinutes()).padStart(2, '0');
19+
const seconds = String(date.getSeconds()).padStart(2, '0');
20+
return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}`;
21+
}
22+
23+
const COLUMNS: Record<string, ColumnDef<AuditLogRecord>> = {
24+
timestamp: {
25+
header: 'Timestamp',
26+
get: (r) => (r.timestamp ? formatTimestamp(r.timestamp) : '-'),
27+
},
28+
authorDisplayName: {
29+
header: 'Author',
30+
get: (r) => r.authorDisplayName || '-',
31+
},
32+
authorEmail: {
33+
header: 'Email',
34+
get: (r) => r.authorEmail || '-',
35+
},
36+
eventType: {
37+
header: 'Event Type',
38+
get: (r) => r.eventType || '-',
39+
},
40+
eventMessage: {
41+
header: 'Message',
42+
get: (r) => r.eventMessage || '-',
43+
},
44+
};
45+
46+
const DEFAULT_COLUMNS = ['timestamp', 'authorDisplayName', 'authorEmail', 'eventType', 'eventMessage'];
47+
48+
const tableRenderer = new TableRenderer(COLUMNS);
49+
50+
/**
51+
* Command to get audit logs for an Account Manager organization.
52+
*/
53+
export default class OrgAudit extends OrgCommand<typeof OrgAudit> {
54+
static args = {
55+
org: Args.string({
56+
description: 'Organization ID or name',
57+
required: true,
58+
}),
59+
};
60+
61+
static description = t('commands.org.audit.description', 'Get audit logs for an Account Manager organization');
62+
63+
static enableJsonFlag = true;
64+
65+
static examples = [
66+
'<%= config.bin %> <%= command.id %> org-id',
67+
'<%= config.bin %> <%= command.id %> "My Organization"',
68+
'<%= config.bin %> <%= command.id %> org-id --json',
69+
];
70+
71+
static flags = {
72+
columns: Flags.string({
73+
description: 'Comma-separated list of columns to display',
74+
}),
75+
extended: Flags.boolean({
76+
char: 'x',
77+
description: 'Show extended columns',
78+
}),
79+
};
80+
81+
async run(): Promise<AuditLogCollection> {
82+
const {org} = this.args;
83+
84+
this.log(t('commands.org.audit.fetching', 'Fetching organization {{org}}...', {org}));
85+
86+
// Get organization first (by ID or name)
87+
let organization;
88+
try {
89+
organization = await getOrg(this.accountManagerOrgsClient, org);
90+
} catch (error) {
91+
// If not found by ID, try by name
92+
if (error instanceof Error && error.message.includes('not found')) {
93+
try {
94+
organization = await getOrgByName(this.accountManagerOrgsClient, org);
95+
} catch {
96+
throw new Error(t('commands.org.audit.orgNotFound', 'Organization {{org}} not found', {org}));
97+
}
98+
} else {
99+
throw error;
100+
}
101+
}
102+
103+
this.log(t('commands.org.audit.fetchingLogs', 'Fetching audit logs...'));
104+
105+
const result = await getOrgAuditLogs(this.accountManagerOrgsClient, organization.id);
106+
107+
if (this.jsonEnabled()) {
108+
return result;
109+
}
110+
111+
if (!result.content || result.content.length === 0) {
112+
this.log(t('commands.org.audit.noResults', 'No audit records found'));
113+
return result;
114+
}
115+
116+
// Determine columns to display
117+
let columnsToShow = DEFAULT_COLUMNS;
118+
if (this.flags.columns) {
119+
columnsToShow = this.flags.columns.split(',').map((c) => c.trim());
120+
} else if (this.flags.extended) {
121+
columnsToShow = Object.keys(COLUMNS);
122+
}
123+
124+
tableRenderer.render(result.content, columnsToShow);
125+
126+
return result;
127+
}
128+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
import {Args, ux} from '@oclif/core';
7+
import cliui from 'cliui';
8+
import {OrgCommand} from '@salesforce/b2c-tooling-sdk/cli';
9+
import {getOrg, getOrgByName} from '@salesforce/b2c-tooling-sdk/operations/orgs';
10+
import type {AccountManagerOrganization} from '@salesforce/b2c-tooling-sdk';
11+
import {t} from '../../i18n/index.js';
12+
13+
/**
14+
* Command to get details of a single Account Manager organization.
15+
*/
16+
export default class OrgGet extends OrgCommand<typeof OrgGet> {
17+
static args = {
18+
org: Args.string({
19+
description: 'Organization ID or name',
20+
required: true,
21+
}),
22+
};
23+
24+
static description = t('commands.org.get.description', 'Get details of an Account Manager organization');
25+
26+
static enableJsonFlag = true;
27+
28+
static examples = [
29+
'<%= config.bin %> <%= command.id %> org-id',
30+
'<%= config.bin %> <%= command.id %> "My Organization"',
31+
'<%= config.bin %> <%= command.id %> org-id --json',
32+
];
33+
34+
async run(): Promise<AccountManagerOrganization> {
35+
const {org} = this.args;
36+
37+
this.log(t('commands.org.get.fetching', 'Fetching organization {{org}}...', {org}));
38+
39+
// Try to get by ID first, then by name
40+
let organization: AccountManagerOrganization;
41+
try {
42+
organization = await getOrg(this.accountManagerOrgsClient, org);
43+
} catch (error) {
44+
// If not found by ID, try by name
45+
if (error instanceof Error && error.message.includes('not found')) {
46+
try {
47+
organization = await getOrgByName(this.accountManagerOrgsClient, org);
48+
} catch (nameError) {
49+
// Preserve the specific error message if it's already a "not found" error
50+
if (nameError instanceof Error && nameError.message.includes('not found')) {
51+
throw nameError;
52+
}
53+
throw new Error(t('commands.org.get.notFound', 'Organization {{org}} not found', {org}));
54+
}
55+
} else {
56+
throw error;
57+
}
58+
}
59+
60+
if (this.jsonEnabled()) {
61+
return organization;
62+
}
63+
64+
this.printOrgDetails(organization);
65+
66+
return organization;
67+
}
68+
69+
private printAccountIds(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
70+
const sfAccountIds = org.sfAccountIds as string[] | undefined;
71+
if (sfAccountIds && sfAccountIds.length > 0) {
72+
ui.div({text: '', padding: [0, 0, 0, 0]});
73+
ui.div({text: 'Account Ids', padding: [1, 0, 0, 0]});
74+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
75+
76+
for (const accountId of sfAccountIds) {
77+
ui.div({text: ` - ${accountId}`, padding: [0, 0, 0, 0]}, {text: '', padding: [0, 0, 0, 0]});
78+
}
79+
}
80+
}
81+
82+
private printAllowedVerifierTypes(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
83+
if (org.allowedVerifierTypes && org.allowedVerifierTypes.length > 0) {
84+
ui.div({text: '', padding: [0, 0, 0, 0]});
85+
ui.div({text: 'Allowed Verifier Types', padding: [1, 0, 0, 0]});
86+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
87+
88+
for (const verifierType of org.allowedVerifierTypes) {
89+
ui.div({text: ` - ${verifierType}`, padding: [0, 0, 0, 0]}, {text: '', padding: [0, 0, 0, 0]});
90+
}
91+
}
92+
}
93+
94+
private printBasicFields(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
95+
ui.div({text: 'Organization Details', padding: [1, 0, 0, 0]});
96+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
97+
98+
const fields: [string, string | undefined][] = [
99+
['ID', org.id],
100+
['Name', org.name],
101+
['2FA Enabled', org.twoFAEnabled ? 'Yes' : 'No'],
102+
['VaaS Enabled', org.vaasEnabled ? 'Yes' : 'No'],
103+
['SF Identity', org.sfIdentityFederation ? 'Yes' : 'No'],
104+
];
105+
106+
for (const [label, value] of fields) {
107+
if (value !== undefined) {
108+
ui.div({text: `${label}:`, width: 20, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]});
109+
}
110+
}
111+
}
112+
113+
private printContactUsers(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
114+
const contactUsers = org.contactUsers as string[] | undefined;
115+
if (contactUsers && contactUsers.length > 0) {
116+
ui.div({text: '', padding: [0, 0, 0, 0]});
117+
ui.div({text: 'Contact Users', padding: [1, 0, 0, 0]});
118+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
119+
120+
for (const userId of contactUsers) {
121+
ui.div({text: ` - ${userId}`, padding: [0, 0, 0, 0]}, {text: '', padding: [0, 0, 0, 0]});
122+
}
123+
}
124+
}
125+
126+
private printEmailDomains(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
127+
const emailsDomains = org.emailsDomains as string[] | undefined;
128+
if (emailsDomains && emailsDomains.length > 0) {
129+
ui.div({text: '', padding: [0, 0, 0, 0]});
130+
ui.div({text: 'Email Domains', padding: [1, 0, 0, 0]});
131+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
132+
133+
for (const domain of emailsDomains) {
134+
ui.div({text: ` - ${domain}`, padding: [0, 0, 0, 0]}, {text: '', padding: [0, 0, 0, 0]});
135+
}
136+
}
137+
}
138+
139+
private printOrgDetails(org: AccountManagerOrganization): void {
140+
const ui = cliui({width: process.stdout.columns || 80});
141+
142+
this.printBasicFields(ui, org);
143+
this.printContactUsers(ui, org);
144+
this.printAllowedVerifierTypes(ui, org);
145+
this.printAccountIds(ui, org);
146+
this.printRealms(ui, org);
147+
this.printEmailDomains(ui, org);
148+
this.printTwoFARoles(ui, org);
149+
this.printPasswordPolicy(ui, org);
150+
151+
ux.stdout(ui.toString());
152+
}
153+
154+
private printPasswordPolicy(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
155+
const passwordMinEntropy = org.passwordMinEntropy as number | undefined;
156+
const passwordHistorySize = org.passwordHistorySize as number | undefined;
157+
const passwordDaysExpiration = org.passwordDaysExpiration as number | undefined;
158+
159+
// Only show section if at least one password policy attribute exists
160+
if (passwordMinEntropy === undefined && passwordHistorySize === undefined && passwordDaysExpiration === undefined) {
161+
return;
162+
}
163+
164+
ui.div({text: '', padding: [0, 0, 0, 0]});
165+
ui.div({text: 'Password Policy', padding: [1, 0, 0, 0]});
166+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
167+
168+
const fields: [string, string | undefined][] = [
169+
['Minimum Password Length', passwordMinEntropy === undefined ? undefined : passwordMinEntropy.toString()],
170+
['Length of Password History', passwordHistorySize === undefined ? undefined : passwordHistorySize.toString()],
171+
[
172+
'Days Until Password Expires',
173+
passwordDaysExpiration === undefined ? undefined : passwordDaysExpiration.toString(),
174+
],
175+
];
176+
177+
for (const [label, value] of fields) {
178+
if (value === undefined) {
179+
continue;
180+
}
181+
ui.div({text: `${label}:`, width: 30, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]});
182+
}
183+
}
184+
185+
private printRealms(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
186+
if (org.realms && org.realms.length > 0) {
187+
ui.div({text: '', padding: [0, 0, 0, 0]});
188+
ui.div({text: 'Realms', padding: [1, 0, 0, 0]});
189+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
190+
ui.div({text: org.realms.join(', '), padding: [0, 0, 0, 0]}, {text: '', padding: [0, 0, 0, 0]});
191+
}
192+
}
193+
194+
private printTwoFARoles(ui: ReturnType<typeof cliui>, org: AccountManagerOrganization): void {
195+
if (org.twoFARoles && org.twoFARoles.length > 0) {
196+
ui.div({text: '', padding: [0, 0, 0, 0]});
197+
ui.div({text: '2FA Roles', padding: [1, 0, 0, 0]});
198+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
199+
200+
for (const role of org.twoFARoles) {
201+
ui.div({text: ` - ${role}`, padding: [0, 0, 0, 0]}, {text: '', padding: [0, 0, 0, 0]});
202+
}
203+
}
204+
}
205+
}

0 commit comments

Comments
 (0)