Basic user access endpoint
This commit is contained in:
parent
b131d676c4
commit
bd86e3d951
9 changed files with 95 additions and 23 deletions
|
@ -45,6 +45,7 @@ import (
|
||||||
reset daily limits for users
|
reset daily limits for users
|
||||||
Account usage not updated "in real time"
|
Account usage not updated "in real time"
|
||||||
max token issue limit
|
max token issue limit
|
||||||
|
user db startup queries -> foreign keys
|
||||||
Sync:
|
Sync:
|
||||||
- "mute" setting
|
- "mute" setting
|
||||||
- figure out what settings are "web" or "phone"
|
- figure out what settings are "web" or "phone"
|
||||||
|
@ -101,6 +102,7 @@ var (
|
||||||
accountPasswordPath = "/v1/account/password"
|
accountPasswordPath = "/v1/account/password"
|
||||||
accountSettingsPath = "/v1/account/settings"
|
accountSettingsPath = "/v1/account/settings"
|
||||||
accountSubscriptionPath = "/v1/account/subscription"
|
accountSubscriptionPath = "/v1/account/subscription"
|
||||||
|
accountAccessPath = "/v1/account/access"
|
||||||
accountSubscriptionSingleRegex = regexp.MustCompile(`^/v1/account/subscription/([-_A-Za-z0-9]{16})$`)
|
accountSubscriptionSingleRegex = regexp.MustCompile(`^/v1/account/subscription/([-_A-Za-z0-9]{16})$`)
|
||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
matrixPushPath = "/_matrix/push/v1/notify"
|
||||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
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)
|
return s.ensureUser(s.handleAccountSubscriptionChange)(w, r, v)
|
||||||
} else if r.Method == http.MethodDelete && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodDelete && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensureUser(s.handleAccountSubscriptionDelete)(w, r, v)
|
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 {
|
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
|
||||||
return s.handleMatrixDiscovery(w)
|
return s.handleMatrixDiscovery(w)
|
||||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||||
|
|
|
@ -307,3 +307,22 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
|
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -266,3 +266,8 @@ type apiAccountResponse struct {
|
||||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||||
Stats *apiAccountStats `json:"stats,omitempty"`
|
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiAccountAccessRequest struct {
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Access string `json:"access"`
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"signup_form_password": "Password",
|
"signup_form_password": "Password",
|
||||||
"signup_form_confirm_password": "Confirm password",
|
"signup_form_confirm_password": "Confirm password",
|
||||||
"signup_form_button_submit": "Sign up",
|
"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_already_have_account": "Already have an account? Sign in!",
|
||||||
"signup_disabled": "Signup is disabled",
|
"signup_disabled": "Signup is disabled",
|
||||||
"signup_error_username_taken": "Username {{username}} is already taken",
|
"signup_error_username_taken": "Username {{username}} is already taken",
|
||||||
|
@ -224,6 +225,7 @@
|
||||||
"prefs_users_add_button": "Add user",
|
"prefs_users_add_button": "Add user",
|
||||||
"prefs_users_edit_button": "Edit user",
|
"prefs_users_edit_button": "Edit user",
|
||||||
"prefs_users_delete_button": "Delete 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_user_header": "User",
|
||||||
"prefs_users_table_base_url_header": "Service URL",
|
"prefs_users_table_base_url_header": "Service URL",
|
||||||
"prefs_users_dialog_title_add": "Add user",
|
"prefs_users_dialog_title_add": "Add user",
|
||||||
|
|
|
@ -18,7 +18,7 @@ class UserManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(user) {
|
async save(user) {
|
||||||
if (user.baseUrl === config.baseUrl) {
|
if (session.exists() && user.baseUrl === config.baseUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await db.users.put(user);
|
await db.users.put(user);
|
||||||
|
|
|
@ -11,12 +11,17 @@ import {NavLink} from "react-router-dom";
|
||||||
import AvatarBox from "./AvatarBox";
|
import AvatarBox from "./AvatarBox";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
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 Login = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const user = { username, password };
|
const user = { username, password };
|
||||||
|
@ -66,11 +71,25 @@ const Login = () => {
|
||||||
fullWidth
|
fullWidth
|
||||||
name="password"
|
name="password"
|
||||||
label={t("signup_form_password")}
|
label={t("signup_form_password")}
|
||||||
type="password"
|
type={showPassword ? "text" : "password"}
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={ev => setPassword(ev.target.value.trim())}
|
onChange={ev => setPassword(ev.target.value.trim())}
|
||||||
autoComplete="current-password"
|
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
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow, Tooltip,
|
||||||
useMediaQuery
|
useMediaQuery
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
@ -38,6 +38,8 @@ import session from "../app/Session";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||||
import {Pref, PrefGroup} from "./Pref";
|
import {Pref, PrefGroup} from "./Pref";
|
||||||
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
const Preferences = () => {
|
const Preferences = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -245,14 +247,17 @@ const UserTable = (props) => {
|
||||||
const [dialogKey, setDialogKey] = useState(0);
|
const [dialogKey, setDialogKey] = useState(0);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogUser, setDialogUser] = useState(null);
|
const [dialogUser, setDialogUser] = useState(null);
|
||||||
|
|
||||||
const handleEditClick = (user) => {
|
const handleEditClick = (user) => {
|
||||||
setDialogKey(prev => prev+1);
|
setDialogKey(prev => prev+1);
|
||||||
setDialogUser(user);
|
setDialogUser(user);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDialogCancel = () => {
|
const handleDialogCancel = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDialogSubmit = async (user) => {
|
const handleDialogSubmit = async (user) => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
try {
|
try {
|
||||||
|
@ -262,6 +267,7 @@ const UserTable = (props) => {
|
||||||
console.log(`[Preferences] Error updating user.`, e);
|
console.log(`[Preferences] Error updating user.`, e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = async (user) => {
|
const handleDeleteClick = async (user) => {
|
||||||
try {
|
try {
|
||||||
await userManager.delete(user.baseUrl);
|
await userManager.delete(user.baseUrl);
|
||||||
|
@ -270,6 +276,7 @@ const UserTable = (props) => {
|
||||||
console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
|
console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table size="small" aria-label={t("prefs_users_table")}>
|
<Table size="small" aria-label={t("prefs_users_table")}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
@ -289,18 +296,24 @@ const UserTable = (props) => {
|
||||||
aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
|
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 aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{user.baseUrl !== config.baseUrl &&
|
{(!session.exists() || user.baseUrl !== config.baseUrl) &&
|
||||||
<>
|
<>
|
||||||
<IconButton onClick={() => handleEditClick(user)}
|
<IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
|
||||||
aria-label={t("prefs_users_edit_button")}>
|
|
||||||
<EditIcon/>
|
<EditIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => handleDeleteClick(user)}
|
<IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
|
||||||
aria-label={t("prefs_users_delete_button")}>
|
|
||||||
<CloseIcon/>
|
<CloseIcon/>
|
||||||
</IconButton>
|
</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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -11,13 +11,16 @@ import AvatarBox from "./AvatarBox";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
||||||
import accountApi, {AccountCreateLimitReachedError, UsernameTakenError} from "../app/AccountApi";
|
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 Signup = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirm, setConfirm] = useState("");
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const user = { username, password };
|
const user = { username, password };
|
||||||
|
@ -70,29 +73,31 @@ const Signup = () => {
|
||||||
fullWidth
|
fullWidth
|
||||||
name="password"
|
name="password"
|
||||||
label={t("signup_form_password")}
|
label={t("signup_form_password")}
|
||||||
type="password"
|
type={showPassword ? "text" : "password"}
|
||||||
id="password"
|
id="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={ev => setPassword(ev.target.value.trim())}
|
onChange={ev => setPassword(ev.target.value.trim())}
|
||||||
/>
|
InputProps={{
|
||||||
<TextField
|
endAdornment: (
|
||||||
margin="dense"
|
<InputAdornment position="end">
|
||||||
required
|
<IconButton
|
||||||
fullWidth
|
aria-label={t("signup_form_toggle_password_visibility")}
|
||||||
name="confirm-password"
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
label={t("signup_form_confirm_password")}
|
onMouseDown={(ev) => ev.preventDefault()}
|
||||||
type="password"
|
edge="end"
|
||||||
id="confirm-password"
|
>
|
||||||
value={confirm}
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
onChange={ev => setConfirm(ev.target.value.trim())}
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={username === "" || password === "" || password !== confirm}
|
disabled={username === "" || password === ""}
|
||||||
sx={{mt: 2, mb: 2}}
|
sx={{mt: 2, mb: 2}}
|
||||||
>
|
>
|
||||||
{t("signup_form_button_submit")}
|
{t("signup_form_button_submit")}
|
||||||
|
|
|
@ -18,6 +18,8 @@ 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, {UnauthorizedError} from "../app/AccountApi";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import PublicIcon from '@mui/icons-material/Public';
|
||||||
|
|
||||||
const publicBaseUrl = "https://ntfy.sh";
|
const publicBaseUrl = "https://ntfy.sh";
|
||||||
|
|
||||||
|
@ -123,6 +125,9 @@ 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"}}>
|
||||||
|
<PublicIcon/>
|
||||||
|
</IconButton>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
|
Loading…
Reference in a new issue