Commit 25ad8978 authored by Leigh B Stoller's avatar Leigh B Stoller
Browse files

Add a Manage Account page portal users can update their profile.

Includes password, so that link is gone from the actions menu.
Note that Portal users can change their email now, since we do
the email verification dance.
parent 0b90ef4f
#!/usr/bin/perl -wT
#
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -42,15 +42,16 @@ use Getopt::Std;
#
sub usage()
{
print("Usage: tbacct [-f] [-b] [-u] ".
print("Usage: tbacct [-f] [-b] [-u] [-v] ".
"<add|del|mod|passwd|wpasswd|email|freeze|thaw|verify|revoke|dots> ".
"<user> [args]\n");
exit(-1);
}
my $optlist = "fbu";
my $optlist = "fbuv";
my $force = 0;
my $batch = 0;
my $update = 0;
my $verified= 0;
#
# Configure variables
......@@ -203,6 +204,9 @@ if (defined($options{"b"})) {
if (defined($options{"u"})) {
$update = 1;
}
if (defined($options{"v"})) {
$verified = 1;
}
if (@ARGV < 2) {
usage();
}
......@@ -926,7 +930,7 @@ sub UpdateEmail()
#
# Only admin people can do this.
#
if (! TBAdmin($UID)) {
if (!TBAdmin($UID) && !$verified) {
fatal("You do not have permission to update email for user $user.");
}
......
require(window.APT_OPTIONS.configObject,
['underscore', 'js/quickvm_sup',
'js/lib/text!template/myaccount.html',
'js/lib/text!template/verify-modal.html',
'js/lib/text!template/oops-modal.html',
'js/lib/text!template/waitwait-modal.html',
// jQuery modules
'formhelpers'],
function (_, sup, myaccountString, verifyString, oopsString, waitwaitString)
{
'use strict';
var myaccountTemplate = _.template(myaccountString);
var verifyTemplate = _.template(verifyString);
var modified = false;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
// Initial form contents.
var fields = JSON.parse(_.unescape($('#form-json')[0].textContent));
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
// Watch for USA
if (fields.country === "USA") {
fields.country = "US";
}
renderForm(fields, null);
// Warn user if they have not saved changes.
window.onbeforeunload = function() {
if (! modified)
return null;
return "You have unsaved changes!";
}
}
function Modified()
{
modified = true;
$('#submit_button').removeAttr("disabled");
}
function renderForm(formfields, errors)
{
var verify = verifyTemplate({
id: 'verify_modal',
label: "Confirm",
});
var myaccount = Formatter(myaccountTemplate({
formfields: formfields,
general_error: (errors.error || ''),
verify_modal: verify,
}), errors);
$('#page-body').html(myaccount);
$('#signup_countries').bfhcountries({ country: formfields.country,
blank: false, ask: true });
$('#signup_states').bfhstates({ country: 'signup_countries',
state: formfields.state,
blank: false, ask: true });
/*
* We have to attached the event handlers after we update the DOM.
*/
$('#myaccount_form').find(".format-me").each(function() {
$(this).change(function() { Modified(); });
});
$('#submit_button').click(function (event) {
event.preventDefault();
// Disable the Stay on Page alert above.
window.onbeforeunload = null;
SubmitForm(1);
return false;
});
$('#verify_modal_submit').click(function (event) {
event.preventDefault();
// Disable the Stay on Page alert above.
window.onbeforeunload = null;
sup.HideModal('#verify_modal');
SubmitForm(1);
return false;
});
}
function Formatter(fieldString, errors)
{
var root = $(fieldString);
var list = root.find('.format-me');
list.each(function (index, item) {
if (item.dataset)
{
var key = item.dataset['key'];
var wrapper = $('<div></div>');
var placeholder = item.placeholder;
if (!placeholder) {
placeholder = item.dataset['placeholder'];
}
wrapper.append('<label class="control-label"> ' +
_.escape(placeholder) + '</label>');
wrapper.append($(item).clone());
if (errors && _.has(errors, key))
{
wrapper.addClass('has-error');
wrapper.append('<label class="control-label" ' +
'for="inputError">' + _.escape(errors[key]) +
'</label>');
}
$(item).after(wrapper);
$(item).remove();
}
});
return root;
}
//
// Submit the form.
//
function SubmitForm(checkonly)
{
// Current form contents as formfields array.
var formfields = {};
var callback = function(json) {
if (!checkonly) {
sup.HideModal("#waitwait-modal");
}
console.info(json);
if (json.code) {
if (json.code == 2) {
// Regenerate with errors.
renderForm(formfields, json.value);
return;
}
// Email not verified, throw up form.
if (json.code == 3) {
sup.ShowModal('#verify_modal');
return;
}
sup.SpitOops("oops", json.value);
return;
}
// Now do the actual create.
if (checkonly) {
SubmitForm(0);
}
else {
window.location.reload();
}
}
// Convert form data into formfields array, like all our
// form handler pages expect.
var fields = $('#myaccount_form').serializeArray();
$.each(fields, function(i, field) {
formfields[field.name] = field.value;
});
if (!checkonly) {
sup.ShowModal("#waitwait-modal");
}
var xmlthing =
sup.CallServerMethod(null, "myaccount", "update",
{"formfields" : formfields,
"checkonly" : checkonly});
xmlthing.done(callback);
}
$(document).ready(initialize);
});
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
chdir("..");
include_once("webtask.php");
chdir("apt");
# We set this in CheckPageArgs
$target_user = null;
#
# Need to check the permission, since we allow admins to edit
# other accounts.
#
function CheckPageArgs()
{
global $this_user, $target_user;
global $ajax_args;
global $APTMAIL;
if (!isset($ajax_args["formfields"])) {
SPITAJAX_ERROR(-1, "Missing formfields");
return -1;
}
$formfields = $ajax_args["formfields"];
if (!isset($formfields["uid"])) {
SPITAJAX_ERROR(-1, "Missing target uid");
return -1;
}
$uid = $formfields["uid"];
if (!TBvalid_uid($uid)) {
SPITAJAX_ERROR(-1, "Invalid target uid");
return -1;
}
$target_user = User::LookupByUid($uid);
if (!$target_user) {
sleep(2);
SPITAJAX_ERROR(-1, "Unknown target uid");
return -1;
}
if ($uid == $this_user->uid())
return 0;
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Not enough permissin");
return -1;
}
return 0;
}
#
# Check form arguments on the fist step, so we can halt progress
# right away.
#
function CheckForm()
{
global $this_user, $target_user;
global $ajax_args;
global $APTMAIL;
$formfields = $ajax_args["formfields"];
$errors = array();
if (!isset($formfields["name"]) ||
strcmp($formfields["name"], "") == 0) {
$errors["name"] = "Missing Field";
}
elseif (! TBvalid_usrname($formfields["name"])) {
$errors["name"] = TBFieldErrorString();
}
# Make sure user name has at least two tokens
$tokens = preg_split("/[\s]+/", $formfields["name"],
-1, PREG_SPLIT_NO_EMPTY);
if (count($tokens) < 2) {
$errors["name"] = "Please provide a first and last name";
}
if (!isset($formfields["email"]) ||
strcmp($formfields["email"], "") == 0) {
$errors["email"] = "Missing Field";
}
elseif (! TBvalid_email($formfields["email"])) {
$errors["email"] = TBFieldErrorString();
}
else {
$tmp = User::LookupByEmail($formfields["email"]);
#
# Treat this error separate. Not allowed.
#
if ($tmp && $tmp->uid() != $target_user->uid()) {
$errors["email"] = "Already in use by another user";
}
}
if (!isset($formfields["affiliation"]) ||
strcmp($formfields["affiliation"], "") == 0) {
$errors["affiliation"] = "Missing Field";
}
elseif (! TBvalid_affiliation($formfields["affiliation"])) {
$errors["affiliation"] = TBFieldErrorString();
}
if (!isset($formfields["country"]) ||
strcmp($formfields["country"], "") == 0) {
$errors["country"] = "Missing Field";
}
elseif (! TBvalid_country($formfields["country"])) {
$errors["country"] = TBFieldErrorString();
}
if (!isset($formfields["state"]) ||
strcmp($formfields["state"], "") == 0) {
$errors["state"] = "Missing Field";
}
elseif (! TBvalid_state($formfields["state"])) {
$errors["state"] = TBFieldErrorString();
}
if (!isset($formfields["city"]) ||
strcmp($formfields["city"], "") == 0) {
$errors["city"] = "Missing Field";
}
elseif (! TBvalid_city($formfields["city"])) {
$errors["city"] = TBFieldErrorString();
}
if (isset($formfields["password1"]) && $formfields["password1"] != "") {
if (!isset($formfields["password2"]) ||
strcmp($formfields["password2"], "") == 0) {
$errors["password2"] = "Missing Field";
}
elseif (strcmp($formfields["password1"], $formfields["password2"])) {
$errors["password2"] = "Does not match password";
}
elseif (! CHECKPASSWORD($formfields["uid"],
$formfields["password1"],
$formfields["name"],
$formfields["email"], $checkerror)) {
$errors["password1"] = "$checkerror";
}
}
# Present these errors before we call out to do anything else.
if (count($errors)) {
SPITAJAX_ERROR(2, $errors);
return -1;
}
#
# Lets get the user to do the email verification now before
# we go any further. We use a session variable to store the
# key we send to the user in email.
#
if ($formfields["email"] != $target_user->email()) {
if (!isset($_SESSION["verify_key"])) {
$_SESSION["verify_key"] = substr(GENHASH(), 0, 16);
$_SESSION["verified"] = 0;
$_SESSION["codesent"] = 0;
}
#
# Once the user verifies okay, we remember that in the session
# in case there is a later error below.
#
if (!$_SESSION["verified"]) {
if (!$_SESSION["codesent"]) {
mail($formfields["email"],
"Confirm your new email",
"Here is your email verification code. Please copy and\n".
"paste this code into the box on the account page.\n\n".
"\t" . $_SESSION["verify_key"] . "\n",
"From: $APTMAIL");
$_SESSION["codesent"] = 1;
}
if (isset($formfields["verify"]) &&
$formfields["verify"] == $_SESSION["verify_key"]) {
#
# Success. Lets remember that in case we get an error
# below and the form is redisplayed.
#
$_SESSION["verified"] = 1;
}
else {
#
# Tell caller to throw up the verification form.
#
SPITAJAX_ERROR(3, 0);
return -1;
}
}
}
return 0;
}
#
# Submit
#
function Do_Update()
{
global $this_user, $target_user;
global $ajax_args;
# For email verification if needed.
session_start();
if (CheckPageArgs() < 0) {
return;
}
if (CheckForm() < 0) {
return;
}
# Allow for form precheck only. So JS code knows it will be fast.
if (isset($ajax_args["checkonly"]) && $ajax_args["checkonly"]) {
SPITAJAX_RESPONSE(0);
return;
}
$formfields = $ajax_args["formfields"];
$args = array();
$errors = array();
$args["name"] = $formfields["name"];
$args["city"] = $formfields["city"];
$args["state"] = $formfields["state"];
$args["country"] = $formfields["country"];
$args["affiliation"] = $formfields["affiliation"];
if (isset($formfields["password1"]) && $formfields["password1"] != "") {
$args["password1"] = $formfields["password1"];
$args["password2"] = $formfields["password2"];
}
if (! User::ModUserInfo($target_user,
$target_user->uid(), $args, $errors)) {
SPITAJAX_ERROR(2, $errors);
return;
}
#
# The user can change their email on this pass, but the backend
# does not allow that, so have to call tbacct directly with an
# extra option to tell it to skip the admin check.
#
if ($formfields["email"] != $target_user->email()) {
$target_uid = $target_user->uid();
$safe_email = $formfields["email"];
if (!HASREALACCOUNT($target_uid)) {
$retval = SUEXEC("nobody", "nobody",
"webtbacct -v email $target_uid $safe_email",
SUEXEC_ACTION_CONTINUE);
}
else {
$retval = SUEXEC($target_uid, "nobody",
"webtbacct -v email $target_uid $safe_email",
SUEXEC_ACTION_CONTINUE);
}
if ($retval) {
$errors["error"] = "Internal error changing password";
SPITAJAX_ERROR(2, $errors);
return;
}
}
session_destroy();
SPITAJAX_RESPONSE(0);
}
# Local Variables:
# mode:php
# End:
?>
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
chdir("..");
include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLogin($check_status);
if (isset($this_user)) {
# Allow unapproved users to edit their profile ...
CheckLoginOrDie(CHECKLOGIN_UNAPPROVED);
}
#
# Verify page arguments.
#
$optargs = OptionalPageArguments("uid", PAGEARG_STRING);
if (!isset($uid)) {
$uid = $this_user->uid();
$target_user = $this_user;
}
elseif (!ISADMIN()) {
SPITUSERERROR("Not enough permission");
return;
}
elseif (!TBvalid_uid($uid)) {
SPITUSERERROR("Invalid user");
}
else {
$target_user = User::LookupByUid($uid);
if (!$target_user) {
sleep(2);
SPITUSERERROR("No such user");
return;
}
}
# We use a session. in case we need to do verification
session_start();
session_unset();
$defaults = array();
# Default to start
$defaults["uid"] = $target_user->uid();
$defaults["name"] = $target_user->name();
$defaults["email"] = $target_user->email();
$defaults["city"] = $target_user->city();
$defaults["state"] = $target_user->state();
$defaults["country"] = $target_user->country();
$defaults["affiliation"] = $target_user->affil();
SPITHEADER(1);
echo "<link rel='stylesheet' href='css/bootstrap-formhelpers.min.css'>\n";
echo "<div id='page-body'></div>\n";
echo "<div id='oops_div'></div>\n";
echo "<div id='waitwait_div'></div>\n";
echo "<script type='text/plain' id='form-json'>\n";
echo htmlentities(json_encode($defaults)) . "\n";
echo "</script>\n";
echo "<script src='js/lib/jquery-2.0.3.min.js'></script>\n";
echo "<script src='js/lib/bootstrap.js'></script>\n";
echo "<script src='js/lib/require.js' data-main='js/myaccount'></script>";
SPITFOOTER();
?>
......@@ -226,10 +226,8 @@ $PAGEHEADER_FUNCTION = function($thinheader = 0, $ignore1 = NULL,
<li class='divider'></li>
<li><a href='getcreds.php'>Download Credentials</a></li>
<li><a href='ssh-keys.php'>Manage SSH Keys</a></li>
<li><a href='signup.php'>Start/Join Project</a></li>\n";
if (!$login_user->IsNonLocal()) {
echo " <li><a href='changepswd.php'>Change Password</a></li>";
}
<li><a href='myaccount.php'>Manage Account</a></li>
<li><a href='signup.php'>Start/Join Project</a></li>";
echo " <li><a href='logout.php'>Logout</a></li>
<li class='divider'></li>
<li><a href='list-datasets.php?all=1'>List Datasets</a></li>
......
<?php
#
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -157,6 +157,11 @@ $routing = array("myprofiles" =>
"Do_AddKey",
"deletekey" =>