feature/scope limits (#159)

* limitations to carriers and speech

* post speech to a different path if scoped user

* add filtering to speech creds

* check reroute when logging in

* faulty checks

* fix access conditionals

* add restrictions to applications and accounts

* apply 1st review comments

* fix routing

* apply review comments

* discussed changes

* useScopedRedirect

* Refactor how we manage scope for navi (#163)

* fix user name

* Fix useScopedRedirect hook (#164)

* Fix useScopedRedirect hook

* Refactor user UI

* Move user-me to own directory

* add scope limits to routes

* scope optional limits

* cleanup

* apply review comments

* Refactor conditional apiUrl logic -- add apiPath to deps in useApiData hook (#168)

* apply review comments - accountFilter remove from AccountScoped Views

* remove account filtering as it is done on back-end now

* Cleanup some things

* Clean up some scope things

* Implement account user PUT method for carriers

* filterScopeOptions accorfing to user scope

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Brandon Lee Kitajchuk <bk@kitajchuk.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
This commit is contained in:
EgleH
2023-01-02 23:37:17 +01:00
committed by GitHub
parent 5af46101e1
commit 93d313b21c
42 changed files with 968 additions and 280 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import type {
LimitField,
LimitUnitOption,
PasswordSettings,
SelectorOptions,
SipGateway,
SmppGateway,
WebHook,
@@ -104,7 +105,7 @@ export const PER_PAGE_SELECTION = [
{ name: "100 / page", value: "100" },
];
export const USER_SCOPE_SELECTION = [
export const USER_SCOPE_SELECTION: SelectorOptions[] = [
{ name: "All Scopes", value: "all" },
{ name: "Admin", value: "admin" },
{ name: "Service Provider", value: "service_provider" },
+56 -29
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useSelectState } from "src/store";
import { getToken } from "src/router/auth";
import { getToken, parseJwt } from "src/router/auth";
import {
DEV_BASE_URL,
API_BASE_URL,
@@ -17,6 +17,7 @@ import {
API_SMPP_GATEWAY,
API_SIP_GATEWAY,
API_PASSWORD_SETTINGS,
USER_ACCOUNT,
} from "./constants";
import { ROUTE_LOGIN } from "src/router/routes";
import {
@@ -260,10 +261,13 @@ export const postSpeechService = (
sid: string,
payload: Partial<SpeechCredential>
) => {
return postFetch<SidResponse, Partial<SpeechCredential>>(
`${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials`,
payload
);
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials`
: `${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials`;
return postFetch<SidResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const postMsTeamsTentant = (payload: Partial<MSTeamsTenant>) => {
@@ -281,10 +285,13 @@ export const postPhoneNumber = (payload: Partial<PhoneNumber>) => {
};
export const postCarrier = (sid: string, payload: Partial<Carrier>) => {
return postFetch<SidResponse, Partial<Carrier>>(
`${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers/`,
payload
);
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/VoipCarriers/`
: `${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers/`;
return postFetch<SidResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const postPredefinedCarrierTemplate = (
@@ -296,6 +303,15 @@ export const postPredefinedCarrierTemplate = (
);
};
export const postPredefinedCarrierTemplateAccount = (
accountSid: string,
predefinedCarrierSid: string
) => {
return postFetch<SidResponse>(
`${API_BASE_URL}/Accounts/${accountSid}/PredefinedCarriers/${predefinedCarrierSid}`
);
};
export const postSipGateway = (payload: Partial<SipGateway>) => {
return postFetch<SidResponse, Partial<SipGateway>>(API_SIP_GATEWAY, payload);
};
@@ -368,10 +384,13 @@ export const putSpeechService = (
sid2: string,
payload: Partial<SpeechCredential>
) => {
return putFetch<EmptyResponse, Partial<SpeechCredential>>(
`${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`,
payload
);
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials/${sid2}`
: `${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`;
return putFetch<EmptyResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const putMsTeamsTenant = (
@@ -396,10 +415,13 @@ export const putCarrier = (
sid2: string,
payload: Partial<Carrier>
) => {
return putFetch<EmptyResponse, Partial<Carrier>>(
`${API_SERVICE_PROVIDERS}/${sid1}/VoipCarriers/${sid2}`,
payload
);
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/VoipCarriers/${sid2}`
: `${API_SERVICE_PROVIDERS}/${sid1}/VoipCarriers/${sid2}`;
return putFetch<EmptyResponse, Partial<Carrier>>(apiUrl, payload);
};
export const putSipGateway = (sid: string, payload: Partial<SipGateway>) => {
@@ -548,22 +570,27 @@ export const useApiData: UseApiData = <Type>(apiPath: string) => {
useEffect(() => {
let ignore = false;
getFetch<Type>(`${API_BASE_URL}/${apiPath}`)
.then(({ json }) => {
if (!ignore) {
setResult(json!);
}
})
.catch((error) => {
if (!ignore) {
setError(error);
}
});
// Don't fetch if api url is empty string ""
if (apiPath) {
getFetch<Type>(`${API_BASE_URL}/${apiPath}`)
.then(({ json }) => {
if (!ignore) {
setResult(json!);
}
})
.catch((error) => {
if (!ignore) {
setError(error);
}
});
}
return function cleanup() {
ignore = true;
};
}, [refetch]);
// Refetch data if refetcher() is called OR api url changes
}, [refetch, apiPath]);
return [result, refetcher, error];
};
+12
View File
@@ -82,6 +82,11 @@ export interface WebhookOption {
value: WebhookMethod;
}
export interface SelectorOptions {
name: string;
value: string;
}
export interface Pcap {
data_url: string;
file_name: string;
@@ -118,7 +123,9 @@ export interface User {
is_active: boolean;
force_change: boolean;
account_sid: string | null;
account_name?: string | null;
service_provider_sid: string | null;
service_provider_name?: string | null;
initial_password?: string;
permissions?: UserPermissions[];
}
@@ -154,6 +161,11 @@ export interface UserJWT {
account_sid?: string | null;
service_provider_sid?: string | null;
permissions: UserPermissions[];
name: string;
}
export interface CurrentUserData {
user: User;
}
export interface ServiceProvider {
+5
View File
@@ -53,6 +53,11 @@ export const LocalLimits = ({
useEffect(() => {
if (hasLength(data)) {
setLocalLimits(data);
setUnit(() => {
return data.find((l) => l.category.includes(LIMIT_MINS))
? LIMIT_MINS
: LIMIT_SESS;
});
} else {
setLocalLimits([]);
}
+2
View File
@@ -5,6 +5,7 @@ import {
Menu,
List,
Edit,
User,
Plus,
Grid,
Phone,
@@ -53,6 +54,7 @@ export const Icons: IconMap = {
Menu,
List,
Edit,
User,
Plus,
Grid,
Phone,
+2
View File
@@ -13,6 +13,7 @@ import { SelectFilter } from "./select-filter";
import { Pagination } from "./pagination";
import { ApplicationFilter } from "./application-filter";
import { SearchFilter } from "./search-filter";
import { ScopedAccess } from "./scoped-access";
export {
Icons,
@@ -32,4 +33,5 @@ export {
Pagination,
ApplicationFilter,
SearchFilter,
ScopedAccess,
};
+5 -1
View File
@@ -3,8 +3,12 @@ import React from "react";
import type { UserData, Scope } from "src/store/types";
export type ScopedAccessProps = {
user?: UserData;
/**
* Minumum required scope
* @see enum `Scope` in src/store/types
*/
scope: Scope;
user?: UserData;
children: React.ReactNode;
};
+2
View File
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Outlet } from "react-router-dom";
import { Button, Icon, classNames } from "jambonz-ui";
import { UserMe } from "./user-me";
import { Navi } from "./navi";
import { Icons } from "src/components";
import { toastSuccess } from "src/store";
@@ -43,6 +44,7 @@ export const Layout = () => {
<Icon subStyle="dark" onClick={handleMenu}>
<Icons.Menu />
</Icon>
<UserMe />
<Button
small
mainStyle="hollow"
+39 -8
View File
@@ -4,6 +4,7 @@ import { Link, useLocation } from "react-router-dom";
import { Icons, ModalForm } from "src/components";
import { naviTop, naviByo } from "./items";
import { UserMe } from "../user-me";
import {
useSelectState,
useDispatch,
@@ -16,7 +17,8 @@ import type { NaviItem } from "./items";
import "./styles.scss";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { Scope, UserData } from "src/store/types";
import { USER_ADMIN } from "src/api/constants";
type CommonProps = {
handleMenu: () => void;
@@ -30,16 +32,17 @@ type NaviProps = CommonProps & {
type ItemProps = CommonProps & {
item: NaviItem;
user?: UserData;
};
const Item = ({ item, handleMenu }: ItemProps) => {
const Item = ({ item, user, handleMenu }: ItemProps) => {
const location = useLocation();
const active = location.pathname.includes(item.route);
const active = location.pathname.includes(item.route(user));
return (
<li>
<Link
to={item.route}
to={item.route(user)}
className={classNames({ navi__link: true, "txt--jean": true, active })}
onClick={handleMenu}
>
@@ -71,6 +74,20 @@ export const Navi = ({
);
}, [accessControl, currentServiceProvider]);
const naviTopFiltered = useMemo(() => {
return naviTop.filter((item) => {
if (item.scope === undefined) {
return true;
} else if (user) {
if (item.restrict) {
return user.access === item.scope;
}
return user.access >= item.scope;
}
});
}, [user]);
const handleSubmit = () => {
postServiceProviders({ name })
.then(({ json }) => {
@@ -121,6 +138,7 @@ export const Navi = ({
<Icon subStyle="white" onClick={handleMenu}>
<Icons.X />
</Icon>
<UserMe />
<Button
small
mainStyle="hollow"
@@ -137,6 +155,7 @@ export const Navi = ({
<select
value={currentServiceProvider?.service_provider_sid}
onChange={(e) => setSid(e.target.value)}
disabled={user?.scope !== USER_ADMIN}
>
{currentServiceProvider ? (
serviceProviders.map((serviceProvider) => {
@@ -172,9 +191,16 @@ export const Navi = ({
</div>
<div className="navi__routes">
<ul>
{naviTop.map((item) => (
<Item key={item.label} item={item} handleMenu={handleMenu} />
))}
{naviTopFiltered.map((item) => {
return (
<Item
key={item.label}
user={user}
item={item}
handleMenu={handleMenu}
/>
);
})}
</ul>
</div>
<div className="navi__byo">
@@ -183,7 +209,12 @@ export const Navi = ({
<div className="navi__routes">
<ul>
{naviByoFiltered.map((item) => (
<Item key={item.label} item={item} handleMenu={handleMenu} />
<Item
key={item.label}
user={user}
item={item}
handleMenu={handleMenu}
/>
))}
</ul>
</div>
+23 -11
View File
@@ -11,6 +11,7 @@ import {
ROUTE_INTERNAL_MS_TEAMS_TENANTS,
} from "src/router/routes";
import { Icons } from "src/components";
import { Scope, UserData } from "src/store/types";
import type { Icon } from "react-feather";
import type { ACL } from "src/store/types";
@@ -18,40 +19,51 @@ import type { ACL } from "src/store/types";
export interface NaviItem {
label: string;
icon: Icon;
route: string;
route: (user?: UserData) => string;
acl?: keyof ACL;
scope?: Scope;
restrict?: boolean;
}
export const naviTop: NaviItem[] = [
{
label: "Users",
icon: Icons.UserCheck,
route: ROUTE_INTERNAL_USERS,
route: () => ROUTE_INTERNAL_USERS,
},
{
label: "Settings",
icon: Icons.Settings,
route: ROUTE_INTERNAL_SETTINGS,
route: () => ROUTE_INTERNAL_SETTINGS,
scope: Scope.service_provider,
},
{
label: "Accounts",
icon: Icons.Activity,
route: ROUTE_INTERNAL_ACCOUNTS,
route: () => ROUTE_INTERNAL_ACCOUNTS,
scope: Scope.service_provider,
},
{
label: "Account",
icon: Icons.Activity,
route: (user) => `${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/edit`,
scope: Scope.account,
restrict: true,
},
{
label: "Applications",
icon: Icons.Grid,
route: ROUTE_INTERNAL_APPLICATIONS,
route: () => ROUTE_INTERNAL_APPLICATIONS,
},
{
label: "Recent Calls",
icon: Icons.List,
route: ROUTE_INTERNAL_RECENT_CALLS,
route: () => ROUTE_INTERNAL_RECENT_CALLS,
},
{
label: "Alerts",
icon: Icons.AlertCircle,
route: ROUTE_INTERNAL_ALERTS,
route: () => ROUTE_INTERNAL_ALERTS,
},
];
@@ -59,22 +71,22 @@ export const naviByo: NaviItem[] = [
{
label: "Carriers",
icon: Icons.Server,
route: ROUTE_INTERNAL_CARRIERS,
route: () => ROUTE_INTERNAL_CARRIERS,
},
{
label: "Speech",
icon: Icons.MessageCircle,
route: ROUTE_INTERNAL_SPEECH,
route: () => ROUTE_INTERNAL_SPEECH,
},
{
label: "Phone Numbers",
icon: Icons.Phone,
route: ROUTE_INTERNAL_PHONE_NUMBERS,
route: () => ROUTE_INTERNAL_PHONE_NUMBERS,
},
{
label: "MS Teams Tenants",
icon: Icons.Users,
route: ROUTE_INTERNAL_MS_TEAMS_TENANTS,
route: () => ROUTE_INTERNAL_MS_TEAMS_TENANTS,
acl: "hasMSTeamsFqdn",
},
];
+4
View File
@@ -58,6 +58,10 @@
.ico {
@include mixins.icosize();
}
.btn {
margin-left: auto;
}
}
&__sps {
+4 -1
View File
@@ -10,7 +10,6 @@
header {
padding: ui-vars.$px03;
display: flex;
justify-content: flex-end;
align-items: center;
@include mixins.mobile() {
@@ -30,6 +29,10 @@
display: flex;
}
}
.btn {
margin-left: auto;
}
}
main {
+34
View File
@@ -0,0 +1,34 @@
import React from "react";
import { Link } from "react-router-dom";
import { Icons } from "src/components";
import { ROUTE_INTERNAL_USERS } from "src/router/routes";
import { useApiData } from "src/api";
import { useSelectState } from "src/store";
import type { CurrentUserData } from "src/api/types";
import "./styles.scss";
export const UserMe = () => {
const user = useSelectState("user");
const [userData] = useApiData<CurrentUserData>("Users/me");
return (
<div className="user">
<Icons.User className="user__icon" />
<div className="user__info">
<Link
to={`${ROUTE_INTERNAL_USERS}/${user?.user_sid}/edit`}
title="Edit user"
className="user__name"
>
<strong>{userData?.user.name}</strong>
</Link>
<div className="user__scope">
<strong>Scope:</strong> {user?.scope}
</div>
</div>
</div>
);
};
@@ -0,0 +1,42 @@
@use "src/styles/vars";
@use "src/styles/mixins";
@use "jambonz-ui/src/styles/vars" as ui-vars;
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
/** User layout **/
.user {
display: flex;
align-items: center;
justify-content: space-between;
font-size: ui-vars.$mxs-size;
line-height: 0.4rem;
@include mixins.mobile() {
padding-left: ui-vars.$px01;
}
&__icon {
color: ui-vars.$jambonz;
width: ui-vars.$h3-size;
height: ui-vars.$h3-size;
padding-right: ui-vars.$px00;
.navi & {
color: ui-vars.$white;
}
}
&__name {
.navi & {
color: ui-vars.$white;
}
}
&__scope {
padding-top: ui-vars.$px01;
.navi & {
color: vars.$jeangrey;
}
}
}
@@ -4,13 +4,20 @@ import { useParams } from "react-router-dom";
import { ApiKeys } from "src/containers/internal/api-keys";
import { useApiData } from "src/api";
import { toastError } from "src/store";
import { toastError, useSelectState } from "src/store";
import { AccountForm } from "./form";
import type { Account, Application, Limit } from "src/api/types";
import {
ROUTE_INTERNAL_ACCOUNTS,
ROUTE_INTERNAL_APPLICATIONS,
} from "src/router/routes";
import { useScopedRedirect } from "src/utils";
import { Scope } from "src/store/types";
export const EditAccount = () => {
const params = useParams();
const user = useSelectState("user");
const [data, refetch, error] = useApiData<Account>(
`Accounts/${params.account_sid}`
);
@@ -19,12 +26,22 @@ export const EditAccount = () => {
);
const [apps] = useApiData<Application[]>("Applications");
useScopedRedirect(
Scope.account,
user?.access !== Scope.account
? ROUTE_INTERNAL_ACCOUNTS
: ROUTE_INTERNAL_APPLICATIONS,
user,
"You do not have access to this resource",
data
);
/** Handle error toast at top level... */
useEffect(() => {
if (error) {
toastError(error.msg);
}
}, [error]);
}, [error, data]);
return (
<>
@@ -21,7 +21,11 @@ import {
LocalLimits,
} from "src/components/forms";
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
import { DEFAULT_WEBHOOK, WEBHOOK_METHODS } from "src/api/constants";
import {
DEFAULT_WEBHOOK,
USER_ACCOUNT,
WEBHOOK_METHODS,
} from "src/api/constants";
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
import type {
@@ -42,6 +46,7 @@ type AccountFormProps = {
export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
const navigate = useNavigate();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [accounts] = useApiData<Account[]>("Accounts");
const [name, setName] = useState("");
@@ -137,6 +142,14 @@ export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (
user?.scope === USER_ACCOUNT &&
user.account_sid !== account?.data?.account_sid
) {
toastError("You do not have permissions to make changes to this Account");
return;
}
setMessage("");
if (accounts) {
@@ -173,6 +186,9 @@ export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
.then(() => {
account.refetch();
toastSuccess("Account updated successfully");
if (user?.scope !== USER_ACCOUNT) {
navigate(ROUTE_INTERNAL_ACCOUNTS);
}
})
.catch((error) => {
toastError(error.msg);
@@ -7,19 +7,46 @@ import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
import { Section, Icons, Spinner, SearchFilter } from "src/components";
import { DeleteAccount } from "./delete";
import { toastError, toastSuccess } from "src/store";
import { hasLength, hasValue, useFilteredResults } from "src/utils";
import {
hasLength,
hasValue,
useFilteredResults,
useScopedRedirect,
} from "src/utils";
import { USER_ACCOUNT } from "src/api/constants";
import { Scope } from "src/store/types";
import type { Account } from "src/api/types";
import { getToken, parseJwt } from "src/router/auth";
export const Accounts = () => {
const token = getToken();
const user = parseJwt(token);
const [accounts, refetch] = useServiceProviderData<Account[]>("Accounts");
const [account, setAccount] = useState<Account | null>(null);
const [filter, setFilter] = useState("");
const filteredAccounts = useFilteredResults<Account>(filter, accounts);
useScopedRedirect(
Scope.service_provider,
`${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/edit`,
user,
"You do not have permissions to manage all accounts"
);
const handleDelete = () => {
if (account) {
if (
user?.scope === USER_ACCOUNT &&
user.account_sid !== account.account_sid
) {
toastError(
"You do not have permissions to make changes to this Account"
);
return;
}
deleteAccount(account.account_sid)
.then(() => {
refetch();
+21 -8
View File
@@ -3,8 +3,12 @@ import { ButtonGroup, H1, M, MS } from "jambonz-ui";
import dayjs from "dayjs";
import { getAlerts, useServiceProviderData } from "src/api";
import { DATE_SELECTION, PER_PAGE_SELECTION } from "src/api/constants";
import { toastError } from "src/store";
import {
DATE_SELECTION,
PER_PAGE_SELECTION,
USER_ACCOUNT,
} from "src/api/constants";
import { toastError, useSelectState } from "src/store";
import { hasLength, hasValue } from "src/utils";
import {
AccountFilter,
@@ -16,8 +20,11 @@ import {
} from "src/components";
import type { Account, Alert, PageQuery } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
export const Alerts = () => {
const user = useSelectState("user");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState("");
const [dateFilter, setDateFilter] = useState("today");
@@ -51,10 +58,14 @@ export const Alerts = () => {
};
useEffect(() => {
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
if (accountSid) {
handleFilterChange();
}
}, [accountSid, pageNumber, dateFilter]);
}, [user, accountSid, pageNumber, dateFilter]);
/** Reset page number when filters change */
useEffect(() => {
@@ -67,10 +78,12 @@ export const Alerts = () => {
<H1 className="h2">Alerts</H1>
</section>
<section className="filters filters--multi">
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
/>
<ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
/>
</ScopedAccess>
<SelectFilter
id="date_filter"
label="Date"
@@ -103,7 +116,7 @@ export const Alerts = () => {
</div>
))
) : (
<M>No data</M>
<M>No data.</M>
)}
</div>
</Section>
@@ -3,22 +3,34 @@ import { H1 } from "jambonz-ui";
import { useParams } from "react-router-dom";
import { useApiData } from "src/api";
import { toastError } from "src/store";
import { toastError, useSelectState } from "src/store";
import { ApplicationForm } from "./form";
import type { Application } from "src/api/types";
import { useScopedRedirect } from "src/utils/use-scoped-redirect";
import { Scope } from "src/store/types";
import { ROUTE_INTERNAL_APPLICATIONS } from "src/router/routes";
export const EditApplication = () => {
const params = useParams();
const user = useSelectState("user");
const [data, refetch, error] = useApiData<Application>(
`Applications/${params.application_sid}`
);
useScopedRedirect(
Scope.account,
ROUTE_INTERNAL_APPLICATIONS,
user,
"You do not have access to this resource",
data
);
useEffect(() => {
if (error) {
toastError(error.msg);
}
}, [error]);
}, [error, data]);
return (
<>
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { Button, ButtonGroup, MS } from "jambonz-ui";
import { Link, useNavigate } from "react-router-dom";
import { toastError, toastSuccess } from "src/store";
import { toastError, toastSuccess, useSelectState } from "src/store";
import { ClipBoard, Section } from "src/components";
import {
Selector,
@@ -49,7 +49,7 @@ import type {
UseApiDataMap,
} from "src/api/types";
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
import { useRedirect } from "src/utils";
import { isUserAccountScope, useRedirect } from "src/utils";
type ApplicationFormProps = {
application?: UseApiDataMap<Application>;
@@ -58,6 +58,7 @@ type ApplicationFormProps = {
export const ApplicationForm = ({ application }: ApplicationFormProps) => {
const navigate = useNavigate();
const { synthesis, recognizers } = useSpeechVendors();
const user = useSelectState("user");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [applications] = useApiData<Application[]>("Applications");
const [applicationName, setApplicationName] = useState("");
@@ -115,6 +116,13 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isUserAccountScope(accountSid, user)) {
toastError(
"You do not have permissions to make changes to these Speech Credentials"
);
return;
}
setMessage("");
if (applications) {
@@ -152,15 +160,18 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
.then(() => {
application.refetch();
toastSuccess("Application updated successfully");
navigate(
`${ROUTE_INTERNAL_APPLICATIONS}/${application.data?.application_sid}/edit`
);
})
.catch((error) => {
toastError(error.msg);
});
} else {
postApplication(payload)
.then(({ json }) => {
.then(() => {
toastSuccess("Application created successfully");
navigate(`${ROUTE_INTERNAL_APPLICATIONS}/${json.sid}/edit`);
navigate(ROUTE_INTERNAL_APPLICATIONS);
})
.catch((error) => {
toastError(error.msg);
@@ -2,8 +2,7 @@ import React, { useEffect, useState } from "react";
import { H1, M, Button, Icon } from "jambonz-ui";
import { Link } from "react-router-dom";
import { deleteApplication, getFetch, useServiceProviderData } from "src/api";
import { API_ACCOUNTS } from "src/api/constants";
import { deleteApplication, useServiceProviderData, useApiData } from "src/api";
import {
ROUTE_INTERNAL_APPLICATIONS,
ROUTE_INTERNAL_ACCOUNTS,
@@ -17,16 +16,25 @@ import {
} from "src/components";
import { DeleteApplication } from "./delete";
import { toastError, toastSuccess, useSelectState } from "src/store";
import { hasLength, hasValue, useFilteredResults } from "src/utils";
import {
isUserAccountScope,
hasLength,
hasValue,
useFilteredResults,
} from "src/utils";
import type { Application, Account } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { USER_ACCOUNT } from "src/api/constants";
export const Applications = () => {
const currentServiceProvider = useSelectState("currentServiceProvider");
const user = useSelectState("user");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState("");
const [application, setApplication] = useState<Application | null>(null);
const [applications, setApplications] = useState<Application[]>();
const [apiUrl, setApiUrl] = useState("");
const [applications, refetch] = useApiData<Application[]>(apiUrl);
const [filter, setFilter] = useState("");
const filteredApplications = useFilteredResults<Application>(
@@ -34,19 +42,18 @@ export const Applications = () => {
applications
);
const getApplications = () => {
getFetch<Application[]>(`${API_ACCOUNTS}/${accountSid}/Applications`)
.then(({ json }) => setApplications(json))
.catch((error) => {
toastError(error.msg);
});
};
const handleDelete = () => {
if (application) {
if (isUserAccountScope(accountSid, user)) {
toastError(
"You do not have permissions to make changes to this Application"
);
return;
}
deleteApplication(application.application_sid)
.then(() => {
getApplications();
// getApplications();
refetch();
setApplication(null);
toastSuccess(
<>
@@ -61,19 +68,14 @@ export const Applications = () => {
};
useEffect(() => {
if (accountSid) {
getApplications();
} else if (accounts && !accounts.length) {
setApplications([]);
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
}, [accountSid, accounts]);
useEffect(() => {
return function cleanup() {
setAccountSid("");
setApplications(undefined);
};
}, [currentServiceProvider]);
if (accountSid) {
setApiUrl(`Accounts/${accountSid}/Applications`);
}
}, [accountSid, user]);
return (
<>
@@ -95,10 +97,12 @@ export const Applications = () => {
placeholder="Filter applications"
filter={[filter, setFilter]}
/>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
/>
<ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
/>
</ScopedAccess>
</section>
<Section {...(hasLength(filteredApplications) && { slim: true })}>
<div className="list">
@@ -3,13 +3,17 @@ import { H1 } from "jambonz-ui";
import { useParams } from "react-router-dom";
import { useApiData } from "src/api";
import { toastError } from "src/store";
import { toastError, useSelectState } from "src/store";
import { CarrierForm } from "./form";
import { Carrier, SipGateway, SmppGateway } from "src/api/types";
import { useScopedRedirect } from "src/utils/use-scoped-redirect";
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
import { Scope } from "src/store/types";
export const EditCarrier = () => {
const params = useParams();
const user = useSelectState("user");
const [data, refetch, error] = useApiData<Carrier>(
`VoipCarriers/${params.voip_carrier_sid}`
);
@@ -20,11 +24,19 @@ export const EditCarrier = () => {
`SmppGateways?voip_carrier_sid=${params.voip_carrier_sid}`
);
useScopedRedirect(
Scope.account,
ROUTE_INTERNAL_CARRIERS,
user,
"You do not have access to this resource",
data
);
useEffect(() => {
if (error) {
toastError(error.msg);
}
}, [error]);
}, [error, data]);
return (
<>
@@ -14,6 +14,7 @@ import {
useApiData,
useServiceProviderData,
postPredefinedCarrierTemplate,
postPredefinedCarrierTemplateAccount,
} from "src/api";
import {
DEFAULT_SIP_GATEWAY,
@@ -24,6 +25,7 @@ import {
NETMASK_OPTIONS,
TCP_MAX_PORT,
TECH_PREFIX_MINLENGTH,
USER_ACCOUNT,
} from "src/api/constants";
import { Icons, Section } from "src/components";
import {
@@ -37,7 +39,13 @@ import {
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
import { toastError, toastSuccess, useSelectState } from "src/store";
import { getIpValidationType, hasLength, isValidPort } from "src/utils";
import {
checkSelectOptions,
getIpValidationType,
isUserAccountScope,
hasLength,
isValidPort,
} from "src/utils";
import type {
Account,
@@ -63,6 +71,7 @@ export const CarrierForm = ({
carrierSmppGateways,
}: CarrierFormProps) => {
const navigate = useNavigate();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const refSipIp = useRef<HTMLInputElement[]>([]);
@@ -437,6 +446,11 @@ export const CarrierForm = ({
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isUserAccountScope(accountSid, user)) {
toastError("You do not have permissions to make changes to this Carrier");
return;
}
setSipMessage("");
setSmppInboundMessage("");
setSmppOutboundMessage("");
@@ -499,6 +513,9 @@ export const CarrierForm = ({
toastSuccess("Carrier updated successfully");
carrier.refetch();
navigate(
`${ROUTE_INTERNAL_CARRIERS}/${carrier.data?.voip_carrier_sid}/edit`
);
})
.catch((error) => {
toastError(error.msg);
@@ -513,7 +530,7 @@ export const CarrierForm = ({
handleSmppGatewayPutPost(json.sid);
toastSuccess("Carrier created successfully");
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
navigate(ROUTE_INTERNAL_CARRIERS);
})
.catch((error) => {
toastError(error.msg);
@@ -529,10 +546,18 @@ export const CarrierForm = ({
)?.predefined_carrier_sid;
if (currentServiceProvider && predefinedCarrierSid) {
postPredefinedCarrierTemplate(
currentServiceProvider.service_provider_sid,
predefinedCarrierSid
)
const postPredefinedCarrier =
user?.scope === USER_ACCOUNT
? postPredefinedCarrierTemplateAccount(
accountSid,
predefinedCarrierSid
)
: postPredefinedCarrierTemplate(
currentServiceProvider.service_provider_sid,
predefinedCarrierSid
);
postPredefinedCarrier
.then(({ json }) => {
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
})
@@ -677,11 +702,24 @@ export const CarrierForm = ({
<em>Prepend a leading + on origination attempts.</em>
</MXS>
<AccountSelect
accounts={accounts}
accounts={
user?.scope === USER_ACCOUNT
? accounts?.filter(
(acct) => user.account_sid === acct.account_sid
)
: accounts
}
account={[accountSid, setAccountSid]}
label="Used By"
required={false}
defaultOption
defaultOption={checkSelectOptions(user, carrier?.data)}
disabled={
user?.scope !== USER_ACCOUNT
? false
: user.account_sid !== accountSid
? true
: false
}
/>
{accountSid && hasLength(applications) && (
<>
@@ -971,6 +1009,7 @@ export const CarrierForm = ({
</label>
</div>
</div>
<button
className="btnty"
title="Delete SIP Gateway"
@@ -1,4 +1,4 @@
import React, { useState, useMemo } from "react";
import React, { useState, useMemo, useEffect } from "react";
import { Link } from "react-router-dom";
import { Button, H1, Icon, M } from "jambonz-ui";
import {
@@ -6,9 +6,10 @@ import {
deleteSipGateway,
deleteSmppGateway,
getFetch,
useApiData,
useServiceProviderData,
} from "src/api";
import { toastSuccess, toastError } from "src/store";
import { toastSuccess, toastError, useSelectState } from "src/store";
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
import {
AccountFilter,
@@ -17,16 +18,30 @@ import {
Spinner,
SearchFilter,
} from "src/components";
import { hasLength, hasValue, useFilteredResults } from "src/utils";
import { API_SIP_GATEWAY, API_SMPP_GATEWAY } from "src/api/constants";
import { ScopedAccess } from "src/components/scoped-access";
import { Gateways } from "./gateways";
import {
isUserAccountScope,
hasLength,
hasValue,
useFilteredResults,
} from "src/utils";
import {
API_SIP_GATEWAY,
API_SMPP_GATEWAY,
USER_ACCOUNT,
} from "src/api/constants";
import { DeleteCarrier } from "./delete";
import type { Account, Carrier, SipGateway, SmppGateway } from "src/api/types";
import { Gateways } from "./gateways";
import { Scope } from "src/store/types";
export const Carriers = () => {
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [apiUrl, setApiUrl] = useState("");
const [carrier, setCarrier] = useState<Carrier | null>(null);
const [carriers, refetch] = useServiceProviderData<Carrier[]>("VoipCarriers");
const [carriers, refetch] = useApiData<Carrier[]>(apiUrl);
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState("");
const [filter, setFilter] = useState("");
@@ -39,7 +54,7 @@ export const Carriers = () => {
: carrier.account_sid === null
)
: [];
}, [accountSid, carriers]);
}, [accountSid, carrier, carriers]);
const filteredCarriers = useFilteredResults<Carrier>(
filter,
@@ -48,6 +63,11 @@ export const Carriers = () => {
const handleDelete = () => {
if (carrier) {
if (isUserAccountScope(accountSid, user)) {
toastError("You do not have permissions to delete this Carrier");
return;
}
deleteCarrier(carrier.voip_carrier_sid)
.then(() => {
Promise.all([
@@ -77,8 +97,8 @@ export const Carriers = () => {
)
);
});
refetch();
setCarrier(null);
refetch();
toastSuccess(
<>
Deleted Carrier <strong>{carrier.name}</strong>
@@ -91,6 +111,16 @@ export const Carriers = () => {
}
};
useEffect(() => {
if (accountSid) {
setApiUrl(`Accounts/${accountSid}/VoipCarriers`);
} else if (currentServiceProvider) {
setApiUrl(
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers`
);
}
}, [user, currentServiceProvider, accountSid]);
return (
<>
<section className="mast">
@@ -123,14 +153,24 @@ export const Carriers = () => {
<div className="item" key={carrier.voip_carrier_sid}>
<div className="item__info">
<div className="item__title">
<Link
to={`${ROUTE_INTERNAL_CARRIERS}/${carrier.voip_carrier_sid}/edit`}
title="Edit Carrier"
className="i"
<ScopedAccess
user={user}
scope={
!accountSid ? Scope.service_provider : Scope.account
}
>
<Link
to={`${ROUTE_INTERNAL_CARRIERS}/${carrier.voip_carrier_sid}/edit`}
title="Edit Carrier"
className="i"
>
<strong>{carrier.name}</strong>
<Icons.ArrowRight />
</Link>
</ScopedAccess>
{!accountSid && user?.scope === USER_ACCOUNT && (
<strong>{carrier.name}</strong>
<Icons.ArrowRight />
</Link>
)}
</div>
<div className="item__meta">
<div>
@@ -150,22 +190,27 @@ export const Carriers = () => {
<Gateways carrier={carrier} />
</div>
</div>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_CARRIERS}/${carrier.voip_carrier_sid}/edit`}
title="Edit Carrier"
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete Carrier"
onClick={() => setCarrier(carrier)}
className="btnty"
>
<Icons.Trash />
</button>
</div>
<ScopedAccess
user={user}
scope={!accountSid ? Scope.service_provider : Scope.account}
>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_CARRIERS}/${carrier.voip_carrier_sid}/edit`}
title="Edit Carrier"
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete Carrier"
onClick={() => setCarrier(carrier)}
className="btnty"
>
<Icons.Trash />
</button>
</div>
</ScopedAccess>
</div>
))
) : (
@@ -88,6 +88,9 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
.then(() => {
phoneNumber.refetch();
toastSuccess("Phone number updated successfully");
navigate(
`${ROUTE_INTERNAL_PHONE_NUMBERS}/${phoneNumber.data?.phone_number_sid}/edit`
);
})
.catch((error) => {
toastError(error.msg);
@@ -98,9 +101,9 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
number: phoneNumberNum,
voip_carrier_sid: sipTrunkSid,
})
.then(({ json }) => {
.then(() => {
toastSuccess("Phone number created successfully");
navigate(`${ROUTE_INTERNAL_PHONE_NUMBERS}/${json.sid}/edit`);
navigate(ROUTE_INTERNAL_PHONE_NUMBERS);
})
.catch((error) => {
toastError(error.msg);
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Button, ButtonGroup, H1, Icon, MS } from "jambonz-ui";
import { Link } from "react-router-dom";
@@ -7,7 +7,7 @@ import {
putPhoneNumber,
useServiceProviderData,
} from "src/api";
import { toastError, toastSuccess } from "src/store";
import { toastError, toastSuccess, useSelectState } from "src/store";
import {
Icons,
Section,
@@ -30,8 +30,12 @@ import {
import { DeletePhoneNumber } from "./delete";
import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
import { USER_ACCOUNT } from "src/api/constants";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
export const PhoneNumbers = () => {
const user = useSelectState("user");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [applications] = useServiceProviderData<Application[]>("Applications");
const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
@@ -101,6 +105,12 @@ export const PhoneNumbers = () => {
}
};
useEffect(() => {
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
}, [user]);
return (
<>
<section className="mast">
@@ -121,11 +131,13 @@ export const PhoneNumbers = () => {
placeholder="Filter phone numbers"
filter={[filter, setFilter]}
/>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
defaultOption
/>
<ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
defaultOption
/>
</ScopedAccess>
</section>
<Section {...(hasLength(filteredPhoneNumbers) && { slim: true })}>
<div className="list">
@@ -3,8 +3,12 @@ import { ButtonGroup, H1, M, MS } from "jambonz-ui";
import dayjs from "dayjs";
import { getRecentCalls, useServiceProviderData } from "src/api";
import { DATE_SELECTION, PER_PAGE_SELECTION } from "src/api/constants";
import { toastError } from "src/store";
import {
DATE_SELECTION,
PER_PAGE_SELECTION,
USER_ACCOUNT,
} from "src/api/constants";
import { toastError, useSelectState } from "src/store";
import {
Section,
AccountFilter,
@@ -16,6 +20,8 @@ import { hasLength, hasValue } from "src/utils";
import { DetailsItem } from "./details";
import type { Account, CallQuery, RecentCall } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
const directionSelection = [
{ name: "either", value: "io" },
@@ -30,6 +36,7 @@ const statusSelection = [
];
export const RecentCalls = () => {
const user = useSelectState("user");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState("");
const [dateFilter, setDateFilter] = useState("today");
@@ -74,8 +81,12 @@ export const RecentCalls = () => {
/** Reset page number when filters change */
useEffect(() => {
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
setPageNumber(1);
}, [accountSid, dateFilter, directionFilter, statusFilter]);
}, [user, accountSid, dateFilter, directionFilter, statusFilter]);
return (
<>
@@ -84,10 +95,12 @@ export const RecentCalls = () => {
</section>
{/* Setting overflow-x auto for now until we have a better responsive solution... */}
<section className="filters filters--multi">
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
/>
<ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
/>
</ScopedAccess>
<SelectFilter
id="date_filter"
label="Date"
@@ -114,7 +127,7 @@ export const RecentCalls = () => {
) : hasLength(calls) ? (
calls.map((call) => <DetailsItem key={call.call_sid} call={call} />)
) : (
<M>No data</M>
<M>No data.</M>
)}
</div>
</Section>
@@ -1,21 +1,35 @@
import React, { useState } from "react";
import { H1, Tabs, Tab, MS } from "jambonz-ui";
import { withSelectState } from "src/utils";
import { useScopedRedirect, withSelectState } from "src/utils";
import { ApiKeys } from "src/containers/internal/api-keys";
import ServiceProviderSettings from "./service-provider-settings";
import AdminSettings from "./admin-settings";
import { ScopedAccess } from "src/components/scoped-access";
import type { ServiceProvider } from "src/api/types";
import { Section } from "src/components";
import { USER_ADMIN } from "src/api/constants";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { useSelectState } from "src/store";
import { Scope } from "src/store/types";
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
type SettingsProps = {
currentServiceProvider: ServiceProvider;
};
export const Settings = ({ currentServiceProvider }: SettingsProps) => {
const user = useSelectState("user");
const [activeTab, setActiveTab] = useState("");
useScopedRedirect(
Scope.service_provider,
`${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/edit`,
user,
"You do not have permissions to manage Settings"
);
return (
<>
<H1 className="h2">Settings</H1>
@@ -24,14 +38,21 @@ export const Settings = ({ currentServiceProvider }: SettingsProps) => {
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>
<Tabs active={[activeTab, setActiveTab]}>
<Tab id="admin" label="Admin">
<AdminSettings />
</Tab>
<Tab id="serviceProvider" label="Service Provider">
<ScopedAccess scope={Scope.admin} user={user}>
<Tabs active={[activeTab, setActiveTab]}>
<Tab id="admin" label="Admin">
<AdminSettings />
</Tab>
<Tab id="serviceProvider" label="Service Provider">
<ServiceProviderSettings />
</Tab>
</Tabs>
</ScopedAccess>
{user?.scope !== USER_ADMIN && (
<ScopedAccess scope={Scope.service_provider} user={user}>
<ServiceProviderSettings />
</Tab>
</Tabs>
</ScopedAccess>
)}
</form>
</Section>
@@ -1,24 +1,50 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { H1 } from "jambonz-ui";
import { useParams } from "react-router-dom";
import { useServiceProviderData } from "src/api";
import { toastError } from "src/store";
import { useApiData } from "src/api";
import { toastError, useSelectState } from "src/store";
import { SpeechServiceForm } from "./form";
import type { SpeechCredential } from "src/api/types";
import { USER_ACCOUNT } from "src/api/constants";
import { useScopedRedirect } from "src/utils/use-scoped-redirect";
import { Scope } from "src/store/types";
import { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
import { useParams } from "react-router-dom";
export const EditSpeechService = () => {
const params = useParams();
const [data, refetch, error] = useServiceProviderData<SpeechCredential>(
`SpeechCredentials/${params.speech_credential_sid}`
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [url, setUrl] = useState("");
const [data, refetch, error] = useApiData<SpeechCredential>(url);
useScopedRedirect(
Scope.account,
ROUTE_INTERNAL_SPEECH,
user,
"You do not have access to this resource",
data
);
const getUrlForSpeech = () => {
if (user && user?.scope === USER_ACCOUNT) {
setUrl(
`Accounts/${user?.account_sid}/SpeechCredentials/${params.speech_credential_sid}`
);
} else {
setUrl(
`ServiceProviders/${currentServiceProvider?.service_provider_sid}/SpeechCredentials/${params.speech_credential_sid}`
);
}
};
useEffect(() => {
getUrlForSpeech();
if (error) {
toastError(error.msg);
}
}, [error]);
}, [error, data, url]);
return (
<>
@@ -28,7 +28,11 @@ import {
VENDOR_IBM,
} from "src/vendor";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { getObscuredSecret } from "src/utils";
import {
checkSelectOptions,
getObscuredSecret,
isUserAccountScope,
} from "src/utils";
import { getObscuredGoogleServiceKey } from "./utils";
import { CredentialStatus } from "./status";
@@ -41,6 +45,7 @@ type SpeechServiceFormProps = {
export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const navigate = useNavigate();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const regions = useRegionVendors();
const [accounts] = useServiceProviderData<Account[]>("Accounts");
@@ -98,6 +103,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isUserAccountScope(accountSid, user)) {
toastError(
"You do not have permissions to make changes to these Speech Credentials"
);
return;
}
if (currentServiceProvider) {
const payload: Partial<SpeechCredential> = {
vendor,
@@ -136,6 +148,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (credential && credential.data) {
toastSuccess("Speech credential updated successfully");
credential.refetch();
navigate(
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
);
}
})
.catch((error) => {
@@ -156,14 +171,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
: null,
client_id: vendor === VENDOR_NUANCE ? clientId : null,
secret: vendor === VENDOR_NUANCE ? secretKey : null,
stt_api_key: sttApiKey || null,
stt_region: sttRegion || null,
tts_api_key: ttsApiKey || null,
tts_region: ttsRegion || null,
})
.then(({ json }) => {
.then(() => {
toastSuccess("Speech credential created successfully");
navigate(`${ROUTE_INTERNAL_SPEECH}/${json.sid}/edit`);
navigate(ROUTE_INTERNAL_SPEECH);
})
.catch((error) => {
toastError(error.msg);
@@ -292,7 +303,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
accounts={accounts}
account={[accountSid, setAccountSid]}
required={false}
defaultOption
defaultOption={checkSelectOptions(user, credential?.data)}
disabled={credential ? true : false}
/>
</fieldset>
@@ -541,7 +552,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
}}
checked={useCustomTts}
/>
<div>Use for custom text-to-speech</div>
<div>Use for custom voice</div>
</label>
<label htmlFor="use_custom_tts">
Custom voice endpoint{useCustomTts && <span>*</span>}
@@ -577,7 +588,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
}}
checked={useCustomStt}
/>
<div>Use for custom speech-to-text</div>
<div>Use for custom speech model</div>
</label>
<label htmlFor="use_custom_stt">
Custom speech endpoint id{useCustomStt && <span>*</span>}
@@ -1,53 +1,71 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Button, H1, Icon, M } from "jambonz-ui";
import { Link } from "react-router-dom";
import { API_ACCOUNTS, API_SERVICE_PROVIDERS } from "src/api/constants";
import { USER_ACCOUNT } from "src/api/constants";
import { AccountFilter, Icons, Section, Spinner } from "src/components";
import { useSelectState, toastError, toastSuccess } from "src/store";
import { getFetch, deleteSpeechService, useServiceProviderData } from "src/api";
import {
deleteSpeechService,
useServiceProviderData,
useApiData,
} from "src/api";
import { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
import { getHumanDateTime, hasLength, hasValue } from "src/utils";
import {
getHumanDateTime,
isUserAccountScope,
hasLength,
hasValue,
useFilteredResults,
} from "src/utils";
import DeleteSpeechService from "./delete";
import { getUsage } from "./utils";
import { CredentialStatus } from "./status";
import type { SpeechCredential, Account } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
export const SpeechServices = () => {
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [apiUrl, setApiUrl] = useState("");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState("");
const [credential, setCredential] = useState<SpeechCredential | null>(null);
const [credentials, setCredentials] = useState<SpeechCredential[]>();
const [credentials, refetch] = useApiData<SpeechCredential[]>(apiUrl);
const [filter] = useState("");
const getSpeechCredentials = (url: string) => {
getFetch<SpeechCredential[]>(url)
.then(({ json }) => {
setCredentials(json);
})
.catch((error) => {
toastError(error.msg);
});
};
const credentialsFiltered = useMemo(() => {
return credentials
? credentials.filter((credential) =>
accountSid
? credential.account_sid === accountSid
: credential.account_sid === null
)
: [];
}, [accountSid, accounts, credentials]);
const filteredCredentials = useFilteredResults<SpeechCredential>(
filter,
credentialsFiltered
);
const handleDelete = () => {
if (credential && currentServiceProvider) {
if (isUserAccountScope(accountSid, user)) {
toastError(
"You do not have permissions to delete these Speech Credentials"
);
return;
}
deleteSpeechService(
currentServiceProvider.service_provider_sid,
credential.speech_credential_sid
)
.then(() => {
if (accountSid) {
getSpeechCredentials(
`${API_ACCOUNTS}/${accountSid}/SpeechCredentials`
);
} else {
getSpeechCredentials(
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/SpeechCredentials`
);
}
setCredential(null);
refetch();
toastSuccess(
<>
Deleted speech service <strong>{credential.vendor}</strong>
@@ -62,13 +80,13 @@ export const SpeechServices = () => {
useEffect(() => {
if (accountSid) {
getSpeechCredentials(`${API_ACCOUNTS}/${accountSid}/SpeechCredentials`);
setApiUrl(`Accounts/${accountSid}/SpeechCredentials`);
} else if (currentServiceProvider) {
getSpeechCredentials(
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/SpeechCredentials`
setApiUrl(
`ServiceProviders/${currentServiceProvider?.service_provider_sid}/SpeechCredentials`
);
}
}, [accountSid, currentServiceProvider]);
}, [currentServiceProvider, accountSid]);
return (
<>
@@ -88,24 +106,34 @@ export const SpeechServices = () => {
defaultOption
/>
</section>
<Section {...(hasLength(credentials) && { slim: true })}>
<Section {...(hasLength(filteredCredentials) && { slim: true })}>
<div className="list">
{!hasValue(credentials) ? (
{!hasValue(filteredCredentials) ? (
<Spinner />
) : hasLength(credentials) ? (
credentials.map((credential) => {
) : hasLength(filteredCredentials) ? (
filteredCredentials.map((credential) => {
return (
<div className="item" key={credential.speech_credential_sid}>
<div className="item__info">
<div className="item__title">
<Link
to={`${ROUTE_INTERNAL_SPEECH}/${credential.speech_credential_sid}/edit`}
title="Edit application"
className="i"
<ScopedAccess
user={user}
scope={
!accountSid ? Scope.service_provider : Scope.account
}
>
<Link
to={`${ROUTE_INTERNAL_SPEECH}/${credential.speech_credential_sid}/edit`}
title="Edit application"
className="i"
>
<strong>Vendor: {credential.vendor}</strong>
<Icons.ArrowRight />
</Link>
</ScopedAccess>
{!accountSid && user?.scope === USER_ACCOUNT && (
<strong>Vendor: {credential.vendor}</strong>
<Icons.ArrowRight />
</Link>
)}
</div>
<div className="item__meta">
<div>
@@ -147,22 +175,27 @@ export const SpeechServices = () => {
</div>
</div>
</div>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_SPEECH}/${credential.speech_credential_sid}/edit`}
title="Edit speech service"
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete speech service"
onClick={() => setCredential(credential)}
className="btnty"
>
<Icons.Trash />
</button>
</div>
<ScopedAccess
user={user}
scope={!accountSid ? Scope.service_provider : Scope.account}
>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_SPEECH}/${credential.speech_credential_sid}/edit`}
title="Edit speech service"
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete speech service"
onClick={() => setCredential(credential)}
className="btnty"
>
<Icons.Trash />
</button>
</div>
</ScopedAccess>
</div>
);
})
@@ -1,12 +1,13 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { MS } from "jambonz-ui";
import { CRED_NOT_TESTED, CRED_OK } from "src/api/constants";
import { CRED_NOT_TESTED, CRED_OK, USER_ACCOUNT } from "src/api/constants";
import { Icons, Spinner } from "src/components";
import { useServiceProviderData } from "src/api";
import { useApiData } from "src/api";
import { getStatus, getReason } from "./utils";
import type { SpeechCredential, CredentialTestResult } from "src/api/types";
import { useSelectState } from "src/store";
type CredentialStatusProps = {
cred: SpeechCredential;
@@ -17,10 +18,11 @@ export const CredentialStatus = ({
cred,
showSummary = false,
}: CredentialStatusProps) => {
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [apiUrl, setApiUrl] = useState("");
const [testResult, testRefetch, testError] =
useServiceProviderData<CredentialTestResult>(
`SpeechCredentials/${cred.speech_credential_sid}/test`
);
useApiData<CredentialTestResult>(apiUrl);
const notTestedTxt =
"In order to test your credentials you need to enable TTS/STT.";
@@ -56,6 +58,18 @@ export const CredentialStatus = ({
testRefetch();
}
useEffect(() => {
if (user && user.scope === USER_ACCOUNT) {
setApiUrl(
`Accounts/${user.account_sid}/SpeechCredentials/${cred.speech_credential_sid}/test`
);
} else if (currentServiceProvider) {
setApiUrl(
`ServiceProviders/${currentServiceProvider.service_provider_sid}/SpeechCredentials/${cred.speech_credential_sid}/test`
);
}
}, [user, cred, currentServiceProvider]);
return (
<>
{!testError && !testResult && (
+17 -11
View File
@@ -13,7 +13,8 @@ import {
import { ROUTE_INTERNAL_USERS } from "src/router/routes";
import { useAuth } from "src/router/auth";
import { ClipBoard, Section } from "src/components";
import { ClipBoard, Section, ScopedAccess } from "src/components";
import { AccountSelect, Passwd, Selector } from "src/components/forms";
import { DeleteUser } from "./delete";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import {
@@ -25,6 +26,7 @@ import {
USER_SP,
} from "src/api/constants";
import { isValidPasswd, getUserScope, hasLength } from "src/utils";
import { Scope } from "src/store/types";
import type {
UserSidResponse,
@@ -35,7 +37,6 @@ import type {
Account,
} from "src/api/types";
import type { IMessage } from "src/store/types";
import { AccountSelect, Passwd, Selector } from "src/components/forms";
type UserFormProps = {
user?: UseApiDataMap<User>;
@@ -52,7 +53,9 @@ export const UserForm = ({ user }: UserFormProps) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [initialPassword, setInitialPassword] = useState("");
const [scope, setScope] = useState<UserScopes>();
const [scope, setScope] = useState<UserScopes | null>(
currentUser?.scope || null
);
const [isActive, setIsActive] = useState(true);
const [forceChange, setForceChange] = useState(true);
const [modal, setModal] = useState(false);
@@ -120,9 +123,9 @@ export const UserForm = ({ user }: UserFormProps) => {
? null
: accountSid,
})
.then(({ json }) => {
.then(() => {
toastSuccess("User created successfully");
navigate(`${ROUTE_INTERNAL_USERS}/${json.user_sid}/edit`);
navigate(ROUTE_INTERNAL_USERS);
})
.catch((error: { msg: IMessage }) => {
toastError(error.msg);
@@ -152,7 +155,7 @@ export const UserForm = ({ user }: UserFormProps) => {
.then(() => {
user.refetch();
toastSuccess("User updated successfully");
navigate(ROUTE_INTERNAL_USERS);
navigate(`${ROUTE_INTERNAL_USERS}/${user.data?.user_sid}/edit`);
})
.catch((error: { msg: IMessage }) => {
toastError(error.msg);
@@ -168,6 +171,9 @@ export const UserForm = ({ user }: UserFormProps) => {
setIsActive(!!user.data.is_active);
setEmail(user.data.email);
setScope(getUserScope(user.data));
if (user.data.account_sid) {
setAccountSid(user.data.account_sid);
}
}
}, [user]);
@@ -178,17 +184,17 @@ export const UserForm = ({ user }: UserFormProps) => {
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>
{currentUser?.scope !== USER_ACCOUNT && (
<ScopedAccess user={currentUser} scope={Scope.service_provider}>
<fieldset>
<label htmlFor="scope">Scope:</label>
<Selector
id="scope"
name="scope"
value={scope}
value={scope || currentUser?.scope}
options={
currentUser?.scope === USER_SP
? USER_SCOPE_SELECTION.filter(
(e) => e.value !== USER_ADMIN && e.value !== "all"
(opt) => opt.value !== USER_ADMIN && opt.value !== "all"
)
: USER_SCOPE_SELECTION.filter((e) => e.value !== "all")
}
@@ -203,7 +209,7 @@ export const UserForm = ({ user }: UserFormProps) => {
/>
</>
)}
{scope !== USER_ACCOUNT && !hasLength(accounts) && (
{scope === USER_ACCOUNT && !hasLength(accounts) && (
<>
<label htmlFor="account">
Account:<span>*</span>
@@ -219,7 +225,7 @@ export const UserForm = ({ user }: UserFormProps) => {
</>
)}
</fieldset>
)}
</ScopedAccess>
{user && user.data && (
<fieldset>
<label htmlFor="user_sid">User SID</label>
+49 -19
View File
@@ -4,7 +4,11 @@ import { Link } from "react-router-dom";
import { useApiData, useServiceProviderData } from "src/api";
import { ROUTE_INTERNAL_USERS } from "src/router/routes";
import { USER_SCOPE_SELECTION, USER_ADMIN } from "src/api/constants";
import {
USER_SCOPE_SELECTION,
USER_ADMIN,
USER_ACCOUNT,
} from "src/api/constants";
import {
Section,
@@ -14,12 +18,21 @@ import {
AccountFilter,
SelectFilter,
} from "src/components";
import { hasLength, hasValue, useFilteredResults } from "src/utils";
import {
filterScopeOptions,
hasLength,
hasValue,
sortUsersAlpha,
useFilteredResults,
} from "src/utils";
import type { Account, User } from "src/api/types";
import { useSelectState } from "src/store";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
export const Users = () => {
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [users] = useApiData<User[]>("Users");
const [filter, setFilter] = useState("");
@@ -55,7 +68,9 @@ export const Users = () => {
return [];
}, [accountSid, scopeFilter, users, accounts, currentServiceProvider]);
const filteredUsers = useFilteredResults<User>(filter, usersFiltered);
const filteredUsers = useFilteredResults<User>(filter, usersFiltered)?.sort(
sortUsersAlpha
);
return (
<>
@@ -74,24 +89,27 @@ export const Users = () => {
filter={[filter, setFilter]}
/>
</section>
<SelectFilter
id="scope"
label="Scope"
filter={[scopeFilter, setScopeFilter]}
options={USER_SCOPE_SELECTION}
/>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
defaultOption={true}
/>
{user && (
<SelectFilter
id="scope"
label="Scope"
filter={[scopeFilter, setScopeFilter]}
options={filterScopeOptions(USER_SCOPE_SELECTION, user)}
/>
)}
<ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter
account={[accountSid, setAccountSid]}
accounts={accounts}
defaultOption={user?.scope !== USER_ACCOUNT}
/>
</ScopedAccess>
</section>
<Section slim>
<div className="grid grid--col4">
<div className="grid grid--col3--users">
<div className="grid__row grid__th">
<div>User Name</div>
<div>Email</div>
<div>Scope</div>
<div>&nbsp;</div>
</div>
@@ -101,9 +119,21 @@ export const Users = () => {
filteredUsers.map((user) => {
return (
<div className="grid__row" key={user.user_sid}>
<div>{user.name}</div>
<div>{user.email}</div>
<div>{user.scope}</div>
<div>
<Link
to={`${ROUTE_INTERNAL_USERS}/${user.user_sid}/edit`}
title="Edit user"
>
<div>{user.name}</div>
</Link>
</div>
<div>
{user.scope === USER_ADMIN
? "All"
: user.account_name
? `Account: ${user.account_name}`
: `Service Provider: ${user.service_provider_name}`}
</div>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_USERS}/${user.user_sid}/edit`}
+14 -2
View File
@@ -6,7 +6,11 @@ import { isValidPasswd } from "src/utils";
import { putUser, useApiData } from "src/api";
import { PasswordSettings, StatusCodes } from "src/api/types";
import { Passwd, Message } from "src/components/forms";
import { ROUTE_LOGIN, ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
import {
ROUTE_LOGIN,
ROUTE_INTERNAL_ACCOUNTS,
ROUTE_INTERNAL_APPLICATIONS,
} from "src/router/routes";
import {
SESS_OLD_PASSWORD,
SESS_USER_SID,
@@ -17,6 +21,8 @@ import {
} from "src/constants";
import type { IMessage } from "src/store/types";
import { USER_ACCOUNT } from "src/api/constants";
import { getToken, parseJwt } from "src/router/auth";
export const CreatePassword = () => {
const [passwdSettings] = useApiData<PasswordSettings>("PasswordSettings");
@@ -50,6 +56,8 @@ export const CreatePassword = () => {
const userSid = sessionStorage.getItem(SESS_USER_SID);
const oldPassword = sessionStorage.getItem(SESS_OLD_PASSWORD);
const token = getToken();
const userData = parseJwt(token);
if (!oldPassword) {
navigate(ROUTE_LOGIN);
@@ -65,7 +73,11 @@ export const CreatePassword = () => {
if (response.status === StatusCodes.NO_CONTENT) {
sessionStorage.clear();
navigate(ROUTE_INTERNAL_ACCOUNTS);
navigate(
userData.scope !== USER_ACCOUNT
? ROUTE_INTERNAL_ACCOUNTS
: ROUTE_INTERNAL_APPLICATIONS
);
} else {
setMessage(MSG_SOMETHING_WRONG);
}
+9 -2
View File
@@ -2,18 +2,21 @@ import React, { useEffect, useState } from "react";
import { Button, H1 } from "jambonz-ui";
import { useLocation, Navigate } from "react-router-dom";
import { toastError } from "src/store";
import { toastError, useSelectState } from "src/store";
import { useAuth } from "src/router/auth";
import { SESS_UNAUTHORIZED, SESS_OLD_PASSWORD } from "src/constants";
import { Passwd, Message } from "src/components/forms";
import {
ROUTE_INTERNAL_ACCOUNTS,
ROUTE_CREATE_PASSWORD,
ROUTE_INTERNAL_APPLICATIONS,
} from "src/router/routes";
import { USER_ACCOUNT } from "src/api/constants";
export const Login = () => {
const { signin, authorized } = useAuth();
const location = useLocation();
const user = useSelectState("user");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
@@ -49,7 +52,11 @@ export const Login = () => {
return (
<Navigate
to={ROUTE_INTERNAL_ACCOUNTS}
to={
user?.scope !== USER_ACCOUNT
? ROUTE_INTERNAL_ACCOUNTS
: ROUTE_INTERNAL_APPLICATIONS
}
state={{ from: location }}
replace
/>
+10 -1
View File
@@ -10,6 +10,7 @@ import {
ROUTE_LOGIN,
ROUTE_CREATE_PASSWORD,
ROUTE_INTERNAL_ACCOUNTS,
ROUTE_INTERNAL_APPLICATIONS,
} from "./routes";
import {
SESS_OLD_PASSWORD,
@@ -20,6 +21,8 @@ import {
} from "src/constants";
import type { UserLogin } from "src/api/types";
import { USER_ACCOUNT } from "src/api/constants";
import type { UserData } from "src/store/types";
interface SignIn {
(username: string, password: string): Promise<UserLogin>;
@@ -85,6 +88,7 @@ export const parseJwt = (token: string) => {
*/
export const useProvideAuth = (): AuthStateContext => {
let token = getToken();
let userData: UserData;
const navigate = useNavigate();
const authorized = token ? true : false;
@@ -95,13 +99,18 @@ export const useProvideAuth = (): AuthStateContext => {
if (response.status === StatusCodes.OK) {
token = response.json.token;
setToken(token);
userData = parseJwt(token);
if (response.json.force_change) {
sessionStorage.setItem(SESS_USER_SID, response.json.user_sid);
sessionStorage.setItem(SESS_OLD_PASSWORD, password);
navigate(ROUTE_CREATE_PASSWORD);
} else {
navigate(ROUTE_INTERNAL_ACCOUNTS);
navigate(
userData.scope !== USER_ACCOUNT
? ROUTE_INTERNAL_ACCOUNTS
: ROUTE_INTERNAL_APPLICATIONS
);
}
resolve(response.json);
+3 -4
View File
@@ -99,17 +99,16 @@
}
}
&--col4 {
&--col3--users {
.grid__row {
grid-template-columns: [col] 30% [col] 30% [col] 30% [col] 10%;
grid-template-columns: [col] 40% [col] 58% [col] 2%;
grid-template-rows: [row] auto [row] auto [row] [row] auto;
display: grid;
justify-content: space-between;
> div:last-child {
text-align: right;
text-align: left;
padding-right: 0;
font-size: 0;
}
}
}
+1
View File
@@ -34,6 +34,7 @@
display: flex;
align-items: center;
grid-gap: ui-vars.$px02;
color: ui-vars.$jambonz;
+ .item__meta {
@include mixins.small() {
-1
View File
@@ -22,7 +22,6 @@
@mixin icosize {
height: ui-vars.$h3-size;
width: ui-vars.$h3-size;
margin-right: auto;
cursor: pointer;
svg {
+65 -1
View File
@@ -4,6 +4,7 @@ import { withAccessControl } from "./with-access-control";
import { withSelectState } from "./with-select-state";
import { useRedirect } from "./use-redirect";
import { useFilteredResults } from "./use-filtered-results";
import { useScopedRedirect } from "./use-scoped-redirect";
import {
FQDN,
FQDN_TOP_LEVEL,
@@ -15,7 +16,16 @@ import {
USER_SP,
} from "src/api/constants";
import type { IpType, PasswordSettings, User, UserScopes } from "src/api/types";
import type {
Carrier,
IpType,
PasswordSettings,
SelectorOptions,
SpeechCredential,
User,
UserScopes,
} from "src/api/types";
import type { UserData } from "src/store/types";
export const hasValue = <Type>(
variable: Type | null | undefined
@@ -148,6 +158,59 @@ export const getUserScope = (user: User): UserScopes => {
}
};
export const isUserAccountScope = (accountSid: string, user?: UserData) => {
return (
user?.scope === USER_ACCOUNT &&
(user?.account_sid !== accountSid || !accountSid)
);
};
export const checkSelectOptions = (
user?: UserData,
resource?: SpeechCredential | Carrier
) => {
if (user?.scope === USER_ACCOUNT) {
if (!resource) {
return false;
}
if (resource && resource?.account_sid) {
return false;
}
if (resource && !resource?.account_sid) {
return true;
}
}
return true;
};
export const sortUsersAlpha = (a: User, b: User) => {
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
};
export const filterScopeOptions = (
optionArray: SelectorOptions[],
user: UserData
) => {
if (user.scope === USER_SP) {
return optionArray.filter((option) => option.value !== USER_ADMIN);
}
if (user.scope === USER_ACCOUNT) {
return optionArray.filter((option) => option.value === USER_ACCOUNT);
}
return optionArray;
};
export {
withSuspense,
useMobileMedia,
@@ -155,4 +218,5 @@ export {
withSelectState,
useRedirect,
useFilteredResults,
useScopedRedirect,
};
+51
View File
@@ -0,0 +1,51 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Account,
Application,
Carrier,
SpeechCredential,
User,
} from "src/api/types";
import { toastError, useSelectState } from "src/store";
import { IMessage, Scope, UserData } from "src/store/types";
export const useScopedRedirect = (
access: Scope,
redirect: string,
user?: UserData,
message?: IMessage,
data?: Account | User | Application | Carrier | SpeechCredential
) => {
const navigate = useNavigate();
const currentServiceProvider = useSelectState("currentServiceProvider");
useEffect(() => {
if (
data &&
user?.access === Scope.account &&
data?.account_sid !== user?.account_sid
) {
toastError("You do not have access.");
navigate(redirect);
}
if (
data &&
user?.access === 1 &&
currentServiceProvider?.service_provider_sid !==
user?.service_provider_sid
) {
toastError("You do not have access.");
navigate(redirect);
}
if (user && access > user.access) {
if (message) toastError(message);
navigate(redirect);
}
}, [user, currentServiceProvider, data]);
};