Advertisement
Guest User

Untitled

a guest
Feb 28th, 2018
200
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 27.27 KB | None | 0 0
  1. # Не больше 3 слов через подчеркивание в именах!!!
  2. # Не используем DBRef
  3. # В качестве идентификаторов используем ObjectId либо UUID, если хотим чтобы
  4. # идентификатор нельзя было узнать с помощью перебора (последние 3 байта
  5. # инкрементируются в ObjectId).
  6. import binascii
  7. import bson
  8. import hashlib
  9. import string
  10. import os
  11. import pymongo
  12. import uuid
  13. import random
  14. from datetime import datetime, timedelta
  15. from typing import Union
  16.  
  17. # TODO: пофиксить регулярки
  18. # 1-32 символов. Первый буква, остальные буквы, числа, которы могут отделяться
  19. # друг от друга одним подчеркиванием.
  20. # USERNAME_RE = r'^(?!.{33})[a-zA-Z](?:_?[a-zA-Z0-9]+)*$'
  21. # Email должен быть не длинее 256 символов, состоять из двух частей,
  22. # разделенных "@", сегменты имени хоста разделены точкой.
  23. EMAIL_RE = r'^(?!.{257})[^\s@]+@(?:[^\s@.]+\.)+[^\s@.]+$'
  24. # Заглушка ебаная
  25. HOSTNAME_RE = r'^(?!.{257})[-a-zA-Z0-9.]+$'
  26. # Формально, длина URL не ограничена, но браузеры имеют ограничения по длине
  27. # URL. Не рекомендуется использовать URL длиной более 2048 символов, так как
  28. # Microsoft Internet Explorer имеет именно такое ограничение[8].
  29. # Валидность URL нас мало волнует. Главное чтобы не прошли URL'ы вида
  30. # "javascript:" и "data:".
  31. SIMPLE_URL_RE = r'^(?!.{2049})https?://\S+$'
  32. USER_ROLES = ['admin', 'agent', 'customer']
  33. TAG_RE = r'^\S{1,32}$'
  34. TICKET_PRIORITIES = ['critical', 'high', 'normal', 'low']
  35. TICKET_STATUSES = ['open', 'pending', 'resolved', 'close']
  36. # Время жизни токена
  37. EXPIRES_IN = timedelta(hours=2)
  38.  
  39.  
  40. # Идет в utils.py
  41. def normalize_email(email: str) -> str:
  42.   # Нужно ли добавить обработку ошибок?
  43.   user, hostname = email.split('@')
  44.   return '{}@{}'.format(user, hostname.lower())
  45.  
  46.  
  47. def normalize_hostname(hostname: str) -> str:
  48.   # >>> 'САЙТ.COM'.encode('idna')
  49.   # b'xn--80aswg.COM'
  50.   return hostname.lower().encode('idna').decode()
  51.  
  52.  
  53. def encode_password(
  54.     password: str,
  55.     salt: bytes,
  56.     iterations: int=100000
  57.   ) -> bytes:
  58.   dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, iterations)
  59.   return binascii.hexlify(dk)
  60.  
  61.  
  62. def random_string(
  63.     length: int,
  64.     chars: str=string.ascii_letters + string.digits
  65.   ) -> str:
  66.   return ''.join(random.choice(chars) for i in range(length))
  67.  
  68.  
  69. def create_shared_collections(db: pymongo.database.Database) -> None:
  70.   collections = db.collection_names()
  71.   if 'users' not in collections:
  72.     db.create_collection('users')
  73.     # Подробнее про валидацию схемы можно прочитать здесь:
  74.     # https://www.mongodb.com/blog/post/mongodb-36-json-schema-validation-expressive-query-syntax
  75.     # Есть какой-то стандарт:
  76.     # http://json-schema.org/latest/json-schema-validation.html
  77.     rv = db.command({
  78.       'collMod': 'users',
  79.       'validator': {
  80.         # Должно существовать одно из двух полей
  81.         # '$or': [
  82.         #   {'username': {'$exists': True}},
  83.         #   {'email': {'$exists': True}}
  84.         # ],
  85.         '$jsonSchema': {
  86.           'bsonType': 'object',
  87.           'required': ['email', 'password_hash', 'salt'],
  88.           'additionalProperties': False,
  89.           'properties': {
  90.             # Если _id не указать, то при insert'е бросит исключение
  91.             '_id': {
  92.               'bsonType': 'objectId'
  93.             },
  94.             # 'username': {
  95.             #   'bsonType': 'string',
  96.             #   'pattern': USERNAME_RE
  97.             # },
  98.             # Перед сохранением должен нормализоваться (доменная часть должна
  99.             # быть переведена в нижний регистр)!
  100.             'email': {
  101.               'bsonType': 'string',
  102.               'pattern': EMAIL_RE
  103.             },
  104.             'password_hash': {
  105.               'bsonType': 'binData',
  106.               'maxLength': 256
  107.             },
  108.             'salt': {
  109.               'bsonType': 'binData',
  110.               'maxLength': 256
  111.             },
  112.             # Человек может не иметь фамилии
  113.             'name': {
  114.               'bsonType': 'string',
  115.               'maxLength': 256
  116.             },
  117.             # Эти поля пока не нужны
  118.             'about_me': {
  119.               'bsonType': 'string',
  120.               'maxLength': 1000
  121.             },
  122.             'url': {
  123.               'bsonType': 'string',
  124.               'pattern': SIMPLE_URL_RE
  125.             },
  126.             'company': {
  127.               'bsonType': 'string',
  128.               'maxLength': 256
  129.             },
  130.             'location': {
  131.               'bsonType': 'string',
  132.               'maxLength': 256
  133.             },
  134.             # Эти данные пользователь не должен напрмяую редактировать. Фото
  135.             # должно загружаться по url'у /upload_photo, а там уже данные
  136.             # коллекции будут обновляться.
  137.             'photo': {
  138.               'bsonType': 'object',
  139.               'required': ['original_id', 'thumbnail_id'],
  140.               'additionalProperties': False,
  141.               'properties': {
  142.                 'original_id': {
  143.                   'bsonType': 'objectId'
  144.                 },
  145.                 'thumbnail_id': {
  146.                   'bsonType': 'objectId'
  147.                 }
  148.               }
  149.             },
  150.             'registered': {
  151.               'bsonType': 'date'
  152.             },
  153.             'deleted': {
  154.               'bsonType': 'bool'
  155.             }
  156.           }
  157.         }
  158.       }
  159.     })
  160.     assert rv == {'ok': 1}
  161.     # >>> pymongo.ASCENDING
  162.     # 1
  163.     # >>> pymongo.DESCENDING
  164.     # -1
  165.     # sparse=True
  166.     # Sparse indexes only contain entries for documents that have the indexed
  167.     # field, even if the index field contains a null value. The index skips
  168.     # over any document that is missing the indexed field. The index is
  169.     # “sparse” because it does not include all documents of a collection. By
  170.     # contrast, non-sparse indexes contain all documents in a collection,
  171.     # storing null values for those documents that do not contain the indexed
  172.     # field.
  173.     db.users.create_index([('email', 1)], unique=True)
  174.     db.users.create_index([('name', 1)])
  175.     db.users.create_index([('company', 1)])
  176.     db.users.create_index([('registered', 1)])
  177.     db.users.create_index([('deleted', 1)])
  178.   # 1 тенант = 1 домен = 1 обслуживаемый ящик
  179.   # Добавить имя, ссылку на логотип и пр.
  180.   if 'tenants' not in collections:
  181.     db.create_collection('tenants')
  182.     rv = db.command({
  183.       'collMod': 'tenants',
  184.       'validator': {
  185.         '$jsonSchema': {
  186.           'bsonType': 'object',
  187.           'required': [
  188.             'owner_id',
  189.             'hostname',
  190.             'imap',
  191.             'smtp'
  192.           ],
  193.           'additionalProperties': False,
  194.           'properties': {
  195.             '_id': {
  196.               'bsonType': 'objectId'
  197.             },
  198.             'owner_id': {
  199.               'bsonType': 'objectId'
  200.             },
  201.             # Перед вставкой переводим в нижний регистр.
  202.             'hostname': {
  203.               'bsonType': 'string',
  204.               'pattern': HOSTNAME_RE
  205.             },
  206.             'imap': {
  207.               'bsonType': 'object',
  208.               'required': ['server', 'username', 'password'],
  209.               'additionalProperties': False,
  210.               'properties': {
  211.                 'server': {
  212.                   'bsonType': 'string',
  213.                   'pattern': HOSTNAME_RE
  214.                 },
  215.                 'username': {
  216.                   'bsonType': 'string',
  217.                   'maxLength': 256
  218.                 },
  219.                 'password': {
  220.                   'bsonType': 'string',
  221.                   'maxLength': 1024
  222.                 },
  223.                 'port': {
  224.                   'bsonType': 'int',
  225.                   'minimum': 0,
  226.                   'maximum': 65535
  227.                 },
  228.                 'ssl': {
  229.                   'bsonType': 'bool'
  230.                 }
  231.               }
  232.             },
  233.             'smtp': {
  234.               'bsonType': 'object',
  235.               'required': ['server', 'username', 'password'],
  236.               'additionalProperties': False,
  237.               'properties': {
  238.                 'server': {
  239.                   'bsonType': 'string',
  240.                   'pattern': HOSTNAME_RE
  241.                 },
  242.                 'username': {
  243.                   'bsonType': 'string',
  244.                   'maxLength': 256
  245.                 },
  246.                 'password': {
  247.                   'bsonType': 'string',
  248.                   'maxLength': 1024
  249.                 },
  250.                 'port': {
  251.                   'bsonType': 'int',
  252.                   'minimum': 0,
  253.                   'maximum': 65535
  254.                 },
  255.                 'ssl': {
  256.                   'bsonType': 'bool'
  257.                 }
  258.               }
  259.             },
  260.             # uid последнего входящего письма
  261.             'last_message_uid': {
  262.               'bsonType': 'long'
  263.             },
  264.             # Время следующей проверки входящих
  265.             'next_fetch_date': {
  266.               'bsonType': 'date'
  267.             },
  268.             # Время следующей рассылки писем
  269.             'next_send_date': {
  270.               'bsonType': 'date'
  271.             },
  272.             'created': {
  273.               'bsonType': 'date'
  274.             },
  275.             # Может быть отключен
  276.             'enabled': {
  277.               'bsonType': 'bool'
  278.             },
  279.             'deleted': {
  280.               'bsonType': 'bool'
  281.             }
  282.           }
  283.         }
  284.       }
  285.     })
  286.     assert rv == {'ok': 1}
  287.     db.tenants.create_index([('owner_id', 1)])
  288.     db.tenants.create_index([('hostname', 1)], unique=True)
  289.     db.tenants.create_index([('enabled', 1)])
  290.     db.tenants.create_index([('next_fetch_date', 1)])
  291.     db.tenants.create_index([('next_send_date', 1)])
  292.     db.tenants.create_index([('deleted', 1)])
  293.   return True
  294.  
  295.  
  296. def create_tenant_collections(db: pymongo.database.Database) -> None:
  297.   collections = db.collection_names()
  298.   if 'users' not in collections:
  299.     db.create_collection('users')
  300.     rv = db.command({
  301.       'collMod': 'users',
  302.       'validator': {
  303.         '$jsonSchema': {
  304.           'bsonType': 'object',
  305.           'required': ['email', 'password_hash', 'salt', 'role'],
  306.           'additionalProperties': False,
  307.           'properties': {
  308.             '_id': {
  309.               'bsonType': 'objectId'
  310.             },
  311.             'email': {
  312.               'bsonType': 'string',
  313.               'pattern': EMAIL_RE
  314.             },
  315.             'password_hash': {
  316.               'bsonType': 'binData',
  317.               'maxLength': 256
  318.             },
  319.             'salt': {
  320.               'bsonType': 'binData',
  321.               'maxLength': 256
  322.             },
  323.             'name': {
  324.               'bsonType': 'string',
  325.               'maxLength': 256
  326.             },
  327.             # Эти поля пока не нужны
  328.             'about_me': {
  329.               'bsonType': 'string',
  330.               'maxLength': 1000
  331.             },
  332.             'url': {
  333.               'bsonType': 'string',
  334.               'pattern': SIMPLE_URL_RE
  335.             },
  336.             'company': {
  337.               'bsonType': 'string',
  338.               'maxLength': 256
  339.             },
  340.             'location': {
  341.               'bsonType': 'string',
  342.               'maxLength': 256
  343.             },
  344.             'photo': {
  345.               'bsonType': 'object',
  346.               'required': ['original_id', 'thumbnail_id'],
  347.               'additionalProperties': False,
  348.               # В коллекции photos хранится
  349.               'properties': {
  350.                 'original_id': {
  351.                   'bsonType': 'objectId'
  352.                 },
  353.                 'thumbnail_id': {
  354.                   'bsonType': 'objectId'
  355.                 }
  356.               }
  357.             },
  358.             'registered': {
  359.               'bsonType': 'date'
  360.             },
  361.             'role': {
  362.               'enum': USER_ROLES
  363.             },
  364.             'deleted': {
  365.               'bsonType': 'bool'
  366.             }
  367.           }
  368.         }
  369.       }
  370.     })
  371.     assert rv == {'ok': 1}
  372.     db.users.create_index([('email', 1)], unique=True)
  373.     db.users.create_index([('registered', 1)])
  374.     db.users.create_index([('role', 1)])
  375.     db.users.create_index([('deleted', 1)])
  376.   if 'access_tokens' not in collections:
  377.     db.create_collection('access_tokens')
  378.     rv = db.command({
  379.       'collMod': 'access_tokens',
  380.       'validator': {
  381.         '$jsonSchema': {
  382.           'bsonType': 'object',
  383.           'required': ['_id', 'user_id', 'expiry'],
  384.           'additionalProperties': False,
  385.           'properties': {
  386.             # >>> db.test.insert({'_id': uuid.uuid4()})
  387.             # UUID('c2adba25-3459-4762-a001-78df28a54a6c')
  388.             # >>> db.test.find_one()
  389.             # {'_id': UUID('c2adba25-3459-4762-a001-78df28a54a6c')}
  390.             '_id': {},
  391.             'user_id': {
  392.               'bsonType': 'objectId'
  393.             },
  394.             'expiry': {
  395.               'bsonType': 'date'
  396.             }
  397.           }
  398.         }
  399.       }
  400.     })
  401.     assert rv == {'ok': 1}
  402.     # Это если потребуется убить все токены опр юзера
  403.     db.access_tokens.create_index([('user_id', 1)])
  404.     db.access_tokens.create_index([('expiry', 1)])
  405.   if 'tags' not in collections:
  406.     db.create_collection('tags')
  407.     rv = db.command({
  408.       'collMod': 'tags',
  409.       'validator': {
  410.         '$jsonSchema': {
  411.           'bsonType': 'object',
  412.           'required': ['name'],
  413.           'additionalProperties': False,
  414.           'properties': {
  415.             '_id': {
  416.               'bsonType': 'objectId'
  417.             },
  418.             'name': {
  419.               'bsonType': 'string',
  420.               'pattern': TAG_RE
  421.             }
  422.           }
  423.         }
  424.       }
  425.     })
  426.     assert rv == {'ok': 1}
  427.     db.tags.create_index([('name', 1)], unique=True)
  428.   if 'tag_count' not in collections:
  429.     db.create_collection('tag_count')
  430.     rv = db.command({
  431.       'collMod': 'tag_count',
  432.       'validator': {
  433.         '$jsonSchema': {
  434.           'bsonType': 'object',
  435.           'required': ['tag_id', 'count'],
  436.           'additionalProperties': False,
  437.           'properties': {
  438.             '_id': {
  439.               'bsonType': 'objectId'
  440.             },
  441.             'tag_id': {
  442.               'bsonType': 'objectId'
  443.             },
  444.             'count': {
  445.               'bsonType': 'long'
  446.             }
  447.           }
  448.         }
  449.       }
  450.     })
  451.     assert rv == {'ok': 1}
  452.     db.tag_count.create_index([('tag_id', 1)], unique=True)
  453.     db.tag_count.create_index([('count', 1)])
  454.   if 'tickets' not in collections:
  455.     db.create_collection('tickets')
  456.     rv = db.command({
  457.       'collMod': 'tickets',
  458.       'validator': {
  459.         '$jsonSchema': {
  460.           'bsonType': 'object',
  461.           'required': [
  462.             'subject',
  463.             'priority',
  464.             'status',
  465.             'created'
  466.           ],
  467.           'additionalProperties': False,
  468.           'properties': {
  469.             '_id': {
  470.               'bsonType': 'objectId'
  471.             },
  472.             'subject': {
  473.               'bsonType': 'string',
  474.               'maxLength': 256
  475.             },
  476.             'agent_comment': {
  477.               'bsonType': 'string',
  478.               'maxLength': 1024
  479.             },
  480.             'priority': {
  481.               'enum': TICKET_PRIORITIES
  482.             },
  483.             'status': {
  484.               'enum': TICKET_STATUSES
  485.             },
  486.             'tag_ids': {
  487.               'bsonType': 'array',
  488.               'uniqueItems': True,
  489.               'maxItems': 5,
  490.               'items': {
  491.                 'bsonType': 'objectId'
  492.               }
  493.             },
  494.             'created': {
  495.               'bsonType': 'object',
  496.               'required': ['user_id', 'date'],
  497.               'additionalProperties': False,
  498.               'properties': {
  499.                 'user_id': {
  500.                   'bsonType': 'objectId'
  501.                 },
  502.                 'date': {
  503.                   'bsonType': 'date'
  504.                 }
  505.               }
  506.             },
  507.             'edited': {
  508.               'bsonType': 'object',
  509.               'required': ['user_id', 'date'],
  510.               'additionalProperties': False,
  511.               'properties': {
  512.                 'user_id': {
  513.                   'bsonType': 'objectId'
  514.                 },
  515.                 'date': {
  516.                   'bsonType': 'date'
  517.                 }
  518.               }
  519.             },
  520.             'replied': {
  521.               'bsonType': 'object',
  522.               'required': ['user_id', 'date'],
  523.               'additionalProperties': False,
  524.               'properties': {
  525.                 'user_id': {
  526.                   'bsonType': 'objectId'
  527.                 },
  528.                 'date': {
  529.                   'bsonType': 'date'
  530.                 }
  531.               }
  532.             },
  533.             'assigned_user_id': {
  534.               'bsonType': 'objectId'
  535.             },
  536.             'solutions': {
  537.               'bsonType': 'array',
  538.               'uniqueItems': True,
  539.               'items': {
  540.                 'bsonType': 'string'
  541.               }
  542.             },
  543.             'deleted': {
  544.               'bsonType': 'bool'
  545.             }
  546.           }
  547.         }
  548.       }
  549.     })
  550.     assert rv == {'ok': 1}
  551.     # https://docs.mongodb.com/manual/core/index-text/
  552.     db.tickets.create_index([('subject', 'text')])
  553.     db.tickets.create_index([('status', 1)])
  554.     db.tickets.create_index([('priority', 1)])
  555.     db.tickets.create_index([('tag_ids', 1)])
  556.     db.tickets.create_index([('deleted', 1)])
  557.   if not 'messages' in collections:
  558.     db.create_collection('messages')
  559.     rv = db.command({
  560.       'collMod': 'messages',
  561.       'validator': {
  562.         '$jsonSchema': {
  563.           'bsonType': 'object',
  564.           'required': [
  565.             'ticket_id',
  566.             'created',
  567.             'content'
  568.           ],
  569.           'additionalProperties': False,
  570.           'properties': {
  571.             '_id': {},
  572.             'ticket_id': {},
  573.             'content': {
  574.               'bsonType': 'string',
  575.               'maxLength': 8192
  576.             },
  577.             'attachments': {
  578.               'bsonType': 'array',
  579.               'items': {
  580.                 'bsonType': 'object',
  581.                 'required': ['id', 'filename'],
  582.                 'additionalProperties': False,
  583.                 'properties': {
  584.                   'id': {},
  585.                   # Для изображений генерируем миниатюру
  586.                   'thumbnail_id': {},
  587.                   'filename': {
  588.                     'bsonType': 'string',
  589.                     'maxLength': 256
  590.                   }
  591.                 }
  592.               }
  593.             },
  594.             'created': {
  595.               'bsonType': 'object',
  596.               'required': ['user_id', 'date'],
  597.               'additionalProperties': False,
  598.               'properties': {
  599.                 'user_id': {
  600.                   'bsonType': 'objectId'
  601.                 },
  602.                 'date': {
  603.                   'bsonType': 'date'
  604.                 }
  605.               }
  606.             },
  607.             'edited': {
  608.               'bsonType': 'object',
  609.               'required': ['user_id', 'date'],
  610.               'additionalProperties': False,
  611.               'properties': {
  612.                 'user_id': {
  613.                   'bsonType': 'objectId'
  614.                 },
  615.                 'date': {
  616.                   'bsonType': 'date'
  617.                 }
  618.               }
  619.             },
  620.             'deleted': {
  621.               'bsonType': 'bool'
  622.             }
  623.           }
  624.         }
  625.       }
  626.     })
  627.     db.messages.create_index([('ticket_id', 1)])
  628.     db.messages.create_index([('content', 'text')])
  629.     db.messages.create_index([('deleted', 1)])
  630.   if 'email_notifications' not in collections:
  631.     db.create_collection('email_notifications')
  632.     rv = db.command({
  633.       'collMod': 'email_notifications',
  634.       'validator': {
  635.         '$jsonSchema': {
  636.           'required': ['type'],
  637.           'properties': {
  638.             'type': {
  639.               'bsonType': 'string'
  640.             },
  641.             'done': {
  642.               'bsonType': 'bool'
  643.             }
  644.           }
  645.         }
  646.       }
  647.     })
  648.     assert rv == {'ok': 1}
  649.     # Примеры:
  650.     #   { 'type': 'send_password', 'user_id': <ObjectId>, password: '<password>' }
  651.     #   { 'type': 'ticket_reply', 'message_id': <ObjectId> }
  652.     db.email_notifications.create_index([('done', 1)])
  653.  
  654.  
  655. class EmailNotificationManager:
  656.   def __init__(self, db: pymongo.database.Database):
  657.     self.collection = db.email_notifications
  658.  
  659.   def create(self, notification) -> bson.ObjectId:
  660.     return self.collection.insert_one(notification).inserted_id
  661.  
  662.  
  663. class AccessTokenManager:
  664.   def __init__(self, db: pymongo.database.Database):
  665.     self.collection = db.access_tokens
  666.  
  667.   def find_by_id(self, id: Union[str, uuid.UUID]) -> Union[None, dict]:
  668.     if not isinstance(id, uuid.UUID):
  669.       id = uuid.UUID(id)
  670.     return self.collection.find({
  671.       '_id': id,
  672.       # Токен должен быть не истекшим
  673.       'expiry': {
  674.         '$gt': datetime.utcnow()
  675.       }
  676.     })
  677.  
  678.   def create(self, data: dict) -> uuid.UUID:
  679.     return self.collection.insert_one(data).inserted_id
  680.  
  681.   def refresh_expiry(self, access_token: dict) -> bool:
  682.     assert access_token['expiry'] > datetime.utcnow()
  683.     return self.collection.update({
  684.       '_id': access_token['_id']
  685.     }, {
  686.       'expiry': access_token['expiry'] + EXPIRES_IN
  687.     }).modified_count > 0
  688.  
  689.  
  690. class UserManager:
  691.   def __init__(self, db: pymongo.database.Database):
  692.     self.collection = db.users
  693.  
  694.   def find(self):
  695.     raise NotImplemented
  696.  
  697.   def find_by_id(
  698.       self,
  699.       id: Union[bson.ObjectId, str],
  700.       filter: dict=None
  701.     ) -> Union[None, dict]:
  702.     if not isinstance(id, bson.ObjectId):
  703.       id = bson.ObjectId(id)
  704.     return self.collection.find_one({
  705.       '_id': id,
  706.       'deleted': {
  707.         '$ne': True
  708.       }
  709.     }, filter)
  710.  
  711.   def find_by_email(
  712.       self,
  713.       email: str,
  714.       filter: dict=None
  715.     ) -> Union[None, dict]:
  716.     return self.collection.find_one({
  717.       'email': normalize_email(email),
  718.       'deleted': {
  719.         '$ne': True
  720.       }
  721.     }, filter)
  722.  
  723.   def create(self, data: dict, send_password: bool=False) -> bson.ObjectId:
  724.     data = dict(data)
  725.     data.pop('_id', 0)
  726.     data['email'] = normalize_email(data['email'])
  727.     password = data.pop('password')
  728.     salt = self._generate_salt()
  729.     data.update({
  730.       # Пароли не хранятся в открытом виде
  731.       'password_hash': self._encode_password(password, salt),
  732.       'salt': salt,
  733.       'registered': datetime.utcnow()
  734.     })
  735.     # print(data)
  736.     rv = self.collection.insert_one(data)
  737.     user_id = rv.inserted_id
  738.     if send_password:
  739.       EmailNotificationManager(self.collection.database).create({
  740.         'type': 'send_password',
  741.         'user_id': user_id,
  742.         'password': password
  743.       })
  744.     return user_id
  745.  
  746.   def update(self, id: Union[bson.ObjectId, str], data: dict) -> bool:
  747.     if not isinstance(id, bson.ObjectId):
  748.       id = bson.ObjectId(id)
  749.     data = dict(data)
  750.     data.pop('_id', 0)
  751.     data.pop('password_hash', 0)
  752.     data.pop('salt', 0)
  753.     missing = object()
  754.     password = data.pop('password', missing)
  755.     if password is not missing:
  756.       # Генерируем новую соль чтобы не тащить ее из базы
  757.       salt = self._generate_salt()
  758.       data.update({
  759.         'password_hash': self._encode_password(password, salt),
  760.         'salt': salt
  761.       })
  762.     rv = self.collection.update_one({'_id': id}, {'$set': data})
  763.     return rv.modified_count > 0
  764.  
  765.   def remove(self, id: Union[bson.ObjectId, str]) -> bool:
  766.     if not isinstance(id, bson.ObjectId):
  767.       id = bson.ObjectId(id)
  768.     rv = self.collection.update_one({'_id': id}, {'$set': {'delete': True}})
  769.     return rv.modified_count > 0
  770.  
  771.   # Какой HTTP-метод для restore? PATCH???
  772.   def restore(self, id: Union[bson.ObjectId, str]) -> bool:
  773.     if not isinstance(id, bson.ObjectId):
  774.       id = bson.ObjectId(id)
  775.     rv = self.collection.update_one({'_id': id}, {'$unset': {'delete': ''}})
  776.     return rv.modified_count > 0
  777.  
  778.   def authenticate(self, email: str, password: str) -> Union[None, dict]:
  779.     user = self.find_by_email(email)
  780.     if user is None:
  781.       return None
  782.     if user['password_hash'] != self._encode_password(password, user['salt']):
  783.       return None
  784.     access_token_manager = AccessTokenManager(self.collection.database)
  785.     access_token = {
  786.       '_id': uuid.uuid4(),
  787.       'user_id': user['_id'],
  788.       'expiry': datetime.utcnow() + EXPIRES_IN
  789.     }
  790.     access_token_id = access_token_manager.create(access_token)
  791.     assert access_token_id == access_token['_id']
  792.     return access_token
  793.  
  794.   def _generate_salt(self) -> bytes:
  795.     return os.urandom(42)
  796.  
  797.   def _encode_password(self, password: str, salt: bytes) -> bytes:
  798.     return encode_password(password, salt)
  799.  
  800.  
  801. class TagCounManager:
  802.   def __init__(self, db: pymongo.database.Database):
  803.     self.db = db
  804.     self.collection = db.tag_count
  805.  
  806.  
  807. class TagManager:
  808.   def __init__(self, db: pymongo.database.Database):
  809.     self.db = db
  810.     self.collection = db.tags
  811.  
  812.   # Нужна ли эта функция?
  813.   def find_by_id(self, id: Union[bson.ObjectId, str]) -> Union[None, dict]:
  814.     return self.collection.find_one({'_id': id})
  815.  
  816.   def find_by_name(self, tag: str) -> Union[None, dict]:
  817.     return self.collection.find_one({'name': tag})
  818.  
  819.   def create(self, tag: str) -> bson.ObjectId:
  820.     return self.collection.insert_one({'name': tag}).inserted_id
  821.  
  822.  
  823. class TicketManager:
  824.   def __init__(self, db: pymongo.database.Database):
  825.     self.db = db
  826.     self.collection = db.tickets
  827.  
  828.   def create(self, data: dict) -> bson.ObjectId:
  829.     data = dict(data)
  830.     data.pop('_id', 0)
  831.     return self.collection.insert_one(data).inserted_id
  832.  
  833.  
  834. client = pymongo.MongoClient()
  835. for db_name in client.database_names():
  836.   if db_name not in ('admin', 'config', 'local'):
  837.     client.drop_database(db_name)
  838. shared_db = client.shared
  839. create_shared_collections(shared_db)
  840. tenant_db = client.test_tenant
  841. create_tenant_collections(tenant_db)
  842. user_manager = UserManager(tenant_db)
  843. i = user_manager.create({
  844.   'email': 'sergey.m@Upsafe.Com',
  845.   'password': '123456',
  846.   'role': 'agent'
  847. }, send_password=True)
  848. print(i)
  849. user = user_manager.find_by_id(i)
  850. print(user)
  851. access_token = user_manager.authenticate('sergey.m@UPSAFE.COM', '123456')
  852. print(access_token)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement