|
|
|
|
@ -92,6 +92,39 @@ def _normalize_fio(s: str) -> str:
|
|
|
|
|
return re.sub(r'\s+', ' ', t).strip() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_FIO_STAFF_SQL = text( |
|
|
|
|
""" |
|
|
|
|
SELECT id, fio, web_login |
|
|
|
|
FROM staff_members |
|
|
|
|
WHERE fio IS NOT NULL |
|
|
|
|
AND lower( |
|
|
|
|
regexp_replace( |
|
|
|
|
trim(replace(replace(coalesce(fio, ''), chr(160), ' '), E'\\t', ' ')), |
|
|
|
|
'[[:space:]]+', ' ', 'g' |
|
|
|
|
) |
|
|
|
|
) = :needle |
|
|
|
|
""" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _staff_rows_by_normalized_fio(hr_conn, fio_raw: str) -> list: |
|
|
|
|
"""Строки staff_members с тем же ФИО, что и ввод, после нормализации (регистр не важен).""" |
|
|
|
|
fio_needle = _normalize_fio(fio_raw) |
|
|
|
|
if not fio_needle: |
|
|
|
|
return [] |
|
|
|
|
needle = fio_needle.lower() |
|
|
|
|
return list(hr_conn.execute(_FIO_STAFF_SQL, {'needle': needle}).mappings().all()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _single_web_login_from_fio(hr_conn, fio_raw: str) -> str | None: |
|
|
|
|
"""Если по ФИО найден ровно один сотрудник — его web_login для входа в HR; иначе None.""" |
|
|
|
|
rows = _staff_rows_by_normalized_fio(hr_conn, fio_raw) |
|
|
|
|
if len(rows) != 1: |
|
|
|
|
return None |
|
|
|
|
wl = (rows[0]['web_login'] or '').strip() |
|
|
|
|
return wl or None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def authenticate_credentials(login: str, password: str) -> AuthUser: |
|
|
|
|
login = (login or '').strip() |
|
|
|
|
password = password or '' |
|
|
|
|
@ -122,25 +155,8 @@ def _authenticate_dev_fio(fio_raw: str) -> AuthUser:
|
|
|
|
|
if not fio_needle: |
|
|
|
|
raise AuthError(401, RU['invalidCredentials']) |
|
|
|
|
|
|
|
|
|
needle = fio_needle.lower() |
|
|
|
|
|
|
|
|
|
with hr_eng.connect() as hr_conn: |
|
|
|
|
rows = hr_conn.execute( |
|
|
|
|
text( |
|
|
|
|
""" |
|
|
|
|
SELECT id, fio, web_login |
|
|
|
|
FROM staff_members |
|
|
|
|
WHERE fio IS NOT NULL |
|
|
|
|
AND lower( |
|
|
|
|
regexp_replace( |
|
|
|
|
trim(replace(replace(coalesce(fio, ''), chr(160), ' '), E'\\t', ' ')), |
|
|
|
|
'[[:space:]]+', ' ', 'g' |
|
|
|
|
) |
|
|
|
|
) = :needle |
|
|
|
|
""" |
|
|
|
|
), |
|
|
|
|
{'needle': needle}, |
|
|
|
|
).mappings().all() |
|
|
|
|
rows = _staff_rows_by_normalized_fio(hr_conn, fio_raw) |
|
|
|
|
if len(rows) != 1: |
|
|
|
|
raise AuthError(401, RU['invalidCredentials']) |
|
|
|
|
s = rows[0] |
|
|
|
|
@ -208,41 +224,51 @@ def _authenticate_via_hr(login: str, password: str) -> AuthUser:
|
|
|
|
|
if hr_eng is None: |
|
|
|
|
raise AuthError(500, RU['hrDatabaseUrlMissing']) |
|
|
|
|
|
|
|
|
|
user_row_sql = text( |
|
|
|
|
'SELECT id, username, password_hash, role FROM users ' |
|
|
|
|
'WHERE LOWER(TRIM(username)) = LOWER(TRIM(:login))' |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
with hr_eng.connect() as hr_conn: |
|
|
|
|
u = hr_conn.execute( |
|
|
|
|
text( |
|
|
|
|
'SELECT id, username, password_hash, role FROM users ' |
|
|
|
|
'WHERE LOWER(TRIM(username)) = LOWER(TRIM(:login))' |
|
|
|
|
), |
|
|
|
|
{'login': login}, |
|
|
|
|
).mappings().first() |
|
|
|
|
u = hr_conn.execute(user_row_sql, {'login': login}).mappings().first() |
|
|
|
|
if not u: |
|
|
|
|
wl = _single_web_login_from_fio(hr_conn, login) |
|
|
|
|
if wl: |
|
|
|
|
u = hr_conn.execute(user_row_sql, {'login': wl}).mappings().first() |
|
|
|
|
|
|
|
|
|
if not u or not u['password_hash']: |
|
|
|
|
raise AuthError(401, RU['invalidCredentials']) |
|
|
|
|
if not _verify_password(password, u['password_hash']): |
|
|
|
|
raise AuthError(401, RU['invalidCredentials']) |
|
|
|
|
|
|
|
|
|
hr_username = (u['username'] or '').strip() |
|
|
|
|
if not hr_username: |
|
|
|
|
raise AuthError(401, RU['invalidCredentials']) |
|
|
|
|
|
|
|
|
|
s = hr_conn.execute( |
|
|
|
|
text( |
|
|
|
|
'SELECT id, fio FROM staff_members ' |
|
|
|
|
"WHERE LOWER(TRIM(COALESCE(web_login, ''))) = LOWER(TRIM(:login))" |
|
|
|
|
), |
|
|
|
|
{'login': login}, |
|
|
|
|
{'login': hr_username}, |
|
|
|
|
).mappings().first() |
|
|
|
|
if not s: |
|
|
|
|
raise AuthError(403, RU['noStaffForLogin']) |
|
|
|
|
|
|
|
|
|
staff_id = int(s['id']) |
|
|
|
|
fio = s['fio'] or login |
|
|
|
|
fio = s['fio'] or hr_username |
|
|
|
|
app_role = map_hr_role_to_app(u['role']) |
|
|
|
|
canonical_login = hr_username |
|
|
|
|
|
|
|
|
|
# UPSERT через ORM: ищем по staff_id, затем по login, затем создаём |
|
|
|
|
# UPSERT через ORM: ищем по staff_id, затем по логину HR, затем по вводу (ФИО/старый логин) |
|
|
|
|
session = get_session() |
|
|
|
|
user = session.query(User).filter(User.staff_id == staff_id).first() |
|
|
|
|
if not user: |
|
|
|
|
# при первом входе staff_id ещё не проставлен — ищем по login |
|
|
|
|
user = session.query(User).filter(User.login == canonical_login).first() |
|
|
|
|
if not user: |
|
|
|
|
user = session.query(User).filter(User.login == login).first() |
|
|
|
|
if user: |
|
|
|
|
user.login = login |
|
|
|
|
user.login = canonical_login |
|
|
|
|
user.full_name = fio |
|
|
|
|
user.role = app_role |
|
|
|
|
user.password_hash = HR_MANAGED_PASSWORD_PLACEHOLDER |
|
|
|
|
@ -250,7 +276,7 @@ def _authenticate_via_hr(login: str, password: str) -> AuthUser:
|
|
|
|
|
user.is_active = True |
|
|
|
|
else: |
|
|
|
|
user = User( |
|
|
|
|
login=login, |
|
|
|
|
login=canonical_login, |
|
|
|
|
password_hash=HR_MANAGED_PASSWORD_PLACEHOLDER, |
|
|
|
|
full_name=fio, |
|
|
|
|
role=app_role, |
|
|
|
|
|