perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Normalize account emails consistently

alice c8d6a678 dd087123

+58 -6
+6 -1
lib/ATProto/PDS/API/Server.pm
··· 459 459 ); 460 460 my $account = $c->store->get_account_by_did($token->{did}); 461 461 xrpc_error(404, 'AccountNotFound', 'Account was not found') unless $account; 462 - my $email = $body->{email} // q(); 462 + my $email = _normalize_email($body->{email}) // q(); 463 463 xrpc_error(400, 'InvalidEmail', 'Token was not issued for that email') 464 464 unless length($email) 465 465 && ($token->{email} // q()) eq $email ··· 899 899 return undef unless defined $email && length $email; 900 900 return undef unless $c->config_value('testing_auto_confirm_email', 1); 901 901 return time; 902 + } 903 + 904 + sub _normalize_email ($email) { 905 + return undef unless defined $email; 906 + return lc $email; 902 907 } 903 908 904 909 sub _require_action_token ($c, %args) {
+13 -2
lib/ATProto/PDS/Store/SQLite.pm
··· 164 164 my $account_id = $args{account_id} // $args{id} // _random_id(); 165 165 my $handle = $args{handle} // die 'handle is required'; 166 166 my $now = $args{created_at} // time; 167 + my $email = _normalize_email($args{email}); 167 168 168 169 _execute_sql( 169 170 $self->dbh, ··· 180 181 $account_id, 181 182 $did, 182 183 $handle, 183 - $args{email}, 184 + $email, 184 185 $args{password_hash}, 185 186 $args{password_salt}, 186 187 $now, ··· 225 226 next unless $allowed{$key}; 226 227 my $column = $key eq 'did_doc' ? 'did_doc_json' : $key; 227 228 push @sets, "$column = ?"; 228 - push @bind, $key eq 'did_doc' ? _maybe_json($changes{$key}) : $changes{$key}; 229 + push @bind, $key eq 'did_doc' 230 + ? _maybe_json($changes{$key}) 231 + : $key eq 'email' 232 + ? _normalize_email($changes{$key}) 233 + : $changes{$key}; 229 234 } 230 235 return $self->get_account_by_did($did) unless @sets; 231 236 ··· 282 287 } 283 288 284 289 sub get_account_by_email ($self, $email) { 290 + $email = _normalize_email($email); 285 291 return $self->_row_to_account($self->dbh->selectrow_hashref( 286 292 q{SELECT * FROM accounts WHERE email = ?}, 287 293 undef, 288 294 $email, 289 295 )); 296 + } 297 + 298 + sub _normalize_email ($email) { 299 + return undef unless defined $email; 300 + return lc $email; 290 301 } 291 302 292 303 sub get_account_by_identifier ($self, $identifier) {
+29 -1
t/email-confirmation.t
··· 68 68 ); 69 69 70 70 $t->post_ok('/xrpc/com.atproto.server.confirmEmail' => json => { 71 - email => 'alice@example.test', 71 + email => 'ALICE@example.test', 72 72 token => $token->{token}, 73 73 })->status_is(400) 74 74 ->json_is('/error' => 'InvalidEmail'); ··· 76 76 ok( 77 77 !defined $app->store->get_account_by_did($alice->{did})->{email_confirmed_at}, 78 78 'stale confirmation tokens cannot confirm a changed email address', 79 + ); 80 + 81 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 82 + handle => 'bob.example.test', 83 + email => 'bob@example.test', 84 + password => 'hunter22', 85 + })->status_is(200); 86 + my $bob = $t->tx->res->json; 87 + 88 + $t->post_ok('/xrpc/com.atproto.server.requestEmailConfirmation' => { 89 + Authorization => "Bearer $bob->{accessJwt}", 90 + } => json => {})->status_is(200); 91 + 92 + my $bob_token = $app->store->latest_action_token( 93 + did => $bob->{did}, 94 + purpose => 'email_confirm', 95 + ); 96 + ok($bob_token, 'case-insensitive confirmation flow also issues a token'); 97 + 98 + $t->post_ok('/xrpc/com.atproto.server.confirmEmail' => json => { 99 + email => 'BOB@example.test', 100 + token => $bob_token->{token}, 101 + })->status_is(200) 102 + ->json_is({}); 103 + 104 + ok( 105 + defined $app->store->get_account_by_did($bob->{did})->{email_confirmed_at}, 106 + 'email confirmation accepts case-insensitive email matches', 79 107 ); 80 108 81 109 done_testing;
+7
t/server-auth.t
··· 66 66 ->json_is('/handle' => 'alice.localhost') 67 67 ->json_is('/email' => 'alice@example.com'); 68 68 69 + $t->post_ok('/xrpc/com.atproto.server.createSession' => json => { 70 + identifier => 'ALICE@example.com', 71 + password => 'password123', 72 + })->status_is(200) 73 + ->json_is('/did' => $did) 74 + ->json_is('/email' => 'alice@example.com'); 75 + 69 76 $t->get_ok('/xrpc/com.atproto.admin.getInviteCodes' => { 70 77 Authorization => 'Bearer admin-secret', 71 78 })->status_is(403)
+3 -2
t/store-sqlite.t
··· 32 32 id => 'acct-1', 33 33 did => 'did:web:pds.example.com:users:alice', 34 34 handle => 'alice.example.com', 35 - email => 'alice@example.com', 35 + email => 'Alice@Example.com', 36 36 password_hash => 'sha256:abc', 37 37 did_doc => { id => 'did:web:pds.example.com:users:alice' }, 38 38 ); ··· 47 47 ); 48 48 49 49 is($account->{handle}, 'alice.example.com', 'account round-trips'); 50 - is($store->get_account_by_email('alice@example.com')->{did}, $account->{did}, 'lookup by email works'); 50 + is($account->{email}, 'alice@example.com', 'account email is normalized to lowercase'); 51 + is($store->get_account_by_email('ALICE@example.com')->{did}, $account->{did}, 'lookup by email is case-insensitive'); 51 52 52 53 $store->create_session( 53 54 id => 'sess-1',