VORKOUT-8 #13
@ -6,9 +6,10 @@ from typing import Optional
|
|||||||
from sqlalchemy import func, insert, select
|
from sqlalchemy import func, insert, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
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.db.tables.account import account_table
|
||||||
from api.schemas.account.account import User
|
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]:
|
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.
|
Получает юзера по id.
|
||||||
"""
|
"""
|
||||||
@ -66,7 +67,7 @@ async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[
|
|||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return UserUpdate.model_validate(user)
|
return AllUser.model_validate(user)
|
||||||
|
|
||||||
|
|
||||||
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[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()
|
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.
|
Создает нове поле в таблице account_table.
|
||||||
"""
|
"""
|
||||||
@ -112,7 +113,7 @@ async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id:
|
|||||||
email=user.email,
|
email=user.email,
|
||||||
bind_tenant_id=user.bind_tenant_id,
|
bind_tenant_id=user.bind_tenant_id,
|
||||||
role=user.role.value,
|
role=user.role.value,
|
||||||
meta=user.meta,
|
meta=user.meta or {},
|
||||||
creator_id=creator_id,
|
creator_id=creator_id,
|
||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
status=user.status.value,
|
status=user.status.value,
|
||||||
@ -121,6 +122,7 @@ async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id:
|
|||||||
res = await connection.execute(query)
|
res = await connection.execute(query)
|
||||||
|
|
||||||
await connection.commit()
|
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
|
||||||
|
@ -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 import User
|
||||||
from api.schemas.account.account_keyring import AccountKeyring
|
from api.schemas.account.account_keyring import AccountKeyring
|
||||||
|
from api.schemas.endpoints.account import AllUser
|
||||||
|
|
||||||
from api.utils.key_id_gen import KeyIdGenerator
|
from api.utils.key_id_gen import KeyIdGenerator
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
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 = (
|
query = (
|
||||||
select(account_table, account_keyring_table)
|
select(account_table, account_keyring_table)
|
||||||
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
|
.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
|
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)
|
password = AccountKeyring.model_validate(password_data)
|
||||||
return user, password
|
return user, password
|
||||||
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
from typing import Optional
|
from datetime import datetime, timedelta, timezone
|
||||||
from datetime import datetime, timezone
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import insert, select
|
from sqlalchemy import insert, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
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.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]:
|
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()
|
await connection.commit()
|
||||||
|
|
||||||
return key
|
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()
|
||||||
|
@ -17,7 +17,7 @@ from api.db.logic.account import (
|
|||||||
from api.db.tables.account import AccountStatus
|
from api.db.tables.account import AccountStatus
|
||||||
from api.schemas.account.account import User
|
from api.schemas.account.account import User
|
||||||
from api.schemas.base import bearer_schema
|
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.auth import get_current_user
|
||||||
from api.services.update_data_validation import update_user_data_changes
|
from api.services.update_data_validation import update_user_data_changes
|
||||||
from api.services.user_role_validation import db_user_role_validation
|
from api.services.user_role_validation import db_user_role_validation
|
||||||
@ -61,9 +61,9 @@ async def get_account(
|
|||||||
return user
|
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(
|
async def create_account(
|
||||||
user: UserUpdate,
|
user: UserCreate,
|
||||||
connection: AsyncConnection = Depends(get_connection_dep),
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import EmailStr, Field, TypeAdapter
|
from pydantic import EmailStr, Field, TypeAdapter
|
||||||
|
|
||||||
from api.db.tables.account import AccountRole, AccountStatus
|
from api.db.tables.account import AccountRole, AccountStatus
|
||||||
|
|
||||||
from api.schemas.base import Base
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +20,17 @@ class UserUpdate(Base):
|
|||||||
status: Optional[AccountStatus] = None
|
status: Optional[AccountStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(Base):
|
||||||
|
name: Optional[str] = Field(None, max_length=100)
|
||||||
cyrussmeat
commented
Валидация идёт на уровне этой модели или на другом дескрипторе? Просто тут name, login, role, status не должны быть Optional. Валидация идёт на уровне этой модели или на другом дескрипторе?
Просто тут name, login, role, status не должны быть Optional.
vlad.dev
commented
На уровне этой, поправлю на обязательные На уровне этой, поправлю на обязательные
|
|||||||
|
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):
|
class AllUser(Base):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
from fastapi import Request, HTTPException
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
from api.db.logic.auth import get_user
|
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.db.tables.account import AccountStatus
|
||||||
|
from api.schemas.endpoints.account import AllUser
|
||||||
from api.utils.hasher import Hasher
|
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"):
|
if not hasattr(request.state, "current_user"):
|
||||||
return HTTPException(status_code=401, detail="Unauthorized")
|
return HTTPException(status_code=401, detail="Unauthorized")
|
||||||
return request.state.current_user
|
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)
|
sql_user, sql_password = await get_user(connection, username)
|
||||||
|
|
||||||
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
||||||
return None
|
return None
|
||||||
hasher = Hasher()
|
|
||||||
if not hasher.verify_data(password, sql_password.key_value):
|
if not hasher.verify_data(password, sql_password.key_value):
|
||||||
return None
|
return None
|
||||||
return sql_user
|
return sql_user
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
# Хешер для работы с паролем.
|
# Хешер для работы с паролем.
|
||||||
|
|
||||||
@ -14,3 +16,10 @@ class Hasher:
|
|||||||
def verify_data(self, password: str, hashed: str) -> bool:
|
def verify_data(self, password: str, hashed: str) -> bool:
|
||||||
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
||||||
return self.hash_data(password) == hashed
|
return self.hash_data(password) == hashed
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_password() -> str:
|
||||||
|
return secrets.token_urlsafe(20)
|
||||||
|
|
||||||
|
|
||||||
|
hasher = Hasher()
|
||||||
|
@ -1,32 +1,23 @@
|
|||||||
import os
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import os
|
||||||
import secrets
|
|
||||||
|
|
||||||
from api.db.connection.session import get_connection
|
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
|
from api.utils.key_id_gen import KeyIdGenerator
|
||||||
|
|
||||||
INIT_LOCK_FILE = "../init.lock"
|
INIT_LOCK_FILE = "../init.lock"
|
||||||
DEFAULT_LOGIN = "vorkout"
|
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():
|
async def init():
|
||||||
if os.path.exists(INIT_LOCK_FILE):
|
if os.path.exists(INIT_LOCK_FILE):
|
||||||
print("Sorry, service is already initialized")
|
print("Sorry, service is already initialized")
|
||||||
return
|
return
|
||||||
|
|
||||||
async with get_connection() as conn:
|
async with get_connection() as conn:
|
||||||
password = generate_password()
|
password = hasher.generate_password()
|
||||||
hashed_password = hash_password(password)
|
hashed_password = hasher.hash_data(password)
|
||||||
|
|
||||||
create_user_query = account_table.insert().values(
|
create_user_query = account_table.insert().values(
|
||||||
name=DEFAULT_LOGIN,
|
name=DEFAULT_LOGIN,
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
// import { Auth, Tokens } from '../types/auth';
|
|
||||||
import axiosRetry from 'axios-retry';
|
import axiosRetry from 'axios-retry';
|
||||||
import { Auth, Tokens } from '@/types/auth';
|
import { Auth, Tokens } from '@/types/auth';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { AuthService } from '@/services/authService';
|
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}://${
|
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
|
||||||
import.meta.env.VITE_APP_API_URL
|
import.meta.env.VITE_APP_API_URL
|
||||||
@ -115,10 +114,13 @@ const api = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createUser(user: UserUpdate): Promise<User> {
|
async createUser(user: UserCreate): Promise<User> {
|
||||||
const response = await base.post<User>('/account', user);
|
const response = await base.post<User>('/account', user);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// keyrings
|
||||||
|
async setPassword(userId: number, password: string): Promise<any> {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useUserSelector } from '@/store/userStore';
|
import { useUserSelector } from '@/store/userStore';
|
||||||
import { UserUpdate } from '@/types/user';
|
import { UserCreate as NewUserCreate } from '@/types/user';
|
||||||
import { UserService } from '@/services/userService';
|
import { UserService } from '@/services/userService';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
@ -55,9 +55,10 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) {
|
|||||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
|
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
|
||||||
setFileList(newFileList);
|
setFileList(newFileList);
|
||||||
|
|
||||||
const onFinish = async (values: UserUpdate) => {
|
const onFinish = async (values: NewUserCreate) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await UserService.createUser(values);
|
await UserService.createUser(values);
|
||||||
|
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
message.info(t('createdAccountMessage'), 4);
|
message.info(t('createdAccountMessage'), 4);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import api from '@/api/api';
|
import api from '@/api/api';
|
||||||
import { AllUserResponse, User, UserUpdate } from '@/types/user';
|
import { AllUserResponse, User, UserCreate } from '@/types/user';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
static async getProfile(): Promise<User> {
|
static async getProfile(): Promise<User> {
|
||||||
@ -24,7 +24,7 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createUser(user: UserUpdate): Promise<User> {
|
static async createUser(user: UserCreate): Promise<User> {
|
||||||
console.log('createUser');
|
console.log('createUser');
|
||||||
const createdUser = api.createUser(user);
|
const createdUser = api.createUser(user);
|
||||||
return createdUser;
|
return createdUser;
|
||||||
|
Loading…
Reference in New Issue
Block a user
Может тут два пароля с разными id создаться, получается?
Нет, не должны
Есть какие-то предпосылки? Я просто не вижу такой возможности
Ну, просто в моменте у нас может быть несколько ключей одного типа (API KEY как минимум), но с разными key_id, и по идее уникальность тут обеспечивается в том числе key_id, т.е. если key_type:PASSWORD присваивать разные key_id, то в случае двойной сработки create_password_key (я не увидел блокировки на двойной вызов этого метода чисто из-за сбоя сетевого стека, к примеру), то мы получим два ключа key_type:PASSWORD ?
решается тем, что паролю надо key_id прописывать статичный, кмк.
Тогда на основе чего его генерить?
У нас там сейчас
f"{datetime.now().strftime('%Y-%m-%d')}-{random_number}"
, гдеrandom_number
четырехзначное число, брать id пользователя?А я не вижу необходимости генерить его конкретно для PASSWORD, просто прописать статичный литерал 'password' к примеру и сделать фоллбэк в случае, если при создании key_type: PASSWORD на конкретном пользователе уже существует такой ключ, я бы сделал его либо на обновление, либо просто пропуск.