diff --git a/frontend/src/api/backend/base.ts b/frontend/src/api/backend/base.ts index 178f13cb..9e3ca73d 100644 --- a/frontend/src/api/backend/base.ts +++ b/frontend/src/api/backend/base.ts @@ -124,7 +124,7 @@ export async function post({ url, params, data, noAuth }: PostArgs, abortControl interface PutArgs { url: string; params?: queryString.StringifiableRecord; - data?: Record; + data?: Record; } export async function put({ url, params, data }: PutArgs, abortController?: AbortController) { const apiUrl = buildUrl({ url, params }); diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts index 1bfccb4b..9cd5b526 100644 --- a/frontend/src/api/backend/index.ts +++ b/frontend/src/api/backend/index.ts @@ -38,6 +38,7 @@ export * from "./models"; export * from "./refreshToken"; export * from "./renewCertificate"; export * from "./responseTypes"; +export * from "./setPermissions"; export * from "./testHttpCertificate"; export * from "./toggleDeadHost"; export * from "./toggleProxyHost"; diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts index 4650849c..328c8fc6 100644 --- a/frontend/src/api/backend/models.ts +++ b/frontend/src/api/backend/models.ts @@ -5,10 +5,10 @@ export interface AppVersion { } export interface UserPermissions { - id: number; - createdOn: string; - modifiedOn: string; - userId: number; + id?: number; + createdOn?: string; + modifiedOn?: string; + userId?: number; visibility: string; proxyHosts: string; redirectionHosts: string; diff --git a/frontend/src/api/backend/setPermissions.ts b/frontend/src/api/backend/setPermissions.ts new file mode 100644 index 00000000..80616e97 --- /dev/null +++ b/frontend/src/api/backend/setPermissions.ts @@ -0,0 +1,17 @@ +import * as api from "./base"; +import type { UserPermissions } from "./models"; + +export async function setPermissions( + userId: number, + data: UserPermissions, + abortController?: AbortController, +): Promise { + // Remove readonly fields + return await api.put( + { + url: `/users/${userId}/permissions`, + data, + }, + abortController, + ); +} diff --git a/frontend/src/locale/lang/en.json b/frontend/src/locale/lang/en.json index 1373bfc0..1d270543 100644 --- a/frontend/src/locale/lang/en.json +++ b/frontend/src/locale/lang/en.json @@ -58,6 +58,12 @@ "offline": "Offline", "online": "Online", "password": "Password", + "permissions.hidden": "Hidden", + "permissions.manage": "Manage", + "permissions.view": "View Only", + "permissions.visibility.all": "All Items", + "permissions.visibility.title": "Item Visibility", + "permissions.visibility.user": "Created Items Only", "proxy-hosts.actions-title": "Proxy Host #{id}", "proxy-hosts.add": "Add Proxy Host", "proxy-hosts.count": "{count} Proxy Hosts", @@ -95,6 +101,7 @@ "user.new": "New User", "user.new-password": "New Password", "user.nickname": "Nickname", + "user.set-permissions": "Set Permissions for {name}", "user.switch-dark": "Switch to Dark mode", "user.switch-light": "Switch to Light mode", "users.actions-title": "User #{id}", diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 64ebf373..541e3732 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -176,6 +176,24 @@ "password": { "defaultMessage": "Password" }, + "permissions.hidden": { + "defaultMessage": "Hidden" + }, + "permissions.manage": { + "defaultMessage": "Manage" + }, + "permissions.view": { + "defaultMessage": "View Only" + }, + "permissions.visibility.all": { + "defaultMessage": "All Items" + }, + "permissions.visibility.title": { + "defaultMessage": "Item Visibility" + }, + "permissions.visibility.user": { + "defaultMessage": "Created Items Only" + }, "proxy-hosts.actions-title": { "defaultMessage": "Proxy Host #{id}" }, @@ -287,6 +305,9 @@ "user.nickname": { "defaultMessage": "Nickname" }, + "user.set-permissions": { + "defaultMessage": "Set Permissions for {name}" + }, "user.switch-dark": { "defaultMessage": "Switch to Dark mode" }, diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx new file mode 100644 index 00000000..e3368418 --- /dev/null +++ b/frontend/src/modals/PermissionsModal.tsx @@ -0,0 +1,231 @@ +import { useQueryClient } from "@tanstack/react-query"; +import cn from "classnames"; +import { Field, Form, Formik } from "formik"; +import { useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { setPermissions } from "src/api/backend"; +import { Button, Loading } from "src/components"; +import { useUser } from "src/hooks"; +import { intl } from "src/locale"; + +interface Props { + userId: number; + onClose: () => void; +} +export function PermissionsModal({ userId, onClose }: Props) { + const queryClient = useQueryClient(); + const [errorMsg, setErrorMsg] = useState(null); + const { data, isLoading, error } = useUser(userId); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + setErrorMsg(null); + try { + await setPermissions(userId, values); + onClose(); + queryClient.invalidateQueries({ queryKey: ["users"] }); + queryClient.invalidateQueries({ queryKey: ["user"] }); + } catch (err: any) { + setErrorMsg(intl.formatMessage({ id: err.message })); + } + setSubmitting(false); + }; + + const getPermissionButtons = (field: any, form: any) => { + return ( +
+
+ form.setFieldValue(field.name, "manage")} + /> + + form.setFieldValue(field.name, "view")} + /> + + form.setFieldValue(field.name, "hidden")} + /> + +
+
+ ); + }; + + const isAdmin = data?.roles.indexOf("admin") !== -1; + + return ( + + {!isLoading && error && {error?.message || "Unknown error"}} + {isLoading && } + {!isLoading && data && ( + + {({ isSubmitting }) => ( +
+ + + {intl.formatMessage({ id: "user.set-permissions" }, { name: data?.name })} + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
+ + + {({ field, form }: any) => ( +
+ form.setFieldValue(field.name, "user")} + /> + + form.setFieldValue(field.name, "all")} + /> + +
+ )} +
+
+ {!isAdmin && ( + <> +
+ + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
+
+ + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
+
+ + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
+
+ + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
+
+ + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
+
+ + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
+ + )} +
+ + + + +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/modals/index.ts b/frontend/src/modals/index.ts index 80501c8e..d6224c97 100644 --- a/frontend/src/modals/index.ts +++ b/frontend/src/modals/index.ts @@ -1,3 +1,4 @@ export * from "./ChangePasswordModal"; export * from "./DeleteConfirmModal"; +export * from "./PermissionsModal"; export * from "./UserModal"; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 452b3205..22eb6707 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -128,6 +128,7 @@ const Dashboard = () => { - check mobile - fix bad jwt not refreshing entire page - add help docs for host types +- REDO SCREENSHOTS in docs folder More for api, then implement here: - Properly implement refresh tokens diff --git a/frontend/src/pages/Users/Table.tsx b/frontend/src/pages/Users/Table.tsx index 7085c2e0..5d8498bf 100644 --- a/frontend/src/pages/Users/Table.tsx +++ b/frontend/src/pages/Users/Table.tsx @@ -12,10 +12,21 @@ interface Props { isFetching?: boolean; currentUserId?: number; onEditUser?: (id: number) => void; + onEditPermissions?: (id: number) => void; + onSetPassword?: (id: number) => void; onDeleteUser?: (id: number) => void; onNewUser?: () => void; } -export default function Table({ data, isFetching, currentUserId, onEditUser, onDeleteUser, onNewUser }: Props) { +export default function Table({ + data, + isFetching, + currentUserId, + onEditUser, + onEditPermissions, + onSetPassword, + onDeleteUser, + onNewUser, +}: Props) { const columnHelper = createColumnHelper(); const columns = useMemo( () => [ @@ -92,16 +103,30 @@ export default function Table({ data, isFetching, currentUserId, onEditUser, onD {intl.formatMessage({ id: "user.edit" })} - - - {intl.formatMessage({ id: "action.permissions" })} - - - - {intl.formatMessage({ id: "user.change-password" })} - {currentUserId !== info.row.original.id ? ( <> + { + e.preventDefault(); + onEditPermissions?.(info.row.original.id); + }} + > + + {intl.formatMessage({ id: "action.permissions" })} + + { + e.preventDefault(); + onSetPassword?.(info.row.original.id); + }} + > + + {intl.formatMessage({ id: "user.change-password" })} +
({ diff --git a/frontend/src/pages/Users/TableWrapper.tsx b/frontend/src/pages/Users/TableWrapper.tsx index 86835f5c..fe9b2fe6 100644 --- a/frontend/src/pages/Users/TableWrapper.tsx +++ b/frontend/src/pages/Users/TableWrapper.tsx @@ -5,12 +5,13 @@ import { deleteUser } from "src/api/backend"; import { Button, LoadingPage } from "src/components"; import { useUser, useUsers } from "src/hooks"; import { intl } from "src/locale"; -import { DeleteConfirmModal, UserModal } from "src/modals"; +import { DeleteConfirmModal, PermissionsModal, UserModal } from "src/modals"; import { showSuccess } from "src/notifications"; import Table from "./Table"; export default function TableWrapper() { const [editUserId, setEditUserId] = useState(0 as number | "new"); + const [editUserPermissionsId, setEditUserPermissionsId] = useState(0); const [deleteUserId, setDeleteUserId] = useState(0); const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]); const { data: currentUser } = useUser("me"); @@ -62,10 +63,14 @@ export default function TableWrapper() { isFetching={isFetching} currentUserId={currentUser?.id} onEditUser={(id: number) => setEditUserId(id)} + onEditPermissions={(id: number) => setEditUserPermissionsId(id)} onDeleteUser={(id: number) => setDeleteUserId(id)} onNewUser={() => setEditUserId("new")} /> {editUserId ? setEditUserId(0)} /> : null} + {editUserPermissionsId ? ( + setEditUserPermissionsId(0)} /> + ) : null} {deleteUserId ? (