fix email verification

This commit is contained in:
Kohaku-Blueleaf
2025-10-05 03:29:32 +08:00
parent 84ecb94474
commit 75eb9725ac
4 changed files with 199 additions and 23 deletions

View File

@@ -13,51 +13,63 @@ server {
# API PROXY RULES # API PROXY RULES
# #
# These specific locations are evaluated BEFORE the general SPA rule. # These specific locations are evaluated BEFORE the general SPA rule.
# The order of these proxy rules matters. # The order of these proxy rules matters - most specific first.
# ================================================================= # =================================================================
# 1. Standard API prefixes (highest priority for matching) # 1. API endpoints (all routes under /api/)
# Covers: /api/auth/*, /api/repos/*, /api/models/*, /api/datasets/*,
# /api/spaces/*, /api/users/*, /api/organizations/*, /api/whoami-v2, etc.
location /api/ { location /api/ {
proxy_pass http://hub-api:48888; proxy_pass http://hub-api:48888;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
# 2. Organization API endpoints (mounted at /org/)
# Covers: /org/create, /org/{name}, /org/{name}/members, /org/users/{username}/orgs
location /org/ { location /org/ {
proxy_pass http://hub-api:48888; proxy_pass http://hub-api:48888;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
# 2. Git LFS routes (critical for large file uploads/downloads) # 3. Git LFS endpoints
# Matches URLs like /kohaku/test-2.git/info/lfs/objects/batch # Covers: /{type}s/{namespace}/{name}.git/info/lfs/* and /{namespace}/{name}.git/info/lfs/*
location ~ \.git/info/lfs/ { location ~ \.git/info/lfs/ {
proxy_pass http://hub-api:48888; proxy_pass http://hub-api:48888;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Important for large file uploads
proxy_request_buffering off;
client_body_buffer_size 128k;
} }
# 3. File resolution routes (for downloads) # 4. Public file resolution routes (no /api prefix)
# This is the most specific pattern and should be checked first. # These are public-facing download endpoints
# Matches /models/user/repo/resolve/... # Pattern: /{type}s/{namespace}/{name}/resolve/{revision}/{path}
location ~ ^/(models|datasets|spaces)/[^/]+/[^/]+/resolve/ { location ~ ^/(models|datasets|spaces)/[^/]+/[^/]+/resolve/ {
proxy_pass http://hub-api:48888; proxy_pass http://hub-api:48888;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
# This is the legacy pattern. # 5. Legacy public file resolution route (no /api prefix, no type prefix)
# Matches /user/repo/resolve/... # Pattern: /{namespace}/{name}/resolve/{revision}/{path}
# By placing it after the more specific rule above, we avoid conflicts. # Must come AFTER the specific routes to avoid catching frontend routes
location ~ ^/[^/]+/[^/]+/resolve/ { location ~ ^/[^/]+/[^/]+/resolve/ {
proxy_pass http://hub-api:48888; proxy_pass http://hub-api:48888;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
@@ -70,4 +82,4 @@ server {
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
} }

View File

@@ -208,12 +208,14 @@
<script setup> <script setup>
import { repoAPI } from "@/utils/api"; import { repoAPI } from "@/utils/api";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { ElMessage } from "element-plus";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const router = useRouter(); const router = useRouter();
const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuthStore();
const { isAuthenticated } = storeToRefs(authStore); const { isAuthenticated } = storeToRefs(authStore);
@@ -269,6 +271,21 @@ async function loadStats() {
} }
onMounted(() => { onMounted(() => {
// Check for verification error messages in query params
if (route.query.error) {
const errorType = route.query.error;
const message = route.query.message || "An error occurred";
if (errorType === "invalid_token") {
ElMessage.error(decodeURIComponent(message));
// Clean up URL
router.replace("/");
} else if (errorType === "user_not_found") {
ElMessage.error("User account not found");
router.replace("/");
}
}
loadStats(); loadStats();
}); });
</script> </script>

View File

@@ -8,17 +8,27 @@ from ..config import cfg
def send_verification_email(to_email: str, username: str, token: str) -> bool: def send_verification_email(to_email: str, username: str, token: str) -> bool:
"""Send email verification email.""" """Send email verification email.
Note: HTML emails have limitations:
- Must use inline CSS (no <style> tags or external stylesheets)
- Table-based layouts are most reliable (flexbox/grid not supported)
- Limited CSS properties (many modern CSS features don't work)
- Different email clients render differently (Outlook, Gmail, Apple Mail, etc.)
- Must use absolute URLs for images
- Background images often blocked
"""
if not cfg.smtp.enabled: if not cfg.smtp.enabled:
print( print(
f"[EMAIL] SMTP disabled. Verification link: {cfg.app.base_url}/auth/verify?token={token}" f"[EMAIL] SMTP disabled. Verification link: {cfg.app.base_url}/api/auth/verify-email?token={token}"
) )
return True return True
subject = "Verify your Kohaku Hub account" subject = "Verify your Kohaku Hub account"
verify_link = f"{cfg.app.base_url}/auth/verify?token={token}" verify_link = f"{cfg.app.base_url}/api/auth/verify-email?token={token}"
body = f""" # Plain text version (fallback)
text_body = f"""
Hello {username}, Hello {username},
Please verify your email address by clicking the link below: Please verify your email address by clicking the link below:
@@ -30,15 +40,121 @@ This link will expire in 24 hours.
If you didn't create this account, please ignore this email. If you didn't create this account, please ignore this email.
Best regards, Best regards,
Kohaku Hub Kohaku Hub Team
"""
# HTML version (styled with table-based layout and inline CSS)
html_body = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<!-- Main container table -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f5f5f5;">
<tr>
<td style="padding: 40px 20px;">
<!-- Content card -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
<!-- Header -->
<tr>
<td style="padding: 40px 40px 20px 40px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px 8px 0 0;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 600;">
🎉 Welcome to Kohaku Hub!
</h1>
</td>
</tr>
<!-- Body -->
<tr>
<td style="padding: 40px;">
<p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #333333;">
Hello <strong>{username}</strong>,
</p>
<p style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #555555;">
Thank you for signing up! Please verify your email address to activate your account and start using Kohaku Hub.
</p>
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="text-align: center; padding: 20px 0;">
<a href="{verify_link}" style="display: inline-block; padding: 16px 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 6px rgba(102, 126, 234, 0.4);">
✉️ Verify Email Address
</a>
</td>
</tr>
</table>
<p style="margin: 30px 0 20px 0; font-size: 14px; line-height: 1.6; color: #666666;">
Or copy and paste this link into your browser:
</p>
<p style="margin: 0 0 30px 0; padding: 12px; background-color: #f8f9fa; border-left: 4px solid #667eea; border-radius: 4px; font-size: 13px; word-break: break-all; color: #555555;">
{verify_link}
</p>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-top: 1px solid #e5e7eb; margin-top: 30px; padding-top: 20px;">
<tr>
<td style="padding: 15px; background-color: #fef3c7; border-radius: 6px; border-left: 4px solid #f59e0b;">
<p style="margin: 0; font-size: 14px; color: #92400e; line-height: 1.5;">
⏰ <strong>Important:</strong> This link will expire in 24 hours.
</p>
</td>
</tr>
</table>
<p style="margin: 30px 0 0 0; font-size: 14px; line-height: 1.6; color: #999999;">
If you didn't create this account, you can safely ignore this email.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px 40px; background-color: #f8f9fa; border-radius: 0 0 8px 8px; text-align: center;">
<p style="margin: 0 0 10px 0; font-size: 14px; color: #666666;">
Best regards,<br>
<strong>The Kohaku Hub Team</strong>
</p>
<p style="margin: 0; font-size: 12px; color: #999999;">
This is an automated email, please do not reply.
</p>
</td>
</tr>
</table>
<!-- Disclaimer -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 600px; margin: 20px auto 0 auto;">
<tr>
<td style="padding: 20px; text-align: center; font-size: 12px; color: #999999; line-height: 1.5;">
© 2025 Kohaku Hub. All rights reserved.<br>
You received this email because you registered an account.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""" """
try: try:
msg = MIMEMultipart() msg = MIMEMultipart("alternative")
msg["From"] = cfg.smtp.from_email msg["From"] = cfg.smtp.from_email
msg["To"] = to_email msg["To"] = to_email
msg["Subject"] = subject msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
# Attach both plain text and HTML versions
# Email clients will choose which to display (preferring HTML if supported)
part1 = MIMEText(text_body, "plain")
part2 = MIMEText(html_body, "html")
msg.attach(part1)
msg.attach(part2)
with smtplib.SMTP(cfg.smtp.host, cfg.smtp.port) as server: with smtplib.SMTP(cfg.smtp.host, cfg.smtp.port) as server:
if cfg.smtp.use_tls: if cfg.smtp.use_tls:

View File

@@ -84,8 +84,9 @@ def register(req: RegisterRequest):
@router.get("/verify-email") @router.get("/verify-email")
def verify_email(token: str): def verify_email(token: str, response: Response):
"""Verify email with token.""" """Verify email with token and automatically log in user."""
from fastapi.responses import RedirectResponse
verification = EmailVerification.get_or_none( verification = EmailVerification.get_or_none(
(EmailVerification.token == token) (EmailVerification.token == token)
@@ -93,15 +94,45 @@ def verify_email(token: str):
) )
if not verification: if not verification:
raise HTTPException(400, detail="Invalid or expired verification token") # Redirect to login with error message
return RedirectResponse(
url=f"/?error=invalid_token&message=Invalid+or+expired+verification+token",
status_code=302,
)
# Update user # Get user
user = User.get_or_none(User.id == verification.user)
if not user:
return RedirectResponse(url="/?error=user_not_found", status_code=302)
# Update user email verification status
User.update(email_verified=True).where(User.id == verification.user).execute() User.update(email_verified=True).where(User.id == verification.user).execute()
# Delete verification token # Delete verification token
EmailVerification.delete().where(EmailVerification.id == verification.id).execute() EmailVerification.delete().where(EmailVerification.id == verification.id).execute()
return {"success": True, "message": "Email verified successfully"} # Create session for auto-login
session_id = generate_token()
session_secret = generate_session_secret()
Session.create(
session_id=session_id,
user_id=user.id,
secret=session_secret,
expires_at=get_expiry_time(cfg.auth.session_expire_hours),
)
# Set session cookie
response = RedirectResponse(url=f"/{user.username}", status_code=302)
response.set_cookie(
key="session_id",
value=session_id,
httponly=True,
max_age=cfg.auth.session_expire_hours * 3600,
samesite="lax",
)
return response
@router.post("/login") @router.post("/login")