Skip to content

Commit d75b255

Browse files
committed
feat(marketplace): access audit list and per-user block controls
Add the access audit section to the allow-list page: an audit list of everyone with effective access (filterable by status and granting rule), block/unblock controls per user, and the rule-match counts that flag allow-list rules which currently grant access to nobody.
1 parent 0522d8e commit d75b255

11 files changed

Lines changed: 2458 additions & 2 deletions

File tree

client/app/api/system/Admin.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
} from 'types/course/announcements';
66
import { CourseListData } from 'types/system/courses';
77
import { InstanceListData, InstancePermissions } from 'types/system/instances';
8-
import { AllowlistRulePreviewData } from 'types/system/marketplaceAccess';
8+
import {
9+
AllowlistRulePreviewData,
10+
MarketplaceAccessData,
11+
} from 'types/system/marketplaceAccess';
912
import {
1013
AllowlistRuleData,
1114
AllowlistRuleFormData,
@@ -248,4 +251,34 @@ export default class AdminAPI extends BaseSystemAPI {
248251
`${AdminAPI.#urlPrefix}/marketplace_allowlist_rules/${id}`,
249252
);
250253
}
254+
255+
/**
256+
* Fetches the marketplace access audit list (everyone with effective access, blocked flagged).
257+
*/
258+
indexMarketplaceAccess(): Promise<AxiosResponse<MarketplaceAccessData>> {
259+
return this.client.get(`${AdminAPI.#urlPrefix}/marketplace_access`);
260+
}
261+
262+
/**
263+
* Blocks (disables) a user's marketplace access. Returns the created block's id.
264+
*/
265+
blockMarketplaceUser(
266+
userId: number,
267+
): Promise<AxiosResponse<{ id: number; userId: number }>> {
268+
return this.client.post(
269+
`${AdminAPI.#urlPrefix}/marketplace_access_blocks`,
270+
{
271+
user_id: userId,
272+
},
273+
);
274+
}
275+
276+
/**
277+
* Removes a block, re-enabling the user's marketplace access.
278+
*/
279+
unblockMarketplaceUser(blockId: number): Promise<AxiosResponse> {
280+
return this.client.delete(
281+
`${AdminAPI.#urlPrefix}/marketplace_access_blocks/${blockId}`,
282+
);
283+
}
251284
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { useState } from 'react';
2+
import { defineMessages } from 'react-intl';
3+
import { FilterList } from '@mui/icons-material';
4+
import {
5+
Badge,
6+
Button,
7+
Checkbox,
8+
Divider,
9+
FormControlLabel,
10+
IconButton,
11+
Menu,
12+
Tooltip,
13+
Typography,
14+
} from '@mui/material';
15+
16+
import useTranslation from 'lib/hooks/useTranslation';
17+
18+
export interface RuleOption {
19+
id: number;
20+
label: string;
21+
}
22+
23+
interface Props {
24+
showActive: boolean;
25+
showBlocked: boolean;
26+
onToggleActive: () => void;
27+
onToggleBlocked: () => void;
28+
/** Empty when the marketplace is open to everyone — the rule group is then meaningless. */
29+
ruleOptions: RuleOption[];
30+
/**
31+
* Ids the admin has UNchecked. Tracking exclusions rather than inclusions means a newly added
32+
* rule is filtered in by default, with no state to resynchronise when `ruleOptions` changes.
33+
*/
34+
uncheckedRuleIds: Set<number>;
35+
onToggleRule: (id: number) => void;
36+
onClear: () => void;
37+
}
38+
39+
const translations = defineMessages({
40+
trigger: {
41+
id: 'system.admin.admin.MarketplaceAccessFilter.trigger',
42+
defaultMessage: 'Filter',
43+
},
44+
status: {
45+
id: 'system.admin.admin.MarketplaceAccessFilter.status',
46+
defaultMessage: 'Status',
47+
},
48+
active: {
49+
id: 'system.admin.admin.MarketplaceAccessFilter.active',
50+
defaultMessage: 'Active',
51+
},
52+
blocked: {
53+
id: 'system.admin.admin.MarketplaceAccessFilter.blocked',
54+
defaultMessage: 'Blocked',
55+
},
56+
allowedByRule: {
57+
id: 'system.admin.admin.MarketplaceAccessFilter.allowedByRule',
58+
defaultMessage: 'Allowed by rule',
59+
},
60+
clearAll: {
61+
id: 'system.admin.admin.MarketplaceAccessFilter.clearAll',
62+
defaultMessage: 'Clear all',
63+
},
64+
});
65+
66+
const MarketplaceAccessFilter = ({
67+
showActive,
68+
showBlocked,
69+
onToggleActive,
70+
onToggleBlocked,
71+
ruleOptions,
72+
uncheckedRuleIds,
73+
onToggleRule,
74+
onClear,
75+
}: Props): JSX.Element => {
76+
const { t } = useTranslation();
77+
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
78+
79+
const activeCount =
80+
(showActive ? 0 : 1) + (showBlocked ? 0 : 1) + uncheckedRuleIds.size;
81+
82+
const label = t(translations.trigger);
83+
84+
return (
85+
<>
86+
<Tooltip title={label}>
87+
<Badge badgeContent={activeCount} className="shrink-0" color="primary">
88+
<IconButton
89+
aria-label={label}
90+
color="primary"
91+
onClick={(event): void => setAnchor(event.currentTarget)}
92+
>
93+
<FilterList />
94+
</IconButton>
95+
</Badge>
96+
</Tooltip>
97+
98+
<Menu
99+
anchorEl={anchor}
100+
onClose={(): void => setAnchor(null)}
101+
open={Boolean(anchor)}
102+
>
103+
<div className="flex min-w-[16rem] flex-col px-4 py-2">
104+
<Typography color="text.secondary" variant="caption">
105+
{t(translations.status)}
106+
</Typography>
107+
108+
<FormControlLabel
109+
control={
110+
<Checkbox checked={showActive} onChange={onToggleActive} />
111+
}
112+
label={t(translations.active)}
113+
/>
114+
115+
<FormControlLabel
116+
control={
117+
<Checkbox checked={showBlocked} onChange={onToggleBlocked} />
118+
}
119+
label={t(translations.blocked)}
120+
/>
121+
122+
{ruleOptions.length > 0 && (
123+
<>
124+
<Divider className="my-2" />
125+
126+
<Typography color="text.secondary" variant="caption">
127+
{t(translations.allowedByRule)}
128+
</Typography>
129+
130+
{ruleOptions.map((option) => (
131+
<FormControlLabel
132+
key={option.id}
133+
control={
134+
<Checkbox
135+
checked={!uncheckedRuleIds.has(option.id)}
136+
onChange={(): void => onToggleRule(option.id)}
137+
/>
138+
}
139+
label={option.label}
140+
/>
141+
))}
142+
</>
143+
)}
144+
145+
<Button className="mt-2 self-end" onClick={onClear} size="small">
146+
{t(translations.clearAll)}
147+
</Button>
148+
</div>
149+
</Menu>
150+
</>
151+
);
152+
};
153+
154+
export default MarketplaceAccessFilter;

0 commit comments

Comments
 (0)