diff --git a/api/api/db/logic/account.py b/api/api/db/logic/account.py index e6fe163..78be803 100644 --- a/api/api/db/logic/account.py +++ b/api/api/db/logic/account.py @@ -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 diff --git a/api/api/db/logic/auth.py b/api/api/db/logic/auth.py index 20e9573..bb02986 100644 --- a/api/api/db/logic/auth.py +++ b/api/api/db/logic/auth.py @@ -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 diff --git a/api/api/db/logic/keyring.py b/api/api/db/logic/keyring.py index 91469a6..4856d0a 100644 --- a/api/api/db/logic/keyring.py +++ b/api/api/db/logic/keyring.py @@ -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(), + 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() diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index 6bc6dd4..f16d750 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -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), ): diff --git a/api/api/schemas/endpoints/account.py b/api/api/schemas/endpoints/account.py index 4817122..52afb63 100644 --- a/api/api/schemas/endpoints/account.py +++ b/api/api/schemas/endpoints/account.py @@ -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) + 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 diff --git a/api/api/services/auth.py b/api/api/services/auth.py index 761949a..d4b0161 100644 --- a/api/api/services/auth.py +++ b/api/api/services/auth.py @@ -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 diff --git a/api/api/utils/hasher.py b/api/api/utils/hasher.py index 61d4c0e..3224245 100644 --- a/api/api/utils/hasher.py +++ b/api/api/utils/hasher.py @@ -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() diff --git a/api/api/utils/init.py b/api/api/utils/init.py index 06161a9..bc93be1 100644 --- a/api/api/utils/init.py +++ b/api/api/utils/init.py @@ -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, diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 1fef22c..aa4eb6d 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -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 { + async createUser(user: UserCreate): Promise { const response = await base.post('/account', user); return response.data; }, + + // keyrings + async setPassword(userId: number, password: string): Promise {}, }; export default api; diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index a1cd8f8..d7f4097 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -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); diff --git a/client/src/services/userService.ts b/client/src/services/userService.ts index 725e8cd..c8a3172 100644 --- a/client/src/services/userService.ts +++ b/client/src/services/userService.ts @@ -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 { @@ -24,7 +24,7 @@ export class UserService { return user; } - static async createUser(user: UserUpdate): Promise { + static async createUser(user: UserCreate): Promise { console.log('createUser'); const createdUser = api.createUser(user); return createdUser;