"ntfy tier" CLI command
parent
e3b39f670f
commit
a32e8abc12
|
@ -8,20 +8,27 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||||
testMessage := util.RandomString(10)
|
testMessage := util.RandomString(10)
|
||||||
|
|
||||||
app, _, _, _ := newTestApp()
|
app, _, _, _ := newTestApp()
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
||||||
time.Sleep(3 * time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
|
|
||||||
|
|
||||||
app2, _, stdout, _ := newTestApp()
|
_, err := util.Retry(func() (*int, error) {
|
||||||
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
app2, _, stdout, _ := newTestApp()
|
||||||
require.Contains(t, stdout.String(), testMessage)
|
if err := app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), testMessage) {
|
||||||
|
return nil, fmt.Errorf("test message %s not found in topic", testMessage)
|
||||||
|
}
|
||||||
|
return util.Int(1), nil
|
||||||
|
}, time.Second, 2*time.Second, 5*time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
|
||||||
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||||
|
|
73
cmd/tier.go
73
cmd/tier.go
|
@ -56,8 +56,27 @@ var cmdTier = &cli.Command{
|
||||||
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
||||||
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
|
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `Add a new tier to the ntfy user database.
|
||||||
FIXME
|
|
||||||
|
Tiers can be used to grant users higher limits based, such as daily message limits, attachment size, or
|
||||||
|
make it possible for users to reserve topics.
|
||||||
|
|
||||||
|
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||||
|
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ntfy tier add pro # Add tier with code "pro", using the defaults
|
||||||
|
ntfy tier add \ # Add a tier with custom limits
|
||||||
|
--name="Pro" \
|
||||||
|
--message-limit=10000 \
|
||||||
|
--message-expiry-duration=24h \
|
||||||
|
--email-limit=50 \
|
||||||
|
--reservation-limit=10 \
|
||||||
|
--attachment-file-size-limit=100M \
|
||||||
|
--attachment-total-size-limit=1G \
|
||||||
|
--attachment-expiry-duration=12h \
|
||||||
|
--attachment-bandwidth-limit=5G \
|
||||||
|
pro
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -78,8 +97,20 @@ FIXME
|
||||||
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
||||||
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
|
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `Updates a tier to change the limits.
|
||||||
FIXME
|
|
||||||
|
After updating a tier, you may have to restart the ntfy server to apply them
|
||||||
|
to all visitors.
|
||||||
|
|
||||||
|
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||||
|
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ntfy tier change --name="Pro" pro # Update the name of an existing tier
|
||||||
|
ntfy tier change \ # Update multiple limits and fields
|
||||||
|
--message-expiry-duration=24h \
|
||||||
|
--stripe-price-id=price_1234 \
|
||||||
|
pro
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -88,8 +119,16 @@ FIXME
|
||||||
Usage: "Removes a tier",
|
Usage: "Removes a tier",
|
||||||
UsageText: "ntfy tier remove CODE",
|
UsageText: "ntfy tier remove CODE",
|
||||||
Action: execTierDel,
|
Action: execTierDel,
|
||||||
Description: `
|
Description: `Remove a tier from the ntfy user database.
|
||||||
FIXME
|
|
||||||
|
You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
|
||||||
|
to remove or switch their tier first.
|
||||||
|
|
||||||
|
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||||
|
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
ntfy tier del pro
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -97,22 +136,26 @@ FIXME
|
||||||
Aliases: []string{"l"},
|
Aliases: []string{"l"},
|
||||||
Usage: "Shows a list of tiers",
|
Usage: "Shows a list of tiers",
|
||||||
Action: execTierList,
|
Action: execTierList,
|
||||||
Description: `
|
Description: `Shows a list of all configured tiers.
|
||||||
FIXME
|
|
||||||
|
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||||
|
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Description: `Manage tier of the ntfy server.
|
Description: `Manage tiers of the ntfy server.
|
||||||
|
|
||||||
The command allows you to add/remove/change tier in the ntfy user database. Tiers are used
|
The command allows you to add/remove/change tiers in the ntfy user database. Tiers are used
|
||||||
to grant users higher limits based on their tier.
|
to grant users higher limits, such as daily message limits, attachment size, or make it
|
||||||
|
possible for users to reserve topics.
|
||||||
|
|
||||||
This is a server-only command. It directly manages the user.db as defined in the server config
|
This is a server-only command. It directly manages the user.db as defined in the server config
|
||||||
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
|
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||||
to the related command 'ntfy access'.
|
|
||||||
|
|
||||||
FIXME
|
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ntfy tier add pro # Add tier with code "pro", using the defaults
|
||||||
|
ntfy tier change --name="Pro" pro # Update the name of an existing tier
|
||||||
|
ntfy tier del pro # Delete an existing tier
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
|
||||||
|
s, conf, port := newTestServerWithAuth(t)
|
||||||
|
defer test.StopServer(t, s, port)
|
||||||
|
|
||||||
|
app, _, _, stderr := newTestApp()
|
||||||
|
require.Nil(t, runTierCommand(app, conf, "add", "--name", "Pro", "--message-limit", "1234", "pro"))
|
||||||
|
require.Contains(t, stderr.String(), "tier added\n\ntier pro (id: ti_")
|
||||||
|
|
||||||
|
err := runTierCommand(app, conf, "add", "pro")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.Equal(t, "tier pro already exists", err.Error())
|
||||||
|
|
||||||
|
app, _, _, stderr = newTestApp()
|
||||||
|
require.Nil(t, runTierCommand(app, conf, "list"))
|
||||||
|
require.Contains(t, stderr.String(), "tier pro (id: ti_")
|
||||||
|
require.Contains(t, stderr.String(), "- Name: Pro")
|
||||||
|
require.Contains(t, stderr.String(), "- Message limit: 1234")
|
||||||
|
|
||||||
|
app, _, _, stderr = newTestApp()
|
||||||
|
require.Nil(t, runTierCommand(app, conf, "change", "--message-limit", "999", "pro"))
|
||||||
|
require.Contains(t, stderr.String(), "- Message limit: 999")
|
||||||
|
|
||||||
|
app, _, _, stderr = newTestApp()
|
||||||
|
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
|
||||||
|
require.Contains(t, stderr.String(), "tier pro removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTierCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||||
|
userArgs := []string{
|
||||||
|
"ntfy",
|
||||||
|
"tier",
|
||||||
|
"--auth-file=" + conf.AuthFile,
|
||||||
|
"--auth-default-access=" + conf.AuthDefault.String(),
|
||||||
|
}
|
||||||
|
return app.Run(append(userArgs, args...))
|
||||||
|
}
|
12
cmd/user.go
12
cmd/user.go
|
@ -139,22 +139,22 @@ Example:
|
||||||
Action: execUserList,
|
Action: execUserList,
|
||||||
Description: `Shows a list of all configured users, including the everyone ('*') user.
|
Description: `Shows a list of all configured users, including the everyone ('*') user.
|
||||||
|
|
||||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
|
||||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
|
||||||
|
|
||||||
This command is an alias to calling 'ntfy access' (display access control list).
|
This command is an alias to calling 'ntfy access' (display access control list).
|
||||||
|
|
||||||
|
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||||
|
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Description: `Manage users of the ntfy server.
|
Description: `Manage users of the ntfy server.
|
||||||
|
|
||||||
|
The command allows you to add/remove/change users in the ntfy user database, as well as change
|
||||||
|
passwords or roles.
|
||||||
|
|
||||||
This is a server-only command. It directly manages the user.db as defined in the server config
|
This is a server-only command. It directly manages the user.db as defined in the server config
|
||||||
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
|
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
|
||||||
to the related command 'ntfy access'.
|
to the related command 'ntfy access'.
|
||||||
|
|
||||||
The command allows you to add/remove/change users in the ntfy user database, as well as change
|
|
||||||
passwords or roles.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ntfy user list # Shows list of users (alias: 'ntfy access')
|
ntfy user list # Shows list of users (alias: 'ntfy access')
|
||||||
ntfy user add phil # Add regular user phil
|
ntfy user add phil # Add regular user phil
|
||||||
|
|
14
log/event.go
14
log/event.go
|
@ -11,13 +11,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tagField = "tag"
|
tagField = "tag"
|
||||||
errorField = "error"
|
errorField = "error"
|
||||||
|
timestampFormat = "2006-01-02T15:04:05.999Z07:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event represents a single log event
|
// Event represents a single log event
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Timestamp int64 `json:"time"`
|
Timestamp string `json:"time"`
|
||||||
Level Level `json:"level"`
|
Level Level `json:"level"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
fields Context
|
fields Context
|
||||||
|
@ -25,8 +26,9 @@ type Event struct {
|
||||||
|
|
||||||
// newEvent creates a new log event
|
// newEvent creates a new log event
|
||||||
func newEvent() *Event {
|
func newEvent() *Event {
|
||||||
|
now := time.Now()
|
||||||
return &Event{
|
return &Event{
|
||||||
Timestamp: time.Now().UnixMilli(),
|
Timestamp: now.Format(timestampFormat),
|
||||||
fields: make(Context),
|
fields: make(Context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,8 +72,8 @@ func (e *Event) Tag(tag string) *Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time sets the time field
|
// Time sets the time field
|
||||||
func (e *Event) Time(time time.Time) *Event {
|
func (e *Event) Time(t time.Time) *Event {
|
||||||
e.Timestamp = time.UnixMilli()
|
e.Timestamp = t.Format(timestampFormat)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ func TestLog_TagContextFieldFields(t *testing.T) {
|
||||||
Tag("mytag").
|
Tag("mytag").
|
||||||
Field("field2", 123).
|
Field("field2", 123).
|
||||||
Field("field1", "value1").
|
Field("field1", "value1").
|
||||||
Time(time.Unix(123, 0)).
|
Time(time.Unix(123, 999000000).UTC()).
|
||||||
Info("hi there %s", "phil")
|
Info("hi there %s", "phil")
|
||||||
log.
|
log.
|
||||||
Tag("not-stripe").
|
Tag("not-stripe").
|
||||||
|
@ -48,11 +48,11 @@ func TestLog_TagContextFieldFields(t *testing.T) {
|
||||||
}).
|
}).
|
||||||
Tag("stripe").
|
Tag("stripe").
|
||||||
Err(err).
|
Err(err).
|
||||||
Time(time.Unix(456, 0)).
|
Time(time.Unix(456, 123000000).UTC()).
|
||||||
Debug("Subscription status %s", "active")
|
Debug("Subscription status %s", "active")
|
||||||
|
|
||||||
expected := `{"time":123000,"level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
|
expected := `{"time":"1970-01-01T00:02:03.999Z","level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
|
||||||
{"time":456000,"level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
|
{"time":"1970-01-01T00:07:36.123Z","level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
|
||||||
`
|
`
|
||||||
require.Equal(t, expected, out.String())
|
require.Equal(t, expected, out.String())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,9 @@ func logv(v *visitor) *log.Event {
|
||||||
|
|
||||||
// logr creates a new log event with HTTP request and visitor fields
|
// logr creates a new log event with HTTP request and visitor fields
|
||||||
func logvr(v *visitor, r *http.Request) *log.Event {
|
func logvr(v *visitor, r *http.Request) *log.Event {
|
||||||
return logv(v).Fields(httpContext(r))
|
return logv(v).
|
||||||
|
Fields(httpContext(r)).
|
||||||
|
Fields(requestLimiterFields(v.RequestLimiter()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// logvrm creates a new log event with HTTP request, visitor fields and message fields
|
// logvrm creates a new log event with HTTP request, visitor fields and message fields
|
||||||
|
|
|
@ -37,7 +37,7 @@ import (
|
||||||
- HIGH Rate limiting: Sensitive endpoints (account/login/change-password/...)
|
- HIGH Rate limiting: Sensitive endpoints (account/login/change-password/...)
|
||||||
- HIGH Account limit creation triggers when account is taken!
|
- HIGH Account limit creation triggers when account is taken!
|
||||||
- HIGH Docs
|
- HIGH Docs
|
||||||
- HIGH CLI "ntfy tier [add|list|delete]"
|
- HIGH make request limit independent of message limit again
|
||||||
- HIGH Self-review
|
- HIGH Self-review
|
||||||
- MEDIUM: Test for expiring messages after reservation removal
|
- MEDIUM: Test for expiring messages after reservation removal
|
||||||
- MEDIUM: Test new token endpoints & never-expiring token
|
- MEDIUM: Test new token endpoints & never-expiring token
|
||||||
|
@ -235,8 +235,8 @@ func (s *Server) Run() error {
|
||||||
}
|
}
|
||||||
log.Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
|
log.Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
|
||||||
if log.IsFile() {
|
if log.IsFile() {
|
||||||
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s, log file is %s\n", listenStr, s.config.Version, log.File())
|
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
|
||||||
fmt.Fprintln(os.Stderr, "No more output is expected.")
|
fmt.Fprintf(os.Stderr, "Logs are written to %s\n", log.File())
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", s.handle)
|
mux.HandleFunc("/", s.handle)
|
||||||
|
|
Loading…
Reference in New Issue