계정 생성 API 가 아예 없었다(그동안 users 를 손으로 INSERT 했다). 로그인 화면은 있는데 그 뒤에 설 계정을 만들 방법이 제품에 없는 상태였다. - auth_service.signup: 가입 = **새 회사(테넌트) 1개 + 첫 계정 1개**. users.company_id 가 NOT NULL 이고 모든 도메인이 company 로 스코프돼서, 회사 없는 계정은 아무것도 못 만든다 - services/external/google_identity: 구글 ID 토큰의 서명·iss·만료에 더해 **aud(우리 client_id)와 email_verified 를 본다.** aud 검사가 빠지면 남의 앱에 발급된 '진짜' 구글 토큰으로 우리 계정에 들어온다 — 서명도 발급자도 전부 맞으므로 다른 검사로는 안 걸린다 - users.provider/provider_uid 추가, password NULL 허용, id 20→64자(google_<sub> 가 20자를 넘는다). provider 에 server_default 를 같이 준 이유: ORM default 는 raw INSERT(테스트 시드)에 안 먹어서 NOT NULL 컬럼이면 그 경로가 통째로 깨진다 - attempt_login: 소셜 계정을 먼저 끊는다. 안 끊으면 bcrypt 가 None 해시를 만나 500 이다 - 같은 이메일이라도 id/pw 계정과 구글 계정을 **잇지 않는다.** 이으면 계정 선점이다 — 남의 이메일로 먼저 만들어 둔 계정에 그 사람의 구글 로그인이 들어간다 → DECISIONS 1-5 - LoginPage 는 admin 과 공유라 selfServe 로 갈랐다. admin 은 가입 링크도 구글 버튼도 안 뜬다 (admin 라우터에 /signup 이 없어 404 가 난다) - GOOGLE_CLIENT_ID 는 루트 .env 한 곳. compose 가 VITE_GOOGLE_CLIENT_ID 로 흘려보낸다 — 두 곳에 적으면 백엔드 aud 대조와 화면 버튼이 조용히 갈라진다 ★ 이미 도는 DB 는 init.sql 을 다시 적용해야 한다(말미 ALTER 섹션). pytest: auth 13건 + 구글 토큰 검증 8건(진짜 RSA 서명으로 aud·iss·만료·email_verified·변조 거절 확인) 통과. 전체 527 passed / 8 failed(전부 기존 실패, 인증과 무관). tsc·eslint·vite build 통과.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
from typing import Optional
|
|
|
|
from pydantic import Field
|
|
|
|
from common.enums import AuthProvider, UserRole
|
|
from common.models.gmodel import Res_WebPacketProtocol, WebPacketProtocol
|
|
|
|
|
|
# 라우터 폴더마다 protocol.py 를 두고 Req_/Res_ 를 정의한다 (protocol 규약).
|
|
class AuthProtocol(WebPacketProtocol):
|
|
pass
|
|
|
|
|
|
class Req_Login(AuthProtocol):
|
|
id: str = ""
|
|
password: str = ""
|
|
|
|
|
|
class Req_Signup(AuthProtocol):
|
|
"""id/pw 가입. 가입 = 새 테넌트(회사) 1개 + 그 회사의 첫 계정 1개다.
|
|
|
|
★ 이메일을 필수로 받는 이유: 같은 이메일이 이미 구글로 가입돼 있는지 판단할 근거가 없으면
|
|
한 사람에게 계정이 둘 생긴다. 지금 이메일 인증 절차는 없다 — 소유 증명이 아니라
|
|
**중복 판정용** 이다."""
|
|
|
|
id: str = ""
|
|
password: str = ""
|
|
name: Optional[str] = None
|
|
email: str = ""
|
|
company_name: Optional[str] = None # 상호. 비우면 이름 → 아이디 순으로 채운다
|
|
|
|
|
|
class Req_GoogleLogin(AuthProtocol):
|
|
"""구글 로그인. 프론트(GIS)가 받은 ID 토큰을 그대로 넘긴다.
|
|
|
|
필드 이름이 `credential` 인 이유는 GIS 콜백이 주는 이름 그대로이기 때문이다 —
|
|
`access_token`/`id_token` 으로 바꿔 부르면 우리 토큰과 헷갈린다."""
|
|
|
|
credential: str = ""
|
|
|
|
|
|
class Res_Login(Res_WebPacketProtocol):
|
|
access_token: str = ""
|
|
refresh_token: str = ""
|
|
token_type: str = "bearer"
|
|
|
|
|
|
class Req_UpdateMe(AuthProtocol):
|
|
# 본인 정보 수정. role·company·id 는 받지 않는다(자기 권한·소속 변경 불가).
|
|
name: Optional[str] = None
|
|
email: Optional[str] = None
|
|
contact_number: Optional[str] = None
|
|
password: Optional[str] = None # 비밀번호 변경(옵션). 비우면 유지
|
|
|
|
|
|
class Res_RefreshToken(Res_WebPacketProtocol):
|
|
access_token: str = ""
|
|
token_type: str = "bearer"
|
|
|
|
|
|
class CompanyData(WebPacketProtocol):
|
|
company_id: str = ""
|
|
name: str = ""
|
|
|
|
|
|
class Res_Me(Res_WebPacketProtocol):
|
|
user_id: str = ""
|
|
id: str = ""
|
|
name: Optional[str] = None
|
|
email: Optional[str] = None
|
|
contact_number: Optional[str] = None
|
|
role: UserRole = UserRole.USER
|
|
# 이 계정이 무엇으로 로그인하는가. 구글 계정에는 바꿀 비밀번호가 없어서(update_me 가 막는다)
|
|
# 내 정보 화면이 붙을 때 이 값으로 갈라야 한다.
|
|
provider: AuthProvider = AuthProvider.LOCAL
|
|
company: Optional[CompanyData] = Field(default=None)
|