diff --git a/binary_dependencies/nginx_1.8.0-1_amd64.deb b/binary_dependencies/nginx_1.8.0-1_amd64.deb index 8f15911c1..d57da9f3a 100644 Binary files a/binary_dependencies/nginx_1.8.0-1_amd64.deb and b/binary_dependencies/nginx_1.8.0-1_amd64.deb differ diff --git a/conf/http-base.conf b/conf/http-base.conf index 3c3d57372..b7b2f01a9 100644 --- a/conf/http-base.conf +++ b/conf/http-base.conf @@ -1,5 +1,12 @@ # vim: ft=nginx +set_real_ip_from 0.0.0.0/0; +real_ip_recursive on; +log_format lb_pp '$remote_addr ($proxy_protocol_addr) ' + '- $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent"' + types_hash_max_size 2048; include /usr/local/nginx/conf/mime.types.default; diff --git a/conf/nginx.conf b/conf/nginx.conf index 5e49b1977..ebbed4e47 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -26,18 +26,19 @@ http { # This header must be set only for HTTPS add_header Strict-Transport-Security "max-age=63072000; preload"; - } server { - include proxy-protocol.conf; include server-base.conf; listen 8443 default proxy_protocol; - ssl on; # This header must be set only for HTTPS add_header Strict-Transport-Security "max-age=63072000; preload"; + + real_ip_header proxy_protocol; + + access_log /dev/stdout lb_pp; } } diff --git a/conf/proxy-protocol.conf b/conf/proxy-protocol.conf deleted file mode 100644 index ba00507f5..000000000 --- a/conf/proxy-protocol.conf +++ /dev/null @@ -1,8 +0,0 @@ -# vim: ft=nginx - -set_real_ip_from 0.0.0.0/0; -real_ip_header proxy_protocol; -log_format elb_pp '$proxy_protocol_addr - $remote_user [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent"'; -access_log /dev/stdout elb_pp; diff --git a/data/model/legacy.py b/data/model/legacy.py index 464131f55..f34ef7cae 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -2928,4 +2928,3 @@ def revert_tag(repository, tag_name, docker_image_id): return create_or_update_tag(repository.namespace_user.username, repository.name, tag_name, docker_image_id, reversion=True) - diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 36dc96102..0342c9ea0 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -13,6 +13,8 @@ from data import model from data.billing import PLANS import features +import uuid +import json def carderror_response(e): return {'carderror': e.message}, 402 @@ -96,6 +98,48 @@ def get_invoices(customer_id): } +def get_invoice_fields(user): + try: + cus = billing.Customer.retrieve(user.stripe_id) + except stripe.APIConnectionError: + abort(503, message='Cannot contact Stripe') + + if not 'metadata' in cus: + cus.metadata = {} + + return json.loads(cus.metadata.get('invoice_fields') or '[]'), cus + + +def create_billing_invoice_field(user, title, value): + new_field = { + 'uuid': str(uuid.uuid4()).split('-')[0], + 'title': title, + 'value': value + } + + invoice_fields, cus = get_invoice_fields(user) + invoice_fields.append(new_field) + + if not 'metadata' in cus: + cus.metadata = {} + + cus.metadata['invoice_fields'] = json.dumps(invoice_fields) + cus.save() + return new_field + + +def delete_billing_invoice_field(user, field_uuid): + invoice_fields, cus = get_invoice_fields(user) + invoice_fields = [field for field in invoice_fields if not field['uuid'] == field_uuid] + + if not 'metadata' in cus: + cus.metadata = {} + + cus.metadata['invoice_fields'] = json.dumps(invoice_fields) + cus.save() + return True + + @resource('/v1/plans/') @show_if(features.BILLING) class ListPlans(ApiResource): @@ -367,3 +411,159 @@ class OrgnaizationInvoiceList(ApiResource): return get_invoices(organization.stripe_id) raise Unauthorized() + + +@resource('/v1/user/invoice/fields') +@internal_only +@show_if(features.BILLING) +class UserInvoiceFieldList(ApiResource): + """ Resource for listing and creating a user's custom invoice fields. """ + schemas = { + 'InvoiceField': { + 'id': 'InvoiceField', + 'type': 'object', + 'description': 'Description of an invoice field', + 'required': [ + 'title', 'value' + ], + 'properties': { + 'title': { + 'type': 'string', + 'description': 'The title of the field being added', + }, + 'value': { + 'type': 'string', + 'description': 'The value of the field being added', + }, + }, + }, + } + + @require_user_admin + @nickname('listUserInvoiceFields') + def get(self): + """ List the invoice fields for the current user. """ + user = get_authenticated_user() + if not user.stripe_id: + raise NotFound() + + return {'fields': get_invoice_fields(user)[0]} + + @require_user_admin + @nickname('createUserInvoiceField') + @validate_json_request('InvoiceField') + def post(self): + """ Creates a new invoice field. """ + user = get_authenticated_user() + if not user.stripe_id: + raise NotFound() + + data = request.get_json() + created_field = create_billing_invoice_field(user, data['title'], data['value']) + return created_field + + +@resource('/v1/user/invoice/field/') +@internal_only +@show_if(features.BILLING) +class UserInvoiceField(ApiResource): + """ Resource for deleting a user's custom invoice fields. """ + @require_user_admin + @nickname('deleteUserInvoiceField') + def delete(self, field_uuid): + """ Deletes the invoice field for the current user. """ + user = get_authenticated_user() + if not user.stripe_id: + raise NotFound() + + result = delete_billing_invoice_field(user, field_uuid) + if not result: + abort(404) + + return 'Okay', 201 + + +@resource('/v1/organization//invoice/fields') +@path_param('orgname', 'The name of the organization') +@related_user_resource(UserInvoiceFieldList) +@internal_only +@show_if(features.BILLING) +class OrganizationInvoiceFieldList(ApiResource): + """ Resource for listing and creating an organization's custom invoice fields. """ + schemas = { + 'InvoiceField': { + 'id': 'InvoiceField', + 'type': 'object', + 'description': 'Description of an invoice field', + 'required': [ + 'title', 'value' + ], + 'properties': { + 'title': { + 'type': 'string', + 'description': 'The title of the field being added', + }, + 'value': { + 'type': 'string', + 'description': 'The value of the field being added', + }, + }, + }, + } + + @require_scope(scopes.ORG_ADMIN) + @nickname('listOrgInvoiceFields') + def get(self, orgname): + """ List the invoice fields for the organization. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.get_organization(orgname) + if not organization.stripe_id: + raise NotFound() + + return {'fields': get_invoice_fields(organization)[0]} + + abort(403) + + @require_scope(scopes.ORG_ADMIN) + @nickname('createOrgInvoiceField') + @validate_json_request('InvoiceField') + def post(self, orgname): + """ Creates a new invoice field. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.get_organization(orgname) + if not organization.stripe_id: + raise NotFound() + + data = request.get_json() + created_field = create_billing_invoice_field(organization, data['title'], data['value']) + return created_field + + abort(403) + + +@resource('/v1/organization//invoice/field/') +@path_param('orgname', 'The name of the organization') +@related_user_resource(UserInvoiceField) +@internal_only +@show_if(features.BILLING) +class OrganizationInvoiceField(ApiResource): + """ Resource for deleting an organization's custom invoice fields. """ + @require_scope(scopes.ORG_ADMIN) + @nickname('deleteOrgInvoiceField') + def delete(self, orgname, field_uuid): + """ Deletes the invoice field for the current user. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.get_organization(orgname) + if not organization.stripe_id: + raise NotFound() + + result = delete_billing_invoice_field(organization, field_uuid) + if not result: + abort(404) + + return 'Okay', 201 + + abort(403) diff --git a/static/css/directives/ui/billing-invoices.css b/static/css/directives/ui/billing-invoices.css index 10011237c..299a5e937 100644 --- a/static/css/directives/ui/billing-invoices.css +++ b/static/css/directives/ui/billing-invoices.css @@ -1,3 +1,6 @@ +.billing-invoices-element .fields-menu { + float: right; +} .billing-invoices-element .invoice-title { padding: 6px; @@ -22,4 +25,20 @@ .billing-invoices-element .fa-download { color: #aaa; +} + +.billing-invoices-element .fa-trash-o { + float: right; + margin-top: -3px; + margin-right: -14px !important; + font-size: 14px; + padding: 2px; + padding-left: 6px; + padding-right: 6px; + border-radius: 4px; +} + +.billing-invoices-element .invoice-field { + padding-top: 6px; + padding-bottom: 6px; } \ No newline at end of file diff --git a/static/directives/billing-invoices.html b/static/directives/billing-invoices.html index 802abc358..83200cb64 100644 --- a/static/directives/billing-invoices.html +++ b/static/directives/billing-invoices.html @@ -9,6 +9,30 @@
+ + @@ -39,4 +63,23 @@
Billing Date/Time
+ +
+
+
+ + +
+
+ + +
+
+
+ + diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index 5a5b2ad0a..6e6532a5e 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -3,12 +3,12 @@