mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-04-30 09:28:35 -05:00
fix email verification
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user