From fea53b0ac34a9785826d4bad4e57d0c1865d599c Mon Sep 17 00:00:00 2001 From: Ben Lei Date: Thu, 20 Oct 2016 13:25:49 +0800 Subject: [PATCH 1/3] Update to use new plugin init mechanism --- forgot_password/__init__.py | 180 +----------------------------------- forgot_password/handlers.py | 175 +++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 176 deletions(-) create mode 100644 forgot_password/handlers.py diff --git a/forgot_password/__init__.py b/forgot_password/__init__.py index 9c6ac2d..cc2d6de 100644 --- a/forgot_password/__init__.py +++ b/forgot_password/__init__.py @@ -13,184 +13,12 @@ # limitations under the License. import logging -import skygear -from skygear import error as skyerror -from skygear.error import SkygearException -from skygear.utils.db import conn - -from . import template -from .util import email as emailutil -from .util import user as userutil -from .options import options as forgetoptions +from .options import options as forgot_password_options +from .handlers import register_handlers logger = logging.getLogger(__name__) -def mail_is_configured(): - """ - Returns true if mail is configured - """ - return bool(forgetoptions.smtp_host) - - -@skygear.op('user:forgot-password') -def forgot_password(email): - """ - Lambda function to handle forgot password request. - """ - if not mail_is_configured(): - logger.error('Mail server is not configured. Configure SMTP_HOST.') - raise SkygearException('mail server is not configured', - skyerror.UnexpectedError) - - if email is None: - raise SkygearException('email is not found', - skyerror.ResourceNotFound) - - with conn() as c: - user = userutil.get_user_from_email(c, email) - if not user: - logger.debug('Unable to find user_id') - raise SkygearException('user_id is not found', - skyerror.ResourceNotFound) - if not user.email: - logger.debug('User does not have email address. This user cannot ' - 'reset password.') - raise SkygearException('email is not found', - skyerror.ResourceNotFound) - - logger.debug('Found user with email address.') - - user_record = userutil.get_user_record(c, user.id) - code = userutil.generate_code(user) - url_prefix = forgetoptions.url_prefix - link = '{0}/reset-password?code={1}&user_id={2}'.format( - url_prefix, code, user.id) - - template_params = { - 'appname': forgetoptions.appname, - 'link': link, - 'url_prefix': url_prefix, - 'email': user.email, - 'user_id': user.id, - 'code': code, - 'user': user, - 'user_record': user_record, - } - - text = template.reset_email_text(**template_params) - if text: - logger.debug('Generated plain text reset password email.') - - html = template.reset_email_html(**template_params) - if html: - logger.debug('Generated html reset password email.') - - sender = forgetoptions.sender - subject = forgetoptions.subject - - try: - logger.debug('About to send email to user.') - mailer = emailutil.Mailer( - smtp_host=forgetoptions.smtp_host, - smtp_port=forgetoptions.smtp_port, - smtp_mode=forgetoptions.smtp_mode, - smtp_login=forgetoptions.smtp_login, - smtp_password=forgetoptions.smtp_password, - ) - mailer.send_mail(sender, email, subject, text, html=html) - logger.info('Successfully sent reset password email to user.') - except Exception as ex: - logger.exception('An error occurred sending reset password email ' - 'to user.') - raise SkygearException(str(ex), skyerror.UnexpectedError) - return {'status': 'OK'} - - -@skygear.op('user:reset-password') -def reset_password(user_id, code, new_password): - """ - Lambda function to handle reset password request. - """ - if not user_id: - raise SkygearException('user_id is not found', - skyerror.ResourceNotFound) - if not code: - raise SkygearException('code is not found', - skyerror.ResourceNotFound) - - with conn() as c: - user = userutil.get_user_and_validate_code(c, user_id, code) - if not user: - logger.debug('User ID is not found or the code is not valid.') - raise SkygearException('user_id is not found or code invalid', - skyerror.ResourceNotFound) - - if not user.email: - raise SkygearException('email is not found', - skyerror.ResourceNotFound) - - logger.debug('Found user and the verification code is valid.') - - userutil.set_new_password(c, user.id, new_password) - logger.info('Successfully reset password for user.') - return {'status': 'OK'} - - -def reset_password_response(**kwargs): - """ - A shorthand for returning the reset password form as a response. - """ - body = template.reset_password_form(**kwargs) - return skygear.Response(body, content_type='text/html') - - -@skygear.handler('reset-password') -def reset_password_handler(request): - """ - A handler for handling reset password request. - """ - code = request.values.get('code') - user_id = request.values.get('user_id') - - with conn() as c: - user = userutil.get_user_and_validate_code(c, user_id, code) - if not user: - logger.debug('User ID is not found or the code is not valid.') - error_msg = 'User not found or code is invalid.' - body = template.reset_password_error(error=error_msg) - return skygear.Response(body, content_type='text/html') - user_record = userutil.get_user_record(c, user.id) - - logger.debug('Found user and the verification code is valid.') - - template_params = { - 'user': user, - 'user_record': user_record, - 'code': code, - 'user_id': user_id, - } - - if request.method == 'POST': - password = request.values.get('password') - if not password: - return reset_password_response( - error='Password cannot be empty.', - **template_params - ) - - if password != request.values.get('confirm'): - return reset_password_response( - error='Confirm password does not match new password.', - **template_params - ) - - with conn() as c: - userutil.set_new_password(c, user.id, password) - logger.info('Successfully reset password for user.') - - body = template.reset_password_success() - return skygear.Response(body, content_type='text/html') - - return reset_password_response(**template_params) +def includeme(settings): + register_handlers(forgot_password_options) diff --git a/forgot_password/handlers.py b/forgot_password/handlers.py new file mode 100644 index 0000000..9aec091 --- /dev/null +++ b/forgot_password/handlers.py @@ -0,0 +1,175 @@ +# Copyright 2016 Oursky Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import skygear +from skygear import error as skyerror +from skygear.error import SkygearException +from skygear.utils.db import conn + +from . import template +from .util import email as emailutil +from .util import user as userutil + + +logger = logging.getLogger(__name__) + + +class InvalidConfiguration(Exception): + pass + + +def reset_password_response(**kwargs): + """ + A shorthand for returning the reset password form as a response. + """ + body = template.reset_password_form(**kwargs) + return skygear.Response(body, content_type='text/html') + + +def register_handlers(settings): + if not settings.smtp_host: + logger.error('Mail server is not configured. Configure SMTP_HOST.') + raise InvalidConfiguration('mail server is not configured') + + @skygear.op('user:forgot-password') + def forgot_password(email): + """ + Lambda function to handle forgot password request. + """ + if email is None: + raise SkygearException('email is not found', + skyerror.ResourceNotFound) + + with conn() as c: + user = userutil.get_user_from_email(c, email) + if not user: + raise SkygearException('user_id is not found', + skyerror.ResourceNotFound) + if not user.email: + raise SkygearException('email is not found', + skyerror.ResourceNotFound) + + user_record = userutil.get_user_record(c, user.id) + code = userutil.generate_code(user) + url_prefix = settings.url_prefix + link = '{0}/reset-password?code={1}&user_id={2}'.format( + url_prefix, code, user.id) + + template_params = { + 'appname': settings.appname, + 'link': link, + 'url_prefix': url_prefix, + 'email': user.email, + 'user_id': user.id, + 'code': code, + 'user': user, + 'user_record': user_record, + } + + text = template.reset_email_text(**template_params) + html = template.reset_email_html(**template_params) + + sender = settings.sender + subject = settings.subject + + try: + mailer = emailutil.Mailer( + smtp_host=settings.smtp_host, + smtp_port=settings.smtp_port, + smtp_mode=settings.smtp_mode, + smtp_login=settings.smtp_login, + smtp_password=settings.smtp_password, + ) + mailer.send_mail(sender, email, subject, text, html=html) + logger.info('Successfully sent reset password email to user.') + except Exception as ex: + logger.exception('An error occurred sending reset password' + ' email to user.') + raise SkygearException(str(ex), skyerror.UnexpectedError) + + return {'status': 'OK'} + + @skygear.op('user:reset-password') + def reset_password(user_id, code, new_password): + """ + Lambda function to handle reset password request. + """ + if not user_id: + raise SkygearException('user_id is not found', + skyerror.ResourceNotFound) + if not code: + raise SkygearException('code is not found', + skyerror.ResourceNotFound) + + with conn() as c: + user = userutil.get_user_and_validate_code(c, user_id, code) + if not user: + raise SkygearException('user_id is not found or code invalid', + skyerror.ResourceNotFound) + + if not user.email: + raise SkygearException('email is not found', + skyerror.ResourceNotFound) + + userutil.set_new_password(c, user.id, new_password) + logger.info('Successfully reset password for user.') + + return {'status': 'OK'} + + @skygear.handler('reset-password') + def reset_password_handler(request): + """ + A handler for handling reset password request. + """ + code = request.values.get('code') + user_id = request.values.get('user_id') + + with conn() as c: + user = userutil.get_user_and_validate_code(c, user_id, code) + if not user: + error_msg = 'User not found or code is invalid.' + body = template.reset_password_error(error=error_msg) + return skygear.Response(body, content_type='text/html') + user_record = userutil.get_user_record(c, user.id) + + template_params = { + 'user': user, + 'user_record': user_record, + 'code': code, + 'user_id': user_id, + } + + if request.method == 'POST': + password = request.values.get('password') + if not password: + return reset_password_response( + error='Password cannot be empty.', + **template_params + ) + + if password != request.values.get('confirm'): + return reset_password_response( + error='Confirm password does not match new password.', + **template_params + ) + + with conn() as c: + userutil.set_new_password(c, user.id, password) + logger.info('Successfully reset password for user.') + + body = template.reset_password_success() + return skygear.Response(body, content_type='text/html') + + return reset_password_response(**template_params) From 1e7c9e755c587e55d5a6b87f7747b7c90d34fe21 Mon Sep 17 00:00:00 2001 From: Ben Lei Date: Thu, 20 Oct 2016 14:32:31 +0800 Subject: [PATCH 2/3] Use skygear settings for reading configuration Ref. #5 --- README.md | 4 +-- forgot_password/__init__.py | 13 +++++--- forgot_password/handlers.py | 31 ++++++++++--------- forgot_password/options.py | 62 ------------------------------------- forgot_password/settings.py | 48 ++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 84 deletions(-) delete mode 100644 forgot_password/options.py create mode 100644 forgot_password/settings.py diff --git a/README.md b/README.md index a4104e4..6621e97 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ SMTP settings are required for the plugin to send outgoing email. ### Other settings -* `FORGOT_PASSWORD_APPNAME` - the app name that will appear in the built-in +* `FORGOT_PASSWORD_APP_NAME` - the app name that will appear in the built-in template. If you use a different template, you do not need to supply an app name. The default app name is the Skygear Server app name. * `FORGOT_PASSWORD_URL_PREFIX` - the URL prefix for accessing the Skygear @@ -53,7 +53,7 @@ Here are a list of templates you can override: * `templates/forgot_password/reset_password.html` - HTML form for user to enter a new password. - + * `templates/forgot_password/reset_password_error.html` - HTML page to show when there is an error with the code and User ID of the request. diff --git a/forgot_password/__init__.py b/forgot_password/__init__.py index cc2d6de..5066afb 100644 --- a/forgot_password/__init__.py +++ b/forgot_password/__init__.py @@ -11,14 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging -from .options import options as forgot_password_options -from .handlers import register_handlers +from skygear.settings import add_parser as add_setting_parser -logger = logging.getLogger(__name__) +from .settings import get_settings_parser, get_smtp_settings_parser +from .handlers import register_handlers def includeme(settings): - register_handlers(forgot_password_options) + register_handlers(settings.forgot_password, settings.forgot_password_smtp) + + +add_setting_parser('forgot_password', get_settings_parser()) +add_setting_parser('forgot_password_smtp', get_smtp_settings_parser()) diff --git a/forgot_password/handlers.py b/forgot_password/handlers.py index 9aec091..82d1102 100644 --- a/forgot_password/handlers.py +++ b/forgot_password/handlers.py @@ -26,10 +26,6 @@ logger = logging.getLogger(__name__) -class InvalidConfiguration(Exception): - pass - - def reset_password_response(**kwargs): """ A shorthand for returning the reset password form as a response. @@ -38,16 +34,19 @@ def reset_password_response(**kwargs): return skygear.Response(body, content_type='text/html') -def register_handlers(settings): - if not settings.smtp_host: - logger.error('Mail server is not configured. Configure SMTP_HOST.') - raise InvalidConfiguration('mail server is not configured') - +def register_handlers(settings, smtp_settings): @skygear.op('user:forgot-password') def forgot_password(email): """ Lambda function to handle forgot password request. """ + if smtp_settings.host is None: + logger.error('Mail server is not configured. Configure SMTP_HOST.') + raise SkygearException( + 'mail server is not configured', + skyerror.UnexpectedError + ) + if email is None: raise SkygearException('email is not found', skyerror.ResourceNotFound) @@ -64,11 +63,13 @@ def forgot_password(email): user_record = userutil.get_user_record(c, user.id) code = userutil.generate_code(user) url_prefix = settings.url_prefix + if url_prefix.endswith('/'): + url_prefix = url_prefix[:-1] link = '{0}/reset-password?code={1}&user_id={2}'.format( url_prefix, code, user.id) template_params = { - 'appname': settings.appname, + 'appname': settings.app_name, 'link': link, 'url_prefix': url_prefix, 'email': user.email, @@ -86,11 +87,11 @@ def forgot_password(email): try: mailer = emailutil.Mailer( - smtp_host=settings.smtp_host, - smtp_port=settings.smtp_port, - smtp_mode=settings.smtp_mode, - smtp_login=settings.smtp_login, - smtp_password=settings.smtp_password, + smtp_host=smtp_settings.host, + smtp_port=smtp_settings.port, + smtp_mode=smtp_settings.mode, + smtp_login=smtp_settings.login, + smtp_password=smtp_settings.password, ) mailer.send_mail(sender, email, subject, text, html=html) logger.info('Successfully sent reset password email to user.') diff --git a/forgot_password/options.py b/forgot_password/options.py deleted file mode 100644 index e9aea3e..0000000 --- a/forgot_password/options.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2016 Oursky Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -from skygear.options import options as skyoptions - - -class Namespace: - @property - def appname(self): - return os.getenv('FORGOT_PASSWORD_APPNAME', skyoptions.appname) - - @property - def url_prefix(self): - url_prefix = os.getenv('FORGOT_PASSWORD_URL_PREFIX', - os.getenv('URL_PREFIX', skyoptions.skygear_endpoint)) # noqa - if url_prefix.endswith('/'): - url_prefix = url_prefix[:-1] - return url_prefix - - @property - def sender(self): - return os.getenv('FORGOT_PASSWORD_SENDER', 'no-reply@skygeario.com') - - @property - def subject(self): - return os.getenv('FORGOT_PASSWORD_SUBJECT', - 'Reset password instructions') - - @property - def smtp_host(self): - return os.getenv('SMTP_HOST') - - @property - def smtp_port(self): - return int(os.getenv('SMTP_PORT', '25')) - - @property - def smtp_mode(self): - return os.getenv('SMTP_MODE', 'normal') - - @property - def smtp_login(self): - return os.getenv('SMTP_LOGIN') - - @property - def smtp_password(self): - return os.getenv('SMTP_PASSWORD') - - -options = Namespace() diff --git a/forgot_password/settings.py b/forgot_password/settings.py new file mode 100644 index 0000000..236fdef --- /dev/null +++ b/forgot_password/settings.py @@ -0,0 +1,48 @@ +# Copyright 2016 Oursky Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from skygear.options import options as skyoptions +from skygear.settings import SettingsParser + + +def get_settings_parser(): + parser = SettingsParser('FORGOT_PASSWORD') + + parser.add_setting('app_name', default=skyoptions.appname) + parser.add_setting('url_prefix', default=skyoptions.skygear_endpoint) + parser.add_setting( + 'sender', + resolve=False, + default='no-reply@skygeario.com' + ) + parser.add_setting( + 'subject', + resolve=False, + default='Reset password instructions' + ) + + return parser + + +def get_smtp_settings_parser(): + parser = SettingsParser('SMTP') + + parser.add_setting('host', resolve=False, required=False) + parser.add_setting('port', resolve=False, default=25, atype=int) + parser.add_setting('mode', resolve=False, default='normal') + parser.add_setting('login', resolve=False, default='') + parser.add_setting('password', resolve=False, default='') + + return parser From 173932cae8ad20a41ac3b87917db44187715d8c8 Mon Sep 17 00:00:00 2001 From: Ben Lei Date: Thu, 20 Oct 2016 15:32:44 +0800 Subject: [PATCH 3/3] Refactor handler registration --- forgot_password/handlers.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/forgot_password/handlers.py b/forgot_password/handlers.py index 82d1102..0e573d1 100644 --- a/forgot_password/handlers.py +++ b/forgot_password/handlers.py @@ -26,15 +26,10 @@ logger = logging.getLogger(__name__) -def reset_password_response(**kwargs): +def register_forgot_password_op(settings, smtp_settings): """ - A shorthand for returning the reset password form as a response. + Register lambda function handling forgot password request """ - body = template.reset_password_form(**kwargs) - return skygear.Response(body, content_type='text/html') - - -def register_handlers(settings, smtp_settings): @skygear.op('user:forgot-password') def forgot_password(email): """ @@ -62,9 +57,11 @@ def forgot_password(email): user_record = userutil.get_user_record(c, user.id) code = userutil.generate_code(user) + url_prefix = settings.url_prefix if url_prefix.endswith('/'): url_prefix = url_prefix[:-1] + link = '{0}/reset-password?code={1}&user_id={2}'.format( url_prefix, code, user.id) @@ -93,7 +90,7 @@ def forgot_password(email): smtp_login=smtp_settings.login, smtp_password=smtp_settings.password, ) - mailer.send_mail(sender, email, subject, text, html=html) + mailer.send_mail(sender, user.email, subject, text, html=html) logger.info('Successfully sent reset password email to user.') except Exception as ex: logger.exception('An error occurred sending reset password' @@ -102,6 +99,11 @@ def forgot_password(email): return {'status': 'OK'} + +def register_reset_password_op(settings): + """ + Register lambda function handling reset password request + """ @skygear.op('user:reset-password') def reset_password(user_id, code, new_password): """ @@ -129,6 +131,19 @@ def reset_password(user_id, code, new_password): return {'status': 'OK'} + +def reset_password_response(**kwargs): + """ + A shorthand for returning the reset password form as a response. + """ + body = template.reset_password_form(**kwargs) + return skygear.Response(body, content_type='text/html') + + +def register_reset_password_handler(setting): + """ + Register HTTP handler for reset password request + """ @skygear.handler('reset-password') def reset_password_handler(request): """ @@ -174,3 +189,9 @@ def reset_password_handler(request): return skygear.Response(body, content_type='text/html') return reset_password_response(**template_params) + + +def register_handlers(settings, smtp_settings): + register_forgot_password_op(settings, smtp_settings) + register_reset_password_op(settings) + register_reset_password_handler(settings)