使用了AI重构项目,并完善了一部分后台问题

This commit is contained in:
2026-08-05 04:47:11 +08:00
parent 146b6b1e6c
commit ac740c24fd
35 changed files with 2876 additions and 980 deletions
+79 -8
View File
@@ -1,15 +1,25 @@
package config
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
type Config struct {
DataDir string
DatabasePath string
UploadDir string
JWTSecret string
DataDir string
DatabasePath string
UploadDir string
JWTSecret string
EncryptionKey []byte
}
func New(dataDir string) *Config {
@@ -26,10 +36,71 @@ func New(dataDir string) *Config {
jwtSecret = "your-secret-key-change-in-production"
}
encryptionSource := os.Getenv("CONFIG_ENCRYPTION_KEY")
if encryptionSource == "" {
// Keep existing installations decryptable while allowing production deployments
// to use a dedicated key that is independent from JWT signing.
encryptionSource = jwtSecret
}
encryptionKey := sha256.Sum256([]byte(encryptionSource))
return &Config{
DataDir: dataDir,
DatabasePath: filepath.Join(dataDir, "home.db"),
UploadDir: uploadDir,
JWTSecret: jwtSecret,
DataDir: dataDir,
DatabasePath: filepath.Join(dataDir, "home.db"),
UploadDir: uploadDir,
JWTSecret: jwtSecret,
EncryptionKey: encryptionKey[:],
}
}
// EncryptSecret encrypts a value for storage in the application database.
func (c *Config) EncryptSecret(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", nil
}
block, err := aes.NewCipher(c.EncryptionKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nil, nonce, []byte(value), nil)
encoded := base64.RawStdEncoding.EncodeToString(append(nonce, ciphertext...))
return "v1:" + encoded, nil
}
// DecryptSecret decrypts a value previously returned by EncryptSecret.
func (c *Config) DecryptSecret(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", nil
}
if !strings.HasPrefix(value, "v1:") {
return "", errors.New("unsupported encrypted secret format")
}
raw, err := base64.RawStdEncoding.DecodeString(strings.TrimPrefix(value, "v1:"))
if err != nil {
return "", fmt.Errorf("decode encrypted secret: %w", err)
}
block, err := aes.NewCipher(c.EncryptionKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
if len(raw) < gcm.NonceSize() {
return "", errors.New("encrypted secret is too short")
}
plaintext, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
if err != nil {
return "", errors.New("encrypted secret authentication failed")
}
return string(plaintext), nil
}
+26
View File
@@ -0,0 +1,26 @@
package config
import "testing"
func TestSecretRoundTripAndIsolation(t *testing.T) {
first := &Config{EncryptionKey: []byte("01234567890123456789012345678901")}
second := &Config{EncryptionKey: []byte("abcdefghijklmnopqrstuvwxyz123456")}
ciphertext, err := first.EncryptSecret("umami-secret")
if err != nil {
t.Fatal(err)
}
if ciphertext == "umami-secret" || ciphertext == "" {
t.Fatalf("secret was not encrypted: %q", ciphertext)
}
plaintext, err := first.DecryptSecret(ciphertext)
if err != nil || plaintext != "umami-secret" {
t.Fatalf("round trip failed: %q, %v", plaintext, err)
}
if _, err := second.DecryptSecret(ciphertext); err == nil {
t.Fatal("ciphertext decrypted with the wrong key")
}
cleared, err := first.EncryptSecret("")
if err != nil || cleared != "" {
t.Fatalf("empty secret should clear storage: %q, %v", cleared, err)
}
}
+180
View File
@@ -0,0 +1,180 @@
package config
import "strings"
// AboutLink is a configurable link displayed in the about dialog.
type AboutLink struct {
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
Icon string `json:"icon"`
}
// SiteSettings is the single JSON payload stored alongside the SiteConfig row.
// The encrypted Umami credential is intentionally kept in this internal type.
type SiteSettings struct {
SiteName string `json:"siteName"`
SiteURL string `json:"siteURL"`
SiteIcon string `json:"siteIcon"`
SiteDescription string `json:"siteDescription"`
SiteKeywords string `json:"siteKeywords"`
UserName string `json:"userName"`
ProfileImageURL string `json:"profileImageURL"`
ICPNumber string `json:"icpNumber"`
PoliceNumber string `json:"policeNumber"`
PageTitle string `json:"pageTitle"`
Favicon string `json:"favicon"`
IconLibrary string `json:"iconLibrary"`
FontLibrary string `json:"fontLibrary"`
FooterYearStart string `json:"footerYearStart"`
FooterYearEnd string `json:"footerYearEnd"`
ShowVisitTimer bool `json:"showVisitTimer"`
RotatingTexts []string `json:"rotatingTexts"`
GreetingText string `json:"greetingText"`
OnlineStatusText string `json:"onlineStatusText"`
FooterLabel string `json:"footerLabel"`
ShowAbout bool `json:"showAbout"`
ShowSites bool `json:"showSites"`
ShowContacts bool `json:"showContacts"`
ShowThemeToggle bool `json:"showThemeToggle"`
ShowFooter bool `json:"showFooter"`
AboutTitle string `json:"aboutTitle"`
AboutDescription string `json:"aboutDescription"`
AboutLinks []AboutLink `json:"aboutLinks"`
SitePageSize int `json:"sitePageSize"`
OpenLinksNewTab bool `json:"openLinksInNewTab"`
AnalyticsProvider string `json:"analyticsProvider"`
UmamiScript string `json:"umamiScript"`
UmamiWebsiteID string `json:"umamiWebsiteId"`
UmamiAPIMode string `json:"umamiApiMode"`
UmamiAPIURL string `json:"umamiApiUrl"`
UmamiCredential string `json:"umamiCredential"`
UmamiDomains string `json:"umamiDomains"`
UmamiDoNotTrack bool `json:"umamiDoNotTrack"`
UmamiExcludeSearch bool `json:"umamiExcludeSearch"`
UmamiExcludeHash bool `json:"umamiExcludeHash"`
UmamiPerformance bool `json:"umamiPerformance"`
UmamiTag string `json:"umamiTag"`
}
var defaultRotatingTexts = []string{
"你好鸭,欢迎来到我的主页!!",
"随时可以联系我,期待与你交流。",
"愿你历尽千帆,归来仍是少年。",
"梦想还是要有的,万一实现了呢?",
"I hope you have a happy day every day.",
}
func DefaultSiteSettings() *SiteSettings {
return &SiteSettings{
SiteName: "个人主页",
SiteURL: "https://example.com",
SiteIcon: "/favicon.ico",
SiteDescription: "一个基于Vue3的个人主页",
SiteKeywords: "个人主页,Vue3",
UserName: "用户",
PageTitle: "个人主页",
Favicon: "/favicon.ico",
FooterYearStart: "",
FooterYearEnd: "",
ShowVisitTimer: true,
RotatingTexts: append([]string(nil), defaultRotatingTexts...),
GreetingText: "Hi,",
OnlineStatusText: "在线中",
FooterLabel: "Made by",
ShowAbout: true,
ShowSites: true,
ShowContacts: true,
ShowThemeToggle: true,
ShowFooter: true,
AboutTitle: "关于本站",
AboutLinks: []AboutLink{
{Title: "静态原项目", Description: "Home-Vue", URL: "https://github.com/JLinMr/Home-Vue", Icon: "fab fa-github"},
{Title: "动态现项目", Description: "Home-Vue-go", URL: "https://github.com/QWQLwToo/Home-Vue-go", Icon: "fab fa-github"},
},
SitePageSize: 6,
OpenLinksNewTab: true,
AnalyticsProvider: "local",
UmamiAPIMode: "selfhost",
UmamiDoNotTrack: true,
UmamiExcludeSearch: false,
UmamiExcludeHash: false,
UmamiPerformance: false,
}
}
func (s *SiteSettings) Normalize() {
defaults := DefaultSiteSettings()
if strings.TrimSpace(s.SiteName) == "" {
s.SiteName = defaults.SiteName
}
if strings.TrimSpace(s.SiteURL) == "" {
s.SiteURL = defaults.SiteURL
}
if strings.TrimSpace(s.SiteIcon) == "" {
s.SiteIcon = defaults.SiteIcon
}
if strings.TrimSpace(s.SiteDescription) == "" {
s.SiteDescription = defaults.SiteDescription
}
if strings.TrimSpace(s.SiteKeywords) == "" {
s.SiteKeywords = defaults.SiteKeywords
}
if strings.TrimSpace(s.UserName) == "" {
s.UserName = defaults.UserName
}
if strings.TrimSpace(s.PageTitle) == "" {
s.PageTitle = defaults.PageTitle
}
if strings.TrimSpace(s.Favicon) == "" {
s.Favicon = defaults.Favicon
}
if strings.TrimSpace(s.GreetingText) == "" {
s.GreetingText = defaults.GreetingText
}
if strings.TrimSpace(s.OnlineStatusText) == "" {
s.OnlineStatusText = defaults.OnlineStatusText
}
if strings.TrimSpace(s.FooterLabel) == "" {
s.FooterLabel = defaults.FooterLabel
}
if strings.TrimSpace(s.AboutTitle) == "" {
s.AboutTitle = defaults.AboutTitle
}
if s.SitePageSize != 6 && s.SitePageSize != 9 && s.SitePageSize != 12 {
s.SitePageSize = defaults.SitePageSize
}
if s.AnalyticsProvider != "umami" {
s.AnalyticsProvider = "local"
}
if s.UmamiAPIMode != "cloud" {
s.UmamiAPIMode = "selfhost"
}
filteredTexts := make([]string, 0, 8)
for _, text := range s.RotatingTexts {
if value := strings.TrimSpace(text); value != "" {
filteredTexts = append(filteredTexts, value)
}
if len(filteredTexts) == 8 {
break
}
}
if len(filteredTexts) == 0 {
filteredTexts = append([]string(nil), defaults.RotatingTexts...)
}
s.RotatingTexts = filteredTexts
if len(s.AboutLinks) > 8 {
s.AboutLinks = s.AboutLinks[:8]
}
}
func (s *SiteSettings) Clone() *SiteSettings {
copyValue := *s
copyValue.RotatingTexts = append([]string(nil), s.RotatingTexts...)
copyValue.AboutLinks = append([]AboutLink(nil), s.AboutLinks...)
return &copyValue
}