feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
977783c126
commit
9eb8613c05
126 files changed
+17436
No files matched your search
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, EmailStr, field_serializer, field_validator
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.s3 import sign_s3_url
|
||||
from app.core.security import validate_password_strength
|
||||
|
||||
from app.models.schemas.role import RoleResponse
|
||||
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: EmailStr
|
||||
full_name: str
|
||||
is_active: bool = True
|
||||
profile_pic: Optional[str] = None
|
||||
is_online: bool = False
|
||||
last_seen: Optional[datetime] = None
|
||||
|
||||
|
||||
class ChatScopeFields(BaseModel):
|
||||
"""The two chat-scope toggles on the user form.
|
||||
|
||||
Both default to None, meaning "whatever the chosen role already grants".
|
||||
A client that predates these fields keeps its old behaviour; the form
|
||||
sends explicit values because it renders the role's current scope.
|
||||
"""
|
||||
see_all_ai_chats: Optional[bool] = None
|
||||
see_all_org_chats: Optional[bool] = None
|
||||
|
||||
|
||||
class UserCreate(UserBase, ChatScopeFields):
|
||||
password: str
|
||||
role_id: int
|
||||
|
||||
@field_validator('password')
|
||||
@classmethod
|
||||
def _check_strength(cls, value: str) -> str:
|
||||
# The password an admin picks for a new teammate clears the same bar as
|
||||
# one set by a reset — otherwise the policy is one invite away from
|
||||
# being bypassed.
|
||||
return validate_password_strength(value)
|
||||
|
||||
|
||||
class UserUpdate(ChatScopeFields):
|
||||
email: Optional[EmailStr] = None
|
||||
full_name: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
current_password: Optional[str] = None
|
||||
role_id: Optional[int] = None
|
||||
profile_pic: Optional[str] = None
|
||||
is_online: Optional[bool] = None
|
||||
|
||||
class TeammateResponse(BaseModel):
|
||||
"""A colleague as the inbox needs them: enough to pick one and render them.
|
||||
|
||||
No role, no permissions, no timestamps — an agent listing who they can hand
|
||||
a chat to has no business reading the org's permission matrix, which is
|
||||
what UserResponse would give them.
|
||||
"""
|
||||
id: UUID
|
||||
full_name: Optional[str] = None
|
||||
email: EmailStr
|
||||
profile_pic: Optional[str] = None
|
||||
is_online: Optional[bool] = False
|
||||
|
||||
@field_serializer('profile_pic')
|
||||
def _sign_profile_pic(self, v: Optional[str]) -> Optional[str]:
|
||||
return sign_s3_url(v) if v else v
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AdminPasswordReset(BaseModel):
|
||||
"""An admin setting a new password for someone else in their organization.
|
||||
|
||||
No current_password: the admin does not know it — that is the point of a
|
||||
reset. The policy check lives here because nobody is around to see the
|
||||
form's strength meter when the call arrives from a script.
|
||||
"""
|
||||
new_password: str
|
||||
|
||||
@field_validator('new_password')
|
||||
@classmethod
|
||||
def _check_strength(cls, value: str) -> str:
|
||||
return validate_password_strength(value)
|
||||
|
||||
|
||||
class UserStatusUpdate(BaseModel):
|
||||
is_online: bool
|
||||
|
||||
class UserGroupResponse(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class UserResponse(UserBase):
|
||||
id: UUID
|
||||
organization_id: Optional[UUID] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
is_online: Optional[bool] = None
|
||||
last_seen: Optional[datetime] = None
|
||||
profile_pic: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
groups: Optional[List[UserGroupResponse]] = None
|
||||
role: Optional[RoleResponse] = None
|
||||
|
||||
@field_serializer('profile_pic')
|
||||
def _sign_profile_pic(self, v: Optional[str]) -> Optional[str]:
|
||||
"""Sign on the way out, every response — never stored signed."""
|
||||
return sign_s3_url(v) if v else v
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
user: UserResponse
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in new issue
Block a user