使用了AI重构项目,并完善了一部分后台问题
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 ©Value
|
||||
}
|
||||
Reference in New Issue
Block a user