VORKOUT-8 #13

Merged
vlad.dev merged 30 commits from VORKOUT-8 into master 2025-07-02 12:23:44 +05:00
11 changed files with 80 additions and 48 deletions
Showing only changes of commit 692461e266 - Show all commits

View File

@ -6,9 +6,10 @@ from typing import Optional
from sqlalchemy import func, insert, select
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.logic.keyring import create_password_key
from api.db.tables.account import account_table
from api.schemas.account.account import User
from api.schemas.endpoints.account import all_user_adapter, AllUserResponse, UserUpdate
from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate, UserUpdate
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]:
@ -54,7 +55,7 @@ async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Opt
)
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[UserUpdate]:
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]:
"""
Получает юзера по id.
"""
@ -66,7 +67,7 @@ async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[
if not user:
return None
return UserUpdate.model_validate(user)
return AllUser.model_validate(user)
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]:
@ -102,7 +103,7 @@ async def update_user_by_id(connection: AsyncConnection, update_values, user) ->
await connection.commit()
async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id: int) -> Optional[UserUpdate]:
async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]:
"""
Создает нове поле в таблице account_table.
"""
@ -112,7 +113,7 @@ async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id:
email=user.email,
bind_tenant_id=user.bind_tenant_id,
role=user.role.value,
meta=user.meta,
meta=user.meta or {},
creator_id=creator_id,
created_at=datetime.now(timezone.utc),
status=user.status.value,
@ -121,6 +122,7 @@ async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id:
res = await connection.execute(query)
await connection.commit()
user = await get_user_by_id(connection, res.lastrowid)
new_user = await get_user_by_id(connection, res.lastrowid)
await create_password_key(connection, user.password, new_user.id)
return user
return new_user

View File

@ -8,13 +8,14 @@ from api.db.tables.account import account_table, account_keyring_table, KeyType,
from api.schemas.account.account import User
from api.schemas.account.account_keyring import AccountKeyring
from api.schemas.endpoints.account import AllUser
from api.utils.key_id_gen import KeyIdGenerator
from datetime import datetime, timezone
async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[AllUser], Optional[AccountKeyring]]:
query = (
select(account_table, account_keyring_table)
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
@ -45,7 +46,7 @@ async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
for column in account_keyring_table.columns
}
user = User.model_validate(user_data)
user = AllUser.model_validate(user_data)
password = AccountKeyring.model_validate(password_data)
return user, password

View File

@ -1,13 +1,14 @@
from typing import Optional
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Optional
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.tables.account import account_keyring_table
from api.db.tables.account import account_keyring_table, KeyStatus, KeyType
from api.schemas.account.account_keyring import AccountKeyring
from api.utils.hasher import hasher
from api.utils.key_id_gen import KeyIdGenerator
async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]:
@ -67,3 +68,19 @@ async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: i
await connection.commit()
return key
async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int):
if password is None:
password = hasher.generate_password()
stmt = insert(account_keyring_table).values(
owner_id=owner_id,
key_type=KeyType.PASSWORD.value,
key_id=KeyIdGenerator(),
Review

Может тут два пароля с разными id создаться, получается?

Может тут два пароля с разными id создаться, получается?
Review

Нет, не должны
Есть какие-то предпосылки? Я просто не вижу такой возможности

Нет, не должны Есть какие-то предпосылки? Я просто не вижу такой возможности
Review

Ну, просто в моменте у нас может быть несколько ключей одного типа (API KEY как минимум), но с разными key_id, и по идее уникальность тут обеспечивается в том числе key_id, т.е. если key_type:PASSWORD присваивать разные key_id, то в случае двойной сработки create_password_key (я не увидел блокировки на двойной вызов этого метода чисто из-за сбоя сетевого стека, к примеру), то мы получим два ключа key_type:PASSWORD ?

решается тем, что паролю надо key_id прописывать статичный, кмк.

Ну, просто в моменте у нас может быть несколько ключей одного типа (API KEY как минимум), но с разными key_id, и по идее уникальность тут обеспечивается в том числе key_id, т.е. если key_type:PASSWORD присваивать разные key_id, то в случае двойной сработки create_password_key (я не увидел блокировки на двойной вызов этого метода чисто из-за сбоя сетевого стека, к примеру), то мы получим два ключа key_type:PASSWORD ? решается тем, что паролю надо key_id прописывать статичный, кмк.
Review

Тогда на основе чего его генерить?
У нас там сейчас f"{datetime.now().strftime('%Y-%m-%d')}-{random_number}", где random_number четырехзначное число, брать id пользователя?

Тогда на основе чего его генерить? У нас там сейчас `f"{datetime.now().strftime('%Y-%m-%d')}-{random_number}"`, где `random_number` четырехзначное число, брать id пользователя?
Review

А я не вижу необходимости генерить его конкретно для PASSWORD, просто прописать статичный литерал 'password' к примеру и сделать фоллбэк в случае, если при создании key_type: PASSWORD на конкретном пользователе уже существует такой ключ, я бы сделал его либо на обновление, либо просто пропуск.

А я не вижу необходимости генерить его конкретно для PASSWORD, просто прописать статичный литерал 'password' к примеру и сделать фоллбэк в случае, если при создании key_type: PASSWORD на конкретном пользователе уже существует такой ключ, я бы сделал его либо на обновление, либо просто пропуск.
key_value=hasher.hash_data(password),
created_at=datetime.now(timezone.utc),
expiry=datetime.now(timezone.utc) + timedelta(days=365),
status=KeyStatus.ACTIVE.value,
)
await connection.execute(stmt)
await connection.commit()

View File

