Introduce Reservation

This commit is contained in:
binwiederhier 2023-01-02 20:08:37 -05:00
parent 1256ba0429
commit 1733323132
11 changed files with 194 additions and 109 deletions

View file

@ -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 {

View file

@ -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()

View file

@ -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{
Topic: grant.TopicPattern,
Read: grant.AllowRead,
Write: grant.AllowWrite,
})
} }
if len(reservations) > 0 {
response.Reservations = make([]*apiAccountReservation, 0)
for _, r := range reservations {
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
} }

View file

@ -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()))

View file

@ -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())

View file

@ -259,10 +259,9 @@ 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 {
@ -271,7 +270,7 @@ type apiAccountResponse struct {
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"`

View file

@ -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)

View file

@ -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)

View file

@ -13,7 +13,6 @@ type User struct {
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
@ -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

View file

@ -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",

View file

@ -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")}>
{reservation.everyone === "read-write" &&
<>
<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}}/> <LockIcon fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
{t("prefs_access_table_perms_private")} {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>