Files
root 44406b72db Initial commit: img-infinite-canvas AI design workbench MVP
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>
2026-07-07 23:15:37 +08:00

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
}