@ -17,7 +17,7 @@ from api.db.logic.account import (
from api.db.tables.account import AccountStatus
from api.schemas.account.account import User
from api.schemas.base import bearer_schema
from api.schemas.endpoints.account import AllUserResponse, UserUpdate
from api.schemas.endpoints.account import AllUser, AllUserResponse, UserCreate, UserUpdate
from api.services.auth import get_current_user
from api.services.update_data_validation import update_user_data_changes
from api.services.user_role_validation import db_user_role_validation
@ -61,9 +61,9 @@ async def get_account(
return user
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=AllUser)
async def create_account(
user: UserUpdate,
user: UserCreate,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):

View File

@ -1,9 +1,9 @@
from typing import Optional, List
from datetime import datetime
from typing import List, Optional
from pydantic import EmailStr, Field, TypeAdapter
from api.db.tables.account import AccountRole, AccountStatus
from api.schemas.base import Base
@ -20,6 +20,17 @@ class UserUpdate(Base):
status: Optional[AccountStatus] = None
class UserCreate(Base):
name: Optional[str] = Field(None, max_length=100)
Review

Валидация идёт на уровне этой модели или на другом дескрипторе?

Просто тут name, login, role, status не должны быть Optional.

Валидация идёт на уровне этой модели или на другом дескрипторе? Просто тут name, login, role, status не должны быть Optional.
Review

На уровне этой, поправлю на обязательные

На уровне этой, поправлю на обязательные
login: Optional[str] = Field(None, max_length=100)
email: Optional[EmailStr] = None
password: Optional[str] = None
bind_tenant_id: Optional[str] = Field(None, max_length=40)
role: Optional[AccountRole] = None
meta: Optional[dict] = None
status: Optional[AccountStatus] = None
class AllUser(Base):
id: int
name: str

View File

@ -1,27 +1,25 @@
from fastapi import Request, HTTPException
from typing import Optional
from fastapi import HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.logic.auth import get_user
# # from backend.schemas.users.token import TokenData
from api.schemas.account.account import User
from api.db.tables.account import AccountStatus
from api.utils.hasher import Hasher
from api.schemas.endpoints.account import AllUser
from api.utils.hasher import hasher
async def get_current_user(request: Request) -> Optional[User]:
async def get_current_user(request: Request) -> str | HTTPException:
if not hasattr(request.state, "current_user"):
return HTTPException(status_code=401, detail="Unauthorized")
return request.state.current_user
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[User]:
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[AllUser]:
sql_user, sql_password = await get_user(connection, username)
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
return None
hasher = Hasher()
if not hasher.verify_data(password, sql_password.key_value):
return None
return sql_user

View File

@ -1,4 +1,6 @@
import hashlib
import secrets
# Хешер для работы с паролем.
@ -14,3 +16,10 @@ class Hasher:
def verify_data(self, password: str, hashed: str) -> bool:
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
return self.hash_data(password) == hashed
@staticmethod
def generate_password() -> str:
return secrets.token_urlsafe(20)
hasher = Hasher()

View File

@ -1,32 +1,23 @@
import os
import asyncio
import hashlib
import secrets
import os
from api.db.connection.session import get_connection
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
from api.db.tables.account import account_keyring_table, account_table, AccountRole, KeyStatus, KeyType
from api.utils.hasher import hasher
from api.utils.key_id_gen import KeyIdGenerator
INIT_LOCK_FILE = "../init.lock"
DEFAULT_LOGIN = "vorkout"
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
def generate_password() -> str:
return secrets.token_urlsafe(20)
async def init():
if os.path.exists(INIT_LOCK_FILE):
print("Sorry, service is already initialized")
return
async with get_connection() as conn:
password = generate_password()
hashed_password = hash_password(password)
password = hasher.generate_password()
hashed_password = hasher.hash_data(password)
create_user_query = account_table.insert().values(
name=DEFAULT_LOGIN,

View File

@ -1,10 +1,9 @@
import axios from 'axios';
// import { Auth, Tokens } from '../types/auth';
import axiosRetry from 'axios-retry';
import { Auth, Tokens } from '@/types/auth';
import { useAuthStore } from '@/store/authStore';
import { AuthService } from '@/services/authService';
import { User, UserUpdate } from '@/types/user';
import { User, UserCreate } from '@/types/user';
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
import.meta.env.VITE_APP_API_URL
@ -115,10 +114,13 @@ const api = {
return response.data;
},
async createUser(user: UserUpdate): Promise<User> {
async createUser(user: UserCreate): Promise<User> {
const response = await base.post<User>('/account', user);
return response.data;
},
// keyrings
async setPassword(userId: number, password: string): Promise<any> {},
};
export default api;

View File

@ -14,7 +14,7 @@ import {
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserSelector } from '@/store/userStore';
import { UserUpdate } from '@/types/user';
import { UserCreate as NewUserCreate } from '@/types/user';
import { UserService } from '@/services/userService';
import { LoadingOutlined } from '@ant-design/icons';
@ -55,9 +55,10 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) {
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
setFileList(newFileList);
const onFinish = async (values: UserUpdate) => {
const onFinish = async (values: NewUserCreate) => {
setLoading(true);
await UserService.createUser(values);
closeDrawer();
setLoading(false);
message.info(t('createdAccountMessage'), 4);

View File

@ -1,5 +1,5 @@
import api from '@/api/api';
import { AllUserResponse, User, UserUpdate } from '@/types/user';
import { AllUserResponse, User, UserCreate } from '@/types/user';
export class UserService {
static async getProfile(): Promise<User> {
@ -24,7 +24,7 @@ export class UserService {
return user;
}
static async createUser(user: UserUpdate): Promise<User> {
static async createUser(user: UserCreate): Promise<User> {
console.log('createUser');
const createdUser = api.createUser(user);
return createdUser;