Skip to content

Commit a65ecf0

Browse files
committed
Improve blacklist page
1 parent f793084 commit a65ecf0

2 files changed

Lines changed: 174 additions & 78 deletions

File tree

src/lib/api/internal/v1/apis/AdminApi.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export interface AdminGetUsersRequest {
7777
$limit?: number;
7878
}
7979

80+
export interface AdminListEmailProviderBlacklistRequest {
81+
match?: string;
82+
}
83+
84+
export interface AdminListUsernameBlacklistRequest {
85+
match?: string;
86+
}
87+
8088
export interface AdminModifyUserRequest {
8189
userId: string;
8290
userPatchDto?: UserPatchDto;
@@ -211,27 +219,29 @@ export interface AdminApiInterface {
211219

212220
/**
213221
*
222+
* @param {string} [match]
214223
* @param {*} [options] Override http request option.
215224
* @throws {RequiredError}
216225
* @memberof AdminApiInterface
217226
*/
218-
adminListEmailProviderBlacklistRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<EmailProviderBlacklistDto>>>;
227+
adminListEmailProviderBlacklistRaw(requestParameters: AdminListEmailProviderBlacklistRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<EmailProviderBlacklistDto>>>;
219228

220229
/**
221230
*/
222-
adminListEmailProviderBlacklist(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<EmailProviderBlacklistDto>>;
231+
adminListEmailProviderBlacklist(match?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<EmailProviderBlacklistDto>>;
223232

224233
/**
225234
*
235+
* @param {string} [match]
226236
* @param {*} [options] Override http request option.
227237
* @throws {RequiredError}
228238
* @memberof AdminApiInterface
229239
*/
230-
adminListUsernameBlacklistRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<UserNameBlacklistDto>>>;
240+
adminListUsernameBlacklistRaw(requestParameters: AdminListUsernameBlacklistRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<UserNameBlacklistDto>>>;
231241

232242
/**
233243
*/
234-
adminListUsernameBlacklist(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<UserNameBlacklistDto>>;
244+
adminListUsernameBlacklist(match?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<UserNameBlacklistDto>>;
235245

236246
/**
237247
*
@@ -567,9 +577,13 @@ export class AdminApi extends runtime.BaseAPI implements AdminApiInterface {
567577

568578
/**
569579
*/
570-
async adminListEmailProviderBlacklistRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<EmailProviderBlacklistDto>>> {
580+
async adminListEmailProviderBlacklistRaw(requestParameters: AdminListEmailProviderBlacklistRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<EmailProviderBlacklistDto>>> {
571581
const queryParameters: any = {};
572582

583+
if (requestParameters['match'] != null) {
584+
queryParameters['match'] = requestParameters['match'];
585+
}
586+
573587
const headerParameters: runtime.HTTPHeaders = {};
574588

575589

@@ -587,16 +601,20 @@ export class AdminApi extends runtime.BaseAPI implements AdminApiInterface {
587601

588602
/**
589603
*/
590-
async adminListEmailProviderBlacklist(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<EmailProviderBlacklistDto>> {
591-
const response = await this.adminListEmailProviderBlacklistRaw(initOverrides);
604+
async adminListEmailProviderBlacklist(match?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<EmailProviderBlacklistDto>> {
605+
const response = await this.adminListEmailProviderBlacklistRaw({ match: match }, initOverrides);
592606
return await response.value();
593607
}
594608

595609
/**
596610
*/
597-
async adminListUsernameBlacklistRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<UserNameBlacklistDto>>> {
611+
async adminListUsernameBlacklistRaw(requestParameters: AdminListUsernameBlacklistRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<UserNameBlacklistDto>>> {
598612
const queryParameters: any = {};
599613

614+
if (requestParameters['match'] != null) {
615+
queryParameters['match'] = requestParameters['match'];
616+
}
617+
600618
const headerParameters: runtime.HTTPHeaders = {};
601619

602620

@@ -614,8 +632,8 @@ export class AdminApi extends runtime.BaseAPI implements AdminApiInterface {
614632

615633
/**
616634
*/
617-
async adminListUsernameBlacklist(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<UserNameBlacklistDto>> {
618-
const response = await this.adminListUsernameBlacklistRaw(initOverrides);
635+
async adminListUsernameBlacklist(match?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<UserNameBlacklistDto>> {
636+
const response = await this.adminListUsernameBlacklistRaw({ match: match }, initOverrides);
619637
return await response.value();
620638
}
621639

src/routes/(authenticated)/admin/blacklists/+page.svelte

Lines changed: 146 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,72 +13,137 @@
1313
import * as Select from '$lib/components/ui/select';
1414
import { Separator } from '$lib/components/ui/separator';
1515
import { handleApiError } from '$lib/errorhandling/apiErrorHandling';
16-
import { onMount } from 'svelte';
16+
import type { TimeoutHandle } from '$lib/types/WAPI';
1717
1818
// --- state ---
1919
let usernameEntry = $state<string>('');
2020
let matchTypeEntry = $state<MatchTypeEnum>(MatchTypeEnum.Exact);
2121
let usernameBlacklist = $state<UserNameBlacklistDto[]>([]);
22+
let isLoadingUsernames = $state(false);
2223
2324
let emailEntry = $state<string>('');
2425
let emailBlacklist = $state<EmailProviderBlacklistDto[]>([]);
26+
let isLoadingEmails = $state(false);
2527
26-
// Prepare select options
2728
const matchTypes = Object.values(MatchTypeEnum).map((type) => ({ value: type, label: type }));
28-
const triggerLabel = $derived.by(
29-
() => matchTypes.find((m) => m.value === matchTypeEntry)?.label ?? 'Match type'
29+
30+
let triggerLabel = $derived(
31+
matchTypes.find((m) => m.value === matchTypeEntry)?.label ?? 'Match type'
3032
);
3133
32-
// --- load helpers ---
33-
function loadUsernames() {
34-
adminApi.adminListUsernameBlacklist().then((list) => (usernameBlacklist = list));
34+
function limit<T>(arr: T[], size: number) {
35+
return arr.length > size ? arr.slice(0, size) : arr;
3536
}
36-
function loadEmails() {
37-
adminApi.adminListEmailProviderBlacklist().then((list) => (emailBlacklist = list));
37+
38+
// --- API calls ---
39+
async function loadUsernames(filter: string) {
40+
isLoadingUsernames = true;
41+
try {
42+
usernameBlacklist = await adminApi.adminListUsernameBlacklist(
43+
filter.length > 0 ? filter : undefined
44+
);
45+
} catch (err) {
46+
handleApiError(err);
47+
} finally {
48+
isLoadingUsernames = false;
49+
}
3850
}
3951
40-
// --- username handlers ---
41-
function addUsername() {
42-
if (!usernameEntry.trim()) return;
43-
adminApi
44-
.adminAddUsernameBlacklist({
45-
value: usernameEntry,
46-
matchType: matchTypeEntry,
47-
})
48-
.then(() => {
49-
usernameEntry = '';
50-
matchTypeEntry = MatchTypeEnum.Exact;
51-
loadUsernames();
52-
});
52+
async function loadEmails(filter: string) {
53+
isLoadingEmails = true;
54+
try {
55+
emailBlacklist = await adminApi.adminListEmailProviderBlacklist(
56+
filter.length > 0 ? filter : undefined
57+
);
58+
} catch (err) {
59+
handleApiError(err);
60+
} finally {
61+
isLoadingEmails = false;
62+
}
5363
}
54-
function removeUsername(id: string) {
55-
adminApi.adminRemoveUsernameBlacklist(id).then(loadUsernames);
64+
65+
// --- handlers ---
66+
async function addUsernameEntry() {
67+
const value = usernameEntry.trim();
68+
if (!value) return;
69+
70+
try {
71+
await adminApi.adminAddUsernameBlacklist({ value, matchType: matchTypeEntry });
72+
loadUsernames(value);
73+
} catch (err) {
74+
handleApiError(err);
75+
}
5676
}
5777
58-
function uploadDomains(domains: string[]) {
59-
return adminApi
60-
.adminAddEmailProviderBlacklist({ domains })
61-
.then(loadEmails)
62-
.catch(handleApiError);
78+
async function removeUsername(id: string) {
79+
try {
80+
await adminApi.adminRemoveUsernameBlacklist(id);
81+
loadUsernames(usernameEntry);
82+
} catch (err) {
83+
handleApiError(err);
84+
}
6385
}
6486
65-
// --- email-provider handlers ---
66-
function addEmail() {
67-
uploadDomains([emailEntry]);
68-
emailEntry = '';
87+
async function addEmail(domain: string) {
88+
try {
89+
await adminApi.adminAddEmailProviderBlacklist({ domains: [domain] });
90+
} catch (err) {
91+
handleApiError(err);
92+
}
6993
}
70-
function addEmailsBatch() {
71-
navigator.clipboard
72-
.readText()
73-
.then((text) => uploadDomains(text.split(/\r|\n/).filter((s) => s.trim().length != 0)));
94+
95+
async function addEmailEntry() {
96+
const value = emailEntry.trim();
97+
if (!value) return;
98+
await addEmail(value);
99+
loadEmails(value);
74100
}
75-
function removeEmail(id: string) {
76-
adminApi.adminRemoveEmailProviderBlacklist(id).then(loadEmails);
101+
102+
async function addEmailsBatch() {
103+
try {
104+
const text = await navigator.clipboard.readText();
105+
const domains = text
106+
.split(/\r?\n/)
107+
.map((s) => s.trim())
108+
.filter((s) => s.length > 0);
109+
if (domains.length) {
110+
await adminApi.adminAddEmailProviderBlacklist({ domains });
111+
loadEmails('');
112+
}
113+
} catch (err) {
114+
handleApiError(err);
115+
}
77116
}
78117
79-
onMount(() => {
80-
loadUsernames();
81-
loadEmails();
118+
async function removeEmail(id: string) {
119+
try {
120+
await adminApi.adminRemoveEmailProviderBlacklist(id);
121+
loadEmails(emailEntry);
122+
} catch (err) {
123+
handleApiError(err);
124+
}
125+
}
126+
127+
let usernameDebounce: TimeoutHandle | undefined;
128+
$effect(() => {
129+
clearTimeout(usernameDebounce);
130+
if (usernameEntry.length == 0) {
131+
loadUsernames('');
132+
return;
133+
}
134+
135+
usernameDebounce = setTimeout(() => loadUsernames(usernameEntry), 400);
136+
});
137+
138+
let emailDebounce: TimeoutHandle | undefined;
139+
$effect(() => {
140+
clearTimeout(emailDebounce);
141+
if (emailEntry.length == 0) {
142+
loadEmails('');
143+
return;
144+
}
145+
146+
emailDebounce = setTimeout(() => loadEmails(emailEntry), 400);
82147
});
83148
</script>
84149

@@ -87,34 +152,39 @@
87152
<Card>
88153
<CardHeader>
89154
<CardTitle>Username Blacklist</CardTitle>
90-
<div class="mt-4 flex items-center gap-2">
91-
<TextInput label="Username" placeholder="e.g. baduser123" bind:value={usernameEntry} />
92-
93-
<Select.Root type="single" name="matchType" bind:value={matchTypeEntry}>
94-
<Select.Trigger class="w-[150px]">{triggerLabel}</Select.Trigger>
95-
<Select.Content>
96-
<Select.Group>
97-
<Select.Label>Match Type</Select.Label>
98-
{#each matchTypes as m (m.value)}
99-
<Select.Item value={m.value} label={m.label}>{m.label}</Select.Item>
100-
{/each}
101-
</Select.Group>
102-
</Select.Content>
103-
</Select.Root>
104-
105-
<Button onclick={addUsername}>Add</Button>
106-
</div>
155+
<TextInput placeholder="e.g. baduser123" bind:value={usernameEntry}>
156+
{#snippet after()}
157+
<Select.Root type="single" name="matchType" bind:value={matchTypeEntry}>
158+
<Select.Trigger class="w-[150px]">{triggerLabel}</Select.Trigger>
159+
<Select.Content>
160+
<Select.Group>
161+
<Select.Label>Match Type</Select.Label>
162+
{#each matchTypes as m (m.value)}
163+
<Select.Item value={m.value} label={m.label}>{m.label}</Select.Item>
164+
{/each}
165+
</Select.Group>
166+
</Select.Content>
167+
</Select.Root>
168+
169+
<Button onclick={addUsernameEntry} disabled={isLoadingUsernames}>Add</Button>
170+
{/snippet}
171+
</TextInput>
107172
</CardHeader>
108173
<CardContent>
109174
<ScrollArea class="max-h-64">
110175
<div class="space-y-2">
111-
{#each usernameBlacklist as item (item.id)}
176+
{#each limit(usernameBlacklist, 10) as item (item.id)}
112177
<div class="flex items-center justify-between text-sm">
113178
<div class="flex items-center space-x-2">
114179
<span>{item.value}</span>
115180
<span class="px-2 py-0.5 text-xs rounded bg-gray-800">{item.matchType}</span>
116181
</div>
117-
<Button variant="ghost" size="sm" onclick={() => removeUsername(item.id)}>
182+
<Button
183+
variant="ghost"
184+
size="sm"
185+
onclick={() => removeUsername(item.id)}
186+
disabled={isLoadingUsernames}
187+
>
118188
<Trash2 size="16" class="text-red-500" />
119189
</Button>
120190
</div>
@@ -129,19 +199,27 @@
129199
<Card>
130200
<CardHeader>
131201
<CardTitle>Email-Provider Blacklist</CardTitle>
132-
<div class="mt-4 flex items-center gap-2">
133-
<TextInput label="Email domain" placeholder="e.g. gmail.com" bind:value={emailEntry} />
134-
<Button onclick={addEmail}>Add</Button>
135-
<Button onclick={addEmailsBatch}>Batch Upload From Clipboard</Button>
136-
</div>
202+
<TextInput placeholder="e.g. gmail.com" bind:value={emailEntry}>
203+
{#snippet after()}
204+
<Button onclick={addEmailEntry} disabled={isLoadingEmails}>Add</Button>
205+
<Button onclick={addEmailsBatch} disabled={isLoadingEmails}
206+
>Batch Upload From Clipboard</Button
207+
>
208+
{/snippet}
209+
</TextInput>
137210
</CardHeader>
138211
<CardContent>
139212
<ScrollArea class="max-h-64">
140213
<div class="space-y-2">
141-
{#each emailBlacklist as item (item.id)}
214+
{#each limit(emailBlacklist, 10) as item (item.id)}
142215
<div class="flex items-center justify-between text-sm">
143216
<span>{item.domain}</span>
144-
<Button variant="ghost" size="sm" onclick={() => removeEmail(item.id)}>
217+
<Button
218+
variant="ghost"
219+
size="sm"
220+
onclick={() => removeEmail(item.id)}
221+
disabled={isLoadingEmails}
222+
>
145223
<Trash2 size="16" class="text-red-500" />
146224
</Button>
147225
</div>

0 commit comments

Comments
 (0)