Start conversion of the user admin/view

This commit is contained in:
Joseph Schorr 2015-04-02 16:34:41 -04:00
parent bcd8a48159
commit f67eeee8c8
20 changed files with 804 additions and 14 deletions

View file

@ -2,7 +2,7 @@ import logging
import json
from random import SystemRandom
from flask import request
from flask import request, abort
from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity
from peewee import IntegrityError
@ -59,7 +59,6 @@ def user_view(user):
logins = model.list_federated_logins(user)
user_response = {
'verified': user.verified,
'anonymous': False,
'username': user.username,
'avatar': avatar.get_data_for_user(user)
@ -68,6 +67,8 @@ def user_view(user):
user_admin = UserAdminPermission(user.username)
if user_admin.can():
user_response.update({
'is_me': True,
'verified': user.verified,
'email': user.email,
'organizations': [org_view(o) for o in organizations],
'logins': [login_view(login) for login in logins],
@ -77,7 +78,7 @@ def user_view(user):
'tag_expiration': user.removed_tag_expiration_s,
})
if features.SUPER_USERS:
if features.SUPER_USERS and SuperUserPermission().can():
user_response.update({
'super_user': user and user == get_authenticated_user() and SuperUserPermission().can()
})
@ -788,6 +789,7 @@ class StarredRepositoryList(ApiResource):
'repository': repository,
}, 201
@resource('/v1/user/starred/<repopath:repository>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class StarredRepository(RepositoryParamResource):
@ -802,3 +804,17 @@ class StarredRepository(RepositoryParamResource):
if repo:
model.unstar_repository(user, repo)
return 'Deleted', 204
@resource('/v1/users/<username>')
class Users(ApiResource):
""" Operations related to retrieving information about other users. """
@nickname('getUserInformation')
def get(self, username):
""" Get user information for the specified user. """
user = model.get_user(username)
if user is None or user.organization or user.robot:
abort(404)
return user_view(user)

View file

@ -39,6 +39,7 @@ STATUS_TAGS = app.config['STATUS_TAGS']
@web.route('/', methods=['GET'], defaults={'path': ''})
@web.route('/organization/<path:path>', methods=['GET'])
@web.route('/user/<path:path>', methods=['GET'])
@no_cache
def index(path, **kwargs):
return render_page_template('index.html', **kwargs)

View file

@ -0,0 +1,3 @@
.authorized-apps-manager .avatar {
margin-right: 4px;
}

View file

@ -0,0 +1,3 @@
.external-login-button i.fa {
margin-right: 4px;
}

View file

@ -0,0 +1,11 @@
.external-logins-manager .empty {
color: #ccc;
}
.external-logins-manager .external-auth-provider td:first-child {
font-size: 18px;
}
.external-logins-manager .external-auth-provider td:first-child i.fa {
margin-right: 6px;
}

View file

@ -0,0 +1,50 @@
.user-view .user-name {
vertical-align: middle;
margin-left: 6px;
}
.user-view h3 {
margin-bottom: 20px;
margin-top: 0px;
}
.user-view .section-description-header {
padding-left: 40px;
position: relative;
margin-bottom: 20px;
}
.user-view .section-description-header:before {
font-family: FontAwesome;
content: "\f05a";
position: absolute;
top: 2px;
left: 6px;
font-size: 27px;
color: #888;
}
.user-view .user-settings-form .row {
padding: 10px;
margin: 0px;
}
.user-view .co-panel {
position: relative;
min-height: 50px;
}
.user-view .co-panel .panel-body {
position: relative;
}
.user-view .co-panel .row {
margin: 10px;
}
.user-view .co-panel .row .panel {
margin-bottom: 20px;
border-bottom: 0px;
box-shadow: none;
}

View file

@ -0,0 +1,56 @@
<div class="authorized-apps-manager-element">
<div class="manager-header">
<h3>Authorized Applications</h3>
</div>
<div class="manager-header section-description-header">
The authorized applications panel lists applications you have authorized to view information and perform actions on your behalf. You can revoke any of your authorizations here by clicking the gear icon and clicking "Revoke Authorization".
</div>
<div class="resource-view" resource="authorizedAppsResource"
error-message="'Cannot load authorized applications'"></div>
<div class="empty" ng-if="!authorizedApps.length">
<div class="empty-primary-msg">You have not authorized any external applications.</div>
</div>
<table class="co-table" ng-if="authorizedApps.length">
<thead>
<td>Application Name</td>
<td>Authorized Permissions</td>
<td class="options-col"></td>
</thead>
<tr class="auth-info" ng-repeat="authInfo in authorizedApps">
<td>
<span class="avatar" size="24" data="authInfo.application.avatar"></span>
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }}
</a>
<span ng-if="!authInfo.application.url"
data-title="{{ authInfo.application.description || authInfo.application.name }}"
bs-tooltip>
{{ authInfo.application.name }}
</span>
<span class="by">{{ authInfo.application.organization.name }}</span>
</td>
<td>
<span class="label label-default scope"
ng-class="{'repo:admin': 'label-primary', 'repo:write': 'label-success', 'repo:create': 'label-success'}[scopeInfo.scope]"
ng-repeat="scopeInfo in authInfo.scopes" data-title="{{ scopeInfo.description }}"
bs-tooltip>
{{ scopeInfo.scope }}
</span>
</td>
<td class="options-col">
<span class="cor-options-menu">
<span class="cor-option" option-click="deleteAccess(authInfo)">
<i class="fa fa-times"></i> Revoke Authorization
</span>
</span>
</td>
</tr>
</table>
</div>
</div>

