diff --git a/app.py b/app.py index 367d2ff..ddc8480 100644 --- a/app.py +++ b/app.py @@ -51,7 +51,7 @@ def activated(object_id): @app.errorhandler(404) def page_not_found(e): - return render_template('404.html'), 404 + return {'error': 'Page not found', 'code': 404} if __name__ == '__main__': app.run(debug=True) diff --git a/config/dev.json b/config/dev.json index fc207a5..a4c622d 100644 --- a/config/dev.json +++ b/config/dev.json @@ -1,31 +1,51 @@ { - "_comment": "Your app information", - "APP_NAME": "My App", - "API_LINK": "http://localhost:8000/api/", - "APP_LINK": "http://localhost:8000/", - - - "_comment": "Testing information NO NEED TO CHANGE", - "TEST_API_URL": "http://localhost:5000/api", - "TEST_PASSWORD": "testing", - "TEST_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5naGlhOCIsInVwZGF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsIm9iamVjdElkIjoiODJqU3NXeTZoVSIsImNyZWF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsInNlc3Npb25Ub2tlbiI6ImVGNkZDUDF5ZkE3RjFESFdWUjYxbTVHbUsifQ.o4nBdmbBOFvNANQ1LffRNy2FUvI2JxQIM-RPDgJ2QMk", - - - "_comment": "Parse credentials", - "PARSE_URL": "https://api.parse.com/1", - "PARSE_REST_KEY": "PLACE YOUR PARSE_REST_KEY", - "PARSE_APP_ID": "PLACE YOUR PARSE_APP_ID", - "PARSE_CLIENT_KEY": "PLACE YOUR PARSE_CLIENT_KEY", - "PARSE_MASTER_KEY": "PLACE YOUR PARSE_MASTER_KEY", - - - "_comment": "JWT_KEY for checking authorization", - "JWT_KEY": "PLACE YOUR JWT KEY HERE", - - - "_comment": "Change SENDGRID to 1 to use built-in and 0 to not", - "SENDGRID": 0, - "SENDGRID_API_KEY": "PLACE YOUR SENDGRID API KEY", - "EMAIL_FROM_NAME": "PLACE YOUR NAME", - "EMAIL_FROM": "PLACE YOUR EMAIL" + "_comment": "Your app information", + "APP_INFO": { + "APP_NAME": "My App", + "API_LINK": "http://localhost:8000/api/", + "APP_LINK": "http://localhost:8000/" + }, + + "_comment": "Config for request_limits", + "REQUEST_LIMITS" : { + "PER_IP_LIMIT": { + "NUM_REQUESTS": 100, + "INTERVAL": 30 + }, + "PARSE_LIMIT": { + "NUM_REQUESTS": 30, + "INTERVAL": 1 + } + }, + + "_comment": "Testing information NO NEED TO CHANGE", + "TEST_API_URL": "http://localhost:5000/api", + "TEST_PASSWORD": "testing", + "TEST_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5naGlhOCIsInVwZGF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsIm9iamVjdElkIjoiODJqU3NXeTZoVSIsImNyZWF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsInNlc3Npb25Ub2tlbiI6ImVGNkZDUDF5ZkE3RjFESFdWUjYxbTVHbUsifQ.o4nBdmbBOFvNANQ1LffRNy2FUvI2JxQIM-RPDgJ2QMk", + + "_comment": "Parse credentials", + "PARSE_INFO": { + "PARSE_URL": "https://api.parse.com/1", + "PARSE_REST_KEY": "PLACE YOUR PARSE_REST_KEY", + "PARSE_APP_ID": "PLACE YOUR PARSE_APP_ID", + "PARSE_CLIENT_KEY": "PLACE YOUR PARSE_CLIENT_KEY", + "PARSE_MASTER_KEY": "PLACE YOUR PARSE_MASTER_KEY" + }, + + "_comment": "JWT_KEY for checking authorization", + "JWT_KEY": "PLACE YOUR JWT KEY HERE", + + "_comment": "Email automation", + "SENDGRID_INFO": { + "SENDGRID_API_KEY": "PLACE YOUR SENDGRID API KEY", + "SENDGRID_NAME": "test user", + "SENDGRID_EMAIL": "email@email.com" + }, + + "_comment": "SMS automation", + "TWILLIO_INFO": { + "TWILLIO_ACCOUNT": "YOUR ACCOUNT SID", + "TWILLIO_AUTH_TOKEN": "YOUR AUTH TOKEN", + "TWILLIO_NUMBER": "YOUR TWILLIO NUMBER" + } } \ No newline at end of file diff --git a/config/test.json b/config/test.json index c3f5012..410a45e 100644 --- a/config/test.json +++ b/config/test.json @@ -1,31 +1,47 @@ { - "_comment": "Your app information", - "APP_NAME": "My App", - "API_LINK": "http://localhost:8000/api/", - "APP_LINK": "http://localhost:8000/", - - - "_comment": "Testing information NO NEED TO CHANGE", - "TEST_API_URL": "http://localhost:5000/api", - "TEST_PASSWORD": "testing", - "TEST_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5naGlhOCIsInVwZGF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsIm9iamVjdElkIjoiODJqU3NXeTZoVSIsImNyZWF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsInNlc3Npb25Ub2tlbiI6ImVGNkZDUDF5ZkE3RjFESFdWUjYxbTVHbUsifQ.o4nBdmbBOFvNANQ1LffRNy2FUvI2JxQIM-RPDgJ2QMk", - - - "_comment": "Parse credentials", - "PARSE_URL": "https://api.parse.com/1", - "PARSE_REST_KEY": "6u6FTDhqC4s84UWJjRA4wJNIxOqxWNT3iLZHfTkj", - "PARSE_APP_ID": "aNcLKlFlOSSlgFHdyelHlMLzgVxUB5MutK2Dsn4K", - "PARSE_CLIENT_KEY": "yZNdTx7QvSPZVNZGKdxlKz4NRhQYDKs4NISxGgkG", - "PARSE_MASTER_KEY": "Tv6qfFe4nfffqsYZuvqe18msieLgeoca2EBFkC1X", - - - "_comment": "JWT_KEY for checking authorization", - "JWT_KEY": "qwertyuiopasdfghjklzxcvbnm123456", - - - "_comment": "Change SENDGRID to 1 to use built-in and 0 to not", - "SENDGRID": 0, - "SENDGRID_API_KEY": "PLACE YOUR SENDGRID API KEY", - "EMAIL_FROM_NAME": "test user", - "EMAIL_FROM": "email@email.com" + "_comment": "Your app information", + "APP_INFO": { + "APP_NAME": "My App", + "API_LINK": "http://localhost:8000/api/", + "APP_LINK": "http://localhost:8000/" + }, + + "_comment": "Config for request_limits", + "REQUEST_LIMITS" : { + "PER_IP_LIMIT": { + "NUM_REQUESTS": 100, + "INTERVAL": 30 + }, + "PARSE_LIMIT": { + "NUM_REQUESTS": 30, + "INTERVAL": 1 + } + }, + + "_comment": "Testing information NO NEED TO CHANGE", + "TEST_API_URL": "http://localhost:5000/api", + "TEST_PASSWORD": "testing", + "TEST_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5naGlhOCIsInVwZGF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsIm9iamVjdElkIjoiODJqU3NXeTZoVSIsImNyZWF0ZWRBdCI6IjIwMTUtMTEtMTlUMjM6Mzc6MjAuMjA0WiIsInNlc3Npb25Ub2tlbiI6ImVGNkZDUDF5ZkE3RjFESFdWUjYxbTVHbUsifQ.o4nBdmbBOFvNANQ1LffRNy2FUvI2JxQIM-RPDgJ2QMk", + + "_comment": "Parse credentials", + "PARSE_INFO": { + "PARSE_URL": "https://api.parse.com/1", + "PARSE_REST_KEY": "6u6FTDhqC4s84UWJjRA4wJNIxOqxWNT3iLZHfTkj", + "PARSE_APP_ID": "aNcLKlFlOSSlgFHdyelHlMLzgVxUB5MutK2Dsn4K", + "PARSE_CLIENT_KEY": "yZNdTx7QvSPZVNZGKdxlKz4NRhQYDKs4NISxGgkG", + "PARSE_MASTER_KEY": "Tv6qfFe4nfffqsYZuvqe18msieLgeoca2EBFkC1X" + }, + + "_comment": "JWT_KEY for checking authorization", + "JWT_KEY": "qwertyuiopasdfghjklzxcvbnm123456", + + "_comment": "Email automation", + "SENDGRID_INFO": {}, + + "_comment": "SMS automation", + "TWILLIO_INFO": { + "TWILLIO_ACCOUNT": "YOUR ACCOUNT SID", + "TWILLIO_AUTH_TOKEN": "YOUR AUTH TOKEN", + "TWILLIO_NUMBER": "YOUR TWILLIO NUMBER" + } } \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index fc7a6dc..e69de29 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +0,0 @@ -from flask_restful import Resource, Api -from src.utils import get_config -import requests -import json - diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py index 82f7f99..3c21a77 100644 --- a/src/controllers/__init__.py +++ b/src/controllers/__init__.py @@ -1,3 +1,6 @@ +# @name <%= app_name %> +# @description +# Create connection between controllers and forms from src.models.user_model import\ UserModel @@ -8,8 +11,6 @@ UserLoginForm,\ UserResetPasswordForm,\ AuthDataForm -from src.models.authentication_model import\ - validate_auth_token from flask_restful import Resource, Api diff --git a/src/controllers/user_controller.py b/src/controllers/user_controller.py index 3f2bda4..8c6bb30 100644 --- a/src/controllers/user_controller.py +++ b/src/controllers/user_controller.py @@ -1,10 +1,14 @@ +# @name <%= app_name %> +# @description +# UserControler handles everything related to users' information from +# registration, verification, authenciation, .... import json from src.controllers import\ BaseUserController from src.models.authentication_model import\ requires_auth,\ - limit + check_all_request_limit _parse_class_name = BaseUserController.model._parse_class_name @@ -12,10 +16,7 @@ class UsersController(BaseUserController): # Require authentication token @requires_auth - # Limit number of requests per IP - @limit(requests=100, window=30, by='ip', group=None) - # Limit number of requests per second - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def get(self): form = self.get_form() if form.validate(): @@ -28,13 +29,12 @@ def get(self): res['params'] = params return res - return {'error':'Unvalid inputs', 'code': 400} + return {'error': 'Unvalid inputs', 'code': 400} class UserController(BaseUserController): @requires_auth - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def get(self, object_id): where = { 'objectId': object_id @@ -48,8 +48,7 @@ def get(self, object_id): return res @requires_auth - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def put(self, object_id): form = self.put_form() if form.validate(): @@ -60,11 +59,10 @@ def put(self, object_id): object_id=object_id) return res - return {'error':'Unvalid inputs', 'code': 400} + return {'error': 'Unvalid inputs', 'code': 400} @requires_auth - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def delete(self, object_id): res = self.model.delete( collection='users', @@ -75,8 +73,7 @@ def delete(self, object_id): class SignupController(BaseUserController): - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def post(self): form = self.signup_form() if form.validate(): @@ -87,26 +84,25 @@ def post(self): ) return res - return {'error':'Unvalid inputs', 'code': 400} + return {'error': 'Unvalid inputs', 'code': 400} class LoginController(BaseUserController): - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def get(self): form = self.login_form() if form.validate(): params = form.filter_data() res = self.model.user_login( - params= params + params=params ) return res - return {'error':'Unvalid inputs', 'code': 400} + return {'error': 'Unvalid inputs', 'code': 400} + class ResetpasswordController(BaseUserController): - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def post(self): form = self.reset_password_form() if form.validate(): @@ -116,11 +112,11 @@ def post(self): where=where ) return res - return {'error':'Unvalid inputs', 'code': 400} + return {'error': 'Unvalid inputs', 'code': 400} + class AuthController(BaseUserController): - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def post(self): form = self.auth_form() if form.validate(): @@ -130,11 +126,11 @@ def post(self): ) return res - return {'error':'Unvalid inputs', 'code': 400} + return {'error': 'Unvalid inputs', 'code': 400} + class UserActivationController(BaseUserController): - @limit(requests=100, window=30, by='ip', group=None) - @limit(requests=30, window=1, by='parse', group='parse') + @check_all_request_limit def get(self, object_id): payload = { 'email_verified': True diff --git a/src/forms/__init__.py b/src/forms/__init__.py index e16e3e1..163e6d4 100644 --- a/src/forms/__init__.py +++ b/src/forms/__init__.py @@ -1,5 +1,8 @@ -import json +# @name <%= app_name %> +# @description +# Utility functions for all Forms. +import json from wtforms import\ Form,\ HiddenField,\ @@ -16,11 +19,13 @@ from flask import\ request +# Maximum number of objects can be returned set by Parse.com PARSE_MAX_LIMIT = 1000 +# BaseAPIForm is for parsing data from request object, getting rid of +# unexpected data, and validate data class BaseAPIForm(Form): objectId = HiddenField() - # where to parse the data from request object (json, args, _headers, form) _data_location = 'json' _formdata = '' @@ -48,6 +53,7 @@ def __init__(self, formdata=None, obj=None, prefix='', **kwargs): obj=obj, prefix=prefix) + def filter_data(self): payload = {} for key in self._formdata.viewkeys() & self.data.viewkeys(): @@ -55,17 +61,17 @@ def filter_data(self): return payload + def validate(self): validate_result = super(BaseAPIForm, self).validate() if not validate_result: for k, v in self.errors.iteritems(): self.error_message = v[0] break - return False - return validate_result + class JSONField(StringField): def pre_validate(self, form): if self.data and type(self.data) is not dict: @@ -73,9 +79,10 @@ def pre_validate(self, form): self.data = json.loads(self.data) except: raise ValidationError("Invalid JSON Field") - return super(JSONField, self).pre_validate(form) + +# 4 BaseForm classes describe where to look for information in request header. class BaseGetForm(BaseAPIForm): _data_location = 'args' @@ -87,11 +94,14 @@ class BaseGetForm(BaseAPIForm): include = StringField(default=None) keys = StringField([validators.required()]) + class BasePostForm(BaseAPIForm): _data_location = 'json' + class BasePutForm(BaseAPIForm): _data_location = 'json' + class BaseDeleteForm(BaseAPIForm): - _data_location = 'args' \ No newline at end of file + _data_location = 'args' diff --git a/src/forms/user_form.py b/src/forms/user_form.py index aec2bfe..8b7ace3 100644 --- a/src/forms/user_form.py +++ b/src/forms/user_form.py @@ -1,3 +1,8 @@ +# @name <%= app_name %> +# @description +# Forms for UserController. All forms are kept minimum so please feel free to +# add more if your application requires more. + from wtforms import\ Form,\ validators,\ @@ -12,12 +17,13 @@ BasePutForm,\ JSONField + class UserGetForm(BaseGetForm): username = StringField() password = HiddenField() - phone = StringField() email = StringField() + class UserPutForm(BasePutForm): username = StringField('Username', [ validators.DataRequired(message="username required") @@ -27,20 +33,23 @@ class UserPutForm(BasePutForm): old_password = PasswordField('Old Password', [ validators.EqualTo('re_old_password')]) re_old_password = PasswordField('Confirm', []) - phone = StringField() email = StringField() + class UserSignupForm(BasePostForm): username = StringField() password = HiddenField() email = StringField() + class UserLoginForm(BaseGetForm): username = StringField() password = HiddenField() + class UserResetPasswordForm(BasePostForm): email = StringField() + class AuthDataForm(BasePostForm): authData = JSONField() diff --git a/src/models/__init__.py b/src/models/__init__.py index d7afa19..3e957ae 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,3 +1,8 @@ +# @name <%= app_name %> +# @description +# Utility functions and information for all Models to communicate with Parse +# database. + import json import requests import urllib @@ -8,6 +13,7 @@ PARSE_MAX_LIMIT = 1000 + class BaseModel(object): _parse_class_name = None _parse_special_classes = ['apps', 'users', 'login', 'roles', @@ -16,93 +22,100 @@ class BaseModel(object): 'requestPasswordReset', 'products', 'roles', 'batch', 'schemas'] - def generate_header(self, master_key = None): + def generate_header(self, master_key=None): + + parse_info = get_config(key='PARSE_INFO') + header = { - "X-Parse-Application-Id": get_config(key="PARSE_APP_ID"), - "X-Parse-REST-API-Key": get_config(key="PARSE_REST_KEY"), - "Content-Type": "application/json" + 'X-Parse-Application-Id': parse_info['PARSE_APP_ID'], + 'X-Parse-REST-API-Key': parse_info['PARSE_REST_KEY'], + 'Content-Type': 'application/json' } if master_key: - header['X-Parse-Master-Key']= get_config(key="PARSE_MASTER_KEY") + header['X-Parse-Master-Key'] = parse_info['PARSE_MASTER_KEY'] return header - def generate_url(self, collection, object_id = None): - base_url = get_config(key="PARSE_URL") + def generate_url(self, collection, object_id=None): + parse_info = get_config(key='PARSE_INFO') + base_url = parse_info['PARSE_URL'] if collection in self._parse_special_classes: - url = "{}/{}".format(base_url, collection) + url = '{}/{}'.format(base_url, collection) else: - url = "{}/classes/{}".format(base_url, collection) + url = '{}/classes/{}'.format(base_url, collection) if object_id is not None: - url = "{}/{}".format(url, object_id) + url = '{}/{}'.format(url, object_id) return url def get(self, collection, params, master_key=None): - url = self.generate_url(collection = collection) - headers= self.generate_header() + url = self.generate_url(collection=collection) + headers = self.generate_header() params = urllib.urlencode(params) + try: res = requests.get(url=url, headers=headers, params=params) return res.json() - except ConnectionError as e: - return {'error': "Cannot connect to database. " - "Please try again later."} + except ConnectionError: + return {'error': 'Cannot connect to database. ' + 'Please try again later.'} except Exception as e: return {'error': e.message} def post(self, collection, payload, master_key=None): - url = self.generate_url(collection = collection) - headers= self.generate_header() + url = self.generate_url(collection=collection) + headers = self.generate_header() try: - res = requests.post(url=url, headers=headers, - data=json.dumps(payload)) + res = requests.post(url=url, + headers=headers, + data=json.dumps(payload) + ) return res.json() except ConnectionError as e: - return {'error': "Cannot connect to database. " - "Please try again later."} + return {'error': 'Cannot connect to database. ' + 'Please try again later.'} except Exception as e: return {'error': e.message} def put(self, collection, object_id, payload, master_key=None): - url = self.generate_url(collection = collection, object_id = object_id) - headers= self.generate_header(master_key=master_key) + url = self.generate_url(collection=collection, object_id=object_id) + headers = self.generate_header(master_key=master_key) try: payload = requests.put(url=url, headers=headers, data=json.dumps(payload)) return payload.json() - except ConnectionError as e: - return {'error': "Cannot connect to database. " - "Please try again later."} + except ConnectionError: + return {'error': 'Cannot connect to database. ' + 'Please try again later.'} except Exception as e: return {'error': e.message} - def delete(self, collection, object_id, master_key = None): - url = self.generate_url(collection = collection, object_id = object_id) + def delete(self, collection, object_id, master_key=None): + url = self.generate_url(collection=collection, object_id=object_id) headers= self.generate_header(master_key=master_key) try: res = requests.delete(url=url, headers=headers) return res.json() - except ConnectionError as e: - return {'error': "Cannot connect to database. " - "Please try again later."} + except ConnectionError: + return {'error': 'Cannot connect to database. ' + 'Please try again later.'} except Exception as e: return {'error': e.message} def login(self, params): - res = self.get(collection = "login", params=params) + res = self.get(collection='login', params=params) return res def signup(self, payload): - res = self.post(collection = "users", payload=payload) + res = self.post(collection='users', payload=payload) return res def password_reset(self, payload): - res = self.post(collection = "requestPasswordReset", payload=payload) + res = self.post(collection='requestPasswordReset', payload=payload) return res diff --git a/src/models/authentication_model.py b/src/models/authentication_model.py index 99b83db..5c41a9b 100644 --- a/src/models/authentication_model.py +++ b/src/models/authentication_model.py @@ -1,3 +1,7 @@ +# @name <%= app_name %> +# @description +# All functions for authentication and authorization purposes + import requests import jwt from flask import\ @@ -46,33 +50,59 @@ def decorated(*args, **kwargs): return decorated -def limit(requests=100, window=30, by='ip', group=None): - def decorator(f): - @wraps(f) - def decorated(*args, **kwargs): - if by is 'ip': - identification = request.remote_addr or 'test' - else: - identification = by - - endpoint = group or request.endpoint - - key = ':'.join(['rl', endpoint, identification]) - - try: - remaining = requests - int(current_app.redis.get(key)) - except (ValueError, TypeError): - remaining = requests - current_app.redis.set(key, 0) - - ttl = current_app.redis.ttl(key) - if ttl == -1: - current_app.redis.expire(key, window) - - if remaining > 0: - current_app.redis.incr(key, 1) - return f(*args, **kwargs) - else: - return {'error': 'Too Many Requests', 'code': 429} - return decorated - return decorator +def check_request_limit(requests=100, window=30, by='ip', group=None): + if by is 'ip': + identification = request.remote_addr or 'test' + else: + identification = by + + endpoint = group or request.endpoint + + key = ':'.join(['rl', endpoint, identification]) + + try: + remaining = requests - int(current_app.redis.get(key)) + except (ValueError, TypeError): + remaining = requests + current_app.redis.set(key, 0) + + ttl = current_app.redis.ttl(key) + if ttl == -1: + current_app.redis.expire(key, window) + + if remaining > 0: + current_app.redis.incr(key, 1) + else: + raise IOError({'error': 'Too Many Requests', 'code': 429}) + + +# limit_wrapper is a decorator to controller number of requests are made to server to avoid attacks +# and limit database cost. By default, it limit 100 requests/30s interval/1 IP address and 30 +# request/second/all IP addresses +def check_all_request_limit(wrapped): + def wrapper(*args, **kwargs): + try: + request_limits = get_config(key='REQUEST_LIMITS') + per_ip_limit = request_limits['PER_IP_LIMIT'] + parse_limit = request_limits['PARSE_LIMIT'] + + # Limit number of requests per IP adress + check_request_limit( + requests=per_ip_limit['NUM_REQUESTS'], + window=per_ip_limit['INTERVAL'], + by='ip', + group=None + ) + + # Limit number of requests per second + check_request_limit( + requests=parse_limit['NUM_REQUESTS'], + window=parse_limit['INTERVAL'], + by='parse', + group='parse' + ) + except IOError as err: + return err.message + + return wrapped(*args, **kwargs) + return wrapper diff --git a/src/models/email_model.py b/src/models/email_model.py new file mode 100644 index 0000000..d24ddfe --- /dev/null +++ b/src/models/email_model.py @@ -0,0 +1,87 @@ +# @name <%= app_name %> +# @description +# Utility functions for sending emails + +import sendgrid +from src.utils import get_config + + +def sendgrid_init(email_to, subject, subs, email_from): + sendgrid_info = get_config(key="SENDGRID_INFO") + sg = sendgrid.SendGridClient(sendgrid_info['SENDGRID_API_KEY']) + message = sendgrid.Mail() + message.add_to(email_to) + message.set_subject(subject) + if email_from is None: + email_from = '{}<{}>'.format( + sendgrid_info['SENDGRID_NAME'], + sendgrid_info['SENDGRID_EMAIL'] + ) + message.set_from(email_from) + for key in subs: + message.add_substitution(key, subs[key]) + + return sg, message + + +def send_email_template(email_to, subject, template_id, subs, email_from): + sg, message = sendgrid_init(email_to, subject, subs, email_from) + message.set_html(' ') + message.add_filter('templates', 'enable', '1') + message.add_filter('templates', 'template_id', template_id) + + return sg.send(message) + + +def send_email(email_to, subject, content, subs, email_from=None): + if get_config(key='SENDGRID_INFO') == {}: + return 200, 'Not send email' + + sg, message = sendgrid_init(email_to, subject, subs, email_from) + message.set_html(content) + + return sg.send(message) + + +def send_activation_email(email, objectId, username=None): + if username is None: + username = '{}<{}>'.format(email.split("@", 1)[0], email) + + app_info = get_config(key='APP_INFO') + + email_to = '{}<{}>'.format(username, email) + subject = 'Activation' + content = "
Congrats! Your {{app}} account has been created successfully.
Click on the button below to activate your account.
Best regards,
The {{app}} team
We have received a request to set a new password for your {{app}} account.
Your new password is shown in the bow below.
Please login to {{app}} and change your password
Best regards,
The {{app}} team
Congrats! Your {{app}} account has been created successfully.
Click on the button below to activate your account.
Best regards,
The {{app}} team
We have received a request to set a new password for your {{app}} account.
Your new password is shown in the bow below.
Please login to {{app}} and change your password
Best regards,
The {{app}} team