binwiederhier 3 лет назад
Родитель
Сommit
bd86e3d951

+ 4 - 0
server/server.go

@@ -45,6 +45,7 @@ import (
 		reset daily limits for users
 		Account usage not updated "in real time"
 		max token issue limit
+		user db startup queries -> foreign keys
 		Sync:
 			- "mute" setting
 			- figure out what settings are "web" or "phone"
@@ -101,6 +102,7 @@ var (
 	accountPasswordPath            = "/v1/account/password"
 	accountSettingsPath            = "/v1/account/settings"
 	accountSubscriptionPath        = "/v1/account/subscription"
+	accountAccessPath              = "/v1/account/access"
 	accountSubscriptionSingleRegex = regexp.MustCompile(`^/v1/account/subscription/([-_A-Za-z0-9]{16})$`)
 	matrixPushPath                 = "/_matrix/push/v1/notify"
 	staticRegex                    = regexp.MustCompile(`^/static/.+`)
@@ -357,6 +359,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.ensureUser(s.handleAccountSubscriptionChange)(w, r, v)
 	} else if r.Method == http.MethodDelete && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
 		return s.ensureUser(s.handleAccountSubscriptionDelete)(w, r, v)
+	} else if r.Method == http.MethodPost && r.URL.Path == accountAccessPath {
+		return s.ensureUser(s.handleAccountAccessAdd)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
 		return s.handleMatrixDiscovery(w)
 	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {

+ 19 - 0
server/server_account.go

@@ -307,3 +307,22 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.
 	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
 	return nil
 }
+
+func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	req, err := readJSONWithLimit[apiAccountAccessRequest](r.Body, jsonBodyBytesLimit)
+	if err != nil {
+		return err
+	}
+	if !topicRegex.MatchString(req.Topic) {
+		return errHTTPBadRequestTopicInvalid
+	}
+	if err := s.userManager.AllowAccess(v.user.Name, req.Topic, true, true); err != nil {
+		return err
+	}
+	if err := s.userManager.AllowAccess(user.Everyone, req.Topic, false, false); err != nil {
+		return err
+	}
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
+	return nil
+}

+ 5 - 0
server/types.go

@@ -266,3 +266,8 @@ type apiAccountResponse struct {
 	Limits        *apiAccountLimits       `json:"limits,omitempty"`
 	Stats         *apiAccountStats        `json:"stats,omitempty"`
 }
+
+type apiAccountAccessRequest struct {
+	Topic  string `json:"topic"`
+	Access string `json:"access"`
+}

+ 2 - 0
web/public/static/langs/en.json

@@ -4,6 +4,7 @@
   "signup_form_password": "Password",
   "signup_form_confirm_password": "Confirm password",
   "signup_form_button_submit": "Sign up",
+  "signup_form_toggle_password_visibility": "Toggle password visibility",
   "signup_already_have_account": "Already have an account? Sign in!",
   "signup_disabled": "Signup is disabled",
   "signup_error_username_taken": "Username {{username}} is already taken",
@@ -224,6 +225,7 @@
   "prefs_users_add_button": "Add user",
   "prefs_users_edit_button": "Edit user",
   "prefs_users_delete_button": "Delete user",
+  "prefs_users_table_cannot_delete_or_edit": "Cannot delete or edit logged in user",
   "prefs_users_table_user_header": "User",
   "prefs_users_table_base_url_header": "Service URL",
   "prefs_users_dialog_title_add": "Add user",

+ 1 - 1
web/src/app/UserManager.js

