feat: add web ui and dynamic private users
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type UserStore struct {
|
||||
Path string
|
||||
Mu sync.Mutex
|
||||
}
|
||||
|
||||
type userRecord struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"passwordHash"`
|
||||
Home string `json:"home"`
|
||||
}
|
||||
|
||||
var validUsername = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$")
|
||||
|
||||
func ValidateUsername(username string) error {
|
||||
if !validUsername.MatchString(username) {
|
||||
return fmt.Errorf("invalid username")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserStore) Load(m *Manager, homeRoot string) error {
|
||||
if u.Path == "" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(u.Path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var records []userRecord
|
||||
if err := json.Unmarshal(data, &records); err != nil {
|
||||
return fmt.Errorf("invalid user store: %w", err)
|
||||
}
|
||||
for _, rec := range records {
|
||||
if err := ValidateUsername(rec.Username); err != nil || rec.Password == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := m.Accounts[rec.Username]; exists {
|
||||
continue
|
||||
}
|
||||
home := filepath.ToSlash(strings.Trim(strings.TrimSpace(rec.Home), "/"))
|
||||
if home == "" {
|
||||
home = filepath.ToSlash(filepath.Join(homeRoot, rec.Username))
|
||||
}
|
||||
a := &Account{Username: rec.Username, PasswordHash: rec.Password, Mask: None, Home: home}
|
||||
a.Rules = append(a.Rules, PathRule{Dir: home, Mask: Read | Write | Delete})
|
||||
m.Accounts[rec.Username] = a
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserStore) Save(m *Manager) error {
|
||||
if u.Path == "" {
|
||||
return nil
|
||||
}
|
||||
u.Mu.Lock()
|
||||
defer u.Mu.Unlock()
|
||||
records := make([]userRecord, 0)
|
||||
for _, a := range m.Accounts {
|
||||
if a.PasswordHash == "" || a.Home == "" || a.Admin {
|
||||
continue
|
||||
}
|
||||
records = append(records, userRecord{Username: a.Username, Password: a.PasswordHash, Home: a.Home})
|
||||
}
|
||||
data, err := json.MarshalIndent(records, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dir := filepath.Dir(u.Path); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tmp := u.Path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, u.Path)
|
||||
}
|
||||
|
||||
func (u *UserStore) Add(m *Manager, username, password, home string) (*Account, error) {
|
||||
if err := ValidateUsername(username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(password) == "" {
|
||||
return nil, fmt.Errorf("password required")
|
||||
}
|
||||
if _, ok := m.Accounts[username]; ok {
|
||||
return nil, fmt.Errorf("user already exists")
|
||||
}
|
||||
home = strings.Trim(home, "/")
|
||||
a := &Account{Username: username, PasswordHash: PasswordHash(password), Mask: None, Home: home}
|
||||
a.Rules = append(a.Rules, PathRule{Dir: home, Mask: Read | Write | Delete})
|
||||
m.Accounts[username] = a
|
||||
return a, u.Save(m)
|
||||
}
|
||||
|
||||
func (u *UserStore) Delete(m *Manager, username string) error {
|
||||
a, ok := m.Accounts[username]
|
||||
if !ok || a.Admin || a.PasswordHash == "" {
|
||||
return fmt.Errorf("managed user not found")
|
||||
}
|
||||
delete(m.Accounts, username)
|
||||
return u.Save(m)
|
||||
}
|
||||
Reference in New Issue
Block a user