341 lines
8.0 KiB
Go
341 lines
8.0 KiB
Go
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)
|
|
}
|