@@ -18,7 +18,7 @@ class UserManager {
     }
 
     async save(user) {
-        if (user.baseUrl === config.baseUrl) {
+        if (session.exists() && user.baseUrl === config.baseUrl) {
             return;
         }
         await db.users.put(user);

+ 20 - 1
web/src/components/Login.js

@@ -11,12 +11,17 @@ import {NavLink} from "react-router-dom";
 import AvatarBox from "./AvatarBox";
 import {useTranslation} from "react-i18next";
 import accountApi, {UnauthorizedError} from "../app/AccountApi";
+import IconButton from "@mui/material/IconButton";
+import {InputAdornment} from "@mui/material";
+import {Visibility, VisibilityOff} from "@mui/icons-material";
 
 const Login = () => {
     const { t } = useTranslation();
     const [error, setError] = useState("");
     const [username, setUsername] = useState("");
     const [password, setPassword] = useState("");
+    const [showPassword, setShowPassword] = useState(false);
+
     const handleSubmit = async (event) => {
         event.preventDefault();
         const user = { username, password };
@@ -66,11 +71,25 @@ const Login = () => {
                     fullWidth
                     name="password"
                     label={t("signup_form_password")}
-                    type="password"
+                    type={showPassword ? "text" : "password"}
                     id="password"
                     value={password}
                     onChange={ev => setPassword(ev.target.value.trim())}
                     autoComplete="current-password"
+                    InputProps={{
+                        endAdornment: (
+                            <InputAdornment position="end">
+                                <IconButton
+                                    aria-label={t("signup_form_toggle_password_visibility")}
+                                    onClick={() => setShowPassword(!showPassword)}
+                                    onMouseDown={(ev) => ev.preventDefault()}
+                                    edge="end"
+                                >
+                                    {showPassword ? <VisibilityOff /> : <Visibility />}
+                                </IconButton>
+                            </InputAdornment>
+                        )
+                    }}
                 />
                 <Button
                     type="submit"

+ 19 - 6
web/src/components/Preferences.js

@@ -10,7 +10,7 @@ import {
     TableBody,
     TableCell,
     TableHead,
-    TableRow,
+    TableRow, Tooltip,
     useMediaQuery
 } from "@mui/material";
 import Typography from "@mui/material/Typography";
@@ -38,6 +38,8 @@ import session from "../app/Session";
 import routes from "./routes";
 import accountApi, {UnauthorizedError} from "../app/AccountApi";
 import {Pref, PrefGroup} from "./Pref";
+import InfoIcon from '@mui/icons-material/Info';
+import {useNavigate} from "react-router-dom";
 
 const Preferences = () => {
     return (
@@ -245,14 +247,17 @@ const UserTable = (props) => {
     const [dialogKey, setDialogKey] = useState(0);
     const [dialogOpen, setDialogOpen] = useState(false);
     const [dialogUser, setDialogUser] = useState(null);
+
     const handleEditClick = (user) => {
         setDialogKey(prev => prev+1);
         setDialogUser(user);
         setDialogOpen(true);
     };
+
     const handleDialogCancel = () => {
         setDialogOpen(false);
     };
+
     const handleDialogSubmit = async (user) => {
         setDialogOpen(false);
         try {
@@ -262,6 +267,7 @@ const UserTable = (props) => {
             console.log(`[Preferences] Error updating user.`, e);
         }
     };
+
     const handleDeleteClick = async (user) => {
         try {
             await userManager.delete(user.baseUrl);
@@ -270,6 +276,7 @@ const UserTable = (props) => {
             console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
         }
     };
+
     return (
         <Table size="small" aria-label={t("prefs_users_table")}>
             <TableHead>
@@ -289,18 +296,24 @@ const UserTable = (props) => {
                                    aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
                         <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
                         <TableCell align="right">
-                            {user.baseUrl !== config.baseUrl &&
+                            {(!session.exists() || user.baseUrl !== config.baseUrl) &&
                                 <>
-                                    <IconButton onClick={() => handleEditClick(user)}
-                                                aria-label={t("prefs_users_edit_button")}>
+                                    <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
                                         <EditIcon/>
                                     </IconButton>
-                                    <IconButton onClick={() => handleDeleteClick(user)}
-                                                aria-label={t("prefs_users_delete_button")}>
+                                    <IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
                                         <CloseIcon/>
                                     </IconButton>
                                 </>
                             }
+                            {session.exists() && user.baseUrl === config.baseUrl &&
+                                <Tooltip title={t("prefs_users_table_cannot_delete_or_edit")}>
+                                    <span>
+                                        <IconButton disabled><EditIcon/></IconButton>
+                                        <IconButton disabled><CloseIcon/></IconButton>
+                                    </span>
+                                </Tooltip>
+                            }
                         </TableCell>
                     </TableRow>
                 ))}

+ 20 - 15
web/src/components/Signup.js

@@ -11,13 +11,16 @@ import AvatarBox from "./AvatarBox";
 import {useTranslation} from "react-i18next";
 import WarningAmberIcon from "@mui/icons-material/WarningAmber";
 import accountApi, {AccountCreateLimitReachedError, UsernameTakenError} from "../app/AccountApi";
+import {InputAdornment} from "@mui/material";
+import IconButton from "@mui/material/IconButton";
+import {Visibility, VisibilityOff} from "@mui/icons-material";
 
 const Signup = () => {
     const { t } = useTranslation();
     const [error, setError] = useState("");
     const [username, setUsername] = useState("");
     const [password, setPassword] = useState("");
-    const [confirm, setConfirm] = useState("");
+    const [showPassword, setShowPassword] = useState(false);
     const handleSubmit = async (event) => {
         event.preventDefault();
         const user = { username, password };
@@ -70,29 +73,31 @@ const Signup = () => {
                     fullWidth
                     name="password"
                     label={t("signup_form_password")}
-                    type="password"
+                    type={showPassword ? "text" : "password"}
                     id="password"
                     autoComplete="current-password"
                     value={password}
                     onChange={ev => setPassword(ev.target.value.trim())}
-                />
-                <TextField
-                    margin="dense"
-                    required
-                    fullWidth
-                    name="confirm-password"
-                    label={t("signup_form_confirm_password")}
-                    type="password"
-                    id="confirm-password"
-                    value={confirm}
-                    onChange={ev => setConfirm(ev.target.value.trim())}
-
+                    InputProps={{
+                        endAdornment: (
+                            <InputAdornment position="end">
+                                <IconButton
+                                    aria-label={t("signup_form_toggle_password_visibility")}
+                                    onClick={() => setShowPassword(!showPassword)}
+                                    onMouseDown={(ev) => ev.preventDefault()}
+                                    edge="end"
+                                >
+                                    {showPassword ? <VisibilityOff /> : <Visibility />}
+                                </IconButton>
+                            </InputAdornment>
+                        )
+                    }}
                 />
                 <Button
                     type="submit"
                     fullWidth
                     variant="contained"
-                    disabled={username === "" || password === "" || password !== confirm}
+                    disabled={username === "" || password === ""}
                     sx={{mt: 2, mb: 2}}
                 >
                     {t("signup_form_button_submit")}

+ 5 - 0
web/src/components/SubscribeDialog.js

@@ -18,6 +18,8 @@ import {useTranslation} from "react-i18next";
 import session from "../app/Session";
 import routes from "./routes";
 import accountApi, {UnauthorizedError} from "../app/AccountApi";
+import IconButton from "@mui/material/IconButton";
+import PublicIcon from '@mui/icons-material/Public';
 
 const publicBaseUrl = "https://ntfy.sh";
 
@@ -123,6 +125,9 @@ const SubscribePage = (props) => {
                     {t("subscribe_dialog_subscribe_description")}
                 </DialogContentText>
                 <div style={{display: 'flex'}} role="row">
+                    <IconButton color="inherit" size="large" edge="start" sx={{height: "45px", marginTop: "5px", color: "grey"}}>
+                        <PublicIcon/>
+                    </IconButton>
                     <TextField
                         autoFocus
                         margin="dense"