Reserved topic stuff
parent
6c0429351a
commit
a91da7cf2c
|
@ -108,6 +108,7 @@ type Config struct {
|
||||||
EnableLogin bool
|
EnableLogin bool
|
||||||
EnableEmailConfirm bool
|
EnableEmailConfirm bool
|
||||||
EnableResetPassword bool
|
EnableResetPassword bool
|
||||||
|
EnableAccountUpgrades bool
|
||||||
Version string // injected by App
|
Version string // injected by App
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,12 @@ import (
|
||||||
message cache duration
|
message cache duration
|
||||||
Keep 10000 messages or keep X days?
|
Keep 10000 messages or keep X days?
|
||||||
Attachment expiration based on plan
|
Attachment expiration based on plan
|
||||||
reserve topics
|
|
||||||
purge accounts that were not logged into in X
|
purge accounts that were not logged into in X
|
||||||
reset daily limits for users
|
reset daily limits for users
|
||||||
Account usage not updated "in real time"
|
|
||||||
max token issue limit
|
max token issue limit
|
||||||
user db startup queries -> foreign keys
|
user db startup queries -> foreign keys
|
||||||
|
UI
|
||||||
|
- Feature flag for "reserve topic" feature
|
||||||
Sync:
|
Sync:
|
||||||
- "mute" setting
|
- "mute" setting
|
||||||
- figure out what settings are "web" or "phone"
|
- figure out what settings are "web" or "phone"
|
||||||
|
@ -447,17 +447,20 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
if !s.config.WebRootIsApp {
|
if !s.config.WebRootIsApp {
|
||||||
appRoot = "/app"
|
appRoot = "/app"
|
||||||
}
|
}
|
||||||
disallowedTopicsStr := `"` + strings.Join(disallowedTopics, `", "`) + `"`
|
response := &apiConfigResponse{
|
||||||
|
BaseURL: "", // Will translate to window.location.origin
|
||||||
|
AppRoot: appRoot,
|
||||||
|
EnableLogin: s.config.EnableLogin,
|
||||||
|
EnableSignup: s.config.EnableSignup,
|
||||||
|
EnableResetPassword: s.config.EnableResetPassword,
|
||||||
|
DisallowedTopics: disallowedTopics,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/javascript")
|
w.Header().Set("Content-Type", "text/javascript")
|
||||||
_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
|
_, err = io.WriteString(w, fmt.Sprintf("// Generated server configuration\nvar config = %s;\n", string(b)))
|
||||||
var config = {
|
|
||||||
baseUrl: window.location.origin,
|
|
||||||
appRoot: "%s",
|
|
||||||
enableLogin: %t,
|
|
||||||
enableSignup: %t,
|
|
||||||
enableResetPassword: %t,
|
|
||||||
disallowedTopics: [%s],
|
|
||||||
};`, appRoot, s.config.EnableLogin, s.config.EnableSignup, s.config.EnableResetPassword, disallowedTopicsStr))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -280,3 +280,12 @@ type apiAccountAccessRequest struct {
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Everyone string `json:"everyone"`
|
Everyone string `json:"everyone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiConfigResponse struct {
|
||||||
|
BaseURL string `json:"base_url"`
|
||||||
|
AppRoot string `json:"app_root"`
|
||||||
|
EnableLogin bool `json:"enable_login"`
|
||||||
|
EnableSignup bool `json:"enable_signup"`
|
||||||
|
EnableResetPassword bool `json:"enable_reset_password"`
|
||||||
|
DisallowedTopics []string `json:"disallowed_topics"`
|
||||||
|
}
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
"subscription_settings_dialog_title": "Subscription settings",
|
"subscription_settings_dialog_title": "Subscription settings",
|
||||||
"subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.",
|
"subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.",
|
||||||
"subscription_settings_dialog_display_name_placeholder": "Display name",
|
"subscription_settings_dialog_display_name_placeholder": "Display name",
|
||||||
|
"subscription_settings_dialog_reserve_topic_label": "Reserve topic and configure access",
|
||||||
"subscription_settings_button_cancel": "Cancel",
|
"subscription_settings_button_cancel": "Cancel",
|
||||||
"subscription_settings_button_save": "Save",
|
"subscription_settings_button_save": "Save",
|
||||||
"notifications_loading": "Loading notifications …",
|
"notifications_loading": "Loading notifications …",
|
||||||
|
@ -159,6 +160,7 @@
|
||||||
"subscribe_dialog_login_button_back": "Back",
|
"subscribe_dialog_login_button_back": "Back",
|
||||||
"subscribe_dialog_login_button_login": "Login",
|
"subscribe_dialog_login_button_login": "Login",
|
||||||
"subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
|
"subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
|
||||||
|
"subscribe_dialog_error_topic_already_reserved": "Topic already reserved",
|
||||||
"subscribe_dialog_error_user_anonymous": "anonymous",
|
"subscribe_dialog_error_user_anonymous": "anonymous",
|
||||||
"account_basics_title": "Account",
|
"account_basics_title": "Account",
|
||||||
"account_basics_username_title": "Username",
|
"account_basics_username_title": "Username",
|
||||||
|
@ -253,6 +255,7 @@
|
||||||
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
|
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
|
||||||
"prefs_reservations_dialog_title_add": "Reserve topic",
|
"prefs_reservations_dialog_title_add": "Reserve topic",
|
||||||
"prefs_reservations_dialog_title_edit": "Edit reserved topic",
|
"prefs_reservations_dialog_title_edit": "Edit reserved topic",
|
||||||
|
"prefs_reservations_dialog_description": "Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
||||||
"prefs_reservations_dialog_topic_label": "Topic",
|
"prefs_reservations_dialog_topic_label": "Topic",
|
||||||
"prefs_reservations_dialog_access_label": "Access",
|
"prefs_reservations_dialog_access_label": "Access",
|
||||||
"priority_min": "min",
|
"priority_min": "min",
|
||||||
|
|
|
@ -231,6 +231,8 @@ class AccountApi {
|
||||||
});
|
});
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
|
} else if (response.status === 409) {
|
||||||
|
throw new TopicReservedError();
|
||||||
} else if (response.status !== 200) {
|
} else if (response.status !== 200) {
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
|
@ -312,6 +314,13 @@ export class UsernameTakenError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TopicReservedError extends Error {
|
||||||
|
constructor(topic) {
|
||||||
|
super("Topic already reserved");
|
||||||
|
this.topic = topic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AccountCreateLimitReachedError extends Error {
|
export class AccountCreateLimitReachedError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Account creation limit reached");
|
super("Account creation limit reached");
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
const config = window.config;
|
const config = window.config;
|
||||||
|
|
||||||
|
if (config.base_url === "") {
|
||||||
|
config.base_url = window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -177,7 +177,7 @@ const Stats = () => {
|
||||||
<PrefGroup>
|
<PrefGroup>
|
||||||
<Pref title={t("account_usage_plan_title")}>
|
<Pref title={t("account_usage_plan_title")}>
|
||||||
<div>
|
<div>
|
||||||
{account?.role === "admin"
|
{account.role === "admin"
|
||||||
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
|
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
|
||||||
: t(`account_usage_plan_code_${planCode}`)}
|
: t(`account_usage_plan_code_${planCode}`)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,28 +187,44 @@ const Stats = () => {
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.topics > 0 ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.topics > 0 ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100} />
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100}
|
||||||
|
color={account?.role !== "admin" && account.stats.topics_remaining === 0 ? 'error' : 'primary'}
|
||||||
|
/>
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref title={t("account_usage_messages_title")}>
|
<Pref title={t("account_usage_messages_title")}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100}
|
||||||
|
color={account?.role !== "admin" && account.stats.messages_remaining === 0 ? 'error' : 'primary'}
|
||||||
|
/>
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref title={t("account_usage_emails_title")}>
|
<Pref title={t("account_usage_emails_title")}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100}
|
||||||
|
color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'}
|
||||||
|
/>
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref title={t("account_usage_attachment_storage_title")} subtitle={t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) })}>
|
<Pref title={t("account_usage_attachment_storage_title")} subtitle={account.role !== "admin" ? t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) }) : null}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
|
||||||
|
color={account.role !== "admin" && account.stats.attachment_total_size_remaining === 0 ? 'error' : 'primary'}
|
||||||
|
/>
|
||||||
</Pref>
|
</Pref>
|
||||||
</PrefGroup>
|
</PrefGroup>
|
||||||
{account.limits.basis === "ip" &&
|
{account.limits.basis === "ip" &&
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
CardActions,
|
CardActions,
|
||||||
CardContent,
|
CardContent,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -44,6 +45,8 @@ import LockIcon from "@mui/icons-material/Lock";
|
||||||
import {Public, PublicOff} from "@mui/icons-material";
|
import {Public, PublicOff} from "@mui/icons-material";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||||
|
|
||||||
const Preferences = () => {
|
const Preferences = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -482,10 +485,11 @@ const Reservations = () => {
|
||||||
const [dialogKey, setDialogKey] = useState(0);
|
const [dialogKey, setDialogKey] = useState(0);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
if (!session.exists() || !account) {
|
if (!session.exists() || !account || account.role === "admin") {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
const reservations = account.reservations || [];
|
const reservations = account.reservations || [];
|
||||||
|
const limitReached = account.role === "user" && account.stats.topics_remaining === 0;
|
||||||
|
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
setDialogKey(prev => prev+1);
|
setDialogKey(prev => prev+1);
|
||||||
|
@ -505,7 +509,7 @@ const Reservations = () => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[Preferences] Error topic reservation.`, e);
|
console.log(`[Preferences] Error topic reservation.`, e);
|
||||||
}
|
}
|
||||||
// FIXME handle 401/403
|
// FIXME handle 401/403/409
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -518,9 +522,15 @@ const Reservations = () => {
|
||||||
{t("prefs_reservations_description")}
|
{t("prefs_reservations_description")}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
|
{reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
|
||||||
|
{limitReached &&
|
||||||
|
<Alert severity="info">
|
||||||
|
You reached your reserved topics limit.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button>
|
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
|
||||||
|
|
||||||
<ReservationsDialog
|
<ReservationsDialog
|
||||||
key={`reservationAddDialog${dialogKey}`}
|
key={`reservationAddDialog${dialogKey}`}
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
|
@ -559,7 +569,7 @@ const ReservationsTable = (props) => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[Preferences] Error topic reservation.`, e);
|
console.log(`[Preferences] Error topic reservation.`, e);
|
||||||
}
|
}
|
||||||
// FIXME handle 401/403
|
// FIXME handle 401/403/409
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = async (reservation) => {
|
const handleDeleteClick = async (reservation) => {
|
||||||
|
@ -670,6 +680,9 @@ const ReservationsDialog = (props) => {
|
||||||
<Dialog open={props.open} onClose={props.onCancel} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
<Dialog open={props.open} onClose={props.onCancel} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
<DialogTitle>{editMode ? t("prefs_reservations_dialog_title_edit") : t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
<DialogTitle>{editMode ? t("prefs_reservations_dialog_title_edit") : t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{t("prefs_reservations_dialog_description")}
|
||||||
|
</DialogContentText>
|
||||||
{!editMode && <TextField
|
{!editMode && <TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
@ -682,37 +695,11 @@ const ReservationsDialog = (props) => {
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="standard"
|
variant="standard"
|
||||||
/>}
|
/>}
|
||||||
<FormControl fullWidth variant="standard">
|
<ReserveTopicSelect
|
||||||
<Select
|
|
||||||
value={everyone}
|
value={everyone}
|
||||||
onChange={(ev) => setEveryone(ev.target.value)}
|
onChange={setEveryone}
|
||||||
aria-label={t("prefs_reservations_dialog_access_label")}
|
sx={{mt: 1}}
|
||||||
sx={{
|
/>
|
||||||
marginTop: 1,
|
|
||||||
"& .MuiSelect-select": {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem value="deny-all">
|
|
||||||
<ListItemIcon><LockIcon /></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} />
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="read-only">
|
|
||||||
<ListItemIcon><PublicOff /></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")} />
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="write-only">
|
|
||||||
<ListItemIcon><PublicOff /></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")} />
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="read-write">
|
|
||||||
<ListItemIcon><Public /></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")} />
|
|
||||||
</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>
|
<Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useState} from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import {Checkbox, FormControl, FormControlLabel, Select, useMediaQuery} from "@mui/material";
|
||||||
|
import theme from "./theme";
|
||||||
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
|
import DialogFooter from "./DialogFooter";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||||
|
import session from "../app/Session";
|
||||||
|
import routes from "./routes";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import {Public, PublicOff} from "@mui/icons-material";
|
||||||
|
|
||||||
|
const ReserveTopicSelect = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const sx = props.sx || {};
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth variant="standard" sx={sx}>
|
||||||
|
<Select
|
||||||
|
value={props.value}
|
||||||
|
onChange={(ev) => props.onChange(ev.target.value)}
|
||||||
|
aria-label={t("prefs_reservations_dialog_access_label")}
|
||||||
|
sx={{
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: "4px",
|
||||||
|
paddingBottom: "4px",
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="deny-all">
|
||||||
|
<ListItemIcon><LockIcon/></ListItemIcon>
|
||||||
|
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="read-only">
|
||||||
|
<ListItemIcon><PublicOff/></ListItemIcon>
|
||||||
|
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="write-only">
|
||||||
|
<ListItemIcon><PublicOff/></ListItemIcon>
|
||||||
|
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="read-write">
|
||||||
|
<ListItemIcon><Public/></ListItemIcon>
|
||||||
|
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReserveTopicSelect;
|
|
@ -6,7 +6,7 @@ import Dialog from '@mui/material/Dialog';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
import api from "../app/Api";
|
import api from "../app/Api";
|
||||||
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
|
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
|
||||||
|
@ -17,14 +17,14 @@ import DialogFooter from "./DialogFooter";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
|
||||||
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 LockIcon from '@mui/icons-material/Lock';
|
||||||
import PublicOffIcon from '@mui/icons-material/PublicOff';
|
import PublicOffIcon from '@mui/icons-material/PublicOff';
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import PopupMenu from "./PopupMenu";
|
import PopupMenu from "./PopupMenu";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||||
|
|
||||||
const publicBaseUrl = "https://ntfy.sh";
|
const publicBaseUrl = "https://ntfy.sh";
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ const SubscribeDialog = (props) => {
|
||||||
const [topic, setTopic] = useState("");
|
const [topic, setTopic] = useState("");
|
||||||
const [showLoginPage, setShowLoginPage] = useState(false);
|
const [showLoginPage, setShowLoginPage] = useState(false);
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
const handleSuccess = async () => {
|
const handleSuccess = async () => {
|
||||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
|
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
|
||||||
const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
|
const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
|
||||||
|
@ -44,6 +45,7 @@ const SubscribeDialog = (props) => {
|
||||||
topic: topic
|
topic: topic
|
||||||
});
|
});
|
||||||
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
|
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
|
||||||
|
await accountApi.sync();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
|
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
|
||||||
if ((e instanceof UnauthorizedError)) {
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
@ -54,6 +56,7 @@ const SubscribeDialog = (props) => {
|
||||||
poller.pollInBackground(subscription); // Dangle!
|
poller.pollInBackground(subscription); // Dangle!
|
||||||
props.onSuccess(subscription);
|
props.onSuccess(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||||
{!showLoginPage && <SubscribePage
|
{!showLoginPage && <SubscribePage
|
||||||
|
@ -78,10 +81,11 @@ const SubscribeDialog = (props) => {
|
||||||
|
|
||||||
const SubscribePage = (props) => {
|
const SubscribePage = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [reserveTopicVisible, setReserveTopicVisible] = useState(false);
|
||||||
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
const [accessAnchorEl, setAccessAnchorEl] = useState(null);
|
const [accessAnchorEl, setAccessAnchorEl] = useState(null);
|
||||||
const [access, setAccess] = useState("public");
|
const [everyone, setEveryone] = useState("deny-all");
|
||||||
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));
|
||||||
|
@ -92,6 +96,8 @@ const SubscribePage = (props) => {
|
||||||
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");
|
||||||
|
|
||||||
|
// Check read access to topic
|
||||||
const success = await api.topicAuth(baseUrl, topic, user);
|
const success = await api.topicAuth(baseUrl, topic, user);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
|
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
|
||||||
|
@ -103,6 +109,24 @@ const SubscribePage = (props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reserve topic (if requested)
|
||||||
|
if (session.exists() && baseUrl === config.baseUrl && reserveTopicVisible) {
|
||||||
|
console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`);
|
||||||
|
try {
|
||||||
|
await accountApi.upsertAccess(topic, everyone);
|
||||||
|
// Account sync later after it was added
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[SubscribeDialog] Error reserving topic`, e);
|
||||||
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
} else if ((e instanceof TopicReservedError)) {
|
||||||
|
setErrorText(t("subscribe_dialog_error_topic_already_reserved"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
};
|
};
|
||||||
|
@ -137,14 +161,7 @@ const SubscribePage = (props) => {
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{t("subscribe_dialog_subscribe_description")}
|
{t("subscribe_dialog_subscribe_description")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<div style={{display: 'flex'}} role="row">
|
<div style={{display: 'flex', paddingBottom: "8px"}} role="row">
|
||||||
{session.exists() &&
|
|
||||||
<IconButton onClick={(ev) => setAccessAnchorEl(ev.currentTarget)} color="inherit" size="large" edge="start" sx={{height: "45px", marginTop: "5px", color: "grey"}}>
|
|
||||||
{access === "public" && <PublicIcon/>}
|
|
||||||
{access === "public-read" && <PublicOffIcon/>}
|
|
||||||
{access === "private" && <LockIcon/>}
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
@ -168,19 +185,19 @@ const SubscribePage = (props) => {
|
||||||
open={!!accessAnchorEl}
|
open={!!accessAnchorEl}
|
||||||
onClose={() => setAccessAnchorEl(null)}
|
onClose={() => setAccessAnchorEl(null)}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={() => setAccess("private")} selected={access === "private"}>
|
<MenuItem onClick={() => setEveryone("private")} selected={everyone === "private"}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LockIcon fontSize="small" />
|
<LockIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
Only I can publish and subscribe
|
Only I can publish and subscribe
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => setAccess("public-read")} selected={access === "public-read"}>
|
<MenuItem onClick={() => setEveryone("public-read")} selected={everyone === "public-read"}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PublicOffIcon fontSize="small" />
|
<PublicOffIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
I can publish, everyone can subscribe
|
I can publish, everyone can subscribe
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => setAccess("public")} selected={access === "public"}>
|
<MenuItem onClick={() => setEveryone("public")} selected={everyone === "public"}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PublicIcon fontSize="small" />
|
<PublicIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -188,8 +205,33 @@ const SubscribePage = (props) => {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</PopupMenu>
|
</PopupMenu>
|
||||||
</div>
|
</div>
|
||||||
|
{session.exists() && !anotherServerVisible &&
|
||||||
|
<FormGroup>
|
||||||
|
<FormControlLabel
|
||||||
|
variant="standard"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
fullWidth
|
||||||
|
checked={reserveTopicVisible}
|
||||||
|
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||||
|
inputProps={{
|
||||||
|
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("subscription_settings_dialog_reserve_topic_label")}
|
||||||
|
/>
|
||||||
|
{reserveTopicVisible &&
|
||||||
|
<ReserveTopicSelect
|
||||||
|
value={everyone}
|
||||||
|
onChange={setEveryone}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
}
|
||||||
|
{!reserveTopicVisible &&
|
||||||
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
sx={{pt: 1}}
|
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={handleUseAnotherChanged}
|
onChange={handleUseAnotherChanged}
|
||||||
|
@ -202,7 +244,6 @@ const SubscribePage = (props) => {
|
||||||
{anotherServerVisible && <Autocomplete
|
{anotherServerVisible && <Autocomplete
|
||||||
freeSolo
|
freeSolo
|
||||||
options={existingBaseUrls}
|
options={existingBaseUrls}
|
||||||
sx={{ maxWidth: 400 }}
|
|
||||||
inputValue={props.baseUrl}
|
inputValue={props.baseUrl}
|
||||||
onInputChange={updateBaseUrl}
|
onInputChange={updateBaseUrl}
|
||||||
renderInput={(params) =>
|
renderInput={(params) =>
|
||||||
|
@ -214,6 +255,8 @@ const SubscribePage = (props) => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>}
|
/>}
|
||||||
|
</FormGroup>
|
||||||
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter status={errorText}>
|
<DialogFooter status={errorText}>
|
||||||
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
|
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Dialog from '@mui/material/Dialog';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import {Checkbox, FormControl, FormControlLabel, Select, useMediaQuery} from "@mui/material";
|
import {Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
import DialogFooter from "./DialogFooter";
|
import DialogFooter from "./DialogFooter";
|
||||||
|
@ -14,11 +14,7 @@ import {useTranslation} from "react-i18next";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
|
||||||
import {Public, PublicOff} from "@mui/icons-material";
|
|
||||||
|
|
||||||
const SubscriptionSettingsDialog = (props) => {
|
const SubscriptionSettingsDialog = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -53,6 +49,8 @@ const SubscriptionSettingsDialog = (props) => {
|
||||||
if ((e instanceof UnauthorizedError)) {
|
if ((e instanceof UnauthorizedError)) {
|
||||||
session.resetAndRedirect(routes.login);
|
session.resetAndRedirect(routes.login);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME handle 409
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
props.onClose();
|
props.onClose();
|
||||||
|
@ -80,7 +78,6 @@ const SubscriptionSettingsDialog = (props) => {
|
||||||
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
|
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="standard"
|
variant="standard"
|
||||||
|
@ -90,45 +87,17 @@ const SubscriptionSettingsDialog = (props) => {
|
||||||
checked={reserveTopicVisible}
|
checked={reserveTopicVisible}
|
||||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
"aria-label": t("xxxxxxxxxxxxxxxxxx")
|
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={t("Reserve topic and configure custom access:")}
|
label={t("subscription_settings_dialog_reserve_topic_label")}
|
||||||
/>
|
/>
|
||||||
{reserveTopicVisible &&
|
{reserveTopicVisible &&
|
||||||
<FormControl variant="standard">
|
<ReserveTopicSelect
|
||||||
<Select
|
|
||||||
value={everyone}
|
value={everyone}
|
||||||
onChange={(ev) => setEveryone(ev.target.value)}
|
onChange={setEveryone}
|
||||||
aria-label={t("prefs_reservations_dialog_access_label")}
|
/>
|
||||||
sx={{
|
|
||||||
"& .MuiSelect-select": {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: "4px",
|
|
||||||
paddingBottom: "4px",
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem value="deny-all">
|
|
||||||
<ListItemIcon><LockIcon/></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="read-only">
|
|
||||||
<ListItemIcon><PublicOff/></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="write-only">
|
|
||||||
<ListItemIcon><PublicOff/></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="read-write">
|
|
||||||
<ListItemIcon><Public/></ListItemIcon>
|
|
||||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
|
|
||||||
</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|
Loading…
Reference in New Issue