mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-04 19:21:58 +00:00
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:
@@ -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
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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([]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
.ico {
|
||||
@include mixins.icosize();
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__sps {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> </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`}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-gap: ui-vars.$px02;
|
||||
color: ui-vars.$jambonz;
|
||||
|
||||
+ .item__meta {
|
||||
@include mixins.small() {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
@mixin icosize {
|
||||
height: ui-vars.$h3-size;
|
||||
width: ui-vars.$h3-size;
|
||||
margin-right: auto;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
|
||||
+65
-1
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
Reference in New Issue
Block a user