From: Dylan William Hardison Date: Sun, 28 Jun 2020 03:00:22 +0000 (-0400) Subject: New Signup Flow X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=d73fcd1f62db32d9b6ae3879d0b8fcc83d845130;p=thirdparty%2Fbugzilla.git New Signup Flow This introduces a much simplified signup flow, over email. This code is live on https://harmony.bugzilla.ninja ## Before 1. Visit createaccount.cgi 2. Type email 3. Get email, click link 4. Create account 5. Re-enter login and password. ## After 1. Type email into any page 2. Get email, click link 3. Create account + login at the same time --- diff --git a/Bugzilla/App.pm b/Bugzilla/App.pm index 274ea5021..613e6517b 100644 --- a/Bugzilla/App.pm +++ b/Bugzilla/App.pm @@ -25,6 +25,7 @@ use Bugzilla::App::API; use Bugzilla::App::BouncedEmails; use Bugzilla::App::CGI; use Bugzilla::App::Main; +use Bugzilla::App::Users; use Bugzilla::App::OAuth2::Clients; use Bugzilla::App::SES; use Bugzilla::App::Static; @@ -189,6 +190,7 @@ sub setup_routes { Bugzilla::App::BouncedEmails->setup_routes($r); Bugzilla::App::CGI->setup_routes($r); Bugzilla::App::Main->setup_routes($r); + Bugzilla::App::Users->setup_routes($r); Bugzilla::App::OAuth2::Clients->setup_routes($r); Bugzilla::App::SES->setup_routes($r); diff --git a/Bugzilla/App/Plugin/Glue.pm b/Bugzilla/App/Plugin/Glue.pm index 849aa21c1..a9961398d 100644 --- a/Bugzilla/App/Plugin/Glue.pm +++ b/Bugzilla/App/Plugin/Glue.pm @@ -85,6 +85,16 @@ sub register { my $name = sprintf '%s.%s.tmpl', $options->{template}, $options->{format}; my $template = Bugzilla->template; + if ($options->{variant}) { + my $name_variant = sprintf '%s.%s+%s.tmpl', $options->{template}, $options->{format}, $options->{variant}; + WARN("loading $name_variant"); + my $rendered = $template->process($name_variant, \%params, $output); + return if $rendered; + + my $error = $template->error; + die $error unless $error->type eq 'file' && $error->info =~ /not found/; + } + WARN("loading $name"); $template->process($name, \%params, $output) or die $template->error; } ); diff --git a/Bugzilla/App/Users.pm b/Bugzilla/App/Users.pm new file mode 100644 index 000000000..f7bad9f66 --- /dev/null +++ b/Bugzilla/App/Users.pm @@ -0,0 +1,212 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::App::Users; +use Mojo::Base 'Mojolicious::Controller'; + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Logging; +use Bugzilla::Mailer qw(MessageToMTA); +use Date::Format qw(ctime); +use Scalar::Util qw(blessed); +use Bugzilla::User; +use List::Util qw(any); +use Try::Tiny; + +sub setup_routes { + my ($class, $r) = @_; + + $r->post('/signup/email')->to('Users#signup_email')->name('signup_email'); + $r->get('/signup/email/:token/verify')->to('Users#signup_email_verify') + ->name('signup_email_verify'); + $r->post('/signup/email/:token/finish')->to('Users#signup_email_finish') + ->name('signup_email_finish'); +} + +sub signup_email { + my ($c) = @_; + my $v = $c->validation; + + try { + Bugzilla::User->new->check_account_creation_enabled; + my $email_regexp = Bugzilla->params->{createemailregexp}; + $v->required('email')->like(qr/$email_regexp/); + $v->csrf_protect; + + ThrowUserError('account_creation_restricted') unless $v->is_valid; + + my $email = $v->param('email'); + Bugzilla::User->check_login_name_for_creation($email); + Bugzilla::Hook::process("user_verify_login", {login => $email}); + + $c->issue_new_user_account_token($email); + $c->render(handler => 'bugzilla'); + } + catch { + $c->bugzilla->error_page($_); + }; +} + +sub signup_email_verify { + my ($c) = @_; + my $token = $c->stash->{token}; + my (undef, $issuedate, $email) = Bugzilla::Token::GetTokenData($token); + + if ($email) { + $c->stash->{signup_token} = $token; + $c->stash->{email} = $email; + $c->stash->{expires} = $issuedate; + } + else { + $c->stash->{missing_token} = 1; + } + + $c->render(handler => 'bugzilla'); +} + +sub signup_email_finish { + my ($c) = @_; + my $v = $c->validation; + try { + $v->optional('create')->equal_to('create'); + $v->optional('cancel')->equal_to('cancel'); + $v->csrf_protect; + $v->required('signup_token')->size(22); + + my $token = $v->param('signup_token'); + if ($v->is_valid) { + my (undef, undef, $email) = Bugzilla::Token::GetTokenData($token); + + $v->error('signup_token', ['invalid_token']) unless $email; + + if ($v->is_valid && $v->param('create') eq 'create') { + $v->optional('realname')->size(1, 255); + $v->required('etiquette'); + $v->required('password')->size(8, 100); + $v->required('password_confirm')->size(8, 100); + if ($v->is_valid && $v->param('password') ne $v->param('password_confirm')) { + $v->error('password_confirm', ['password_mismatch']); + $v->error('password', ['password_mismatch']); + } + if ($v->is_valid) { + my $new_user = Bugzilla::User->create({ + login_name => $email, + realname => $v->param('realname'), + cryptpassword => $v->param('password'), + }); + $c->persist_login($new_user, 'signup'); + $c->redirect_to('/home'); + } + } + elsif ($v->is_valid && $v->param('cancel') eq 'cancel') { + my (undef, undef, $email) = Bugzilla::Token::GetTokenData($token); + my $vars = {}; + $vars->{'message'} = 'account_creation_canceled'; + $vars->{'account'} = $email; + Bugzilla::Token::Cancel($token, $vars->{'message'}); + } + } + ThrowUserError('validation', { v => $v }); + } + catch { + $c->bugzilla->error_page($_); + }; +} + +# This is adapted from issue_new_user_account_token from Bugzilla/Token.pm +# Creates and sends a token to create a new user account. +# It assumes that the login has the correct format and is not already in use. +sub issue_new_user_account_token { + my ($c, $email) = @_; + my $dbh = Bugzilla->dbh; + + # Is there already a pending request for this login name? If yes, do not throw + # an error because the user may have lost their email with the token inside. + # But to prevent using this way to mailbomb an email address, make sure + # the last request is at least 10 minutes old before sending a new email. + + my $pending_requests = $dbh->selectrow_array( + 'SELECT COUNT(*) + FROM tokens + WHERE tokentype = ? + AND ' . $dbh->sql_istrcmp('eventdata', '?') . ' + AND issuedate > ' + . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'), undef, ('signup', $email) + ); + + ThrowUserError('too_soon_for_new_token', {'type' => 'signup'}) + if $pending_requests; + + my ($token, $token_ts) + = Bugzilla::Token::_create_token(undef, 'signup', $email); + + $c->stash->{email} = $email . Bugzilla->params->{'emailsuffix'}; + $c->stash->{expires} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $c->stash->{verify_url} + = $c->url_for('signup_email_verify', token => $token)->to_abs; + + my $message = $c->render_to_string( + handler => 'bugzilla', + format => 'txt', + variant => 'email' + ); + WARN("Email: is\n$message"); + MessageToMTA($message->to_string); +} + +# This is adapted from persist_login in Bugzilla/Auth/Persist/Cookie.pm +sub persist_login { + my ($c, $user, $auth_method) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my $login_cookie + = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); + + my $ip_addr = $c->forwarded_for; + + $dbh->do( + 'INSERT INTO logincookies (cookie, userid, ipaddr, lastused) + VALUES (?, ?, ?, NOW())', undef, $login_cookie, $user->id, $ip_addr + ); + + # Issuing a new cookie is a good time to clean up the old + # cookies. + $dbh->do("DELETE FROM logincookies WHERE lastused < " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY')); + + $dbh->bz_commit_transaction(); + + my %cookie_attr = (httponly => 1, path => '/', expires => time + 604800); + + if (Bugzilla->localconfig->urlbase =~ /^https/) { + $cookie_attr{secure} = 1; + } + + $c->cookie('Bugzilla_login', $user->id, \%cookie_attr); + $c->cookie('Bugzilla_logincookie', $login_cookie, \%cookie_attr); + + my $securemail_groups + = Bugzilla->can('securemail_groups') + ? Bugzilla->securemail_groups + : ['admin']; + + if (any { $user->in_group($_) } @$securemail_groups) { + $auth_method //= 'unknown'; + + Bugzilla->audit( + sprintf "successful login of %s from %s using \"%s\", authenticated by %s", + $user->login, $ip_addr, $c->req->headers->user_agent // '', $auth_method); + } + + return $login_cookie; +} + + +1; diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm index 84cc1bb14..5d8ab0946 100644 --- a/Bugzilla/Config/General.pm +++ b/Bugzilla/Config/General.pm @@ -41,6 +41,13 @@ use constant get_param_list => ( {name => 'announcehtml', type => 'l', default => ''}, + { + name => 'etiquettehtml', + type => 'l', + default => + 'I have read Bugzilla Etiquette and agree to abide by it.' + }, + { name => 'upgrade_notification', type => 's', diff --git a/createaccount.cgi b/createaccount.cgi deleted file mode 100755 index f0fd40730..000000000 --- a/createaccount.cgi +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env perl -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -use 5.10.1; -use strict; -use warnings; - -use lib qw(. lib local/lib/perl5); - -use Bugzilla; -use Bugzilla::Constants; -use Bugzilla::Error; -use Bugzilla::Token; - -# Just in case someone already has an account, let them get the correct footer -# on an error message. The user is logged out just after the account is -# actually created. -my $user = Bugzilla->login(LOGIN_OPTIONAL); -my $cgi = Bugzilla->cgi; -my $template = Bugzilla->template; -my $vars = {doc_section => 'myaccount.html'}; - -print $cgi->header(); - -$user->check_account_creation_enabled; -my $login = $cgi->param('login'); - -if (defined($login)) { - - # Check the hash token to make sure this user actually submitted - # the create account form. - my $token = $cgi->param('token'); - check_hash_token($token, ['create_account']); - - $user->check_and_send_account_creation_confirmation($login); - $vars->{'login'} = $login; - - $template->process("account/created.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; -} - -# Show the standard "would you like to create an account?" form. -$template->process("account/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); diff --git a/js/global.js b/js/global.js index 51fd3b50d..dbb0e4ae5 100644 --- a/js/global.js +++ b/js/global.js @@ -26,6 +26,12 @@ $(function () { $('.hide_mini_login_form').on("click", function (event) { return hide_mini_login_form($(this).data('qs-suffix')); }); + $('.show_mini_signup_form').on("click", function (event) { + return show_mini_signup_form($(this).data('qs-suffix')); + }); + $('.hide_mini_signup_form').on("click", function (event) { + return hide_mini_signup_form($(this).data('qs-suffix')); + }); $('.show_forgot_form').on("click", function (event) { return show_forgot_form($(this).data('qs-suffix')); }); @@ -86,17 +92,29 @@ function manage_old_lists() { function show_mini_login_form( suffix ) { hide_forgot_form(suffix); + hide_mini_signup_form(suffix); $('#mini_login' + suffix).removeClass('bz_default_hidden').find('input[required]:first').focus(); - $('#new_account_container' + suffix).addClass('bz_default_hidden'); return false; } function hide_mini_login_form( suffix ) { $('#mini_login' + suffix).addClass('bz_default_hidden'); - $('#new_account_container' + suffix).removeClass('bz_default_hidden'); return false; } +function show_mini_signup_form( suffix ) { + hide_forgot_form(suffix); + hide_mini_login_form(suffix); + $('#mini_signup' + suffix).removeClass('bz_default_hidden').find('input[required]:first').focus(); + return false; +} + +function hide_mini_signup_form( suffix ) { + $('#mini_signup' + suffix).addClass('bz_default_hidden'); + return false; +} + + function show_forgot_form( suffix ) { hide_mini_login_form(suffix); $('#forgot_form' + suffix).removeClass('bz_default_hidden').find('input[required]:first').focus(); diff --git a/js/signup.js b/js/signup.js new file mode 100644 index 000000000..9c91b5551 --- /dev/null +++ b/js/signup.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +document.addEventListener("DOMContentLoaded", () => { + const password = document.getElementById("password"); + const password_confirm = document.getElementById("password_confirm"); + const on_change = (event) => { + if (password.value == password_confirm.value) { + console.log(password.value); + console.log(password_confirm.value); + password.setCustomValidity(""); + password_confirm.setCustomValidity(""); + } else { + password.setCustomValidity("This password doesn't match"); + password_confirm.setCustomValidity("This password doesn't match"); + } + }; + if (password && password_confirm) { + password.addEventListener("change", on_change); + password_confirm.addEventListener("change", on_change); + } + + const cancel = document.getElementById("signup_cancel"); + if (cancel) { + cancel.addEventListener("click", (event) => { + const not_required = ['etiquette', 'password', 'password_confirm']; + for (const id of not_required) { + const field = document.getElementById(id); + if (field) { + field.required = false; + } + } + }); + } +}); diff --git a/skins/standard/signup.css b/skins/standard/signup.css new file mode 100644 index 000000000..31a724d00 --- /dev/null +++ b/skins/standard/signup.css @@ -0,0 +1,51 @@ +.signup, .fresh-signup { + max-width: 1023px; + min-width: 600px; + margin-left: auto; + margin-right: auto; + display: grid; + grid-template-columns: 1fr 4fr; + grid-gap: 16px; +} + +@media screen and (max-width: 790px) { + .signup, .fresh-signup { + max-width: 600px; + } +} + +.signup label { + grid-column: 1 / 2; +} + +.signup input { + grid-column: 2 / 3; +} + + +.signup .notes { + text-align: left; + grid-column: 2 / 3; +} + +.signup .buttons { + text-align: right; + grid-column: 2 / 3; +} + +.fresh-signup { + grid-template-columns: 4fr 1fr; +} + +.fresh-signup .notes { + text-align: left; + grid-column: 1 / 2; +} + +.fresh-signup input { + grid-column: 1 / 2; +} + +.fresh-signup button { + grid-column: 2 / 2; +} diff --git a/template/en/default/account/auth/signup-small.html.tmpl b/template/en/default/account/auth/signup-small.html.tmpl new file mode 100644 index 000000000..7af502afa --- /dev/null +++ b/template/en/default/account/auth/signup-small.html.tmpl @@ -0,0 +1,56 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Jacob Steenhagen + #%] + +[% PROCESS global/variables.none.tmpl %] + +[%# Use the current script name. If an empty name is returned, + # then we are accessing the home page. %] + +
  • + + + +
  • diff --git a/template/en/default/admin/params/general.html.tmpl b/template/en/default/admin/params/general.html.tmpl index 248d818a9..40c778c3b 100644 --- a/template/en/default/admin/params/general.html.tmpl +++ b/template/en/default/admin/params/general.html.tmpl @@ -56,6 +56,9 @@ _ " class=\"warning\" to make the text red. Anything defined" _ " in skins/standard/global.css will work.", + etiquettehtml => + "When users create accounts, this message will display next to mandatory check box." + upgrade_notification => "$terms.Bugzilla can inform you when a new release is available." _ " The notification will appear on the $terms.Bugzilla homepage," diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index 881880b45..cbef59f2a 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -403,9 +403,7 @@ [% ELSE %]