diff --git a/data/database.py b/data/database.py index 5736ff611..72c2c3e30 100644 --- a/data/database.py +++ b/data/database.py @@ -8,7 +8,7 @@ from peewee import * from data.read_slave import ReadSlaveModel from sqlalchemy.engine.url import make_url from urlparse import urlparse - +from util.names import urn_generator logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ class TeamMemberInvite(BaseModel): email = CharField(null=True) team = ForeignKeyField(Team, index=True) inviter = ForeignKeyField(User, related_name='inviter') - invite_token = CharField(default=uuid_generator) + invite_token = CharField(default=urn_generator(['teaminvite'])) class LoginService(BaseModel): diff --git a/endpoints/api/team.py b/endpoints/api/team.py index 3a99b591b..24243f911 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -10,6 +10,24 @@ from data import model from util.useremails import send_org_invite_email from util.gravatar import compute_hash +def try_accept_invite(code, user): + try: + (team, inviter) = model.confirm_team_invite(code, user) + except model.DataModelException: + return None + + model.delete_matching_notifications(user, 'org_team_invite', code=code) + + orgname = team.organization.username + log_action('org_team_member_invite_accepted', orgname, { + 'member': user.username, + 'team': team.name, + 'inviter': inviter.username + }) + + return team + + def handle_addinvite_team(inviter, team, user=None, email=None): invite = model.add_or_invite_to_team(inviter, team, user, email) if not invite: @@ -323,20 +341,11 @@ class TeamMemberInvite(ApiResource): def put(self, code): """ Accepts an invite to join a team in an organization. """ # Accept the invite for the current user. - try: - (team, inviter) = model.confirm_team_invite(code, get_authenticated_user()) - except model.DataModelException: + team = try_accept_invite(code, get_authenticated_user()) + if not team: raise NotFound() - model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code) - orgname = team.organization.username - log_action('org_team_member_invite_accepted', orgname, { - 'member': get_authenticated_user().username, - 'team': team.name, - 'inviter': inviter.username - }) - return { 'org': orgname, 'team': team.name diff --git a/endpoints/api/user.py b/endpoints/api/user.py index e2e6a0ff4..cf8f05eae 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -12,6 +12,8 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques license_error) from endpoints.api.subscribe import subscribe from endpoints.common import common_login +from endpoints.api.team import try_accept_invite + from data import model from data.billing import get_plan from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission, @@ -20,6 +22,7 @@ from auth.auth_context import get_authenticated_user from auth import scopes from util.gravatar import compute_hash from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email) +from util.names import parse_single_urn import features @@ -179,11 +182,15 @@ class User(ApiResource): return user_view(user) @nickname('createNewUser') + @parse_args + @query_param('inviteCode', 'Invitation code given for creating the user.', type=str, + default='') @internal_only @validate_json_request('NewUser') - def post(self): + def post(self, args): """ Create a new user. """ user_data = request.get_json() + invite_code = args['inviteCode'] existing_user = model.get_user(user_data['username']) if existing_user: @@ -194,6 +201,14 @@ class User(ApiResource): user_data['email']) code = model.create_confirm_email_code(new_user) send_confirmation_email(new_user.username, new_user.email, code.code) + + # Handle any invite codes. + parsed_invite = parse_single_urn(invite_code) + if parsed_invite is not None: + if parsed_invite[0] == 'teaminvite': + # Add the user to the team. + try_accept_invite(invite_code, new_user) + return 'Created', 201 except model.TooManyUsersException as ex: raise license_error(exception=ex) diff --git a/static/directives/signup-form.html b/static/directives/signup-form.html index e6bd400b4..4947a966e 100644 --- a/static/directives/signup-form.html +++ b/static/directives/signup-form.html @@ -1,5 +1,5 @@
-
+ +
diff --git a/static/js/app.js b/static/js/app.js index 17256eb96..03f39f283 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1843,7 +1843,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}). when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}). - when('/confirminvite', {title: 'Confirm Team Invite', templateUrl: '/static/partials/confirm-team-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}). + when('/confirminvite', {title: 'Confirm Invite', templateUrl: '/static/partials/confirm-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}). when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl, pageClass: 'landing-page'}). @@ -2218,6 +2218,36 @@ quayApp.directive('repoBreadcrumb', function () { return directiveDefinitionObject; }); +quayApp.directive('focusablePopoverContent', ['$timeout', '$popover', function ($timeout, $popover) { + return { + restrict: "A", + link: function (scope, element, attrs) { + $body = $('body'); + var hide = function() { + $body.off('click'); + scope.$apply(function() { + scope.$hide(); + }); + }; + + scope.$on('$destroy', function() { + $body.off('click'); + }); + + $timeout(function() { + $body.on('click', function(evt) { + var target = evt.target; + var isPanelMember = $(element).has(target).length > 0 || target == element; + if (!isPanelMember) { + hide(); + } + }); + + $(element).find('input').focus(); + }, 100); + } + }; +}]); quayApp.directive('repoCircle', function () { var directiveDefinitionObject = { @@ -2276,8 +2306,12 @@ quayApp.directive('userSetup', function () { restrict: 'C', scope: { 'redirectUrl': '=redirectUrl', + + 'inviteCode': '=inviteCode', + 'signInStarted': '&signInStarted', - 'signedIn': '&signedIn' + 'signedIn': '&signedIn', + 'userRegistered': '&userRegistered' }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $scope.sendRecovery = function() { @@ -2292,6 +2326,10 @@ quayApp.directive('userSetup', function () { }); }; + $scope.handleUserRegistered = function(username) { + $scope.userRegistered({'username': username}); + }; + $scope.hasSignedIn = function() { return UserService.hasEverLoggedIn(); }; @@ -2390,7 +2428,9 @@ quayApp.directive('signupForm', function () { transclude: true, restrict: 'C', scope: { + 'inviteCode': '=inviteCode', + 'userRegistered': '&userRegistered' }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) { $('.form-signup').popover(); @@ -2411,6 +2451,10 @@ quayApp.directive('signupForm', function () { UIService.hidePopover('#signupButton'); $scope.registering = true; + if ($scope.inviteCode) { + $scope.newUser['inviteCode'] = $scope.inviteCode; + } + ApiService.createNewUser($scope.newUser).then(function() { $scope.registering = false; $scope.awaitingConfirmation = true; @@ -2418,6 +2462,8 @@ quayApp.directive('signupForm', function () { if (Config.MIXPANEL_KEY) { mixpanel.alias($scope.newUser.username); } + + $scope.userRegistered({'username': $scope.newUser.username}); }, function(result) { $scope.registering = false; UIService.showFormError('#signupButton', result); diff --git a/static/js/controllers.js b/static/js/controllers.js index 3e3939a65..324dfda17 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2276,7 +2276,7 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U loadOrganization(); } -function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) { +function TeamViewCtrl($rootScope, $scope, $timeout, Restangular, ApiService, $routeParams) { var teamname = $routeParams.teamname; var orgname = $routeParams.orgname; @@ -2755,6 +2755,7 @@ function TourCtrl($scope, $location) { function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) { // Monitor any user changes and place the current user into the scope. $scope.loading = false; + $scope.inviteCode = $location.search()['code'] || ''; UserService.updateUserIn($scope, function(user) { if (!user.anonymous && !$scope.loading) { @@ -2776,6 +2777,6 @@ function ConfirmInviteCtrl($scope, $location, UserService, ApiService, Notificat }); } }); - + $scope.redirectUrl = window.location.href; } diff --git a/static/partials/confirm-team-invite.html b/static/partials/confirm-invite.html similarity index 79% rename from static/partials/confirm-team-invite.html rename to static/partials/confirm-invite.html index 625e9e262..64f116fb1 100644 --- a/static/partials/confirm-team-invite.html +++ b/static/partials/confirm-invite.html @@ -1,8 +1,10 @@ -
+