View file

@ -0,0 +1,102 @@
<div class="convert-user-to-org-element">
<!-- Step 0 -->
<div class="panel" ng-show="convertStep == 0">
<div class="panel-body" ng-show="user.organizations.length > 0">
<div class="alert alert-info">
Cannot convert this account into an organization, as it is a member of {{user.organizations.length}} other
organization{{user.organizations.length > 1 ? 's' : ''}}. Please leave
{{user.organizations.length > 1 ? 'those organizations' : 'that organization'}} first.
</div>
</div>
<div class="panel-body" ng-show="user.organizations.length == 0">
<div class="alert alert-warning">
Note: Converting a user account into an organization <b>cannot be undone</b>
</div>
<button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process</button>
</div>
</div>
<!-- Step 1 -->
<div class="convert-form" ng-show="convertStep == 1">
<h3>Convert to organization</h3>
<form method="post" name="convertForm" id="convertForm" ng-submit="convertToOrg()">
<div class="form-group">
<label for="orgName">Organization Name</label>
<div class="existing-data">
<span class="avatar" size="24" data="user.avatar"></span>
{{ user.username }}</div>
<span class="description">This will continue to be the namespace for your repositories</span>
</div>
<div class="form-group">
<label for="orgName">Admin User</label>
<input id="adminUsername" name="adminUsername" type="text" class="form-control" placeholder="Admin Username"
ng-model="org.adminUser" required autofocus>
<input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password"
ng-model="org.adminPassword" required>
<span class="description">
The username and password for the account that will become an administrator of the organization.
Note that this account <b>must be a separate registered account</b> from the account that you are
trying to convert, and <b>must already exist</b>.
</span>
</div>
<!-- Plans Table -->
<div class="form-group plan-group" quay-require="['BILLING']">
<label>Organization Plan</label>
<div class="plans-table" plans="orgPlans" current-plan="org.plan"></div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-danger" type="submit"
ng-disabled="convertForm.$invalid || (Features.BILLING && !org.plan)"
analytics-on analytics-event="convert_to_organization">
Convert To Organization
</button>
</div>
</form>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotconvertModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot convert account</h4>
</div>
<div class="modal-body">
Your account could not be converted. Please try again in a moment.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="reallyconvertModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Convert to organization?</h4>
</div>
<div class="modal-body">
<div class="alert alert-danger">You will not be able to login to this account once converted</div>
<div>Are you <b>absolutely sure</b> you would like to convert this account to an organization? Once done, there is no going back.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal" ng-click="reallyConvert()">Absolutely: Convert Now</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -1,6 +1,6 @@
<span class="external-login-button-element">
<span ng-if="provider == 'github'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px" ng-disabled="signingIn">
<a href="javascript:void(0)" ng-class="isLink ? '' : 'btn btn-primary btn-block'" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px" ng-disabled="signingIn">
<i class="fa fa-github fa-lg"></i>
<span ng-if="action != 'attach'">
Sign In with GitHub
@ -15,7 +15,7 @@
</span>
<span ng-if="provider == 'google'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')" ng-disabled="signingIn">
<a href="javascript:void(0)" ng-class="isLink ? '' : 'btn btn-primary btn-block'" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')" ng-disabled="signingIn">
<i class="fa fa-google fa-lg"></i>
<span ng-if="action != 'attach'">Sign In with Google</span>
<span ng-if="action == 'attach'">Attach to Google Account</span>

