Files

114 lines
3.7 KiB
Python
Raw Permalink Normal View History

2016-06-02 23:48:08 +02:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
import logging
import random
2025-10-07 08:19:57 +02:00
import trytond.config as config
2016-06-02 23:48:08 +02:00
from trytond.exceptions import LoginException
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
from trytond.model import Index, ModelSQL, fields
2021-12-11 17:58:42 +01:00
from trytond.pool import Pool, PoolMeta
2016-07-29 11:08:23 +02:00
from trytond.tools import resolve
2016-06-02 23:48:08 +02:00
logger = logging.getLogger(__name__)
def send_sms(text, to):
2025-10-07 08:19:57 +02:00
if func := config.get('authentication_sms', 'function', default=None):
func = resolve(func)
from_ = config.get('authentication_sms', 'from', default=None)
return func(text, to, from_)
2016-06-02 23:48:08 +02:00
logger.error('Could not send SMS to %s: "%s"', to, text)
2018-06-26 19:11:45 +02:00
class User(metaclass=PoolMeta):
2016-06-02 23:48:08 +02:00
__name__ = 'res.user'
mobile = fields.Char('Mobile',
2019-09-30 09:38:55 +02:00
help='Phone number that supports receiving SMS.')
2016-06-02 23:48:08 +02:00
@classmethod
def __setup__(cls):
2025-04-04 00:45:18 +02:00
super().__setup__()
2016-06-02 23:48:08 +02:00
cls._preferences_fields.append('mobile')
@classmethod
def _login_sms(cls, login, parameters):
pool = Pool()
SMSCode = pool.get('res.user.login.sms_code')
user_id = cls._get_login(login)[0]
2016-06-02 23:48:08 +02:00
if user_id:
SMSCode.send(user_id)
if 'sms_code' in parameters:
code = parameters['sms_code']
if not code:
return
if SMSCode.check(user_id, code):
return user_id
msg = SMSCode.fields_get(['code'])['code']['string']
2020-03-17 23:33:09 +01:00
msg = gettext('authentication_sms.msg_user_sms_code', login=login)
2016-06-02 23:48:08 +02:00
raise LoginException('sms_code', msg, type='char')
2020-03-01 12:33:52 +01:00
class UserLoginSMSCode(ModelSQL):
"""This class is separated from the res.user one in order to prevent
locking the res.user table when in a long running process.
2016-06-02 23:48:08 +02:00
"""
__name__ = 'res.user.login.sms_code'
user_id = fields.Integer("User ID")
2016-06-02 23:48:08 +02:00
user = fields.Function(fields.Many2One('res.user', 'User'), 'get_user')
code = fields.Char('Code')
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
2024-07-27 18:36:14 +02:00
cls._sql_indexes.add(Index(
t, (t.user_id, Index.Equality(cardinality='high'))))
2016-06-02 23:48:08 +02:00
@classmethod
def default_code(cls):
length = config.getint('authentication_sms', 'length', default=6)
srandom = random.SystemRandom()
return ''.join(str(srandom.randint(0, 9)) for _ in range(length))
def get_user(self, name):
return self.user_id
@classmethod
def get(cls, user, _now=None):
if _now is None:
_now = datetime.datetime.now()
timeout = datetime.timedelta(
seconds=config.getint('authentication_sms', 'ttl', default=5 * 60))
records = cls.search([
('user_id', '=', user),
])
for record in records:
if abs(record.create_date - _now) < timeout:
yield record
else:
cls.delete([record])
@classmethod
def send(cls, user, mobile=None):
if not list(cls.get(user)) or mobile:
record = cls(user_id=user)
record.save()
name = config.get('authentication_sms', 'name', default='Tryton')
2018-12-29 14:20:29 +01:00
text = gettext('authentication_sms.msg_sms_text',
name=name, code=record.code)
2016-06-02 23:48:08 +02:00
if mobile:
send_sms(text, mobile)
elif record.user.mobile:
send_sms(text, record.user.mobile)
@classmethod
def check(cls, user, code):
for record in cls.get(user):
if record.code == code:
cls.delete([record])
return True
return False