- UserRole 1=일반 2=최고관리자 3=개발자(내부 운영). 개발자 계정은 회원 목록·총계에서 제외해 고객사에 노출하지 않음 - 계정 생성 시 권한 선택(일반/최고관리자) 추가, 개발자는 앱에서 부여 불가(DB 시드 전용) - 사이드바 '개발자' 그룹 신설 — 회사 설정·디자인 시스템을 개발자에게만 노출 - /dev/design 디자인 시스템 페이지: 색 토큰·타이포·버튼·배지·입력·반경, 목록 화면 구조, 반응형 기준, URL 상태 규칙, 회사 커스터마이징 훅 - 공급사 재협상 요청(#15) 플로우 문서 — sessions.custom 기반(DDL 0)
159 lines
6.4 KiB
Python
159 lines
6.4 KiB
Python
import uuid
|
|
|
|
from fastapi import Depends
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import users
|
|
from common.enums import DBWRType, ErrorType, UserRole, UserStatus
|
|
from common.utils.gtime import GTime
|
|
from crud.user_crud import IUserCRUD, UserCRUD
|
|
from router.v1.company.protocol import (
|
|
CompanyUserData,
|
|
Req_CreateCompanyUser,
|
|
Req_UpdateCompanyUser,
|
|
Res_CompanyUser,
|
|
Res_CompanyUserList,
|
|
Res_DeleteCompanyUser,
|
|
)
|
|
from router.v1.validator.dependencies import GetHashedPW
|
|
|
|
|
|
class CompanyUserService:
|
|
"""최고관리자(OWNER)의 자기 회사 유저 관리 로직.
|
|
|
|
- 라우터에서 RequireOwner 로 1차 권한을 거른 뒤 호출된다.
|
|
- company_id 는 토큰값만 쓴다(요청 body 무시) → 남의 회사 데이터 불가.
|
|
- 변경 대상이 OWNER 면 거부한다(최고관리자는 앱에서 수정·삭제 불가).
|
|
"""
|
|
|
|
def __init__(self, user_crud: IUserCRUD = Depends(UserCRUD)):
|
|
self.user_crud = user_crud
|
|
|
|
async def _fetch_managed(self, company_uuid: uuid.UUID, user_id: uuid.UUID):
|
|
"""대상 유저 조회 + 같은 회사 + 비-OWNER 확인. (ErrorType, user|None) 반환."""
|
|
err_type, user = await DB_SESSION_MNG.execute_lambda(
|
|
users.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.user_crud.get_by_user_id(s, user_id),
|
|
)
|
|
if err_type != ErrorType.SUCCESS or user is None:
|
|
return ErrorType.ACCOUNT_NOT_FOUND, None
|
|
if user.company_id != company_uuid:
|
|
return ErrorType.ACCOUNT_NOT_FOUND, None
|
|
if user.role == UserRole.OWNER.value:
|
|
return ErrorType.ACCOUNT_FORBIDDEN, None
|
|
return ErrorType.SUCCESS, user
|
|
|
|
async def list_users(self, company_id: str, search, pg, hide_dev: bool = False) -> Res_CompanyUserList:
|
|
res = Res_CompanyUserList(page=pg.page, size=pg.size)
|
|
company_uuid = uuid.UUID(company_id)
|
|
|
|
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
|
|
users.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.user_crud.list_by_company(s, company_uuid, search, pg.skip, pg.size, hide_dev),
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
res.users = [CompanyUserData.model_validate(r) for r in rows]
|
|
res.total = total
|
|
return res
|
|
|
|
async def get_user(self, company_id: str, user_id: str) -> Res_CompanyUser:
|
|
res = Res_CompanyUser()
|
|
err_type, user = await self._fetch_managed(uuid.UUID(company_id), uuid.UUID(user_id))
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
res.user = CompanyUserData.model_validate(user)
|
|
return res
|
|
|
|
async def create_user(self, company_id: str, req: Req_CreateCompanyUser) -> Res_CompanyUser:
|
|
res = Res_CompanyUser()
|
|
company_uuid = uuid.UUID(company_id)
|
|
|
|
# 1) 로그인 ID 중복 확인 (id 는 전역 unique)
|
|
err_type = await DB_SESSION_MNG.execute_lambda(
|
|
users.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.user_crud.is_user(s, req.id),
|
|
)
|
|
if err_type == ErrorType.DB_ALREADY_SAME_KEY:
|
|
res.result.SetResult(ErrorType.ACCOUNT_ALREADY_EXIST)
|
|
return res
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
# 2) 생성 — 회사는 토큰값. 권한은 일반·관리자만 허용(최고관리자 승격은 앱에서 불가).
|
|
user = users(
|
|
company_id=company_uuid,
|
|
id=req.id,
|
|
password=await GetHashedPW(req.password),
|
|
name=req.name or None,
|
|
email=req.email or None,
|
|
contact_number=req.contact_number or None,
|
|
role=req.role if req.role in (UserRole.USER.value, UserRole.OWNER.value) else UserRole.USER.value,
|
|
)
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[users.DBType()],
|
|
[lambda s: self.user_crud.add_user(s, user)],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
if err_type == ErrorType.DB_ALREADY_SAME_KEY:
|
|
res.result.SetResult(ErrorType.ACCOUNT_ALREADY_EXIST)
|
|
else:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
# 서버 기본값(created_at 등)은 insert 후 객체에 안 실리므로 재조회.
|
|
return await self.get_user(company_id, str(user.user_id))
|
|
|
|
async def update_user(self, company_id: str, user_id: str, req: Req_UpdateCompanyUser) -> Res_CompanyUser:
|
|
res = Res_CompanyUser()
|
|
company_uuid = uuid.UUID(company_id)
|
|
user_uuid = uuid.UUID(user_id)
|
|
|
|
err_type, _ = await self._fetch_managed(company_uuid, user_uuid)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
data = req.model_dump(exclude_unset=True)
|
|
if data.get("status") is not None:
|
|
s = data["status"] # pydantic 은 enum 멤버로 돌려준다 → SMALLINT 값으로 환원
|
|
data["status"] = s.value if isinstance(s, UserStatus) else int(s)
|
|
if data.get("password"):
|
|
data["password"] = await GetHashedPW(data["password"])
|
|
else:
|
|
data.pop("password", None) # 빈 비밀번호는 변경하지 않음
|
|
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[users.DBType()],
|
|
[lambda s: self.user_crud.update_user(s, user_uuid, data)],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
return await self.get_user(company_id, user_id)
|
|
|
|
async def delete_user(self, company_id: str, user_id: str) -> Res_DeleteCompanyUser:
|
|
res = Res_DeleteCompanyUser()
|
|
company_uuid = uuid.UUID(company_id)
|
|
user_uuid = uuid.UUID(user_id)
|
|
|
|
err_type, _ = await self._fetch_managed(company_uuid, user_uuid)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[users.DBType()],
|
|
[lambda s: self.user_crud.update_user(s, user_uuid, {"deleted": True, "updated_at": GTime.UTC()})],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|