mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-03-11 17:34:08 -05:00
fix email verification
This commit is contained in:
@@ -13,51 +13,63 @@ server {
|
||||
# API PROXY RULES
|
||||
#
|
||||
# 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/ {
|
||||
proxy_pass http://hub-api:48888;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
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/ {
|
||||
proxy_pass http://hub-api:48888;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
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)
|
||||
# Matches URLs like /kohaku/test-2.git/info/lfs/objects/batch
|
||||
# 3. Git LFS endpoints
|
||||
# Covers: /{type}s/{namespace}/{name}.git/info/lfs/* and /{namespace}/{name}.git/info/lfs/*
|
||||
location ~ \.git/info/lfs/ {
|
||||
proxy_pass http://hub-api:48888;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
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)
|
||||
# This is the most specific pattern and should be checked first.
|
||||
# Matches /models/user/repo/resolve/...
|
||||
# 4. Public file resolution routes (no /api prefix)
|
||||
# These are public-facing download endpoints
|
||||
# Pattern: /{type}s/{namespace}/{name}/resolve/{revision}/{path}
|
||||
location ~ ^/(models|datasets|spaces)/[^/]+/[^/]+/resolve/ {
|
||||
proxy_pass http://hub-api:48888;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# This is the legacy pattern.
|
||||
# Matches /user/repo/resolve/...
|
||||
# By placing it after the more specific rule above, we avoid conflicts.
|
||||
# 5. Legacy public file resolution route (no /api prefix, no type prefix)
|
||||
# Pattern: /{namespace}/{name}/resolve/{revision}/{path}
|
||||
# Must come AFTER the specific routes to avoid catching frontend routes
|
||||
location ~ ^/[^/]+/[^/]+/resolve/ {
|
||||
proxy_pass http://hub-api:48888;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,4 +82,4 @@ server {
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,12 +208,14 @@
|
||||
<script setup>
|
||||
import { repoAPI } from "@/utils/api";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { ElMessage } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const { isAuthenticated } = storeToRefs(authStore);
|
||||
|
||||
@@ -269,6 +271,21 @@ async function loadStats() {
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,17 +8,27 @@ from ..config import cfg
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
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},
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
msg = MIMEMultipart()
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = cfg.smtp.from_email
|
||||
msg["To"] = to_email
|
||||
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:
|
||||
if cfg.smtp.use_tls:
|
||||
|
||||
@@ -84,8 +84,9 @@ def register(req: RegisterRequest):
|
||||
|
||||
|
||||
@router.get("/verify-email")
|
||||
def verify_email(token: str):
|
||||
"""Verify email with token."""
|
||||
def verify_email(token: str, response: Response):
|
||||
"""Verify email with token and automatically log in user."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
verification = EmailVerification.get_or_none(
|
||||
(EmailVerification.token == token)
|
||||
@@ -93,15 +94,45 @@ def verify_email(token: str):
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
# Delete verification token
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user