From 9ce67b64ed42c9946a51cdd78e25342e9ee92841 Mon Sep 17 00:00:00 2001 From: rr- Date: Sun, 3 Apr 2016 17:01:48 +0200 Subject: [PATCH] server/api: add password reminders --- INSTALL.md | 5 +- config.ini.dist | 3 +- server/szurubooru/api/__init__.py | 1 + .../szurubooru/api/password_reminder_api.py | 53 +++++++++++++++++++ server/szurubooru/app.py | 4 ++ server/szurubooru/config.py | 2 +- server/szurubooru/services/__init__.py | 1 + server/szurubooru/services/mailer.py | 19 +++++++ server/szurubooru/services/user_service.py | 10 +++- 9 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 server/szurubooru/api/password_reminder_api.py create mode 100644 server/szurubooru/services/mailer.py diff --git a/INSTALL.md b/INSTALL.md index 820b4a1..b96680f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -79,8 +79,8 @@ user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox user@host:szuru$ vim config.ini ``` - Pay extra attention to the `[database]` and `[smtp]` sections, and API URL in - `[basic]`. + Pay extra attention to the `[database]` section, `[smtp]` section, API URL + and base URL in `[basic]`. 2. Compile the frontend: @@ -162,6 +162,7 @@ server { ```ini [basic] api_url = http://big.dude/api/ +base_url = http://big.dude/ ``` Then the backend is started with `./server/host-waitress` from within diff --git a/config.ini.dist b/config.ini.dist index d081ee3..ba3d4d4 100644 --- a/config.ini.dist +++ b/config.ini.dist @@ -5,7 +5,8 @@ name = szurubooru debug = 0 secret = change -api_url = http://api.example.com/ # see INSTALL.md +api_url = "http://api.example.com/" # where frontend connects to +base_url = "http://example.com/" # used in absolute links (e.g. password reminder) [database] schema = postgres diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index 75c13b2..291b2ac 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -1,3 +1,4 @@ ''' Falcon-compatible API facades. ''' +from szurubooru.api.password_reminder_api import PasswordReminderApi from szurubooru.api.user_api import UserListApi, UserDetailApi diff --git a/server/szurubooru/api/password_reminder_api.py b/server/szurubooru/api/password_reminder_api.py new file mode 100644 index 0000000..2fc012a --- /dev/null +++ b/server/szurubooru/api/password_reminder_api.py @@ -0,0 +1,53 @@ +''' Exports PasswordReminderApi. ''' + +import hashlib +from szurubooru.api.base_api import BaseApi +from szurubooru.errors import ValidationError, NotFoundError + +class PasswordReminderApi(BaseApi): + ''' API for password reminders. ''' + def __init__(self, config, mailer, user_service): + super().__init__() + self._config = config + self._mailer = mailer + self._user_service = user_service + + def get(self, request, context, user_name): + user = self._user_service.get_by_name(context.session, user_name) + if not user: + raise NotFoundError('User %r not found.' % user_name) + if not user.email: + raise ValidationError( + 'User %r hasn\'t supplied email. Cannot reset password.' % user_name) + token = self._generate_authentication_token(user) + self._mailer.send( + 'noreply@%s' % self._config['basic']['name'], + user.email, + 'Password reset for %s' % self._config['basic']['name'], + 'You (or someone else) requested to reset your password on %s.\n' + 'If you wish to proceed, click this link: %s/password-reset/%s\n' + 'Otherwise, please ignore this email.' % + (self._config['basic']['name'], + self._config['basic']['base_url'].rstrip('/'), + token)) + return {} + + def post(self, request, context, user_name): + user = self._user_service.get_by_name(context.session, user_name) + if not user: + raise NotFoundError('User %r not found.' % user_name) + good_token = self._generate_authentication_token(user) + if not 'token' in context.request: + raise ValidationError('Missing password reset token.') + token = context.request['token'] + if token != good_token: + raise ValidationError('Invalid password reset token.') + new_password = self._user_service.reset_password(user) + context.session.commit() + return {'password': new_password} + + def _generate_authentication_token(self, user): + digest = hashlib.sha256() + digest.update(self._config['basic']['secret'].encode('utf8')) + digest.update(user.password_salt.encode('utf8')) + return digest.hexdigest() diff --git a/server/szurubooru/app.py b/server/szurubooru/app.py index 59859e2..08739e5 100644 --- a/server/szurubooru/app.py +++ b/server/szurubooru/app.py @@ -63,6 +63,7 @@ def create_app(): scoped_session = sqlalchemy.orm.scoped_session(session_maker) # TODO: is there a better way? + mailer = szurubooru.services.Mailer(config) password_service = szurubooru.services.PasswordService(config) auth_service = szurubooru.services.AuthService(config, password_service) user_service = szurubooru.services.UserService(config, password_service) @@ -70,6 +71,8 @@ def create_app(): user_list_api = szurubooru.api.UserListApi(auth_service, user_service) user_detail_api = szurubooru.api.UserDetailApi( config, auth_service, password_service, user_service) + password_reminder_api = szurubooru.api.PasswordReminderApi( + config, mailer, user_service) app = falcon.API( request_type=_CustomRequest, @@ -88,5 +91,6 @@ def create_app(): app.add_route('/users/', user_list_api) app.add_route('/user/{user_name}', user_detail_api) + app.add_route('/password_reminder/{user_name}', password_reminder_api) return app diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py index 4b2827e..3681ddb 100644 --- a/server/szurubooru/config.py +++ b/server/szurubooru/config.py @@ -12,7 +12,7 @@ class Config(object): def __init__(self): self.config = configobj.ConfigObj('../config.ini.dist') if os.path.exists('../config.ini'): - self.config.merge(configobj.ConfigObj('config.ini')) + self.config.merge(configobj.ConfigObj('../config.ini')) self._validate() def __getitem__(self, key): diff --git a/server/szurubooru/services/__init__.py b/server/szurubooru/services/__init__.py index 4bc5671..83e57e2 100644 --- a/server/szurubooru/services/__init__.py +++ b/server/szurubooru/services/__init__.py @@ -3,6 +3,7 @@ Middle layer between REST API and database. All the business logic goes here. ''' +from szurubooru.services.mailer import Mailer from szurubooru.services.auth_service import AuthService from szurubooru.services.user_service import UserService from szurubooru.services.password_service import PasswordService diff --git a/server/szurubooru/services/mailer.py b/server/szurubooru/services/mailer.py new file mode 100644 index 0000000..06e107a --- /dev/null +++ b/server/szurubooru/services/mailer.py @@ -0,0 +1,19 @@ +import smtplib +from email.mime.text import MIMEText + +class Mailer(object): + def __init__(self, config): + self._config = config + + def send(self, sender, recipient, subject, body): + msg = MIMEText(body) + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = recipient + + smtp = smtplib.SMTP( + self._config['smtp']['host'], + int(self._config['smtp']['port'])) + smtp.login(self._config['smtp']['user'], self._config['smtp']['pass']) + smtp.send_message(msg) + smtp.quit() diff --git a/server/szurubooru/services/user_service.py b/server/szurubooru/services/user_service.py index c75cf95..17812dd 100644 --- a/server/szurubooru/services/user_service.py +++ b/server/szurubooru/services/user_service.py @@ -35,10 +35,9 @@ class UserService(object): user = User() user.name = name - user.password = password user.password_salt = self._password_service.create_password() user.password_hash = self._password_service.get_password_hash( - user.password_salt, user.password) + user.password_salt, password) user.email = email user.access_rank = self._config['service']['default_user_rank'] user.creation_time = datetime.now() @@ -50,6 +49,13 @@ class UserService(object): def bump_login_time(self, user): user.last_login_time = datetime.now() + def reset_password(self, user): + password = self._password_service.create_password() + user.password_salt = self._password_service.create_password() + user.password_hash = self._password_service.get_password_hash( + user.password_salt, password) + return password + def get_by_name(self, session, name): ''' Retrieves an user by its name. ''' return session.query(User).filter_by(name=name).first()