server/api: add password reminders
This commit is contained in:
parent
1ed17a2046
commit
9ce67b64ed
9 changed files with 92 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
''' Falcon-compatible API facades. '''
|
||||
|
||||
from szurubooru.api.password_reminder_api import PasswordReminderApi
|
||||
from szurubooru.api.user_api import UserListApi, UserDetailApi
|
||||
|
|
53
server/szurubooru/api/password_reminder_api.py
Normal file
53
server/szurubooru/api/password_reminder_api.py
Normal file
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
19
server/szurubooru/services/mailer.py
Normal file
19
server/szurubooru/services/mailer.py
Normal file
|
@ -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()
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue