Initial commit: Homelab API

This commit is contained in:
Cosmo
2026-02-06 11:19:55 +00:00
commit 5a40127edd
26 changed files with 2807 additions and 0 deletions

133
internal/service/email.go Normal file
View File

@@ -0,0 +1,133 @@
package service
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type EmailService struct {
apiKey string
fromEmail string
fromName string
baseURL string
}
type ResendRequest struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html"`
}
func NewEmailService(apiKey, fromEmail, fromName, baseURL string) *EmailService {
return &EmailService{
apiKey: apiKey,
fromEmail: fromEmail,
fromName: fromName,
baseURL: baseURL,
}
}
func (s *EmailService) SendVerificationEmail(toEmail, username, token string) error {
verifyURL := fmt.Sprintf("%s/verify-email?token=%s", s.baseURL, token)
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #FAF9F6; margin: 0; padding: 20px; }
.container { max-width: 500px; margin: 0 auto; padding: 40px; background: white; border-radius: 24px; }
h2 { color: #0f766e; margin-bottom: 8px; }
.button { display: inline-block; padding: 14px 28px; background: linear-gradient(135deg, #14b8a6, #0f766e); color: white; text-decoration: none; border-radius: 16px; font-weight: 600; }
.link { color: #0f766e; word-break: break-all; }
.footer { margin-top: 30px; font-size: 13px; color: #888; }
</style>
</head>
<body>
<div class="container">
<h2>Привет, %s! 👋</h2>
<p>Спасибо за регистрацию в Pulse! Подтверди свой email, нажав на кнопку ниже:</p>
<p style="margin: 30px 0;"><a href="%s" class="button">Подтвердить email</a></p>
<p>Или скопируй ссылку:<br><a href="%s" class="link">%s</a></p>
<p class="footer">Ссылка действительна 24 часа. Если ты не регистрировался — просто проигнорируй это письмо.</p>
</div>
</body>
</html>`, username, verifyURL, verifyURL, verifyURL)
return s.send(toEmail, "Подтверди свой email — Pulse", html)
}
func (s *EmailService) SendPasswordResetEmail(toEmail, username, token string) error {
resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.baseURL, token)
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #FAF9F6; margin: 0; padding: 20px; }
.container { max-width: 500px; margin: 0 auto; padding: 40px; background: white; border-radius: 24px; }
h2 { color: #0f766e; margin-bottom: 8px; }
.button { display: inline-block; padding: 14px 28px; background: linear-gradient(135deg, #14b8a6, #0f766e); color: white; text-decoration: none; border-radius: 16px; font-weight: 600; }
.link { color: #0f766e; word-break: break-all; }
.footer { margin-top: 30px; font-size: 13px; color: #888; }
</style>
</head>
<body>
<div class="container">
<h2>Сброс пароля</h2>
<p>Привет, %s! Мы получили запрос на сброс пароля для твоего аккаунта в Pulse.</p>
<p style="margin: 30px 0;"><a href="%s" class="button">Сбросить пароль</a></p>
<p>Или скопируй ссылку:<br><a href="%s" class="link">%s</a></p>
<p class="footer">Ссылка действительна 1 час. Если ты не запрашивал сброс пароля — просто проигнорируй это письмо.</p>
</div>
</body>
</html>`, username, resetURL, resetURL, resetURL)
return s.send(toEmail, "Сброс пароля — Pulse", html)
}
func (s *EmailService) send(to, subject, html string) error {
if s.apiKey == "" {
fmt.Printf("[EMAIL] Would send to %s: %s\n", to, subject)
return nil
}
payload := ResendRequest{
From: fmt.Sprintf("%s <%s>", s.fromName, s.fromEmail),
To: []string{to},
Subject: subject,
HTML: html,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+s.apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("resend API error: %d", resp.StatusCode)
}
return nil
}