Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class Settings(BaseSettings):
s3_public_bucket: str = ""
s3_presigned_expiry: int = 3600

smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""

anthropic_api_key: str = ""

model_config = {"env_file": ".env"}
Expand Down
55 changes: 55 additions & 0 deletions app/core/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import asyncio
import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

from app.core.config import settings

logger = logging.getLogger(__name__)


def _send_via_smtp(to: str, subject: str, body_html: str) -> None:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = settings.smtp_user
msg["To"] = to
msg.attach(MIMEText(body_html, "html", "utf-8"))

with smtplib.SMTP(settings.smtp_host, settings.smtp_port) as server:
server.ehlo()
server.starttls()
server.login(settings.smtp_user, settings.smtp_password)
server.sendmail(settings.smtp_user, to, msg.as_string())


async def send_email(to: str, subject: str, body_html: str) -> None:
if not settings.smtp_user or not settings.smtp_password:
logger.warning("[EMAIL] SMTP credentials not set — skipping send to %s", to)
return
try:
await asyncio.to_thread(_send_via_smtp, to, subject, body_html)
except smtplib.SMTPException as e:
logger.error("[EMAIL] Failed to send email to %s: %s", to, e)
raise


async def send_verification_email(to: str, code: str) -> None:
subject = "[VLSI] 이메일 인증 코드"
body = f"""
<p>안녕하세요,</p>
<p>아래 인증 코드를 입력해 주세요. 코드는 <strong>10분</strong> 후 만료됩니다.</p>
<h2 style="letter-spacing:4px">{code}</h2>
<p>본인이 요청하지 않은 경우 이 메일을 무시하세요.</p>
"""
await send_email(to, subject, body)


async def send_temp_password_email(to: str, temp_password: str) -> None:
subject = "[VLSI] 임시 비밀번호 안내"
body = f"""
<p>안녕하세요,</p>
<p>요청하신 임시 비밀번호입니다. 로그인 후 반드시 비밀번호를 변경해 주세요.</p>
<h2 style="letter-spacing:4px">{temp_password}</h2>
"""
await send_email(to, subject, body)
5 changes: 3 additions & 2 deletions app/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.email import send_temp_password_email, send_verification_email
from app.core.security import (
create_access_token,
create_refresh_token,
Expand All @@ -26,7 +27,7 @@ async def send_verification_code(db: AsyncSession, email: str) -> None:
verification = EmailVerification(email=email, code=code, expires_at=expires_at)
db.add(verification)
await db.commit()
print(f"[DEV] Verification code for {email}: {code}")
await send_verification_email(email, code)


async def validate_verification_code(db: AsyncSession, email: str, code: str) -> str:
Expand Down Expand Up @@ -122,7 +123,7 @@ async def reset_password(db: AsyncSession, data: ResetPasswordReq) -> None:
temp_password = "".join(random.choices(string.ascii_letters + string.digits, k=12))
user.password_hash = get_password_hash(temp_password)
await db.commit()
print(f"[DEV] Temporary password for {data.email}: {temp_password}")
await send_temp_password_email(data.email, temp_password)


async def check_email_duplicate(db: AsyncSession, email: str) -> None:
Expand Down
Loading