55import re
66import string
77from abc import ABCMeta , abstractmethod
8+ from asyncio import gather
89from datetime import datetime
9- from typing import Any , final
10+ from typing import Annotated , Any , Iterable , final
1011
12+ from annotated_types import MinLen
1113from event_schema .auth import UserLogin , UserLoginKey
1214from fastapi import APIRouter , Depends
1315from fastapi_sqlalchemy import db
14- from pydantic import constr
1516from sqlalchemy .orm import Session as DbSession
1617
1718from auth_backend .base import Base
18- from auth_backend .exceptions import AlreadyExists , LastAuthMethodDelete
19+ from auth_backend .exceptions import LastAuthMethodDelete
1920from auth_backend .models .db import AuthMethod , User , UserSession
2021from auth_backend .schemas .types .scopes import Scope as TypeScope
2122from auth_backend .settings import get_settings
@@ -32,7 +33,7 @@ def random_string(length: int = 32) -> str:
3233
3334
3435class Session (Base ):
35- token : constr ( min_length = 1 )
36+ token : Annotated [ str , MinLen ( 1 )]
3637 expires : datetime
3738 id : int
3839 user_id : int
@@ -128,6 +129,75 @@ async def _get_user(
128129 async def _convert_data_to_userdata_format (cls , data : Any ) -> UserLogin :
129130 raise NotImplementedError ()
130131
132+ @staticmethod
133+ async def user_updated (
134+ new_user : dict [str , Any ] | None ,
135+ old_user : dict [str , Any ] | None = None ,
136+ ):
137+ """Сообщить всем активированным провайдерам авторизации об обновлении пользователя
138+
139+ Каждый AuthMethod должен вызывать эту функцию при создании или изменении пользователя, но
140+ не более одного раза на один запрос пользователя на изменение. При вызове во всех
141+ активированных (включенных в настройках) AuthMethod выполняется функция on_user_update.
142+
143+ ## Diff-пользователя
144+ `new_user` и `old_user` – словари, представляющие изменения в данных пользователя.
145+
146+ Если `new_user` равен `None`, то пользователь был удален. Если `old_user` равен `None`, то
147+ пользователь был создан. В остальных случаях словарь, в котором обязательно есть ключ
148+ `user_id`.
149+
150+ Словарь может содержать ключи с названиями AuthMethod, в которых данные изменились. В
151+ значениях будут находиться словари с ключами `param` и значениями `value` параметров
152+ AuthMethod.
153+
154+ ### Примеры:
155+
156+ Пользователь id=1 был удален, вместе с ним были удалены параметры email метода Email и
157+ user_id метода GitHub.
158+ ```python
159+ new_user = None
160+ old_user = {'user_id': 1, "email": {"email": "user@example.com"}, "github": {"user_id": "123"}}
161+ ```
162+
163+ Пользователь id=2 сменил пароль.
164+ ```python
165+ new_user = {
166+ "user_id": 2,
167+ "email": {"hashed_password": "somerandomshit", "salt": "blahblah"}
168+ }
169+ old_user = {
170+ "user_id": 2,
171+ "email": {"hashed_password": "tihsmodnaremos", "salt": "abracadabra", "password": "plain_password"}
172+ }
173+ ```
174+ """
175+ exceptions = await gather (
176+ * [m .on_user_update (new_user , old_user ) for m in AuthMethodMeta .active_auth_methods ()],
177+ return_exceptions = True ,
178+ )
179+ if len (exceptions ) > 0 :
180+ logger .error ("Following errors occurred during on_user_update: " )
181+ for exc in exceptions :
182+ logger .error (exc )
183+
184+ @staticmethod
185+ async def on_user_update (new_user : dict [str , Any ], old_user : dict [str , Any ] | None = None ):
186+ """Произвести действия на обновление пользователя, в т.ч. обновление в других провайдерах
187+
188+ Описания входных параметров соответствует параметрам `AuthMethodMeta.user_updated`.
189+ """
190+
191+ @classmethod
192+ def is_active (cls ):
193+ return settings .ENABLED_AUTH_METHODS is None or cls .get_name () in settings .ENABLED_AUTH_METHODS
194+
195+ @staticmethod
196+ def active_auth_methods () -> Iterable [type ['AuthMethodMeta' ]]:
197+ for method in AUTH_METHODS .values ():
198+ if method .is_active ():
199+ yield method
200+
131201
132202class OauthMeta (AuthMethodMeta ):
133203 """Абстрактная авторизация и аутентификация через OAuth"""
@@ -156,7 +226,11 @@ async def _auth_url(*args, **kwargs) -> UrlSchema:
156226 @classmethod
157227 async def _unregister (cls , user_session : UserSession = Depends (UnionAuth (scopes = [], auto_error = True ))):
158228 """Отключает для пользователя метод входа"""
159- await cls ._delete_auth_methods (user_session .user , db_session = db .session )
229+ old_user = {"user_id" : user_session .user .id }
230+ new_user = {"user_id" : user_session .user .id }
231+ old_user_params = await cls ._delete_auth_methods (user_session .user , db_session = db .session )
232+ old_user [cls .get_name ()] = old_user_params
233+ await AuthMethodMeta .user_updated (new_user , old_user )
160234 return None
161235
162236 @classmethod
@@ -175,9 +249,9 @@ async def _get_user(cls, key: str, value: str | int, *, db_session: DbSession) -
175249 return auth_method .user
176250
177251 @classmethod
178- async def _register_auth_method (cls , key : str , value : str | int , user : User , * , db_session ):
252+ async def _register_auth_method (cls , key : str , value : str | int , user : User , * , db_session ) -> AuthMethod :
179253 """Добавление пользователю новый AuthMethod"""
180- AuthMethod .create (
254+ return AuthMethod .create (
181255 user_id = user .id ,
182256 auth_method = cls .get_name (),
183257 param = key ,
@@ -186,9 +260,9 @@ async def _register_auth_method(cls, key: str, value: str | int, user: User, *,
186260 )
187261
188262 @classmethod
189- async def _delete_auth_methods (cls , user : User , * , db_session ):
263+ async def _delete_auth_methods (cls , user : User , * , db_session ) -> list [ AuthMethod ] :
190264 """Удаляет пользователю все AuthMethod конкретной авторизации"""
191- auth_methods = (
265+ auth_methods : list [ AuthMethod ] = (
192266 AuthMethod .query (session = db_session )
193267 .filter (
194268 AuthMethod .user_id == user .id ,
@@ -203,6 +277,7 @@ async def _delete_auth_methods(cls, user: User, *, db_session):
203277 for method in auth_methods :
204278 method .is_deleted = True
205279 db_session .flush ()
280+ return {m .param : m .value for m in auth_methods }
206281
207282 @classmethod
208283 def userdata_process_empty_strings (cls , userdata : UserLogin ) -> UserLogin :
0 commit comments