Skip to content

Commit 4082073

Browse files
authored
Merge pull request metabrainz#2190 from reosarevok/MBS-11828
MBS-11828 / MBS-9426: UI to find and unlock locked usernames
2 parents bf2eb37 + ca55ce9 commit 4082073

File tree

8 files changed

+256
-0
lines changed

8 files changed

+256
-0
lines changed

lib/MusicBrainz/Server/Controller/Admin.pm

+59
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use Try::Tiny;
55
BEGIN { extends 'MusicBrainz::Server::Controller' };
66

77
use MusicBrainz::Server::Translation qw(l ln );
8+
use MusicBrainz::Server::Data::Utils qw( boolean_to_json );
89

910
sub edit_user : Path('/admin/user/edit') Args(1) RequireAuth HiddenOnSlaves SecureForm
1011
{
@@ -202,6 +203,64 @@ sub ip_lookup : Path('/admin/ip-lookup') Args(1) RequireAuth(account_admin) Hidd
202203
);
203204
}
204205

206+
sub locked_username_search : Path('/admin/locked-usernames/search') Args(0) RequireAuth(account_admin) HiddenOnSlaves {
207+
my ($self, $c) = @_;
208+
209+
my $form = $c->form(form => 'Admin::LockedUsernameSearch');
210+
my @results;
211+
my $show_results = 0;
212+
213+
if ($c->form_posted_and_valid($form, $c->req->body_params)) {
214+
try {
215+
@results = $c->model('Editor')->search_old_editor_names(
216+
$form->field('username')->value // '',
217+
$form->field('use_regular_expression')->value,
218+
);
219+
$show_results = 1;
220+
} catch {
221+
my $error = $_;
222+
if ("$error" =~ m/invalid regular expression/) {
223+
$form->field('username')->add_error(l('Invalid regular expression.'));
224+
$c->response->status(400);
225+
} else {
226+
die $error;
227+
}
228+
};
229+
}
230+
231+
$c->stash(
232+
current_view => 'Node',
233+
component_path => 'admin/LockedUsernameSearch',
234+
component_props => {
235+
form => $form->TO_JSON,
236+
@results ? (results => \@results) : (),
237+
showResults => boolean_to_json($show_results),
238+
},
239+
);
240+
}
241+
242+
sub unlock_username : Path('/admin/locked-usernames/unlock') Args(1) RequireAuth(account_admin) HiddenOnSlaves {
243+
my ($self, $c, $username) = @_;
244+
245+
my $form = $c->form(form => 'SecureConfirm');
246+
247+
if ($c->form_posted_and_valid($form)) {
248+
$c->model('MB')->with_transaction(sub {
249+
$c->model('Editor')->unlock_old_editor_name($username);
250+
});
251+
$c->response->redirect($c->uri_for_action('/admin/locked_username_search'));
252+
}
253+
254+
$c->stash(
255+
current_view => 'Node',
256+
component_path => 'admin/LockedUsernameUnlock',
257+
component_props => {
258+
form => $form->TO_JSON,
259+
username => $username,
260+
},
261+
);
262+
}
263+
205264
1;
206265

207266
=head1 COPYRIGHT AND LICENSE

lib/MusicBrainz/Server/Data/Editor.pm

+17
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,23 @@ sub insert
265265
}, $self->sql);
266266
}
267267

268+
sub search_old_editor_names {
269+
my ($self, $name, $use_regular_expression) = @_;
270+
271+
my $condition = $use_regular_expression ? "name ~* ?" : "LOWER(name) = LOWER(?)";
272+
my $query = "SELECT name FROM old_editor_name WHERE $condition LIMIT 100";
273+
274+
@{ $self->sql->select_single_column_array($query, $name) };
275+
}
276+
277+
sub unlock_old_editor_name {
278+
my ($self, $name) = @_;
279+
280+
my $query = "DELETE FROM old_editor_name WHERE name = ?";
281+
282+
$self->sql->do($query, $name);
283+
}
284+
268285
sub update_email
269286
{
270287
my ($self, $editor, $email) = @_;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package MusicBrainz::Server::Form::Admin::LockedUsernameSearch;
2+
3+
use HTML::FormHandler::Moose;
4+
5+
extends 'MusicBrainz::Server::Form';
6+
7+
has '+name' => (default => 'lockedusernamesearch');
8+
9+
has_field 'username' => (type => 'Text');
10+
has_field 'use_regular_expression' => (type => 'Boolean');
11+
has_field 'submit' => (type => 'Submit');
12+
13+
1;
14+
15+
=head1 COPYRIGHT AND LICENSE
16+
17+
Copyright (C) 2021 MetaBrainz Foundation
18+
19+
This file is part of MusicBrainz, the open internet music database,
20+
and is licensed under the GPL version 2, or (at your option) any
21+
later version: http://www.gnu.org/licenses/gpl-2.0.txt
22+
23+
=cut

root/admin/LockedUsernameSearch.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* @flow strict-local
3+
* Copyright (C) 2021 MetaBrainz Foundation
4+
*
5+
* This file is part of MusicBrainz, the open internet music database,
6+
* and is licensed under the GPL version 2, or (at your option) any
7+
* later version: http://www.gnu.org/licenses/gpl-2.0.txt
8+
*/
9+
10+
import * as React from 'react';
11+
12+
import FormRowCheckbox from '../components/FormRowCheckbox';
13+
import FormRowText from '../components/FormRowText';
14+
import FormSubmit from '../components/FormSubmit';
15+
import Layout from '../layout';
16+
import expand2react from '../static/scripts/common/i18n/expand2react';
17+
import bracketed from '../static/scripts/common/utility/bracketed';
18+
19+
type Props = {
20+
+form: FormT<{
21+
+use_regular_expression: ReadOnlyFieldT<boolean>,
22+
+username: ReadOnlyFieldT<string>,
23+
}>,
24+
+results?: $ReadOnlyArray<string>,
25+
+showResults: boolean,
26+
};
27+
28+
const LockedUsernameSearch = ({
29+
form,
30+
results,
31+
showResults,
32+
}: Props): React.Element<typeof Layout> => (
33+
<Layout fullWidth title="Search locked usernames">
34+
<div id="content">
35+
<h1>{'Search locked usernames'}</h1>
36+
37+
<form action="/admin/locked-usernames/search" method="post">
38+
<p>
39+
{expand2react(
40+
'Enter a username or a {link|POSIX regular expression}.',
41+
{
42+
link: 'https://www.postgresql.org/docs/12/' +
43+
'functions-matching.html#FUNCTIONS-POSIX-REGEXP',
44+
},
45+
)}
46+
</p>
47+
48+
<FormRowText
49+
field={form.field.username}
50+
label="Username:"
51+
size={50}
52+
uncontrolled
53+
/>
54+
55+
<FormRowCheckbox
56+
field={form.field.use_regular_expression}
57+
label="Search using regular expression"
58+
uncontrolled
59+
/>
60+
61+
<div className="row no-label">
62+
<FormSubmit
63+
label="Search"
64+
name="lockedusernamesearch.submit"
65+
value="1"
66+
/>
67+
</div>
68+
69+
{showResults ? (
70+
<>
71+
<h3>{'Matching locked names:'}</h3>
72+
{results?.length ? (
73+
<ul>
74+
{results.map(result => (
75+
<li key={result}>
76+
{result}
77+
{' '}
78+
{bracketed(
79+
<a href={`/admin/locked-usernames/unlock/${result}`}>
80+
{'unlock'}
81+
</a>,
82+
)}
83+
</li>
84+
))}
85+
</ul>
86+
) : (
87+
<p>
88+
{'No locked usernames matched your search.'}
89+
</p>
90+
)}
91+
</>
92+
) : null}
93+
</form>
94+
</div>
95+
</Layout>
96+
);
97+
98+
export default LockedUsernameSearch;

root/admin/LockedUsernameUnlock.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* @flow strict-local
3+
* Copyright (C) 2021 MetaBrainz Foundation
4+
*
5+
* This file is part of MusicBrainz, the open internet music database,
6+
* and is licensed under the GPL version 2, or (at your option) any
7+
* later version: http://www.gnu.org/licenses/gpl-2.0.txt
8+
*/
9+
10+
import * as React from 'react';
11+
12+
import FormCsrfToken from '../components/FormCsrfToken';
13+
import FormSubmit from '../components/FormSubmit';
14+
import Layout from '../layout';
15+
import expand2text from '../static/scripts/common/i18n/expand2text';
16+
17+
type Props = {
18+
+$c: CatalystContextT,
19+
+form: SecureConfirmFormT,
20+
+username: string,
21+
};
22+
23+
const LockedUsernameUnlock = ({
24+
$c,
25+
form,
26+
username,
27+
}: Props): React.Element<typeof Layout> => (
28+
<Layout fullWidth title="Unlock username">
29+
<div id="content">
30+
<h1>{'Unlock username'}</h1>
31+
<p>
32+
{expand2text(
33+
`Are you sure you wish to unlock
34+
the username “{username}” for reuse?`,
35+
{username: username},
36+
)}
37+
</p>
38+
<form action={$c.req.uri} method="post" name="confirm">
39+
<FormCsrfToken form={form} />
40+
<FormSubmit
41+
label="Yes, I’m sure"
42+
name="confirm.submit"
43+
value="1"
44+
/>
45+
</form>
46+
</div>
47+
</Layout>
48+
);
49+
50+
export default LockedUsernameUnlock;

root/layout/components/TopMenu.js

+5
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ const AdminMenu = ({user}: UserProp) => (
167167
<li>
168168
<a href="/admin/email-search">{l('Email Search')}</a>
169169
</li>
170+
<li>
171+
<a href="/admin/locked-usernames/search">
172+
{l('Locked Username Search')}
173+
</a>
174+
</li>
170175
</>
171176
) : null}
172177
</ul>

root/server/components.js

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ module.exports = {
3838
'admin/EditBanner': require('../admin/EditBanner'),
3939
'admin/EmailSearch': require('../admin/EmailSearch'),
4040
'admin/IpLookup': require('../admin/IpLookup'),
41+
'admin/LockedUsernameSearch': require('../admin/LockedUsernameSearch'),
42+
'admin/LockedUsernameUnlock': require('../admin/LockedUsernameUnlock'),
4143
'admin/attributes/Attribute': require('../admin/attributes/Attribute'),
4244
'admin/attributes/CannotRemoveAttribute': require('../admin/attributes/CannotRemoveAttribute'),
4345
'admin/attributes/Index': require('../admin/attributes/Index'),

t/lib/t/MusicBrainz/Server/Controller/UnconfirmedEmailAddresses.pm

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ test 'Paths that allow browsing without a confirmed email address' => sub {
9090
'Controller::Admin::edit_user',
9191
'Controller::Admin::email_search',
9292
'Controller::Admin::ip_lookup',
93+
'Controller::Admin::locked_username_search',
94+
'Controller::Admin::unlock_username',
9395
'Controller::Ajax::filter_artist_recordings_form',
9496
'Controller::Ajax::filter_artist_release_groups_form',
9597
'Controller::Ajax::filter_artist_releases_form',

0 commit comments

Comments
 (0)