mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-02-16 20:04:34 +01:00
Not really an improvemenNot really an improvementt
This commit is contained in:
parent
bd86e3d951
commit
3d921f4570
6 changed files with 107 additions and 53 deletions
|
@ -316,10 +316,13 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
|
||||||
if !topicRegex.MatchString(req.Topic) {
|
if !topicRegex.MatchString(req.Topic) {
|
||||||
return errHTTPBadRequestTopicInvalid
|
return errHTTPBadRequestTopicInvalid
|
||||||
}
|
}
|
||||||
|
// FIXME authorize: how do I know if v.user (= auth'd user) is allowed to write the ACL entries
|
||||||
|
everyoneRead := util.Contains([]string{"read-write", "rw", "read-only", "read", "ro"}, req.Everyone)
|
||||||
|
everyoneWrite := util.Contains([]string{"read-write", "rw", "write-only", "write", "wo"}, req.Everyone)
|
||||||
if err := s.userManager.AllowAccess(v.user.Name, req.Topic, true, true); err != nil {
|
if err := s.userManager.AllowAccess(v.user.Name, req.Topic, true, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.userManager.AllowAccess(user.Everyone, req.Topic, false, false); err != nil {
|
if err := s.userManager.AllowAccess(user.Everyone, req.Topic, everyoneRead, everyoneWrite); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
|
@ -268,6 +268,6 @@ type apiAccountResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountAccessRequest struct {
|
type apiAccountAccessRequest struct {
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Access string `json:"access"`
|
Everyone string `json:"everyone"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,8 @@ const (
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX idx_user ON user (user);
|
CREATE UNIQUE INDEX idx_user ON user (user);
|
||||||
CREATE TABLE IF NOT EXISTS user_access (
|
CREATE TABLE IF NOT EXISTS user_access (
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
|
owner_user_id INT,
|
||||||
topic TEXT NOT NULL,
|
topic TEXT NOT NULL,
|
||||||
read INT NOT NULL,
|
read INT NOT NULL,
|
||||||
write INT NOT NULL,
|
write INT NOT NULL,
|
||||||
|
|
|
@ -33,6 +33,7 @@ import Divider from "@mui/material/Divider";
|
||||||
import {Logout, Person, Settings} from "@mui/icons-material";
|
import {Logout, Person, Settings} from "@mui/icons-material";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||||
|
import PopupMenu from "./PopupMenu";
|
||||||
|
|
||||||
const ActionBar = (props) => {
|
const ActionBar = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -189,6 +190,7 @@ const SettingsIcons = (props) => {
|
||||||
<MoreVertIcon/>
|
<MoreVertIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<PopupMenu
|
<PopupMenu
|
||||||
|
horizontal="right"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
@ -259,6 +261,7 @@ const ProfileIcon = () => {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
<PopupMenu
|
<PopupMenu
|
||||||
|
horizontal="right"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
@ -287,45 +290,4 @@ const ProfileIcon = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PopupMenu = (props) => {
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
anchorEl={props.anchorEl}
|
|
||||||
open={props.open}
|
|
||||||
onClose={props.onClose}
|
|
||||||
onClick={props.onClose}
|
|
||||||
PaperProps={{
|
|
||||||
elevation: 0,
|
|
||||||
sx: {
|
|
||||||
overflow: 'visible',
|
|
||||||
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
|
|
||||||
mt: 1.5,
|
|
||||||
'& .MuiAvatar-root': {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
ml: -0.5,
|
|
||||||
mr: 1,
|
|
||||||
},
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
display: 'block',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
right: 19,
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
transform: 'translateY(-50%) rotate(45deg)',
|
|
||||||
zIndex: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
|
||||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionBar;
|
export default ActionBar;
|
||||||
|
|
47
web/src/components/PopupMenu.js
Normal file
47
web/src/components/PopupMenu.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import {Menu} from "@mui/material";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const PopupMenu = (props) => {
|
||||||
|
const horizontal = props.horizontal ?? "left";
|
||||||
|
const arrow = (horizontal === "right") ? { right: 19 } : { left: 19 };
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
anchorEl={props.anchorEl}
|
||||||
|
open={props.open}
|
||||||
|
onClose={props.onClose}
|
||||||
|
onClick={props.onClose}
|
||||||
|
PaperProps={{
|
||||||
|
elevation: 0,
|
||||||
|
sx: {
|
||||||
|
overflow: 'visible',
|
||||||
|
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
|
||||||
|
mt: 1.5,
|
||||||
|
'& .MuiAvatar-root': {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
ml: -0.5,
|
||||||
|
mr: 1,
|
||||||
|
},
|
||||||
|
'&:before': {
|
||||||
|
content: '""',
|
||||||
|
display: 'block',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
transform: 'translateY(-50%) rotate(45deg)',
|
||||||
|
zIndex: 0,
|
||||||
|
...arrow
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
transformOrigin={{ horizontal: horizontal, vertical: 'top' }}
|
||||||
|
anchorOrigin={{ horizontal: horizontal, vertical: 'bottom' }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupMenu;
|
|
@ -20,6 +20,11 @@ import routes from "./routes";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import PublicIcon from '@mui/icons-material/Public';
|
import PublicIcon from '@mui/icons-material/Public';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import PublicOffIcon from '@mui/icons-material/PublicOff';
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import PopupMenu from "./PopupMenu";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
|
||||||
const publicBaseUrl = "https://ntfy.sh";
|
const publicBaseUrl = "https://ntfy.sh";
|
||||||
|
|
||||||
|
@ -75,11 +80,15 @@ const SubscribePage = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
|
const [accessAnchorEl, setAccessAnchorEl] = useState(null);
|
||||||
|
const [access, setAccess] = useState("public");
|
||||||
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
|
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
|
||||||
const topic = props.topic;
|
const topic = props.topic;
|
||||||
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
|
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
|
||||||
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
|
const existingBaseUrls = Array
|
||||||
|
.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
|
||||||
.filter(s => s !== config.baseUrl);
|
.filter(s => s !== config.baseUrl);
|
||||||
|
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
const user = await userManager.get(baseUrl); // May be undefined
|
const user = await userManager.get(baseUrl); // May be undefined
|
||||||
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
|
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
|
||||||
|
@ -97,10 +106,12 @@ const SubscribePage = (props) => {
|
||||||
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUseAnotherChanged = (e) => {
|
const handleUseAnotherChanged = (e) => {
|
||||||
props.setBaseUrl("");
|
props.setBaseUrl("");
|
||||||
setAnotherServerVisible(e.target.checked);
|
setAnotherServerVisible(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscribeButtonEnabled = (() => {
|
const subscribeButtonEnabled = (() => {
|
||||||
if (anotherServerVisible) {
|
if (anotherServerVisible) {
|
||||||
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
|
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
|
||||||
|
@ -110,6 +121,7 @@ const SubscribePage = (props) => {
|
||||||
return validTopic(topic) && !isExistingTopicUrl;
|
return validTopic(topic) && !isExistingTopicUrl;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const updateBaseUrl = (ev, newVal) => {
|
const updateBaseUrl = (ev, newVal) => {
|
||||||
if (validUrl(newVal)) {
|
if (validUrl(newVal)) {
|
||||||
props.setBaseUrl(newVal.replace(/\/$/, '')); // strip trailing slash after https?://
|
props.setBaseUrl(newVal.replace(/\/$/, '')); // strip trailing slash after https?://
|
||||||
|
@ -117,6 +129,7 @@ const SubscribePage = (props) => {
|
||||||
props.setBaseUrl(newVal);
|
props.setBaseUrl(newVal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
|
<DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
|
||||||
|
@ -125,9 +138,13 @@ const SubscribePage = (props) => {
|
||||||
{t("subscribe_dialog_subscribe_description")}
|
{t("subscribe_dialog_subscribe_description")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<div style={{display: 'flex'}} role="row">
|
<div style={{display: 'flex'}} role="row">
|
||||||
<IconButton color="inherit" size="large" edge="start" sx={{height: "45px", marginTop: "5px", color: "grey"}}>
|
{session.exists() &&
|
||||||
<PublicIcon/>
|
<IconButton onClick={(ev) => setAccessAnchorEl(ev.currentTarget)} color="inherit" size="large" edge="start" sx={{height: "45px", marginTop: "5px", color: "grey"}}>
|
||||||
</IconButton>
|
{access === "public" && <PublicIcon/>}
|
||||||
|
{access === "public-read" && <PublicOffIcon/>}
|
||||||
|
{access === "private" && <LockIcon/>}
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
@ -142,10 +159,34 @@ const SubscribePage = (props) => {
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
"aria-label": t("subscribe_dialog_subscribe_topic_placeholder")
|
"aria-label": t("subscribe_dialog_subscribe_topic_placeholder")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => {props.setTopic(randomAlphanumericString(16))}} style={{flexShrink: "0", marginTop: "0.5em"}}>
|
<Button onClick={() => {props.setTopic(randomAlphanumericString(16))}} style={{flexShrink: "0", marginTop: "0.5em"}}>
|
||||||
{t("subscribe_dialog_subscribe_button_generate_topic_name")}
|
{t("subscribe_dialog_subscribe_button_generate_topic_name")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<PopupMenu
|
||||||
|
anchorEl={accessAnchorEl}
|
||||||
|
open={!!accessAnchorEl}
|
||||||
|
onClose={() => setAccessAnchorEl(null)}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => setAccess("private")} selected={access === "private"}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<LockIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
Only I can publish and subscribe
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => setAccess("public-read")} selected={access === "public-read"}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PublicOffIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
I can publish, everyone can subscribe
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => setAccess("public")} selected={access === "public"}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PublicIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
Everyone can publish and subscribe
|
||||||
|
</MenuItem>
|
||||||
|
</PopupMenu>
|
||||||
</div>
|
</div>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
sx={{pt: 1}}
|
sx={{pt: 1}}
|
||||||
|
|
Loading…
Add table
Reference in a new issue