Skip to content

Commit 242ddd7

Browse files
authored
♻️ auth 패키지 구현체 및 DI 도입, email 패키지 분리 (#42)
* ♻️ auth 패키지 구현체 및 DI 도입 * 🔥 불필요한 import 제거 * ♻️ email 로직을 별도 패키지로 분리 및 DI 적용 * ♻️ email sender가 아니라 각각의 패키지 service에서 이메일 전송 내용을 작성하도록 변경
1 parent bb3959e commit 242ddd7

20 files changed

Lines changed: 421 additions & 282 deletions

app/api/v1/router.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
from fastapi import APIRouter
22

33
from app.api.v1.endpoints.application import router as application_router
4-
from app.api.v1.endpoints.auth import router as auth_router
54
from app.api.v1.endpoints.deposit import router as deposit_router
65
from app.api.v1.endpoints.event import router as event_router
76
from app.api.v1.endpoints.s3 import router as s3_router
87
from app.api.v1.endpoints.store import router as store_router
98

109
router = APIRouter()
11-
router.include_router(auth_router)
1210
router.include_router(store_router)
1311
router.include_router(event_router)
1412
router.include_router(application_router)

app/auth/__init__.py

Whitespace-only changes.

app/auth/dependencies.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import Depends
2+
from sqlalchemy.ext.asyncio import AsyncSession
3+
4+
from app.auth.repository import EmailVerificationRepository, UserRepository
5+
from app.auth.repository_impl import EmailVerificationRepositoryImpl, UserRepositoryImpl
6+
from app.auth.service import AuthService
7+
from app.auth.service_impl import AuthServiceImpl
8+
from app.db.session import get_db
9+
from app.email.service import EmailSender
10+
from app.email.service_impl import SmtpEmailSender
11+
12+
13+
def get_user_repository(db: AsyncSession = Depends(get_db)) -> UserRepository:
14+
return UserRepositoryImpl(db)
15+
16+
17+
def get_email_verification_repository(
18+
db: AsyncSession = Depends(get_db),
19+
) -> EmailVerificationRepository:
20+
return EmailVerificationRepositoryImpl(db)
21+
22+
23+
def get_email_sender() -> EmailSender:
24+
return SmtpEmailSender()
25+
26+
27+
def get_auth_service(
28+
user_repo: UserRepository = Depends(get_user_repository),
29+
ev_repo: EmailVerificationRepository = Depends(get_email_verification_repository),
30+
email_sender: EmailSender = Depends(get_email_sender),
31+
) -> AuthService:
32+
return AuthServiceImpl(user_repo, ev_repo, email_sender)

app/auth/models.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from datetime import datetime
2+
from typing import TYPE_CHECKING
3+
from uuid import UUID, uuid4
4+
5+
from sqlalchemy import Boolean, DateTime, Enum, String
6+
from sqlalchemy.orm import Mapped, mapped_column, relationship
7+
from sqlalchemy.sql import func
8+
9+
from app.db.base import Base
10+
11+
if TYPE_CHECKING:
12+
from app.models.deposit import Deposit
13+
14+
15+
class User(Base):
16+
__tablename__ = "users"
17+
18+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
19+
username: Mapped[str] = mapped_column(String(50))
20+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
21+
password_hash: Mapped[str] = mapped_column(String)
22+
role: Mapped[str] = mapped_column(Enum("OWNER", "REVIEWER", name="user_role"))
23+
created_at: Mapped[datetime] = mapped_column(
24+
DateTime(timezone=True), server_default=func.now()
25+
)
26+
27+
deposits: Mapped[list["Deposit"]] = relationship("Deposit", back_populates="user")
28+
29+
30+
class EmailVerification(Base):
31+
__tablename__ = "email_verifications"
32+
33+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
34+
email: Mapped[str] = mapped_column(String(255), index=True)
35+
code: Mapped[str] = mapped_column(String(6))
36+
verification_token: Mapped[str | None] = mapped_column(String, nullable=True)
37+
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
38+
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
39+
created_at: Mapped[datetime] = mapped_column(
40+
DateTime(timezone=True), server_default=func.now()
41+
)

app/auth/repository.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from abc import ABC, abstractmethod
2+
3+
from app.auth.models import EmailVerification, User
4+
5+
6+
class UserRepository(ABC):
7+
@abstractmethod
8+
async def find_by_email(self, email: str) -> User | None: ...
9+
10+
@abstractmethod
11+
async def find_by_id(self, user_id: str) -> User | None: ...
12+
13+
@abstractmethod
14+
async def save(self, user: User) -> User: ...
15+
16+
17+
class EmailVerificationRepository(ABC):
18+
@abstractmethod
19+
async def save(self, verification: EmailVerification) -> None: ...
20+
21+
@abstractmethod
22+
async def find_latest_unverified(
23+
self, email: str, code: str
24+
) -> EmailVerification | None: ...
25+
26+
@abstractmethod
27+
async def update(self, verification: EmailVerification) -> None: ...

app/auth/repository_impl.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from datetime import UTC, datetime
2+
from typing import cast
3+
from uuid import UUID
4+
5+
from sqlalchemy import select
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from app.auth.models import EmailVerification, User
9+
from app.auth.repository import EmailVerificationRepository, UserRepository
10+
11+
12+
class UserRepositoryImpl(UserRepository):
13+
def __init__(self, db: AsyncSession) -> None:
14+
self.db = db
15+
16+
async def find_by_email(self, email: str) -> User | None:
17+
return cast(
18+
User | None,
19+
await self.db.scalar(select(User).where(User.email == email)),
20+
)
21+
22+
async def find_by_id(self, user_id: str) -> User | None:
23+
return cast(
24+
User | None,
25+
await self.db.scalar(select(User).where(User.id == UUID(user_id))),
26+
)
27+
28+
async def save(self, user: User) -> User:
29+
self.db.add(user)
30+
await self.db.commit()
31+
await self.db.refresh(user)
32+
return user
33+
34+
35+
class EmailVerificationRepositoryImpl(EmailVerificationRepository):
36+
def __init__(self, db: AsyncSession) -> None:
37+
self.db = db
38+
39+
async def save(self, verification: EmailVerification) -> None:
40+
self.db.add(verification)
41+
await self.db.commit()
42+
43+
async def find_latest_unverified(
44+
self, email: str, code: str
45+
) -> EmailVerification | None:
46+
return cast(
47+
EmailVerification | None,
48+
await self.db.scalar(
49+
select(EmailVerification)
50+
.where(
51+
EmailVerification.email == email,
52+
EmailVerification.code == code,
53+
EmailVerification.is_verified.is_(False),
54+
EmailVerification.expires_at > datetime.now(UTC),
55+
)
56+
.order_by(EmailVerification.created_at.desc())
57+
),
58+
)
59+
60+
async def update(self, verification: EmailVerification) -> None:
61+
await self.db.commit()
Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from fastapi import APIRouter, Depends, Request, Response
2-
from sqlalchemy.ext.asyncio import AsyncSession
32

43
from app.api.v1.deps import require_login
5-
from app.core.exceptions import AUTH_001, AppException
6-
from app.db.session import get_db
7-
from app.schemas.auth import (
4+
from app.auth.dependencies import get_auth_service
5+
from app.auth.schemas import (
86
AccessTokenResp,
97
AuthResp,
108
ChangePasswordReq,
@@ -17,43 +15,44 @@
1715
ResetPasswordReq,
1816
UserInfo,
1917
)
20-
from app.services import auth as auth_service
18+
from app.auth.service import AuthService
19+
from app.core.exceptions import AUTH_001, AppException
2120

2221
router = APIRouter(prefix="/auth", tags=["Auth"])
2322

2423

2524
@router.post("/email", response_model=None)
2625
async def check_email(
2726
body: EmailCheckReq,
28-
db: AsyncSession = Depends(get_db),
27+
service: AuthService = Depends(get_auth_service),
2928
) -> None:
30-
await auth_service.check_email_duplicate(db, body.email)
29+
await service.check_email_duplicate(body.email)
3130

3231

3332
@router.post("/email/verify", response_model=None)
3433
async def verify_email(
3534
body: EmailVerifyReq,
36-
db: AsyncSession = Depends(get_db),
35+
service: AuthService = Depends(get_auth_service),
3736
) -> None:
38-
await auth_service.send_verification_code(db, body.email)
37+
await service.send_verification_code(body.email)
3938

4039

4140
@router.post("/email/validate", response_model=EmailValidateResp)
4241
async def validate_email(
4342
body: EmailValidateReq,
44-
db: AsyncSession = Depends(get_db),
43+
service: AuthService = Depends(get_auth_service),
4544
) -> EmailValidateResp:
46-
token = await auth_service.validate_verification_code(db, body.email, body.code)
45+
token = await service.validate_verification_code(body.email, body.code)
4746
return EmailValidateResp(verificationToken=token)
4847

4948

5049
@router.post("/user", response_model=AuthResp)
5150
async def register(
5251
body: RegisterReq,
5352
response: Response,
54-
db: AsyncSession = Depends(get_db),
53+
service: AuthService = Depends(get_auth_service),
5554
) -> AuthResp:
56-
user, access_token, refresh_token = await auth_service.register(db, body)
55+
user, access_token, refresh_token = await service.register(body)
5756
response.set_cookie(
5857
key="refresh_token",
5958
value=refresh_token,
@@ -67,21 +66,24 @@ async def register(
6766

6867

6968
@router.get("/token", response_model=AccessTokenResp)
70-
async def refresh_token(request: Request) -> AccessTokenResp:
69+
async def refresh_token(
70+
request: Request,
71+
service: AuthService = Depends(get_auth_service),
72+
) -> AccessTokenResp:
7173
token = request.cookies.get("refresh_token")
7274
if not token:
7375
raise AppException(AUTH_001)
74-
access_token = auth_service.refresh_access_token(token)
76+
access_token = service.refresh_access_token(token)
7577
return AccessTokenResp(accessToken=access_token)
7678

7779

7880
@router.post("/user/session", response_model=AuthResp)
7981
async def login(
8082
body: LoginReq,
8183
response: Response,
82-
db: AsyncSession = Depends(get_db),
84+
service: AuthService = Depends(get_auth_service),
8385
) -> AuthResp:
84-
user, access_token, refresh_token = await auth_service.login(db, body)
86+
user, access_token, refresh_token = await service.login(body)
8587
response.set_cookie(
8688
key="refresh_token",
8789
value=refresh_token,
@@ -105,15 +107,15 @@ async def logout(
105107
@router.patch("/password", response_model=None)
106108
async def change_password(
107109
body: ChangePasswordReq,
108-
db: AsyncSession = Depends(get_db),
110+
service: AuthService = Depends(get_auth_service),
109111
user_id: str = Depends(require_login),
110112
) -> None:
111-
await auth_service.change_password(db, user_id, body.oldPassword, body.newPassword)
113+
await service.change_password(user_id, body.oldPassword, body.newPassword)
112114

113115

114116
@router.post("/password", response_model=None)
115117
async def reset_password(
116118
body: ResetPasswordReq,
117-
db: AsyncSession = Depends(get_db),
119+
service: AuthService = Depends(get_auth_service),
118120
) -> None:
119-
await auth_service.reset_password(db, body)
121+
await service.reset_password(body)
File renamed without changes.

app/auth/service.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from abc import ABC, abstractmethod
2+
3+
from app.auth.models import User
4+
from app.auth.schemas import LoginReq, RegisterReq, ResetPasswordReq
5+
6+
7+
class AuthService(ABC):
8+
@abstractmethod
9+
async def check_email_duplicate(self, email: str) -> None: ...
10+
11+
@abstractmethod
12+
async def send_verification_code(self, email: str) -> None: ...
13+
14+
@abstractmethod
15+
async def validate_verification_code(self, email: str, code: str) -> str: ...
16+
17+
@abstractmethod
18+
async def register(self, data: RegisterReq) -> tuple[User, str, str]: ...
19+
20+
@abstractmethod
21+
def refresh_access_token(self, refresh_token: str) -> str: ...
22+
23+
@abstractmethod
24+
async def login(self, data: LoginReq) -> tuple[User, str, str]: ...
25+
26+
@abstractmethod
27+
async def change_password(
28+
self, user_id: str, old_password: str, new_password: str
29+
) -> None: ...
30+
31+
@abstractmethod
32+
async def reset_password(self, data: ResetPasswordReq) -> None: ...

0 commit comments

Comments
 (0)