View file

@ -0,0 +1,63 @@
<div class="external-logins-manager-element">
<div class="manager-header">
<h3>External Logins</h3>
</div>
<div class="manager-header section-description-header">
The external logins panel lists all supported external login providers, which can be used for one-click OAuth-based login to <span class="registry-name"></span>. Accounts can be attached or detached by clicking the associated button below.
</div>
<table class="co-table">
<thead>
<td>Provider</td>
<td>Account Status</td>
<td>Attach/Detach</td>
</thead>
<!-- GitHub Login -->
<tr class="external-auth-provider" ng-show="Features.GITHUB_LOGIN">
<td>
<i class="fa fa-github"></i> GitHub <span ng-if="KeyService.isEnterprise('github')">Enterprise</span>
</td>
<td>
<span ng-if="hasGithubLogin">
Attached to GitHub <span ng-if="KeyService.isEnterprise('github')">Enterprise</span> account <b><a href="{{githubEndpoint}}{{githubLogin}}" target="_blank">{{githubLogin}}</a></b>
</span>
<span class="empty" ng-if="!hasGithubLogin">
(Not attached to GitHub<span ng-if="KeyService.isEnterprise('github')"> Enterprise</span>)
</span>
</td>
<td>
<span class="external-login-button" provider="github" action="attach" is-link="true"
ng-if="!hasGithubLogin"></span>
<a href="javascript:void(0)" ng-if="hasGithubLogin"
ng-click="detachExternalLogin('github')">Detach Account</a>
</td>
</tr>
<!-- Google Login -->
<tr class="external-auth-provider" ng-show="Features.GOOGLE_LOGIN">
<td>
<i class="fa fa-google"></i> Google Account
</td>
<td>
<span ng-if="hasGoogleLogin">
Attached to Google account <b>{{ googleLogin }}</b>
</span>
<span class="empty" ng-if="!hasGoogleLogin">
(Not attached to a Google account)
</span>
</td>
<td>
<span class="external-login-button" provider="google" action="attach" is-link="true"
ng-if="!hasGoogleLogin"></span>
<a href="javascript:void(0)" ng-if="hasGoogleLogin"
ng-click="detachExternalLogin('google')">Detach Account</a>
</td>
</tr>
</table>
</div>

View file

@ -28,7 +28,7 @@
<table class="co-table" ng-if="robots.length">
<thead>
<td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()"></td>
<td class="caret-col" ng-if="(user || organization.is_admin) && Config.isNewLayout()"></td>
<td>Robot Account Name</td>
<td ng-if="organization && Config.isNewLayout()">Teams</td>
<td ng-if="Config.isNewLayout()">Repository Permissions</td>
@ -37,7 +37,7 @@
<tbody ng-repeat="robotInfo in robots">
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
<td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()">
<td class="caret-col" ng-if="(user || organization.is_admin) && Config.isNewLayout()">
<span ng-if="robotInfo.repositories.length > 0" ng-click="showPermissions(robotInfo)">
<i class="fa"
ng-class="robotInfo.showing_permissions ? 'fa-caret-down' : 'fa-caret-right'"

View file

@ -126,7 +126,10 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
// Organization View Application
.route('/organization/:orgname/application/:clientid', 'manage-application')
// User Admin
// View User
.route('/user/:username', 'user-view')
// DEPRECATED: User Admin
.route('/user/', 'user-admin')
// Sign In

View file

