import logging import stripe from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity from app import app from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, Unauthorized) from endpoints.api.subscribe import subscribe from endpoints.common import common_login from data import model from data.plans import get_plan from auth.permissions import AdministerOrganizationPermission, CreateRepositoryPermission from auth.auth_context import get_authenticated_user from util.gravatar import compute_hash from util.email import (send_confirmation_email, send_recovery_email, send_change_email) logger = logging.getLogger(__name__) def user_view(user): def org_view(o): admin_org = AdministerOrganizationPermission(o.username) return { 'name': o.username, 'gravatar': compute_hash(o.email), 'is_org_admin': admin_org.can(), 'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(), 'preferred_namespace': not (o.stripe_id is None) } organizations = model.get_user_organizations(user.username) def login_view(login): return { 'service': login.service.name, 'service_identifier': login.service_ident, } logins = model.list_federated_logins(user) return { 'verified': user.verified, 'anonymous': False, 'username': user.username, 'email': user.email, 'gravatar': compute_hash(user.email), 'askForPassword': user.password_hash is None, 'organizations': [org_view(o) for o in organizations], 'logins': [login_view(login) for login in logins], 'can_create_repo': True, 'invoice_email': user.invoice_email, 'preferred_namespace': not (user.stripe_id is None) } @resource('/v1/user/') class User(ApiResource): """ Operations related to users. """ schemas = { 'NewUser': { 'id': 'NewUser', 'type': 'object', 'description': 'Fields which must be specified for a new user.', 'required': [ 'username', 'password', 'email', ], 'properties': { 'username': { 'type': 'string', 'description': 'The user\'s username', }, 'password': { 'type': 'string', 'description': 'The user\'s password', }, 'email': { 'type': 'string', 'description': 'The user\'s email address', }, } }, 'UpdateUser': { 'id': 'UpdateUser', 'type': 'object', 'description': 'Fields which can be updated in a user.', 'properties': { 'password': { 'type': 'string', 'description': 'The user\'s password', }, 'invoice_email': { 'type': 'boolean', 'description': 'Whether the user desires to receive an invoice email.', }, 'email': { 'type': 'string', 'description': 'The user\'s email address', }, }, }, } @nickname('getLoggedInUser') def get(self): """ Get user information for the authenticated user. """ user = get_authenticated_user() if user is None or user.organization: return {'anonymous': True} return user_view(user) @nickname('changeUserDetails') @internal_only @validate_json_request('UpdateUser') def put(self): """ Update a users details such as password or email. """ user = get_authenticated_user() if not user: raise Unauthorized() user_data = request.get_json() try: if 'password' in user_data: logger.debug('Changing password for user: %s', user.username) log_action('account_change_password', user.username) model.change_password(user, user_data['password']) if 'invoice_email' in user_data: logger.debug('Changing invoice_email for user: %s', user.username) model.change_invoice_email(user, user_data['invoice_email']) if 'email' in user_data and user_data['email'] != user.email: new_email = user_data['email'] if model.find_user_by_email(new_email): # Email already used. raise request_error(message='E-mail address already used') logger.debug('Sending email to change email address for user: %s', user.username) code = model.create_confirm_email_code(user, new_email=new_email) send_change_email(user.username, user_data['email'], code.code) except model.InvalidPasswordException, ex: raise request_error(exception=ex) return user_view(user) @nickname('createNewUser') @internal_only @validate_json_request('NewUser') def post(self): """ Create a new user. """ user_data = request.get_json() existing_user = model.get_user(user_data['username']) if existing_user: raise request_error(message='The username already exists') try: new_user = model.create_user(user_data['username'], user_data['password'], user_data['email']) code = model.create_confirm_email_code(new_user) send_confirmation_email(new_user.username, new_user.email, code.code) return 'Created', 201 except model.DataModelException as ex: raise request_error(exception=ex) @resource('/v1/user/private') class PrivateRepositories(ApiResource): """ Operations dealing with the available count of private repositories. """ @nickname('getUserPrivateAllowed') def get(self): """ Get the number of private repos this user has, and whether they are allowed to create more. """ user = get_authenticated_user() if not user: raise Unauthorized() private_repos = model.get_private_repo_count(user.username) repos_allowed = 0 if user.stripe_id: cus = stripe.Customer.retrieve(user.stripe_id) if cus.subscription: plan = get_plan(cus.subscription.plan.id) if plan: repos_allowed = plan['privateRepos'] return { 'privateCount': private_repos, 'privateAllowed': (private_repos < repos_allowed) } def conduct_signin(username_or_email, password): needs_email_verification = False invalid_credentials = False verified = model.verify_user(username_or_email, password) if verified: if common_login(verified): return {'success': True} else: needs_email_verification = True else: invalid_credentials = True return { 'needsEmailVerification': needs_email_verification, 'invalidCredentials': invalid_credentials, }, 403 @resource('/v1/user/convert') @internal_only class ConvertToOrganization(ApiResource): """ Operations for converting a user to an organization. """ schemas = { 'ConvertUser': { 'id': 'ConvertUser', 'type': 'object', 'description': 'Information required to convert a user to an organization.', 'required': [ 'adminUser', 'adminPassword', 'plan', ], 'properties': { 'adminUser': { 'type': 'string', 'description': 'The user who will become an org admin\'s username', }, 'adminPassword': { 'type': 'string', 'description': 'The user who will become an org admin\'s password', }, 'plan': { 'type': 'string', 'description': 'The plan to which the organizatino should be subscribed', }, }, }, } @nickname('convertUserToOrganization') @validate_json_request('ConvertUser') def post(self): """ Convert the user to an organization. """ user = get_authenticated_user() if not user: raise Unauthorized() convert_data = request.get_json() # Ensure that the new admin user is the not user being converted. admin_username = convert_data['adminUser'] if admin_username == user.username: raise request_error(reason='invaliduser', message='The admin user is not valid') # Ensure that the sign in credentials work. admin_password = convert_data['adminPassword'] if not model.verify_user(admin_username, admin_password): raise request_error(reason='invaliduser', message='The admin user credentials are not valid') # Subscribe the organization to the new plan. plan = convert_data['plan'] subscribe(user, plan, None, True) # Require business plans # Convert the user to an organization. model.convert_user_to_organization(user, model.get_user(admin_username)) log_action('account_convert', user.username) # And finally login with the admin credentials. return conduct_signin(admin_username, admin_password) @resource('/v1/signin') @internal_only class Signin(ApiResource): """ Operations for signing in the user. """ schemas = { 'SigninUser': { 'id': 'SigninUser', 'type': 'object', 'description': 'Information required to sign in a user.', 'required': [ 'username', 'password', ], 'properties': { 'username': { 'type': 'string', 'description': 'The user\'s username', }, 'password': { 'type': 'string', 'description': 'The user\'s password', }, }, }, } @nickname('signinUser') @validate_json_request('SigninUser') def post(self): """ Sign in the user with the specified credentials. """ signin_data = request.get_json() if not signin_data: raise NotFound() username = signin_data['username'] password = signin_data['password'] return conduct_signin(username, password) @resource('/v1/signout') @internal_only class Signout(ApiResource): """ Resource for signing out users. """ @nickname('logout') def post(self): """ Request that the current user be signed out. """ logout_user() identity_changed.send(app, identity=AnonymousIdentity()) return {'success': True} @resource("/v1/recovery") @internal_only class Recovery(ApiResource): """ Resource for requesting a password recovery email. """ schemas = { 'RequestRecovery': { 'id': 'RequestRecovery', 'type': 'object', 'description': 'Information required to sign in a user.', 'required': [ 'email', ], 'properties': { 'email': { 'type': 'string', 'description': 'The user\'s email address', }, }, }, } @nickname('requestRecoveryEmail') @validate_json_request('RequestRecovery') def post(self): """ Request a password recovery email.""" email = request.get_json()['email'] code = model.create_reset_password_email_code(email) send_recovery_email(email, code.code) return 'Created', 201