live-sfu-demo/internal/auth/store.go

223 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package auth
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"golang.org/x/crypto/bcrypt"
)
// User 表示系统用户
type User struct {
Username string `json:"username"`
Password string `json:"-"` // bcrypt hash,不序列化到前端
Hash string `json:"hash,omitempty"` // 持久化用
Role string `json:"role"`
CreatedAt int64 `json:"created_at"`
}
// UserStore 负责用户持久化与校验,内存为主,可选落盘到 JSON 文件
type UserStore struct {
mu sync.RWMutex
users map[string]*User
filePath string
}
// NewUserStore 创建用户存储,若 filePath 非空则尝试从文件加载;若文件不存在则预置种子用户
func NewUserStore(filePath string) (*UserStore, error) {
s := &UserStore{
users: make(map[string]*User),
filePath: filePath,
}
if filePath != "" {
if err := s.load(); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("load users: %w", err)
}
s.seed()
_ = s.save()
} else if len(s.users) == 0 {
s.seed()
_ = s.save()
}
} else {
s.seed()
}
return s, nil
}
func (s *UserStore) seed() {
seeds := []struct {
username string
password string
role string
}{
{"admin", "Admin123!", RoleAdmin},
{"publisher", "Publisher123!", RolePublisher},
{"viewer", "Viewer123!", RoleViewer},
}
for _, u := range seeds {
hash, _ := bcrypt.GenerateFromPassword([]byte(u.password), bcrypt.DefaultCost)
s.users[u.username] = &User{
Username: u.username,
Password: string(hash),
Hash: string(hash),
Role: u.role,
CreatedAt: time.Now().Unix(),
}
}
}
func (s *UserStore) Get(username string) (*User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[username]
if !ok {
return nil, false
}
cp := *u
return &cp, true
}
func (s *UserStore) List() []*User {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]*User, 0, len(s.users))
for _, u := range s.users {
out = append(out, &User{Username: u.Username, Role: u.Role, CreatedAt: u.CreatedAt})
}
return out
}
func (s *UserStore) Create(username, password, role string) (*User, error) {
if username == "" || password == "" {
return nil, fmt.Errorf("username and password required")
}
if role == "" {
role = RoleViewer
}
if !isValidRole(role) {
return nil, fmt.Errorf("invalid role %q", role)
}
if len(password) < 6 {
return nil, fmt.Errorf("password too short (min 6)")
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.users[username]; exists {
return nil, fmt.Errorf("user %q already exists", username)
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
u := &User{
Username: username,
Password: string(hash),
Hash: string(hash),
Role: role,
CreatedAt: time.Now().Unix(),
}
s.users[username] = u
_ = s.saveLocked()
cp := *u
cp.Password = ""
cp.Hash = ""
return &cp, nil
}
func (s *UserStore) Verify(username, password string) (*User, bool) {
s.mu.RLock()
u, ok := s.users[username]
s.mu.RUnlock()
if !ok {
return nil, false
}
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil {
return nil, false
}
cp := *u
return &cp, true
}
func (s *UserStore) UpdateRole(username, role string) error {
if !isValidRole(role) {
return fmt.Errorf("invalid role %q", role)
}
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[username]
if !ok {
return fmt.Errorf("user %q not found", username)
}
u.Role = role
return s.saveLocked()
}
func (s *UserStore) load() error {
data, err := os.ReadFile(s.filePath)
if err != nil {
return err
}
var list []*User
if err := json.Unmarshal(data, &list); err != nil {
return err
}
for _, u := range list {
if u.Password == "" && u.Hash != "" {
u.Password = u.Hash
}
if u.Hash == "" && u.Password != "" {
u.Hash = u.Password
}
s.users[u.Username] = u
}
return nil
}
func (s *UserStore) save() error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.saveLocked()
}
func (s *UserStore) saveLocked() error {
if s.filePath == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(s.filePath), 0755); err != nil {
return err
}
list := make([]*User, 0, len(s.users))
for _, u := range s.users {
if u.Hash == "" {
u.Hash = u.Password
}
list = append(list, u)
}
data, _ := json.MarshalIndent(list, "", " ")
return os.WriteFile(s.filePath, data, 0644)
}
const (
RoleAdmin = "admin"
RolePublisher = "publisher"
RoleViewer = "viewer"
RoleGuest = "guest"
)
func isValidRole(r string) bool {
switch r {
case RoleAdmin, RolePublisher, RoleViewer:
return true
default:
return false
}
}
func AllRoles() []string { return []string{RoleAdmin, RolePublisher, RoleViewer} }