@ -0,0 +1,41 @@
/**
* Element for managing the applications authorized by a user.
*/
angular.module('quay').directive('authorizedAppsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/authorized-apps-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, ApiService) {
$scope.$watch('isEnabled', function(enabled) {
if (!enabled) { return; }
loadAuthedApps();
});
var loadAuthedApps = function() {
if ($scope.authorizedAppsResource) { return; }
$scope.authorizedAppsResource = ApiService.listUserAuthorizationsAsResource().get(function(resp) {
$scope.authorizedApps = resp['authorizations'];
});
};
$scope.deleteAccess = function(accessTokenInfo) {
var params = {
'access_token_uuid': accessTokenInfo['uuid']
};
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
}, ApiService.errorDisplay('Could not revoke authorization'));
};
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,62 @@
/**
* Displays a panel for converting the current user to an organization.
*/
angular.module('quay').directive('convertUserToOrg', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/convert-user-to-org.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'user': '=user'
},
controller: function($scope, $element, Features, PlanService, Config) {
$scope.convertStep = 0;
$scope.showConvertForm = function() {
if (Features.BILLING) {
PlanService.getMatchingBusinessPlan(function(plan) {
$scope.org.plan = plan;
});
PlanService.getPlans(function(plans) {
$scope.orgPlans = plans;
});
}
$scope.convertStep = 1;
};
$scope.convertToOrg = function() {
$('#reallyconvertModal').modal({});
};
$scope.reallyConvert = function() {
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
$scope.loading = true;
var data = {
'adminUser': $scope.org.adminUser,
'adminPassword': $scope.org.adminPassword,
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
};
ApiService.convertUserToOrganization(data).then(function(resp) {
CookieService.putPermanent('quay.namespace', $scope.cuser.username);
UserService.load();
$location.path('/');
}, function(resp) {
$scope.loading = false;
if (resp.data.reason == 'invaliduser') {
$('#invalidadminModal').modal({});
} else {
$('#cannotconvertModal').modal({});
}
});
};
}
};
return directiveDefinitionObject;
});

View file

@ -11,6 +11,7 @@ angular.module('quay').directive('externalLoginButton', function () {
scope: {
'signInStarted': '&signInStarted',
'redirectUrl': '=redirectUrl',
'isLink': '=isLink',
'provider': '@provider',
'action': '@action'
},

View file

@ -0,0 +1,55 @@
/**
* Element for managing the applications authorized by a user.
*/
angular.module('quay').directive('externalLoginsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/external-logins-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
},
controller: function($scope, $element, ApiService, UserService, Features, Config, KeyService) {
$scope.Features = Features;
$scope.Config = Config;
$scope.KeyService = KeyService;
UserService.updateUserIn($scope, function(user) {
$scope.cuser = jQuery.extend({}, user);
if ($scope.cuser.logins) {
for (var i = 0; i < $scope.cuser.logins.length; i++) {
var login = $scope.cuser.logins[i];
login.metadata = login.metadata || {};
if (login.service == 'github') {
$scope.hasGithubLogin = true;
$scope.githubLogin = login.metadata['service_username'];
$scope.githubEndpoint = KeyService['githubEndpoint'];
}
if (login.service == 'google') {
$scope.hasGoogleLogin = true;
$scope.googleLogin = login.metadata['service_username'];
}
}
}
});
$scope.detachExternalLogin = function(kind) {
var params = {
'servicename': kind
};
ApiService.detachExternalLogin(null, params).then(function() {
$scope.hasGithubLogin = false;
$scope.hasGoogleLogin = false;
UserService.load();
}, ApiService.errorDisplay('Count not detach service'));
};
}
};
return directiveDefinitionObject;
});

View file

