Introduce Reservation
This commit is contained in:
parent
1256ba0429
commit
1733323132
11 changed files with 194 additions and 109 deletions
|
@ -183,11 +183,15 @@ func showUserAccess(c *cli.Context, manager *user.Manager, username string) erro
|
||||||
|
|
||||||
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 {
|
for _, u := range users {
|
||||||
|
grants, err := manager.Grants(u.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role)
|
fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role)
|
||||||
if u.Role == user.RoleAdmin {
|
if u.Role == user.RoleAdmin {
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
|
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
|
||||||
} else if len(u.Grants) > 0 {
|
} else if len(grants) > 0 {
|
||||||
for _, grant := range u.Grants {
|
for _, grant := range grants {
|
||||||
if grant.AllowRead && grant.AllowWrite {
|
if grant.AllowRead && grant.AllowWrite {
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
|
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
|
||||||
} else if grant.AllowRead {
|
} else if grant.AllowRead {
|
||||||
|
|
|
@ -1308,7 +1308,7 @@ func (s *Server) runFirebaseKeepaliver() {
|
||||||
if s.firebaseClient == nil {
|
if s.firebaseClient == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v := newVisitor(s.config, s.messageCache, netip.IPv4Unspecified(), nil) // Background process, not a real visitor, uses IP 0.0.0.0
|
v := newVisitor(s.config, s.messageCache, s.userManager, netip.IPv4Unspecified(), nil) // Background process, not a real visitor, uses IP 0.0.0.0
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(s.config.FirebaseKeepaliveInterval):
|
case <-time.After(s.config.FirebaseKeepaliveInterval):
|
||||||
|
@ -1579,7 +1579,7 @@ func (s *Server) visitorFromID(visitorID string, ip netip.Addr, user *user.User)
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
v, exists := s.visitors[visitorID]
|
v, exists := s.visitors[visitorID]
|
||||||
if !exists {
|
if !exists {
|
||||||
s.visitors[visitorID] = newVisitor(s.config, s.messageCache, ip, user)
|
s.visitors[visitorID] = newVisitor(s.config, s.messageCache, s.userManager, ip, user)
|
||||||
return s.visitors[visitorID]
|
return s.visitors[visitorID]
|
||||||
}
|
}
|
||||||
v.Keepalive()
|
v.Keepalive()
|
||||||
|
|
|
@ -94,16 +94,27 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
||||||
Upgradable: true,
|
Upgradable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(v.user.Grants) > 0 {
|
reservations, err := s.userManager.Reservations(v.user.Name)
|
||||||
response.Access = make([]*apiAccountGrant, 0)
|
if err != nil {
|
||||||
for _, grant := range v.user.Grants {
|
return err
|
||||||
if grant.Owner {
|
}
|
||||||
response.Access = append(response.Access, &apiAccountGrant{
|
if len(reservations) > 0 {
|
||||||
Topic: grant.TopicPattern,
|
response.Reservations = make([]*apiAccountReservation, 0)
|
||||||
Read: grant.AllowRead,
|
for _, r := range reservations {
|
||||||
Write: grant.AllowWrite,
|
var everyone string
|
||||||
})
|
if r.AllowEveryoneRead && r.AllowEveryoneWrite {
|
||||||
|
everyone = "read-write"
|
||||||
|
} else if r.AllowEveryoneRead && !r.AllowEveryoneWrite {
|
||||||
|
everyone = "read-only"
|
||||||
|
} else if !r.AllowEveryoneRead && r.AllowEveryoneWrite {
|
||||||
|
everyone = "write-only"
|
||||||
|
} else {
|
||||||
|
everyone = "deny-all"
|
||||||
}
|
}
|
||||||
|
response.Reservations = append(response.Reservations, &apiAccountReservation{
|
||||||
|
Topic: r.TopicPattern,
|
||||||
|
Everyone: everyone,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -356,9 +367,13 @@ func (s *Server) handleAccountAccessDelete(w http.ResponseWriter, r *http.Reques
|
||||||
if !topicRegex.MatchString(topic) {
|
if !topicRegex.MatchString(topic) {
|
||||||
return errHTTPBadRequestTopicInvalid
|
return errHTTPBadRequestTopicInvalid
|
||||||
}
|
}
|
||||||
|
reservations, err := s.userManager.Reservations(v.user.Name) // FIXME replace with HasReservation
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
authorized := false
|
authorized := false
|
||||||
for _, grant := range v.user.Grants {
|
for _, r := range reservations {
|
||||||
if grant.TopicPattern == topic && grant.Owner {
|
if r.TopicPattern == topic {
|
||||||
authorized = true
|
authorized = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,7 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
|
||||||
func TestToFirebaseSender_Abuse(t *testing.T) {
|
func TestToFirebaseSender_Abuse(t *testing.T) {
|
||||||
sender := &testFirebaseSender{allowed: 2}
|
sender := &testFirebaseSender{allowed: 2}
|
||||||
client := newFirebaseClient(sender, &testAuther{})
|
client := newFirebaseClient(sender, &testAuther{})
|
||||||
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"), nil)
|
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), nil, netip.MustParseAddr("1.2.3.4"), nil)
|
||||||
|
|
||||||
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
|
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
|
||||||
require.Equal(t, 1, len(sender.Messages()))
|
require.Equal(t, 1, len(sender.Messages()))
|
||||||
|
|
|
@ -72,7 +72,7 @@ func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) {
|
||||||
func TestMatrix_WriteMatrixError(t *testing.T) {
|
func TestMatrix_WriteMatrixError(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil)
|
r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil)
|
||||||
v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4"), nil)
|
v := newVisitor(newTestConfig(t), nil, 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.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, 200, w.Result().StatusCode)
|
||||||
require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String())
|
require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String())
|
||||||
|
|
|
@ -259,22 +259,21 @@ type apiAccountStats struct {
|
||||||
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
|
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountGrant struct {
|
type apiAccountReservation struct {
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Read bool `json:"read"`
|
Everyone string `json:"everyone"`
|
||||||
Write bool `json:"write"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountResponse struct {
|
type apiAccountResponse struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role string `json:"role,omitempty"`
|
Role string `json:"role,omitempty"`
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
||||||
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
||||||
Access []*apiAccountGrant `json:"access,omitempty"`
|
Reservations []*apiAccountReservation `json:"reservations,omitempty"`
|
||||||
Plan *apiAccountPlan `json:"plan,omitempty"`
|
Plan *apiAccountPlan `json:"plan,omitempty"`
|
||||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||||
Stats *apiAccountStats `json:"stats,omitempty"`
|
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountAccessRequest struct {
|
type apiAccountAccessRequest struct {
|
||||||
|
|
|
@ -26,6 +26,7 @@ var (
|
||||||
type visitor struct {
|
type visitor struct {
|
||||||
config *Config
|
config *Config
|
||||||
messageCache *messageCache
|
messageCache *messageCache
|
||||||
|
userManager *user.Manager // May be nil!
|
||||||
ip netip.Addr
|
ip netip.Addr
|
||||||
user *user.User
|
user *user.User
|
||||||
messages int64 // Number of messages sent
|
messages int64 // Number of messages sent
|
||||||
|
@ -57,7 +58,7 @@ type visitorInfo struct {
|
||||||
AttachmentFileSizeLimit int64
|
AttachmentFileSizeLimit int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *user.User) *visitor {
|
func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Manager, ip netip.Addr, user *user.User) *visitor {
|
||||||
var requestLimiter, emailsLimiter, accountLimiter *rate.Limiter
|
var requestLimiter, emailsLimiter, accountLimiter *rate.Limiter
|
||||||
var messages, emails int64
|
var messages, emails int64
|
||||||
if user != nil {
|
if user != nil {
|
||||||
|
@ -76,6 +77,7 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *u
|
||||||
return &visitor{
|
return &visitor{
|
||||||
config: conf,
|
config: conf,
|
||||||
messageCache: messageCache,
|
messageCache: messageCache,
|
||||||
|
userManager: userManager, // May be nil!
|
||||||
ip: ip,
|
ip: ip,
|
||||||
user: user,
|
user: user,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
|
@ -192,7 +194,7 @@ func (v *visitor) Info() (*visitorInfo, error) {
|
||||||
info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
|
info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
|
||||||
info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
|
info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
|
||||||
}
|
}
|
||||||
var attachmentsBytesUsed int64
|
var attachmentsBytesUsed int64 // FIXME Maybe move this to endpoint?
|
||||||
var err error
|
var err error
|
||||||
if v.user != nil {
|
if v.user != nil {
|
||||||
attachmentsBytesUsed, err = v.messageCache.AttachmentBytesUsedByUser(v.user.Name)
|
attachmentsBytesUsed, err = v.messageCache.AttachmentBytesUsedByUser(v.user.Name)
|
||||||
|
@ -203,12 +205,12 @@ func (v *visitor) Info() (*visitorInfo, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var topics int64
|
var topics int64
|
||||||
if v.user != nil {
|
if v.user != nil && v.userManager != nil {
|
||||||
for _, grant := range v.user.Grants {
|
reservations, err := v.userManager.Reservations(v.user.Name) // FIXME dup call, move this to endpoint?
|
||||||
if grant.Owner {
|
if err != nil {
|
||||||
topics++
|
return nil, err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
topics = int64(len(reservations))
|
||||||
}
|
}
|
||||||
info.Messages = messages
|
info.Messages = messages
|
||||||
info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)
|
info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)
|
||||||
|
|
|
@ -92,7 +92,7 @@ const (
|
||||||
SELECT read, write
|
SELECT read, write
|
||||||
FROM user_access a
|
FROM user_access a
|
||||||
JOIN user u ON u.id = a.user_id
|
JOIN user u ON u.id = a.user_id
|
||||||
WHERE (u.user = '*' OR u.user = ?) AND ? LIKE a.topic
|
WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic
|
||||||
ORDER BY u.user DESC
|
ORDER BY u.user DESC
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
@ -123,11 +123,19 @@ const (
|
||||||
DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id
|
DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id
|
||||||
`
|
`
|
||||||
selectUserAccessQuery = `
|
selectUserAccessQuery = `
|
||||||
SELECT topic, read, write, IIF(owner_user_id IS NOT NULL AND user_id = owner_user_id,1,0) AS owner
|
SELECT topic, read, write
|
||||||
FROM user_access
|
FROM user_access
|
||||||
WHERE user_id = (SELECT id FROM user WHERE user = ?)
|
WHERE user_id = (SELECT id FROM user WHERE user = ?)
|
||||||
ORDER BY write DESC, read DESC, topic
|
ORDER BY write DESC, read DESC, topic
|
||||||
`
|
`
|
||||||
|
selectUserReservationsQuery = `
|
||||||
|
SELECT a_user.topic, a_user.read, a_user.write, a_everyone.read AS everyone_read, a_everyone.write AS everyone_write
|
||||||
|
FROM user_access a_user
|
||||||
|
LEFT JOIN user_access a_everyone ON a_user.topic = a_everyone.topic AND a_everyone.user_id = (SELECT id FROM user WHERE user = ?)
|
||||||
|
WHERE a_user.user_id = a_user.owner_user_id
|
||||||
|
AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?)
|
||||||
|
ORDER BY a_user.topic
|
||||||
|
`
|
||||||
selectOtherAccessCountQuery = `
|
selectOtherAccessCountQuery = `
|
||||||
SELECT count(*)
|
SELECT count(*)
|
||||||
FROM user_access
|
FROM user_access
|
||||||
|
@ -354,7 +362,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
|
||||||
}
|
}
|
||||||
// Select the read/write permissions for this user/topic combo. The query may return two
|
// Select the read/write permissions for this user/topic combo. The query may return two
|
||||||
// rows (one for everyone, and one for the user), but prioritizes the user.
|
// rows (one for everyone, and one for the user), but prioritizes the user.
|
||||||
rows, err := a.db.Query(selectTopicPermsQuery, username, topic)
|
rows, err := a.db.Query(selectTopicPermsQuery, Everyone, username, topic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -479,15 +487,10 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
||||||
} else if err := rows.Err(); err != nil {
|
} else if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
grants, err := a.readGrants(username)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user := &User{
|
user := &User{
|
||||||
Name: username,
|
Name: username,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
Role: Role(role),
|
Role: Role(role),
|
||||||
Grants: grants,
|
|
||||||
Stats: &Stats{
|
Stats: &Stats{
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
Emails: emails,
|
Emails: emails,
|
||||||
|
@ -513,7 +516,8 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Manager) readGrants(username string) ([]Grant, error) {
|
// Grants returns all user-specific access control entries
|
||||||
|
func (a *Manager) Grants(username string) ([]Grant, error) {
|
||||||
rows, err := a.db.Query(selectUserAccessQuery, username)
|
rows, err := a.db.Query(selectUserAccessQuery, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -522,8 +526,8 @@ func (a *Manager) readGrants(username string) ([]Grant, error) {
|
||||||
grants := make([]Grant, 0)
|
grants := make([]Grant, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var topic string
|
var topic string
|
||||||
var read, write, owner bool
|
var read, write bool
|
||||||
if err := rows.Scan(&topic, &read, &write, &owner); err != nil {
|
if err := rows.Scan(&topic, &read, &write); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err := rows.Err(); err != nil {
|
} else if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -532,12 +536,39 @@ func (a *Manager) readGrants(username string) ([]Grant, error) {
|
||||||
TopicPattern: fromSQLWildcard(topic),
|
TopicPattern: fromSQLWildcard(topic),
|
||||||
AllowRead: read,
|
AllowRead: read,
|
||||||
AllowWrite: write,
|
AllowWrite: write,
|
||||||
Owner: owner,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return grants, nil
|
return grants, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reservations returns all user-owned topics, and the associated everyone-access
|
||||||
|
func (a *Manager) Reservations(username string) ([]Reservation, error) {
|
||||||
|
rows, err := a.db.Query(selectUserReservationsQuery, Everyone, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
reservations := make([]Reservation, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var topic string
|
||||||
|
var read, write bool
|
||||||
|
var everyoneRead, everyoneWrite sql.NullBool
|
||||||
|
if err := rows.Scan(&topic, &read, &write, &everyoneRead, &everyoneWrite); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reservations = append(reservations, Reservation{
|
||||||
|
TopicPattern: topic,
|
||||||
|
AllowRead: read,
|
||||||
|
AllowWrite: write,
|
||||||
|
AllowEveryoneRead: everyoneRead.Bool, // false if null
|
||||||
|
AllowEveryoneWrite: everyoneWrite.Bool, // false if null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return reservations, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ChangePassword changes a user's password
|
// ChangePassword changes a user's password
|
||||||
func (a *Manager) ChangePassword(username, password string) error {
|
func (a *Manager) ChangePassword(username, password string) error {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
|
||||||
|
|
|
@ -9,14 +9,13 @@ import (
|
||||||
|
|
||||||
// User is a struct that represents a user
|
// User is a struct that represents a user
|
||||||
type User struct {
|
type User struct {
|
||||||
Name string
|
Name string
|
||||||
Hash string // password hash (bcrypt)
|
Hash string // password hash (bcrypt)
|
||||||
Token string // Only set if token was used to log in
|
Token string // Only set if token was used to log in
|
||||||
Role Role
|
Role Role
|
||||||
Grants []Grant
|
Prefs *Prefs
|
||||||
Prefs *Prefs
|
Plan *Plan
|
||||||
Plan *Plan
|
Stats *Stats
|
||||||
Stats *Stats
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auther is an interface for authentication and authorization
|
// Auther is an interface for authentication and authorization
|
||||||
|
@ -91,7 +90,15 @@ type Grant struct {
|
||||||
TopicPattern string // May include wildcard (*)
|
TopicPattern string // May include wildcard (*)
|
||||||
AllowRead bool
|
AllowRead bool
|
||||||
AllowWrite bool
|
AllowWrite bool
|
||||||
Owner bool // This user owns this ACL entry
|
}
|
||||||
|
|
||||||
|
// Reservation is a struct that represents the ownership over a topic by a user
|
||||||
|
type Reservation struct {
|
||||||
|
TopicPattern string
|
||||||
|
AllowRead bool
|
||||||
|
AllowWrite bool
|
||||||
|
AllowEveryoneRead bool
|
||||||
|
AllowEveryoneWrite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission represents a read or write permission to a topic
|
// Permission represents a read or write permission to a topic
|
||||||
|
|
|
@ -239,21 +239,22 @@
|
||||||
"prefs_users_dialog_button_save": "Save",
|
"prefs_users_dialog_button_save": "Save",
|
||||||
"prefs_appearance_title": "Appearance",
|
"prefs_appearance_title": "Appearance",
|
||||||
"prefs_appearance_language_title": "Language",
|
"prefs_appearance_language_title": "Language",
|
||||||
"prefs_access_title": "Reserved topics",
|
"prefs_reservations_title": "Reserved topics",
|
||||||
"prefs_access_description": "You may reserve topic names for personal use here, and define access to a topic for other users.",
|
"prefs_reservations_description": "You may reserve topic names for personal use here, and define access to a topic for other users.",
|
||||||
"prefs_access_add_button": "Add reserved topic",
|
"prefs_reservations_add_button": "Add reserved topic",
|
||||||
"prefs_access_edit_button": "Edit topic access",
|
"prefs_reservations_edit_button": "Edit topic access",
|
||||||
"prefs_access_delete_button": "Reset topic access",
|
"prefs_reservations_delete_button": "Reset topic access",
|
||||||
"prefs_access_table": "Reserved topics table",
|
"prefs_reservations_table": "Reserved topics table",
|
||||||
"prefs_access_table_topic_header": "Topic",
|
"prefs_reservations_table_topic_header": "Topic",
|
||||||
"prefs_access_table_access_header": "Access",
|
"prefs_reservations_table_access_header": "Access",
|
||||||
"prefs_access_table_perms_private": "Only I can publish and subscribe",
|
"prefs_reservations_table_everyone_deny_all": "Only I can publish and subscribe",
|
||||||
"prefs_access_table_perms_public_read": "I can publish, everyone can subscribe",
|
"prefs_reservations_table_everyone_read_only": "I can publish and subscribe, everyone can subscribe",
|
||||||
"prefs_access_table_perms_public": "Everyone can publish and subscribe",
|
"prefs_reservations_table_everyone_write_only": "I can publish and subscribe, everyone can publish",
|
||||||
"prefs_access_dialog_title_add": "Reserve topic",
|
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
|
||||||
"prefs_access_dialog_title_edit": "Edit reserved topic",
|
"prefs_reservations_dialog_title_add": "Reserve topic",
|
||||||
"prefs_access_dialog_topic_label": "Topic",
|
"prefs_reservations_dialog_title_edit": "Edit reserved topic",
|
||||||
"prefs_access_dialog_access_label": "Access",
|
"prefs_reservations_dialog_topic_label": "Topic",
|
||||||
|
"prefs_reservations_dialog_access_label": "Access",
|
||||||
"priority_min": "min",
|
"priority_min": "min",
|
||||||
"priority_low": "low",
|
"priority_low": "low",
|
||||||
"priority_default": "default",
|
"priority_default": "default",
|
||||||
|
|
|
@ -50,7 +50,7 @@ const Preferences = () => {
|
||||||
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
|
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Notifications/>
|
<Notifications/>
|
||||||
<Access/>
|
<Reservations/>
|
||||||
<Users/>
|
<Users/>
|
||||||
<Appearance/>
|
<Appearance/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -476,7 +476,7 @@ const Language = () => {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
const Access = () => {
|
const Reservations = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { account } = useOutletContext();
|
const { account } = useOutletContext();
|
||||||
const [dialogKey, setDialogKey] = useState(0);
|
const [dialogKey, setDialogKey] = useState(0);
|
||||||
|
@ -506,19 +506,19 @@ const Access = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ padding: 1 }} aria-label={t("prefs_access_title")}>
|
<Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
|
||||||
<CardContent sx={{ paddingBottom: 1 }}>
|
<CardContent sx={{ paddingBottom: 1 }}>
|
||||||
<Typography variant="h5" sx={{marginBottom: 2}}>
|
<Typography variant="h5" sx={{marginBottom: 2}}>
|
||||||
{t("prefs_access_title")}
|
{t("prefs_reservations_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
{t("prefs_access_description")}
|
{t("prefs_reservations_description")}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{account.access.length > 0 && <AccessTable entries={account.access}/>}
|
{account.reservations.length > 0 && <ReservationsTable reservations={account.reservations}/>}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button onClick={handleAddClick}>{t("prefs_access_add_button")}</Button>
|
<Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button>
|
||||||
<AccessDialog
|
<ReservationsDialog
|
||||||
key={`accessAddDialog${dialogKey}`}
|
key={`accessAddDialog${dialogKey}`}
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
entry={null}
|
entry={null}
|
||||||
|
@ -531,7 +531,7 @@ const Access = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessTable = (props) => {
|
const ReservationsTable = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dialogKey, setDialogKey] = useState(0);
|
const [dialogKey, setDialogKey] = useState(0);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
@ -557,37 +557,59 @@ const AccessTable = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table size="small" aria-label={t("prefs_access_table")}>
|
<Table size="small" aria-label={t("prefs_reservations_table")}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{paddingLeft: 0}}>{t("prefs_access_table_topic_header")}</TableCell>
|
<TableCell sx={{paddingLeft: 0}}>{t("prefs_reservations_table_topic_header")}</TableCell>
|
||||||
<TableCell>{t("prefs_access_table_access_header")}</TableCell>
|
<TableCell>{t("prefs_reservations_table_access_header")}</TableCell>
|
||||||
<TableCell/>
|
<TableCell/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{props.entries.map(entry => (
|
{props.reservations.map(reservation => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={entry.topic}
|
key={reservation.topic}
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_access_table_topic_header")}>{entry.topic}</TableCell>
|
<TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_reservations_table_topic_header")}>{reservation.topic}</TableCell>
|
||||||
<TableCell aria-label={t("prefs_access_table_access_header")}>
|
<TableCell aria-label={t("prefs_reservations_table_access_header")}>
|
||||||
<LockIcon fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
|
{reservation.everyone === "read-write" &&
|
||||||
{t("prefs_access_table_perms_private")}
|
<>
|
||||||
|
<Public fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
|
||||||
|
{t("prefs_reservations_table_everyone_read_write")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{reservation.everyone === "read-only" &&
|
||||||
|
<>
|
||||||
|
<PublicOff fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
|
||||||
|
{t("prefs_reservations_table_everyone_read_only")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{reservation.everyone === "write-only" &&
|
||||||
|
<>
|
||||||
|
<PublicOff fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
|
||||||
|
{t("prefs_reservations_table_everyone_write_only")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{reservation.everyone === "deny-all" &&
|
||||||
|
<>
|
||||||
|
<LockIcon fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
|
||||||
|
{t("prefs_reservations_table_everyone_deny_all")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton onClick={() => handleEditClick(entry)} aria-label={t("prefs_access_edit_button")}>
|
<IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
|
||||||
<EditIcon/>
|
<EditIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => handleDeleteClick(entry)} aria-label={t("prefs_access_delete_button")}>
|
<IconButton onClick={() => handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}>
|
||||||
<CloseIcon/>
|
<CloseIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<AccessDialog
|
<ReservationsDialog
|
||||||
key={`accessEditDialog${dialogKey}`}
|
key={`accessEditDialog${dialogKey}`}
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
entry={dialogEntry}
|
entry={dialogEntry}
|
||||||
|
@ -599,7 +621,7 @@ const AccessTable = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessDialog = (props) => {
|
const ReservationsDialog = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [topic, setTopic] = useState("");
|
const [topic, setTopic] = useState("");
|
||||||
const [access, setAccess] = useState("private");
|
const [access, setAccess] = useState("private");
|
||||||
|
@ -621,15 +643,15 @@ const AccessDialog = (props) => {
|
||||||
}
|
}
|
||||||
}, [editMode, props]);
|
}, [editMode, props]);
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.onCancel} maxWidth="xs" fullWidth fullScreen={fullScreen}>
|
<Dialog open={props.open} onClose={props.onCancel} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
<DialogTitle>{editMode ? t("prefs_access_dialog_title_edit") : t("prefs_access_dialog_title_add")}</DialogTitle>
|
<DialogTitle>{editMode ? t("prefs_reservations_dialog_title_edit") : t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{!editMode && <TextField
|
{!editMode && <TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="topic"
|
id="topic"
|
||||||
label={t("prefs_access_dialog_topic_label")}
|
label={t("prefs_reservations_dialog_topic_label")}
|
||||||
aria-label={t("prefs_access_dialog_topic_label")}
|
aria-label={t("prefs_reservations_dialog_topic_label")}
|
||||||
value={topic}
|
value={topic}
|
||||||
onChange={ev => setTopic(ev.target.value)}
|
onChange={ev => setTopic(ev.target.value)}
|
||||||
type="url"
|
type="url"
|
||||||
|
@ -640,7 +662,7 @@ const AccessDialog = (props) => {
|
||||||
<Select
|
<Select
|
||||||
value={access}
|
value={access}
|
||||||
onChange={(ev) => setAccess(ev.target.value)}
|
onChange={(ev) => setAccess(ev.target.value)}
|
||||||
aria-label={t("prefs_access_dialog_access_label")}
|
aria-label={t("prefs_reservations_dialog_access_label")}
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
"& .MuiSelect-select": {
|
"& .MuiSelect-select": {
|
||||||
|
@ -649,17 +671,21 @@ const AccessDialog = (props) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value="private">
|
<MenuItem value="deny-all">
|
||||||
<ListItemIcon><LockIcon /></ListItemIcon>
|
<ListItemIcon><LockIcon /></ListItemIcon>
|
||||||
<ListItemText primary={t("prefs_access_table_perms_private")} />
|
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem value="public-read">
|
<MenuItem value="read-only">
|
||||||
<ListItemIcon><PublicOff /></ListItemIcon>
|
<ListItemIcon><PublicOff /></ListItemIcon>
|
||||||
<ListItemText primary={t("prefs_access_table_perms_public_read")} />
|
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem value="public">
|
<MenuItem value="write-only">
|
||||||
|
<ListItemIcon><PublicOff /></ListItemIcon>
|
||||||
|
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="read-write">
|
||||||
<ListItemIcon><Public /></ListItemIcon>
|
<ListItemIcon><Public /></ListItemIcon>
|
||||||
<ListItemText primary={t("prefs_access_table_perms_public")} />
|
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
Loading…
Reference in a new issue