package storage import ( "crypto/rand" "encoding/base64" "encoding/json" "errors" "os" "path/filepath" "strings" "sync" "sproutgate-backend/internal/models" ) type AdminConfig struct { Token string `json:"token"` } type AuthConfig struct { JWTSecret string `json:"jwtSecret"` Issuer string `json:"issuer"` } type EmailConfig struct { FromName string `json:"fromName"` FromAddress string `json:"fromAddress"` Username string `json:"username"` Password string `json:"password"` SMTPHost string `json:"smtpHost"` SMTPPort int `json:"smtpPort"` Encryption string `json:"encryption"` } type Store struct { dataDir string usersDir string pendingDir string resetDir string secondaryDir string adminConfigPath string authConfigPath string emailConfigPath string adminToken string jwtSecret []byte issuer string emailConfig EmailConfig mu sync.Mutex } func NewStore(dataDir string) (*Store, error) { if dataDir == "" { dataDir = "./data" } absDir, err := filepath.Abs(dataDir) if err != nil { return nil, err } usersDir := filepath.Join(absDir, "users") pendingDir := filepath.Join(absDir, "pending") resetDir := filepath.Join(absDir, "reset") secondaryDir := filepath.Join(absDir, "secondary") configDir := filepath.Join(absDir, "config") if err := os.MkdirAll(usersDir, 0755); err != nil { return nil, err } if err := os.MkdirAll(pendingDir, 0755); err != nil { return nil, err } if err := os.MkdirAll(resetDir, 0755); err != nil { return nil, err } if err := os.MkdirAll(secondaryDir, 0755); err != nil { return nil, err } if err := os.MkdirAll(configDir, 0755); err != nil { return nil, err } store := &Store{ dataDir: absDir, usersDir: usersDir, pendingDir: pendingDir, resetDir: resetDir, secondaryDir: secondaryDir, adminConfigPath: filepath.Join(configDir, "admin.json"), authConfigPath: filepath.Join(configDir, "auth.json"), emailConfigPath: filepath.Join(configDir, "email.json"), } if err := store.loadOrCreateAdminConfig(); err != nil { return nil, err } if err := store.loadOrCreateAuthConfig(); err != nil { return nil, err } if err := store.loadOrCreateEmailConfig(); err != nil { return nil, err } return store, nil } func (s *Store) DataDir() string { return s.dataDir } func (s *Store) AdminToken() string { return s.adminToken } func (s *Store) JWTSecret() []byte { return s.jwtSecret } func (s *Store) JWTIssuer() string { return s.issuer } func (s *Store) EmailConfig() EmailConfig { return s.emailConfig } func (s *Store) loadOrCreateAdminConfig() error { defaultToken := "shumengya520" if _, err := os.Stat(s.adminConfigPath); errors.Is(err, os.ErrNotExist) { cfg := AdminConfig{Token: defaultToken} if err := writeJSONFile(s.adminConfigPath, cfg); err != nil { return err } s.adminToken = cfg.Token return nil } var cfg AdminConfig if err := readJSONFile(s.adminConfigPath, &cfg); err != nil { return err } if strings.TrimSpace(cfg.Token) == "" { cfg.Token = defaultToken if err := writeJSONFile(s.adminConfigPath, cfg); err != nil { return err } } s.adminToken = cfg.Token return nil } func (s *Store) loadOrCreateAuthConfig() error { if _, err := os.Stat(s.authConfigPath); errors.Is(err, os.ErrNotExist) { secret, err := generateSecret() if err != nil { return err } cfg := AuthConfig{ JWTSecret: base64.StdEncoding.EncodeToString(secret), Issuer: "sproutgate", } if err := writeJSONFile(s.authConfigPath, cfg); err != nil { return err } s.jwtSecret = secret s.issuer = cfg.Issuer return nil } var cfg AuthConfig if err := readJSONFile(s.authConfigPath, &cfg); err != nil { return err } secretBytes, err := base64.StdEncoding.DecodeString(cfg.JWTSecret) if err != nil || len(secretBytes) == 0 { secretBytes, err = generateSecret() if err != nil { return err } cfg.JWTSecret = base64.StdEncoding.EncodeToString(secretBytes) if strings.TrimSpace(cfg.Issuer) == "" { cfg.Issuer = "sproutgate" } if err := writeJSONFile(s.authConfigPath, cfg); err != nil { return err } } if strings.TrimSpace(cfg.Issuer) == "" { cfg.Issuer = "sproutgate" if err := writeJSONFile(s.authConfigPath, cfg); err != nil { return err } } s.jwtSecret = secretBytes s.issuer = cfg.Issuer return nil } func (s *Store) loadOrCreateEmailConfig() error { if _, err := os.Stat(s.emailConfigPath); errors.Is(err, os.ErrNotExist) { cfg := EmailConfig{ FromName: "萌芽账户认证中心", FromAddress: "notice@smyhub.com", Username: "", Password: "tyh@19900420", SMTPHost: "smtp.qiye.aliyun.com", SMTPPort: 465, Encryption: "SSL", } if err := writeJSONFile(s.emailConfigPath, cfg); err != nil { return err } if cfg.Username == "" { cfg.Username = cfg.FromAddress } s.emailConfig = cfg return nil } var cfg EmailConfig if err := readJSONFile(s.emailConfigPath, &cfg); err != nil { return err } if strings.TrimSpace(cfg.FromName) == "" { cfg.FromName = "萌芽账户认证中心" } if strings.TrimSpace(cfg.FromAddress) == "" { cfg.FromAddress = "notice@smyhub.com" } if strings.TrimSpace(cfg.Username) == "" { cfg.Username = cfg.FromAddress } if strings.TrimSpace(cfg.SMTPHost) == "" { cfg.SMTPHost = "smtp.qiye.aliyun.com" } if cfg.SMTPPort == 0 { cfg.SMTPPort = 465 } if strings.TrimSpace(cfg.Encryption) == "" { cfg.Encryption = "SSL" } if err := writeJSONFile(s.emailConfigPath, cfg); err != nil { return err } s.emailConfig = cfg return nil } func generateSecret() ([]byte, error) { secret := make([]byte, 32) _, err := rand.Read(secret) return secret, err } func (s *Store) ListUsers() ([]models.UserRecord, error) { s.mu.Lock() defer s.mu.Unlock() entries, err := os.ReadDir(s.usersDir) if err != nil { return nil, err } users := make([]models.UserRecord, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue } if !strings.HasSuffix(entry.Name(), ".json") { continue } var record models.UserRecord path := filepath.Join(s.usersDir, entry.Name()) if err := readJSONFile(path, &record); err != nil { return nil, err } users = append(users, record) } return users, nil } func (s *Store) GetUser(account string) (models.UserRecord, bool, error) { s.mu.Lock() defer s.mu.Unlock() path := s.userFilePath(account) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return models.UserRecord{}, false, nil } var record models.UserRecord if err := readJSONFile(path, &record); err != nil { return models.UserRecord{}, false, err } return record, true, nil } func (s *Store) CreateUser(record models.UserRecord) error { s.mu.Lock() defer s.mu.Unlock() path := s.userFilePath(record.Account) if _, err := os.Stat(path); err == nil { return errors.New("account already exists") } if record.CreatedAt == "" { record.CreatedAt = models.NowISO() } record.UpdatedAt = record.CreatedAt return writeJSONFile(path, record) } func (s *Store) SaveUser(record models.UserRecord) error { s.mu.Lock() defer s.mu.Unlock() path := s.userFilePath(record.Account) record.UpdatedAt = models.NowISO() return writeJSONFile(path, record) } func (s *Store) DeleteUser(account string) error { s.mu.Lock() defer s.mu.Unlock() path := s.userFilePath(account) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil } return os.Remove(path) } func (s *Store) userFilePath(account string) string { return filepath.Join(s.usersDir, userFileName(account)) } func userFileName(account string) string { encoded := base64.RawURLEncoding.EncodeToString([]byte(account)) return encoded + ".json" } func readJSONFile(path string, target any) error { raw, err := os.ReadFile(path) if err != nil { return err } return json.Unmarshal(raw, target) } func writeJSONFile(path string, value any) error { raw, err := json.MarshalIndent(value, "", " ") if err != nil { return err } return os.WriteFile(path, raw, 0644) }