@ -1,6 +1,6 @@
(function() {
/**
* User admin/settings page.
* DEPRECATED: User admin/settings page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('user-admin', 'user-admin.html', UserAdminCtrl, {
@ -160,11 +160,7 @@
$scope.updatingUser = false;
$scope.changeEmailSent = true;
$scope.sentEmail = $scope.cuser.email;
// Reset the form.
delete $scope.cuser['email'];
$scope.changeEmailForm.$setPristine();
}, function(result) {
$scope.updatingUser = false;
UIService.showFormError('#changeEmailForm', result);

View file

@ -0,0 +1,113 @@
(function() {
/**
* Page that displays details about an user.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('user-view', 'user-view.html', UserViewCtrl, {
'newLayout': true,
'title': 'User {{ user.username }}',
'description': 'User {{ user.username }}'
}, ['layout'])
}]);
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService, AvatarService) {
var username = $routeParams.username;
$scope.showInvoicesCounter = 0;
$scope.showAppsCounter = 0;
$scope.changeEmailInfo = {};
$scope.changePasswordInfo = {};
UserService.updateUserIn($scope);
var loadRepositories = function() {
var options = {
'public': false,
'private': true,
'sort': true,
'namespace': username,
};
$scope.repositoriesResource = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories;
});
};
var loadUser = function() {
$scope.userResource = ApiService.getUserInformationAsResource({'username': username}).get(function(user) {
$scope.user = user;
// Load the repositories.
$timeout(function() {
loadRepositories();
}, 10);
});
};
// Load the user.
loadUser();
$scope.showInvoices = function() {
$scope.showInvoicesCounter++;
};
$scope.showApplications = function() {
$scope.showAppsCounter++;
};
$scope.changePassword = function() {
UIService.hidePopover('#changePasswordForm');
$scope.changePasswordInfo.state = 'changing';
var data = {
'password': $scope.changePasswordInfo.password
};
ApiService.changeUserDetails(data).then(function(resp) {
$scope.changePasswordInfo.state = 'changed';
// Reset the form
delete $scope.changePasswordInfo['password']
delete $scope.changePasswordInfo['repeatPassword']
// Reload the user.
UserService.load();
}, function(result) {
$scope.changePasswordInfo.state = 'change-error';
UIService.showFormError('#changePasswordForm', result);
});
};
$scope.generateClientToken = function() {
var generateToken = function(password) {
var data = {
'password': password
};
ApiService.generateUserClientKey(data).then(function(resp) {
$scope.generatedClientToken = resp['key'];
$('#clientTokenModal').modal({});
}, ApiService.errorDisplay('Could not generate token'));
};
UIService.showPasswordDialog('Enter your password to generated an encrypted version:', generateToken);
};
$scope.changeEmail = function() {
UIService.hidePopover('#changeEmailForm');
var details = {
'email': $scope.changeEmailInfo.email
};
$scope.changeEmailInfo.state = 'sending';
ApiService.changeUserDetails(details).then(function() {
$scope.changeEmailInfo.state = 'sent';
delete $scope.changeEmailInfo['email'];
}, function(result) {
$scope.changeEmailInfo.state = 'send-error';
UIService.showFormError('#changeEmailForm', result);
});
};
}
})();

View file

@ -376,7 +376,6 @@
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="invalidadminModal">
<div class="modal-dialog">

View file

@ -0,0 +1,215 @@
<div class="resource-view user-view"
resource="userResource"
error-message="'User not found'">
<div class="page-content">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">
<span class="avatar" size="32" data="user.avatar"></span>
<span class="user-name">{{ user.username }}</span>
</span>
</div>
<div class="cor-tab-panel">
<div class="cor-tabs" quay-show="user.is_me">
<span class="cor-tab" tab-active="true" tab-title="Repositories" tab-target="#repos">
<i class="fa fa-hdd-o"></i>
</span>
<span class="cor-tab" tab-title="Robot Accounts" tab-target="#robots">
<i class="fa fa-wrench"></i>
</span>
<span class="cor-tab" tab-title="User Settings" tab-target="#settings">
<i class="fa fa-gears"></i>
</span>
<span class="cor-tab" tab-title="Billing" tab-target="#usage"
quay-show="Features.BILLING">
<i class="fa fa-credit-card"></i>
</span>
<span class="cor-tab" tab-title="Billing Invoices" tab-target="#invoices"
tab-init="showInvoices()" quay-show="Features.BILLING">
<i class="fa ci-invoice"></i>
</span>
<span class="cor-tab" tab-title="External Logins" tab-target="#external">
<i class="fa fa-external-link-square"></i>
</span>
<span class="cor-tab" tab-title="Authorized Applications" tab-target="#applications"
tab-init="showApplications()">
<i class="fa ci-application"></i>
</span>
</div> <!-- /cor-tabs -->
<div class="cor-tab-content">
<!-- Repositories -->
<div id="repos" class="tab-pane active">
<h3>Repositories</h3>
<div class="repo-list-grid"
repositories-resource="repositoriesResource"
starred="false"
namespace="namespace"
hide-title="true">
</div>
</div>
<!-- Robot Accounts -->
<div id="robots" class="tab-pane">
<div class="robots-manager" user="user"></div>
</div>
<!-- External Logins -->
<div id="external" class="tab-pane">
<div class="external-logins-manager" user="user"></div>
</div>
<!-- Applications -->
<div id="applications" class="tab-pane">
<div class="authorized-apps-manager" user="user" is-enabled="showAppsCounter"></div>
</div>
<!-- Plan and Usage -->
<div id="usage" class="tab-pane" quay-require="['BILLING']">
<h3>Plan Usage and Billing</h3>
<div class="plan-manager" user="user"></div>
</div>
<!-- Billing Invoices -->
<div id="invoices" class="tab-pane" quay-require="['BILLING']">
<h3>Billing Invoices</h3>
<div class="billing-invoices" user="user"
makevisible="showInvoicesCounter"></div>
</div>
<!-- Settings -->
<div id="settings" class="tab-pane">
<h3>User Settings</h3>
<!-- E-mail address -->
<div class="co-panel" quay-show="Features.MAILING">
<div class="co-panel-heading"><i class="fa fa-envelope-o"></i> E-mail Address</div>
<div class="panel-body" style="padding-top: 5px;">
<div class="alert alert-success" ng-show="changeEmailInfo.state == 'sent'">
An e-mail has been sent to {{ sentEmail }} to verify the change.
</div>
<div class="cor-loader" ng-show="changeEmailInfo.state == 'sending'"></div>
<div ng-show="changeEmailInfo.state != 'sending'">
<form class="form-change user-settings-form"
id="changeEmailForm" name="changeEmailForm"
ng-submit="changeEmail(); changeEmailForm.$setPristine()"
ng-show="!awaitingConfirmation && !registering">
<div class="row">
<table class="col-md-6">
<tr>
<td>Current E-mail Address:</td>
<td>{{ user.email }}</td>
</tr>
<tr>
<td>New E-mail Address:</td>
<td>
<input type="email" class="form-control"
placeholder="Your new e-mail address"
ng-model="changeEmailInfo.email" required>
</td>
</tr>
</table>
</div>
<button class="btn btn-primary"
ng-disabled="changeEmailForm.$invalid || changeEmail.email == user.email"
type="submit">
Change E-mail Address
</button>
</form>
</div>
</div>
</div> <!-- /E-mail -->
<!-- Password -->
<div class="co-panel" style="margin-bottom: 0px">
<div class="co-panel-heading"><i class="fa fa-lock"></i> Password</div>
<div class="panel-body" style="padding-top: 5px;">
<div class="cor-loader" ng-show="changePasswordInfo.state == 'changing'"></div>
<!-- Encrypted Password -->
<div class="row" ng-show="changePasswordInfo.state !='changing'">
<div class="panel">
<div class="panel-title">Generate Encrypted Password</div>
<div class="panel-body">
<div class="alert alert-info" ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
Due to Docker storing passwords entered on the command line in <strong>plaintext</strong>, it is highly recommended to use the button below to generate an an encrypted version of your password.
</div>
<div class="alert alert-warning" ng-if="Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
This installation is set to <strong>require</strong> encrypted passwords when
using the Docker command line interface. To generate an encrypted password, click the button below.
</div>
<button class="btn btn-primary" ng-click="generateClientToken()">
<i class="fa fa-key" style="margin-right: 6px;"></i>Generate Encrypted Password
</button>
</div>
</div>
</div>
<!-- Change Password -->
<div class="row" ng-show="changePasswordInfo.state !='changing'">
<div class="panel">
<div class="panel-title">Change Password</div>
<span class="help-block" ng-show="changePasswordInfo.state == 'changed'">
Password changed successfully
</span>
<div class="panel-body">
<div class="alert alert-warning">Note: Changing your password will also invalidate any generated encrypted passwords.</div>
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword(); changePasswordForm.$setPristine()"
ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="changePasswordInfo.password" required
ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="changePasswordInfo.repeatPassword"
match="changePasswordInfo.password" required ng-pattern="/^.{8,}$/">
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="change_pass">Change Password</button>
</form>
</div>
</div>
</div>
</div>
</div> <!-- /Password -->
<!-- Convert -->
<div class="co-panel" quay-show="Config.AUTHENTICATION_TYPE == 'Database'">
<div class="co-panel-heading"><i class="fa fa-group"></i> Convert to organization</div>
<div class="panel-body" style="padding-top: 5px;">
<div class="convert-user-to-org" user="user"></div>
</div>
</div>
</div> <!-- /Convert -->
</div> <!-- /cor-tab-content -->
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="clientTokenModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Encrypted Password</h4>
</div>
<div class="modal-body">
<div style="margin-bottom: 10px;">Your generated encrypted password:</div>
<div class="copy-box" value="generatedClientToken"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Dismiss</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>