44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
3.0 KiB
Go
77 lines
3.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
loginEmailFromName = "hello"
|
|
loginEmailFromEmail = "hello@auth.moteva.local"
|
|
loginEmailBrandName = "Moteva"
|
|
)
|
|
|
|
type EmailTemplateOptions struct {
|
|
FromName string
|
|
FromEmail string
|
|
BrandName string
|
|
}
|
|
|
|
func buildLoginEmail(toEmail string, code string, opts EmailTemplateOptions) *EmailMessage {
|
|
toEmail = normalizeEmail(toEmail)
|
|
code = strings.TrimSpace(code)
|
|
fromName := fallbackString(opts.FromName, loginEmailFromName)
|
|
fromEmail := fallbackString(opts.FromEmail, loginEmailFromEmail)
|
|
brandName := fallbackString(opts.BrandName, loginEmailBrandName)
|
|
return &EmailMessage{
|
|
FromName: fromName,
|
|
FromEmail: fromEmail,
|
|
ToEmail: toEmail,
|
|
Subject: "Welcome to " + brandName + "!",
|
|
Code: code,
|
|
Text: fmt.Sprintf(
|
|
"Hi,\n\nWelcome to us! Enter this code within the next 10 minutes to log in:\n\n%s\n\nPlease ignore this email if this wasn't you trying to create a %s account.\n\nThe %s Team",
|
|
code,
|
|
brandName,
|
|
brandName,
|
|
),
|
|
HTML: loginEmailHTML(code, brandName),
|
|
}
|
|
}
|
|
|
|
func loginEmailHTML(code string, brandName string) string {
|
|
escapedCode := html.EscapeString(code)
|
|
escapedBrand := html.EscapeString(brandName)
|
|
return `<!doctype html>
|
|
<html>
|
|
<body style="margin:0;padding:28px;background:#fff;font-family:Arial,Helvetica,sans-serif;color:#141922;">
|
|
<main style="max-width:1160px;background:#f8f8f8;border-radius:36px;overflow:hidden;">
|
|
<section style="padding:64px 40px 84px;">
|
|
<div style="font-size:48px;font-weight:800;letter-spacing:-1px;margin-bottom:72px;">
|
|
<span style="display:inline-grid;place-items:center;width:64px;height:64px;border-radius:50%;background:#050505;color:#fff;font-size:28px;margin-right:16px;vertical-align:middle;">M</span>
|
|
<span style="vertical-align:middle;">` + escapedBrand + `</span>
|
|
</div>
|
|
<p style="font-size:30px;font-weight:800;margin:0 0 30px;">Hi,</p>
|
|
<p style="font-size:30px;line-height:1.4;margin:0 0 110px;">Welcome to us! Enter this code within the next 10 minutes to log in:</p>
|
|
<div style="text-align:center;font-size:96px;line-height:1;font-weight:900;text-decoration:underline;letter-spacing:2px;margin-bottom:110px;">` + escapedCode + `</div>
|
|
<p style="font-size:29px;line-height:1.4;margin:0 0 32px;">Please ignore this email if this wasn't you trying to create a ` + escapedBrand + ` account.</p>
|
|
<p style="font-size:30px;font-weight:900;margin:0;">The ` + escapedBrand + ` Team</p>
|
|
</section>
|
|
<footer style="background:#000;color:#fff;padding:44px 40px;">
|
|
<div style="color:#50e6ff;font-size:25px;font-weight:900;letter-spacing:.4px;margin-bottom:28px;">THE DESIGN AGENT WHO CREATES BY YOUR SIDE.</div>
|
|
<div style="font-size:22px;font-weight:800;line-height:1.35;">DIVE INTO THE AUTO-DESIGN AGENT<br/>CRAFTED FOR LIMITLESS CREATIVITY.</div>
|
|
</footer>
|
|
</main>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
func fallbackString(value string, fallback string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|