Files
replypier/backend/app/models/schemas/user.py
T
WangShen007 20fef2b9f3 chore: settle product naming across app, widget and deployments
One word everywhere: ReplyPier in UI copy, package and image names, the
replypier.site domains (app/api/docs/help), REPLYPIER_* settings and the
--rp-* design tokens.

- public widget API is window.ReplyPier with replypierId / replypierBaseUrl /
  replypierConfig / replypierToken, loader at webclient/replypier.min.js;
  both bundles rebuilt from source
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and the demo runtime
  config rewritten; the demo config ships no third-party keys
- migration adds the ReplyPier managed model value to the AI model enum
- local compose project, database, role, volume and Docker network carry the
  same name
2026-10-26 00:57:22 +08:00

146 lines
4.3 KiB
Python

"""
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