Sfoglia il codice sorgente

Fix almost all tests

binwiederhier 3 anni fa
parent
commit
d9722a9825

+ 10 - 10
cmd/access.go

@@ -71,7 +71,7 @@ func execUserAccess(c *cli.Context) error {
 	if c.NArg() > 3 {
 		return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
 	}
-	manager, err := createAuthManager(c)
+	manager, err := createUserManager(c)
 	if err != nil {
 		return err
 	}
@@ -96,7 +96,7 @@ func execUserAccess(c *cli.Context) error {
 	return changeAccess(c, manager, username, topic, perms)
 }
 
-func changeAccess(c *cli.Context, manager user.Manager, username string, topic string, perms string) error {
+func changeAccess(c *cli.Context, manager *user.Manager, username string, topic string, perms string) error {
 	if !util.Contains([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
 		return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)")
 	}
@@ -123,7 +123,7 @@ func changeAccess(c *cli.Context, manager user.Manager, username string, topic s
 	return showUserAccess(c, manager, username)
 }
 
-func resetAccess(c *cli.Context, manager user.Manager, username, topic string) error {
+func resetAccess(c *cli.Context, manager *user.Manager, username, topic string) error {
 	if username == "" {
 		return resetAllAccess(c, manager)
 	} else if topic == "" {
@@ -132,7 +132,7 @@ func resetAccess(c *cli.Context, manager user.Manager, username, topic string) e
 	return resetUserTopicAccess(c, manager, username, topic)
 }
 
-func resetAllAccess(c *cli.Context, manager user.Manager) error {
+func resetAllAccess(c *cli.Context, manager *user.Manager) error {
 	if err := manager.ResetAccess("", ""); err != nil {
 		return err
 	}
@@ -140,7 +140,7 @@ func resetAllAccess(c *cli.Context, manager user.Manager) error {
 	return nil
 }
 
-func resetUserAccess(c *cli.Context, manager user.Manager, username string) error {
+func resetUserAccess(c *cli.Context, manager *user.Manager, username string) error {
 	if err := manager.ResetAccess(username, ""); err != nil {
 		return err
 	}
@@ -148,7 +148,7 @@ func resetUserAccess(c *cli.Context, manager user.Manager, username string) erro
 	return showUserAccess(c, manager, username)
 }
 
-func resetUserTopicAccess(c *cli.Context, manager user.Manager, username string, topic string) error {
+func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string, topic string) error {
 	if err := manager.ResetAccess(username, topic); err != nil {
 		return err
 	}
@@ -156,14 +156,14 @@ func resetUserTopicAccess(c *cli.Context, manager user.Manager, username string,
 	return showUserAccess(c, manager, username)
 }
 
-func showAccess(c *cli.Context, manager user.Manager, username string) error {
+func showAccess(c *cli.Context, manager *user.Manager, username string) error {
 	if username == "" {
 		return showAllAccess(c, manager)
 	}
 	return showUserAccess(c, manager, username)
 }
 
-func showAllAccess(c *cli.Context, manager user.Manager) error {
+func showAllAccess(c *cli.Context, manager *user.Manager) error {
 	users, err := manager.Users()
 	if err != nil {
 		return err
@@ -171,7 +171,7 @@ func showAllAccess(c *cli.Context, manager user.Manager) error {
 	return showUsers(c, manager, users)
 }
 
-func showUserAccess(c *cli.Context, manager user.Manager, username string) error {
+func showUserAccess(c *cli.Context, manager *user.Manager, username string) error {
 	users, err := manager.User(username)
 	if err == user.ErrNotFound {
 		return fmt.Errorf("user %s does not exist", username)
@@ -181,7 +181,7 @@ func showUserAccess(c *cli.Context, manager user.Manager, username string) error
 	return showUsers(c, manager, []*user.User{users})
 }
 
-func showUsers(c *cli.Context, manager user.Manager, users []*user.User) error {
+func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error {
 	for _, u := range users {
 		fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role)
 		if u.Role == user.RoleAdmin {

+ 0 - 1
cmd/subscribe_unix.go

@@ -1,5 +1,4 @@
 //go:build linux || dragonfly || freebsd || netbsd || openbsd
-// +build linux dragonfly freebsd netbsd openbsd
 
 package cmd
 

+ 7 - 7
cmd/user.go

@@ -161,7 +161,7 @@ func execUserAdd(c *cli.Context) error {
 	} else if !user.AllowedRole(role) {
 		return errors.New("role must be either 'user' or 'admin'")
 	}
-	manager, err := createAuthManager(c)
+	manager, err := createUserManager(c)
 	if err != nil {
 		return err
 	}
@@ -190,7 +190,7 @@ func execUserDel(c *cli.Context) error {
 	} else if username == userEveryone {
 		return errors.New("username not allowed")
 	}
-	manager, err := createAuthManager(c)
+	manager, err := createUserManager(c)
 	if err != nil {
 		return err
 	}
@@ -212,7 +212,7 @@ func execUserChangePass(c *cli.Context) error {
 	} else if username == userEveryone {
 		return errors.New("username not allowed")
 	}
-	manager, err := createAuthManager(c)
+	manager, err := createUserManager(c)
 	if err != nil {
 		return err
 	}
@@ -240,7 +240,7 @@ func execUserChangeRole(c *cli.Context) error {
 	} else if username == userEveryone {
 		return errors.New("username not allowed")
 	}
-	manager, err := createAuthManager(c)
+	manager, err := createUserManager(c)
 	if err != nil {
 		return err
 	}
@@ -255,7 +255,7 @@ func execUserChangeRole(c *cli.Context) error {
 }
 
 func execUserList(c *cli.Context) error {
-	manager, err := createAuthManager(c)
+	manager, err := createUserManager(c)
 	if err != nil {
 		return err
 	}
@@ -266,7 +266,7 @@ func execUserList(c *cli.Context) error {
 	return showUsers(c, manager, users)
 }
 
-func createAuthManager(c *cli.Context) (user.Manager, error) {
+func createUserManager(c *cli.Context) (*user.Manager, error) {
 	authFile := c.String("auth-file")
 	authDefaultAccess := c.String("auth-default-access")
 	if authFile == "" {
@@ -278,7 +278,7 @@ func createAuthManager(c *cli.Context) (user.Manager, error) {
 	}
 	authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
 	authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
-	return user.NewSQLiteAuthManager(authFile, authDefaultRead, authDefaultWrite)
+	return user.NewManager(authFile, authDefaultRead, authDefaultWrite)
 }
 
 func readPasswordAndConfirm(c *cli.Context) (string, error) {

+ 0 - 29
go.sum

@@ -1,18 +1,12 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
 cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
-cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
-cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
 cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
 cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
-cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
-cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
 cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
-cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
-cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
 cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs=
 cloud.google.com/go/iam v0.9.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
 cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
@@ -21,14 +15,11 @@ cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcb
 cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
 firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
 firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
-github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
 github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
 github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/MicahParks/keyfunc v1.7.0 h1:LBd4tBj6FwGs2S4GXniQbgrG0PXzIldyGDKWch8slhg=
-github.com/MicahParks/keyfunc v1.7.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
 github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
 github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -84,8 +75,6 @@ github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1V
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
-github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
 github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=
 github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
@@ -94,11 +83,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
-github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
 github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
 github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -109,13 +95,10 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4=
-github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
 github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@@ -124,8 +107,6 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
-golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
 golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -141,13 +122,9 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
-golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
 golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
 golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -164,8 +141,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -184,8 +159,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
-google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
 google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
 google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -197,8 +170,6 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd h1:OjndDrsik+Gt+e6fs45z9AxiewiKyLKYpA45W5Kpkks=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
 google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
 google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

+ 1 - 8
server/file_cache_test.go

@@ -56,13 +56,6 @@ func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) {
 	require.NoFileExists(t, dir+"/abcdefghijkX")
 }
 
-func TestFileCache_Write_FailedFileSizeLimit(t *testing.T) {
-	dir, c := newTestFileCache(t)
-	_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1025)))
-	require.Equal(t, util.ErrLimitReached, err)
-	require.NoFileExists(t, dir+"/abcdefghijkl")
-}
-
 func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) {
 	dir, c := newTestFileCache(t)
 	_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000))
@@ -95,7 +88,7 @@ func TestFileCache_RemoveExpired(t *testing.T) {
 
 func newTestFileCache(t *testing.T) (dir string, cache *fileCache) {
 	dir = t.TempDir()
-	cache, err := newFileCache(dir, 10*1024, 1*1024)
+	cache, err := newFileCache(dir, 10*1024)
 	require.Nil(t, err)
 	return dir, cache
 }

+ 20 - 3
server/message_cache.go

@@ -40,7 +40,7 @@ const (
 			attachment_expires INT NOT NULL,
 			attachment_url TEXT NOT NULL,
 			sender TEXT NOT NULL,
-			user TEXT NOT NULL,		
+			user TEXT NOT NULL,
 			encoding TEXT NOT NULL,
 			published INT NOT NULL
 		);
@@ -95,7 +95,7 @@ const (
 
 // Schema management queries
 const (
-	currentSchemaVersion          = 9
+	currentSchemaVersion          = 10
 	createSchemaVersionTableQuery = `
 		CREATE TABLE IF NOT EXISTS schemaVersion (
 			id INT PRIMARY KEY,
@@ -193,6 +193,11 @@ const (
 	migrate8To9AlterMessagesTableQuery = `
 		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);	
 	`
+
+	// 9 -> 10
+	migrate9To10AlterMessagesTableQuery = `
+		ALTER TABLE messages ADD COLUMN user TEXT NOT NULL DEFAULT('');
+	`
 )
 
 type messageCache struct {
@@ -614,8 +619,9 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
 		return migrateFrom7(db)
 	} else if schemaVersion == 8 {
 		return migrateFrom8(db)
+	} else if schemaVersion == 9 {
+		return migrateFrom9(db)
 	}
-	// TODO add user column
 	return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
 }
 
@@ -731,5 +737,16 @@ func migrateFrom8(db *sql.DB) error {
 	if _, err := db.Exec(updateSchemaVersion, 9); err != nil {
 		return err
 	}
+	return migrateFrom9(db)
+}
+
+func migrateFrom9(db *sql.DB) error {
+	log.Info("Migrating cache database schema: from 9 to 10")
+	if _, err := db.Exec(migrate9To10AlterMessagesTableQuery); err != nil {
+		return err
+	}
+	if _, err := db.Exec(updateSchemaVersion, 10); err != nil {
+		return err
+	}
 	return nil // Update this when a new version is added
 }

+ 55 - 29
server/server.go

@@ -43,12 +43,15 @@ import (
 		"user list" shows * twice
 		"ntfy access everyone user4topic <bla>" twice -> UNIQUE constraint error
 		Account usage not updated "in real time"
+		Attachment expiration based on plan
+		Plan: Keep 10000 messages or keep X days?
 		Sync:
 			- "mute" setting
 			- figure out what settings are "web" or "phone"
 		UI:
 		- Subscription dotmenu dropdown: Move to nav bar, or make same as profile dropdown
 		- "Logout and delete local storage" option
+		- Delete local storage when deleting account
 		Pages:
 		- Home
 		- Password reset
@@ -61,7 +64,8 @@ import (
 		- APIs
 		- CRUD tokens
 		- Expire tokens
-		-
+		- userManager can be nil
+		- visitor with/without user
 */
 
 // Server is the main server, providing the UI and API for ntfy
@@ -77,7 +81,7 @@ type Server struct {
 	visitors          map[string]*visitor // ip:<ip> or user:<user>
 	firebaseClient    *firebaseClient
 	messages          int64
-	userManager       user.Manager
+	userManager       *user.Manager // Might be nil!
 	messageCache      *messageCache
 	fileCache         *fileCache
 	closeChan         chan bool
@@ -165,9 +169,9 @@ func New(conf *Config) (*Server, error) {
 			return nil, err
 		}
 	}
-	var auther user.Manager
+	var userManager *user.Manager
 	if conf.AuthFile != "" {
-		auther, err = user.NewSQLiteAuthManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite)
+		userManager, err = user.NewManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite)
 		if err != nil {
 			return nil, err
 		}
@@ -178,7 +182,7 @@ func New(conf *Config) (*Server, error) {
 		if err != nil {
 			return nil, err
 		}
-		firebaseClient = newFirebaseClient(sender, auther)
+		firebaseClient = newFirebaseClient(sender, userManager)
 	}
 	return &Server{
 		config:         conf,
@@ -187,7 +191,7 @@ func New(conf *Config) (*Server, error) {
 		firebaseClient: firebaseClient,
 		smtpSender:     mailer,
 		topics:         topics,
-		userManager:    auther,
+		userManager:    userManager,
 		visitors:       make(map[string]*visitor),
 	}, nil
 }
@@ -341,27 +345,27 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
 		return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == accountPath {
-		return s.handleAccountCreate(w, r, v)
+		return s.ensureAccountsEnabled(s.handleAccountCreate)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == accountPath {
-		return s.handleAccountGet(w, r, v)
+		return s.handleAccountGet(w, r, v) // Allowed by anonymous
 	} else if r.Method == http.MethodDelete && r.URL.Path == accountPath {
-		return s.handleAccountDelete(w, r, v)
+		return s.ensureWithAccount(s.handleAccountDelete)(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == accountPasswordPath {
-		return s.handleAccountPasswordChange(w, r, v)
+		return s.ensureWithAccount(s.handleAccountPasswordChange)(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == accountTokenPath {
-		return s.handleAccountTokenIssue(w, r, v)
+		return s.ensureWithAccount(s.handleAccountTokenIssue)(w, r, v)
 	} else if r.Method == http.MethodPatch && r.URL.Path == accountTokenPath {
-		return s.handleAccountTokenExtend(w, r, v)
+		return s.ensureWithAccount(s.handleAccountTokenExtend)(w, r, v)
 	} else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath {
-		return s.handleAccountTokenDelete(w, r, v)
+		return s.ensureWithAccount(s.handleAccountTokenDelete)(w, r, v)
 	} else if r.Method == http.MethodPatch && r.URL.Path == accountSettingsPath {
-		return s.handleAccountSettingsChange(w, r, v)
+		return s.ensureWithAccount(s.handleAccountSettingsChange)(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath {
-		return s.handleAccountSubscriptionAdd(w, r, v)
+		return s.ensureWithAccount(s.handleAccountSubscriptionAdd)(w, r, v)
 	} else if r.Method == http.MethodPatch && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
-		return s.handleAccountSubscriptionChange(w, r, v)
+		return s.ensureWithAccount(s.handleAccountSubscriptionChange)(w, r, v)
 	} else if r.Method == http.MethodDelete && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
-		return s.handleAccountSubscriptionDelete(w, r, v)
+		return s.ensureWithAccount(s.handleAccountSubscriptionDelete)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
 		return s.handleMatrixDiscovery(w)
 	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
@@ -804,7 +808,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
 	} else if m.Time > time.Now().Add(s.config.AttachmentExpiryDuration).Unix() {
 		return errHTTPBadRequestAttachmentsExpiryBeforeDelivery
 	}
-	stats, err := v.Stats()
+	stats, err := v.Info()
 	if err != nil {
 		return err
 	}
@@ -1182,7 +1186,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
 	return topics, nil
 }
 
-func (s *Server) updateStatsAndPrune() {
+func (s *Server) execManager() {
 	log.Debug("Manager: Starting")
 	defer log.Debug("Manager: Finished")
 
@@ -1203,8 +1207,10 @@ func (s *Server) updateStatsAndPrune() {
 	log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors)
 
 	// Delete expired user tokens
-	if err := s.userManager.RemoveExpiredTokens(); err != nil {
-		log.Warn("Error expiring user tokens: %s", err.Error())
+	if s.userManager != nil {
+		if err := s.userManager.RemoveExpiredTokens(); err != nil {
+			log.Warn("Error expiring user tokens: %s", err.Error())
+		}
 	}
 
 	// Delete expired attachments
@@ -1293,7 +1299,7 @@ func (s *Server) runManager() {
 	for {
 		select {
 		case <-time.After(s.config.ManagerInterval):
-			s.updateStatsAndPrune()
+			s.execManager()
 		case <-s.closeChan:
 			return
 		}
@@ -1399,6 +1405,24 @@ func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
 	}
 }
 
+func (s *Server) ensureAccountsEnabled(next handleFunc) handleFunc {
+	return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
+		if s.userManager != nil {
+			return errHTTPNotFound
+		}
+		return next(w, r, v)
+	}
+}
+
+func (s *Server) ensureWithAccount(next handleFunc) handleFunc {
+	return s.ensureAccountsEnabled(func(w http.ResponseWriter, r *http.Request, v *visitor) error {
+		if v.user != nil {
+			return errHTTPNotFound
+		}
+		return next(w, r, v)
+	})
+}
+
 // transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
 // before passing it on to the next handler. This is meant to be used in combination with handlePublish.
 func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
@@ -1502,17 +1526,17 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
 // Note that this function will always return a visitor, even if an error occurs.
 func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
 	ip := extractIPAddress(r, s.config.BehindProxy)
-	var user *user.User // may stay nil if no auth header!
-	if user, err = s.authenticate(r); err != nil {
+	var u *user.User // may stay nil if no auth header!
+	if u, err = s.authenticate(r); err != nil {
 		log.Debug("authentication failed: %s", err.Error())
 		err = errHTTPUnauthorized // Always return visitor, even when error occurs!
 	}
-	if user != nil {
-		v = s.visitorFromUser(user, ip)
+	if u != nil {
+		v = s.visitorFromUser(u, ip)
 	} else {
 		v = s.visitorFromIP(ip)
 	}
-	v.user = user // Update user -- FIXME race?
+	v.user = u    // Update user -- FIXME race?
 	return v, err // Always return visitor, even when error occurs!
 }
 
@@ -1521,17 +1545,19 @@ func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
 // support the WebSocket JavaScript class, which does not support passing headers during the initial request. The auth
 // query param is effectively double base64 encoded. Its format is base64(Basic base64(user:pass)).
 func (s *Server) authenticate(r *http.Request) (user *user.User, err error) {
-	value := r.Header.Get("Authorization")
+	value := strings.TrimSpace(r.Header.Get("Authorization"))
 	queryParam := readQueryParam(r, "authorization", "auth")
 	if queryParam != "" {
 		a, err := base64.RawURLEncoding.DecodeString(queryParam)
 		if err != nil {
 			return nil, err
 		}
-		value = string(a)
+		value = strings.TrimSpace(string(a))
 	}
 	if value == "" {
 		return nil, nil
+	} else if s.userManager == nil {
+		return nil, errHTTPUnauthorized
 	}
 	if strings.HasPrefix(value, "Bearer") {
 		return s.authenticateBearerAuth(value)

+ 2 - 2
server/server_account.go

@@ -45,11 +45,11 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
 func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
-	stats, err := v.Stats()
+	stats, err := v.Info()
 	if err != nil {
 		return err
 	}
-	response := &apiAccountSettingsResponse{
+	response := &apiAccountResponse{
 		Stats: &apiAccountStats{
 			Messages:                     stats.Messages,
 			MessagesRemaining:            stats.MessagesRemaining,

+ 3 - 3
server/server_firebase.go

@@ -28,10 +28,10 @@ var (
 // The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable.
 type firebaseClient struct {
 	sender firebaseSender
-	auther user.Manager
+	auther user.Auther
 }
 
-func newFirebaseClient(sender firebaseSender, auther user.Manager) *firebaseClient {
+func newFirebaseClient(sender firebaseSender, auther user.Auther) *firebaseClient {
 	return &firebaseClient{
 		sender: sender,
 		auther: auther,
@@ -112,7 +112,7 @@ func (c *firebaseSenderImpl) Send(m *messaging.Message) error {
 //     On Android, this will trigger the app to poll the topic and thereby displaying new messages.
 //   - If UpstreamBaseURL is set, messages are forwarded as poll requests to an upstream server and then forwarded
 //     to Firebase here. This is mainly for iOS to support self-hosted servers.
-func toFirebaseMessage(m *message, auther user.Manager) (*messaging.Message, error) {
+func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, error) {
 	var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format
 	var apnsConfig *messaging.APNSConfig
 	switch m.Event {

+ 5 - 2
server/server_firebase_test.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"heckel.io/ntfy/user"
 	"net/netip"
 	"strings"
 	"sync"
@@ -17,7 +18,9 @@ type testAuther struct {
 	Allow bool
 }
 
-func (t testAuther) AuthenticateUser(_, _ string) (*user.User, error) {
+var _ user.Auther = (*testAuther)(nil)
+
+func (t testAuther) Authenticate(_, _ string) (*user.User, error) {
 	return nil, errors.New("not used")
 }
 
@@ -323,7 +326,7 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
 func TestToFirebaseSender_Abuse(t *testing.T) {
 	sender := &testFirebaseSender{allowed: 2}
 	client := newFirebaseClient(sender, &testAuther{})
-	visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"))
+	visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"), nil)
 
 	require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
 	require.Equal(t, 1, len(sender.Messages()))

+ 1 - 1
server/server_matrix_test.go

@@ -72,7 +72,7 @@ func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) {
 func TestMatrix_WriteMatrixError(t *testing.T) {
 	w := httptest.NewRecorder()
 	r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil)
-	v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4"))
+	v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4"), nil)
 	require.Nil(t, writeMatrixError(w, r, v, &errMatrix{"https://ntfy.example.com/upABCDEFGHI?up=1", errHTTPBadRequestMatrixPushkeyBaseURLMismatch}))
 	require.Equal(t, 200, w.Result().StatusCode)
 	require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String())

+ 32 - 56
server/server_test.go

@@ -6,6 +6,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"heckel.io/ntfy/user"
 	"io"
 	"log"
 	"math/rand"
@@ -171,7 +172,7 @@ func TestServer_StaticSites(t *testing.T) {
 
 	rr = request(t, s, "GET", "/static/css/home.css", "", nil)
 	require.Equal(t, 200, rr.Code)
-	require.Contains(t, rr.Body.String(), `html, body {`)
+	require.Contains(t, rr.Body.String(), `/* general styling */`)
 
 	rr = request(t, s, "GET", "/docs", "", nil)
 	require.Equal(t, 301, rr.Code)
@@ -353,7 +354,7 @@ func TestServer_PublishAtAndPrune(t *testing.T) {
 		"In": "1h",
 	})
 	require.Equal(t, 200, response.Code)
-	s.updateStatsAndPrune() // Fire pruning
+	s.execManager() // Fire pruning
 
 	response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
 	messages := toMessages(t, response.Body.String())
@@ -625,8 +626,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) {
 	c.AuthFile = filepath.Join(t.TempDir(), "user.db")
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
 		"Authorization": basicAuth("phil:phil"),
@@ -642,9 +642,8 @@ func TestServer_Auth_Success_User(t *testing.T) {
 	c.AuthDefaultWrite = false
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser))
-	require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true))
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
 		"Authorization": basicAuth("ben:ben"),
@@ -659,10 +658,9 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
 	c.AuthDefaultWrite = false
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser))
-	require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true))
-	require.Nil(t, manager.AllowAccess("ben", "anothertopic", true, true))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true))
+	require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", true, true))
 
 	response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{
 		"Authorization": basicAuth("ben:ben"),
@@ -682,8 +680,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
 	c.AuthDefaultWrite = false
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
 		"Authorization": basicAuth("phil:INVALID"),
@@ -698,9 +695,8 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
 	c.AuthDefaultWrite = false
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser))
-	require.Nil(t, manager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic!
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic!
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
 		"Authorization": basicAuth("ben:ben"),
@@ -715,10 +711,9 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
 	c.AuthDefaultWrite = true // Open by default
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
-	require.Nil(t, manager.AllowAccess(user.Everyone, "private", false, false))
-	require.Nil(t, manager.AllowAccess(user.Everyone, "announcements", true, false))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
+	require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", false, false))
+	require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", true, false))
 
 	response := request(t, s, "PUT", "/mytopic", "test", nil)
 	require.Equal(t, 200, response.Code)
@@ -748,8 +743,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
 	c.AuthDefaultWrite = false
 	s := newTestServer(t, c)
 
-	manager := s.userManager.(user.Manager)
-	require.Nil(t, manager.AddUser("ben", "some pass", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin))
 
 	u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass"))))
 	response := request(t, s, "GET", u, "", nil)
@@ -760,27 +754,6 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
 	require.Equal(t, 401, response.Code)
 }
 
-/*
-func TestServer_Curl_Publish_Poll(t *testing.T) {
-	s, port := test.StartServer(t)
-	defer test.StopServer(t, s, port)
-
-	cmd := exec.Command("sh", "-c", fmt.Sprintf(`curl -sd "This is a test" localhost:%d/mytopic`, port))
-	require.Nil(t, cmd.Run())
-	b, err := cmd.CombinedOutput()
-	require.Nil(t, err)
-	msg := toMessage(t, string(b))
-	require.Equal(t, "This is a test", msg.Message)
-
-	cmd = exec.Command("sh", "-c", fmt.Sprintf(`curl "localhost:%d/mytopic?poll=1"`, port))
-	require.Nil(t, cmd.Run())
-	b, err = cmd.CombinedOutput()
-	require.Nil(t, err)
-	msg = toMessage(t, string(b))
-	require.Equal(t, "This is a test", msg.Message)
-}
-*/
-
 type testMailer struct {
 	count int
 	mu    sync.Mutex
@@ -1306,7 +1279,7 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
 
 	// Prune and makes sure it's gone
 	time.Sleep(time.Second) // Sigh ...
-	s.updateStatsAndPrune()
+	s.execManager()
 	require.NoFileExists(t, file)
 	response = request(t, s, "GET", path, "", nil)
 	require.Equal(t, 404, response.Code)
@@ -1360,7 +1333,7 @@ func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) {
 	require.Equal(t, 41301, err.Code)
 }
 
-func TestServer_PublishAttachmentUserStats(t *testing.T) {
+func TestServer_PublishAttachmentAccountStats(t *testing.T) {
 	content := util.RandomString(4999) // > 4096
 
 	c := newTestConfig(t)
@@ -1374,14 +1347,14 @@ func TestServer_PublishAttachmentUserStats(t *testing.T) {
 	require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
 
 	// User stats
-	response = request(t, s, "GET", "/user/stats", "", nil)
+	response = request(t, s, "GET", "/v1/account", "", nil)
 	require.Equal(t, 200, response.Code)
-	var stats visitorStats
-	require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&stats))
-	require.Equal(t, int64(5000), stats.AttachmentFileSizeLimit)
-	require.Equal(t, int64(6000), stats.VisitorAttachmentBytesTotal)
-	require.Equal(t, int64(4999), stats.AttachmentBytes)
-	require.Equal(t, int64(1001), stats.VisitorAttachmentBytesRemaining)
+	var account *apiAccountResponse
+	require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&account))
+	require.Equal(t, int64(5000), account.Limits.AttachmentFileSize)
+	require.Equal(t, int64(6000), account.Limits.AttachmentTotalSize)
+	require.Equal(t, int64(4999), account.Stats.AttachmentTotalSize)
+	require.Equal(t, int64(1001), account.Stats.AttachmentTotalSizeRemaining)
 }
 
 func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
@@ -1391,7 +1364,8 @@ func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/bla", nil)
 	r.RemoteAddr = "8.9.10.11"
 	r.Header.Set("X-Forwarded-For", "  ") // Spaces, not empty!
-	v := s.visitor(r)
+	v, err := s.visitor(r)
+	require.Nil(t, err)
 	require.Equal(t, "8.9.10.11", v.ip.String())
 }
 
@@ -1402,7 +1376,8 @@ func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/bla", nil)
 	r.RemoteAddr = "8.9.10.11"
 	r.Header.Set("X-Forwarded-For", "1.1.1.1")
-	v := s.visitor(r)
+	v, err := s.visitor(r)
+	require.Nil(t, err)
 	require.Equal(t, "1.1.1.1", v.ip.String())
 }
 
@@ -1413,7 +1388,8 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/bla", nil)
 	r.RemoteAddr = "8.9.10.11"
 	r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ")
-	v := s.visitor(r)
+	v, err := s.visitor(r)
+	require.Nil(t, err)
 	require.Equal(t, "234.5.2.1", v.ip.String())
 }
 
@@ -1442,7 +1418,7 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
 	go func() {
 		log.Printf("Updating stats")
 		start := time.Now()
-		s.updateStatsAndPrune()
+		s.execManager()
 		log.Printf("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond))
 		statsChan <- true
 	}()

+ 1 - 1
server/types.go

@@ -252,7 +252,7 @@ type apiAccountStats struct {
 	AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
 }
 
-type apiAccountSettingsResponse struct {
+type apiAccountResponse struct {
 	Username      string                  `json:"username"`
 	Role          string                  `json:"role,omitempty"`
 	Language      string                  `json:"language,omitempty"`

+ 25 - 25
server/visitor.go

@@ -40,7 +40,7 @@ type visitor struct {
 	mu                  sync.Mutex
 }
 
-type visitorStats struct {
+type visitorInfo struct {
 	Basis                        string // "ip", "role" or "plan"
 	Messages                     int64
 	MessagesLimit                int64
@@ -165,30 +165,30 @@ func (v *visitor) IncrEmails() {
 	}
 }
 
-func (v *visitor) Stats() (*visitorStats, error) {
+func (v *visitor) Info() (*visitorInfo, error) {
 	v.mu.Lock()
 	messages := v.messages
 	emails := v.emails
 	v.mu.Unlock()
-	stats := &visitorStats{}
+	info := &visitorInfo{}
 	if v.user != nil && v.user.Role == user.RoleAdmin {
-		stats.Basis = "role"
-		stats.MessagesLimit = 0
-		stats.EmailsLimit = 0
-		stats.AttachmentTotalSizeLimit = 0
-		stats.AttachmentFileSizeLimit = 0
+		info.Basis = "role"
+		info.MessagesLimit = 0
+		info.EmailsLimit = 0
+		info.AttachmentTotalSizeLimit = 0
+		info.AttachmentFileSizeLimit = 0
 	} else if v.user != nil && v.user.Plan != nil {
-		stats.Basis = "plan"
-		stats.MessagesLimit = v.user.Plan.MessagesLimit
-		stats.EmailsLimit = v.user.Plan.EmailsLimit
-		stats.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit
-		stats.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit
+		info.Basis = "plan"
+		info.MessagesLimit = v.user.Plan.MessagesLimit
+		info.EmailsLimit = v.user.Plan.EmailsLimit
+		info.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit
+		info.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit
 	} else {
-		stats.Basis = "ip"
-		stats.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish)
-		stats.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish)
-		stats.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
-		stats.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
+		info.Basis = "ip"
+		info.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish)
+		info.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish)
+		info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
+		info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
 	}
 	var attachmentsBytesUsed int64
 	var err error
@@ -200,13 +200,13 @@ func (v *visitor) Stats() (*visitorStats, error) {
 	if err != nil {
 		return nil, err
 	}
-	stats.Messages = messages
-	stats.MessagesRemaining = zeroIfNegative(stats.MessagesLimit - stats.Messages)
-	stats.Emails = emails
-	stats.EmailsRemaining = zeroIfNegative(stats.EmailsLimit - stats.Emails)
-	stats.AttachmentTotalSize = attachmentsBytesUsed
-	stats.AttachmentTotalSizeRemaining = zeroIfNegative(stats.AttachmentTotalSizeLimit - stats.AttachmentTotalSize)
-	return stats, nil
+	info.Messages = messages
+	info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)
+	info.Emails = emails
+	info.EmailsRemaining = zeroIfNegative(info.EmailsLimit - info.Emails)
+	info.AttachmentTotalSize = attachmentsBytesUsed
+	info.AttachmentTotalSizeRemaining = zeroIfNegative(info.AttachmentTotalSizeLimit - info.AttachmentTotalSize)
+	return info, nil
 }
 
 func zeroIfNegative(value int64) int64 {

+ 31 - 31
user/manager.go

@@ -121,9 +121,9 @@ const (
 	selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
 )
 
-// SQLiteManager is an implementation of Manager. It stores users and access control list
+// Manager is an implementation of Manager. It stores users and access control list
 // in a SQLite database.
-type SQLiteManager struct {
+type Manager struct {
 	db           *sql.DB
 	defaultRead  bool
 	defaultWrite bool
@@ -131,10 +131,10 @@ type SQLiteManager struct {
 	mu           sync.Mutex
 }
 
-var _ Manager = (*SQLiteManager)(nil)
+var _ Auther = (*Manager)(nil)
 
-// NewSQLiteAuthManager creates a new SQLiteManager instance
-func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQLiteManager, error) {
+// NewManager creates a new Manager instance
+func NewManager(filename string, defaultRead, defaultWrite bool) (*Manager, error) {
 	db, err := sql.Open("sqlite3", filename)
 	if err != nil {
 		return nil, err
@@ -142,7 +142,7 @@ func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQL
 	if err := setupAuthDB(db); err != nil {
 		return nil, err
 	}
-	manager := &SQLiteManager{
+	manager := &Manager{
 		db:           db,
 		defaultRead:  defaultRead,
 		defaultWrite: defaultWrite,
@@ -155,7 +155,7 @@ func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQL
 // Authenticate checks username and password and returns a user if correct. The method
 // returns in constant-ish time, regardless of whether the user exists or the password is
 // correct or incorrect.
-func (a *SQLiteManager) Authenticate(username, password string) (*User, error) {
+func (a *Manager) Authenticate(username, password string) (*User, error) {
 	if username == Everyone {
 		return nil, ErrUnauthenticated
 	}
@@ -171,7 +171,7 @@ func (a *SQLiteManager) Authenticate(username, password string) (*User, error) {
 	return user, nil
 }
 
-func (a *SQLiteManager) AuthenticateToken(token string) (*User, error) {
+func (a *Manager) AuthenticateToken(token string) (*User, error) {
 	user, err := a.userByToken(token)
 	if err != nil {
 		return nil, ErrUnauthenticated
@@ -180,7 +180,7 @@ func (a *SQLiteManager) AuthenticateToken(token string) (*User, error) {
 	return user, nil
 }
 
-func (a *SQLiteManager) CreateToken(user *User) (*Token, error) {
+func (a *Manager) CreateToken(user *User) (*Token, error) {
 	token := util.RandomString(tokenLength)
 	expires := time.Now().Add(userTokenExpiryDuration)
 	if _, err := a.db.Exec(insertTokenQuery, user.Name, token, expires.Unix()); err != nil {
@@ -192,7 +192,7 @@ func (a *SQLiteManager) CreateToken(user *User) (*Token, error) {
 	}, nil
 }
 
-func (a *SQLiteManager) ExtendToken(user *User) (*Token, error) {
+func (a *Manager) ExtendToken(user *User) (*Token, error) {
 	newExpires := time.Now().Add(userTokenExpiryDuration)
 	if _, err := a.db.Exec(updateTokenExpiryQuery, newExpires.Unix(), user.Name, user.Token); err != nil {
 		return nil, err
@@ -203,7 +203,7 @@ func (a *SQLiteManager) ExtendToken(user *User) (*Token, error) {
 	}, nil
 }
 
-func (a *SQLiteManager) RemoveToken(user *User) error {
+func (a *Manager) RemoveToken(user *User) error {
 	if user.Token == "" {
 		return ErrUnauthorized
 	}
@@ -213,14 +213,14 @@ func (a *SQLiteManager) RemoveToken(user *User) error {
 	return nil
 }
 
-func (a *SQLiteManager) RemoveExpiredTokens() error {
+func (a *Manager) RemoveExpiredTokens() error {
 	if _, err := a.db.Exec(deleteExpiredTokensQuery, time.Now().Unix()); err != nil {
 		return err
 	}
 	return nil
 }
 
-func (a *SQLiteManager) ChangeSettings(user *User) error {
+func (a *Manager) ChangeSettings(user *User) error {
 	settings, err := json.Marshal(user.Prefs)
 	if err != nil {
 		return err
@@ -231,13 +231,13 @@ func (a *SQLiteManager) ChangeSettings(user *User) error {
 	return nil
 }
 
-func (a *SQLiteManager) EnqueueStats(user *User) {
+func (a *Manager) EnqueueStats(user *User) {
 	a.mu.Lock()
 	defer a.mu.Unlock()
 	a.statsQueue[user.Name] = user
 }
 
-func (a *SQLiteManager) userStatsQueueWriter() {
+func (a *Manager) userStatsQueueWriter() {
 	ticker := time.NewTicker(userStatsQueueWriterInterval)
 	for range ticker.C {
 		if err := a.writeUserStatsQueue(); err != nil {
@@ -246,7 +246,7 @@ func (a *SQLiteManager) userStatsQueueWriter() {
 	}
 }
 
-func (a *SQLiteManager) writeUserStatsQueue() error {
+func (a *Manager) writeUserStatsQueue() error {
 	a.mu.Lock()
 	if len(a.statsQueue) == 0 {
 		a.mu.Unlock()
@@ -273,7 +273,7 @@ func (a *SQLiteManager) writeUserStatsQueue() error {
 
 // Authorize returns nil if the given user has access to the given topic using the desired
 // permission. The user param may be nil to signal an anonymous user.
-func (a *SQLiteManager) Authorize(user *User, topic string, perm Permission) error {
+func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
 	if user != nil && user.Role == RoleAdmin {
 		return nil // Admin can do everything
 	}
@@ -301,7 +301,7 @@ func (a *SQLiteManager) Authorize(user *User, topic string, perm Permission) err
 	return a.resolvePerms(read, write, perm)
 }
 
-func (a *SQLiteManager) resolvePerms(read, write bool, perm Permission) error {
+func (a *Manager) resolvePerms(read, write bool, perm Permission) error {
 	if perm == PermissionRead && read {
 		return nil
 	} else if perm == PermissionWrite && write {
@@ -312,7 +312,7 @@ func (a *SQLiteManager) resolvePerms(read, write bool, perm Permission) error {
 
 // AddUser adds a user with the given username, password and role. The password should be hashed
 // before it is stored in a persistence layer.
-func (a *SQLiteManager) AddUser(username, password string, role Role) error {
+func (a *Manager) AddUser(username, password string, role Role) error {
 	if !AllowedUsername(username) || !AllowedRole(role) {
 		return ErrInvalidArgument
 	}
@@ -328,7 +328,7 @@ func (a *SQLiteManager) AddUser(username, password string, role Role) error {
 
 // RemoveUser deletes the user with the given username. The function returns nil on success, even
 // if the user did not exist in the first place.
-func (a *SQLiteManager) RemoveUser(username string) error {
+func (a *Manager) RemoveUser(username string) error {
 	if !AllowedUsername(username) {
 		return ErrInvalidArgument
 	}
@@ -345,7 +345,7 @@ func (a *SQLiteManager) RemoveUser(username string) error {
 }
 
 // Users returns a list of users. It always also returns the Everyone user ("*").
-func (a *SQLiteManager) Users() ([]*User, error) {
+func (a *Manager) Users() ([]*User, error) {
 	rows, err := a.db.Query(selectUsernamesQuery)
 	if err != nil {
 		return nil, err
@@ -380,7 +380,7 @@ func (a *SQLiteManager) Users() ([]*User, error) {
 
 // User returns the user with the given username if it exists, or ErrNotFound otherwise.
 // You may also pass Everyone to retrieve the anonymous user and its Grant list.
-func (a *SQLiteManager) User(username string) (*User, error) {
+func (a *Manager) User(username string) (*User, error) {
 	if username == Everyone {
 		return a.everyoneUser()
 	}
@@ -391,7 +391,7 @@ func (a *SQLiteManager) User(username string) (*User, error) {
 	return a.readUser(rows)
 }
 
-func (a *SQLiteManager) userByToken(token string) (*User, error) {
+func (a *Manager) userByToken(token string) (*User, error) {
 	rows, err := a.db.Query(selectUserByTokenQuery, token)
 	if err != nil {
 		return nil, err
@@ -399,7 +399,7 @@ func (a *SQLiteManager) userByToken(token string) (*User, error) {
 	return a.readUser(rows)
 }
 
-func (a *SQLiteManager) readUser(rows *sql.Rows) (*User, error) {
+func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 	defer rows.Close()
 	var username, hash, role string
 	var settings, planCode sql.NullString
@@ -446,7 +446,7 @@ func (a *SQLiteManager) readUser(rows *sql.Rows) (*User, error) {
 	return user, nil
 }
 
-func (a *SQLiteManager) everyoneUser() (*User, error) {
+func (a *Manager) everyoneUser() (*User, error) {
 	grants, err := a.readGrants(Everyone)
 	if err != nil {
 		return nil, err
@@ -459,7 +459,7 @@ func (a *SQLiteManager) everyoneUser() (*User, error) {
 	}, nil
 }
 
-func (a *SQLiteManager) readGrants(username string) ([]Grant, error) {
+func (a *Manager) readGrants(username string) ([]Grant, error) {
 	rows, err := a.db.Query(selectUserAccessQuery, username)
 	if err != nil {
 		return nil, err
@@ -484,7 +484,7 @@ func (a *SQLiteManager) readGrants(username string) ([]Grant, error) {
 }
 
 // ChangePassword changes a user's password
-func (a *SQLiteManager) ChangePassword(username, password string) error {
+func (a *Manager) ChangePassword(username, password string) error {
 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
 	if err != nil {
 		return err
@@ -497,7 +497,7 @@ func (a *SQLiteManager) ChangePassword(username, password string) error {
 
 // ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
 // all existing access control entries (Grant) are removed, since they are no longer needed.
-func (a *SQLiteManager) ChangeRole(username string, role Role) error {
+func (a *Manager) ChangeRole(username string, role Role) error {
 	if !AllowedUsername(username) || !AllowedRole(role) {
 		return ErrInvalidArgument
 	}
@@ -514,7 +514,7 @@ func (a *SQLiteManager) ChangeRole(username string, role Role) error {
 
 // AllowAccess adds or updates an entry in th access control list for a specific user. It controls
 // read/write access to a topic. The parameter topicPattern may include wildcards (*).
-func (a *SQLiteManager) AllowAccess(username string, topicPattern string, read bool, write bool) error {
+func (a *Manager) AllowAccess(username string, topicPattern string, read bool, write bool) error {
 	if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) {
 		return ErrInvalidArgument
 	}
@@ -526,7 +526,7 @@ func (a *SQLiteManager) AllowAccess(username string, topicPattern string, read b
 
 // ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
 // empty) for an entire user. The parameter topicPattern may include wildcards (*).
-func (a *SQLiteManager) ResetAccess(username string, topicPattern string) error {
+func (a *Manager) ResetAccess(username string, topicPattern string) error {
 	if !AllowedUsername(username) && username != Everyone && username != "" {
 		return ErrInvalidArgument
 	} else if !AllowedTopicPattern(topicPattern) && topicPattern != "" {
@@ -544,7 +544,7 @@ func (a *SQLiteManager) ResetAccess(username string, topicPattern string) error
 }
 
 // DefaultAccess returns the default read/write access if no access control entry matches
-func (a *SQLiteManager) DefaultAccess() (read bool, write bool) {
+func (a *Manager) DefaultAccess() (read bool, write bool) {
 	return a.defaultRead, a.defaultWrite
 }
 

+ 3 - 2
user/manager_test.go

@@ -2,6 +2,7 @@ package user_test
 
 import (
 	"github.com/stretchr/testify/require"
+	"heckel.io/ntfy/user"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -234,9 +235,9 @@ func TestSQLiteAuth_ChangeRole(t *testing.T) {
 	require.Equal(t, 0, len(ben.Grants))
 }
 
-func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *user.SQLiteAuthManager {
+func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *user.Manager {
 	filename := filepath.Join(t.TempDir(), "user.db")
-	a, err := user.NewSQLiteAuthManager(filename, defaultRead, defaultWrite)
+	a, err := user.NewManager(filename, defaultRead, defaultWrite)
 	require.Nil(t, err)
 	return a
 }

+ 1 - 43
user/types.go

@@ -6,57 +6,15 @@ import (
 	"regexp"
 )
 
-// Manager is a generic interface to implement password and token based authentication and authorization
-type Manager interface {
+type Auther interface {
 	// Authenticate checks username and password and returns a user if correct. The method
 	// returns in constant-ish time, regardless of whether the user exists or the password is
 	// correct or incorrect.
 	Authenticate(username, password string) (*User, error)
 
-	AuthenticateToken(token string) (*User, error)
-	CreateToken(user *User) (*Token, error)
-	ExtendToken(user *User) (*Token, error)
-	RemoveToken(user *User) error
-	RemoveExpiredTokens() error
-	ChangeSettings(user *User) error
-	EnqueueStats(user *User)
-
 	// Authorize returns nil if the given user has access to the given topic using the desired
 	// permission. The user param may be nil to signal an anonymous user.
 	Authorize(user *User, topic string, perm Permission) error
-
-	// AddUser adds a user with the given username, password and role. The password should be hashed
-	// before it is stored in a persistence layer.
-	AddUser(username, password string, role Role) error
-
-	// RemoveUser deletes the user with the given username. The function returns nil on success, even
-	// if the user did not exist in the first place.
-	RemoveUser(username string) error
-
-	// Users returns a list of users. It always also returns the Everyone user ("*").
-	Users() ([]*User, error)
-
-	// User returns the user with the given username if it exists, or ErrNotFound otherwise.
-	// You may also pass Everyone to retrieve the anonymous user and its Grant list.
-	User(username string) (*User, error)
-
-	// ChangePassword changes a user's password
-	ChangePassword(username, password string) error
-
-	// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
-	// all existing access control entries (Grant) are removed, since they are no longer needed.
-	ChangeRole(username string, role Role) error
-
-	// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
-	// read/write access to a topic. The parameter topicPattern may include wildcards (*).
-	AllowAccess(username string, topicPattern string, read bool, write bool) error
-
-	// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
-	// empty) for an entire user. The parameter topicPattern may include wildcards (*).
-	ResetAccess(username string, topicPattern string) error
-
-	// DefaultAccess returns the default read/write access if no access control entry matches
-	DefaultAccess() (read bool, write bool)
 }
 
 // User is a struct that represents a user