mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-02-09 02:29:45 +00:00
Compare commits
16 Commits
feat/whisp
...
v0.8.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42052a04e | ||
|
|
fd749e1a2e | ||
|
|
2f868d955d | ||
|
|
f5995ce19d | ||
|
|
27cc99287d | ||
|
|
bb460c82ef | ||
|
|
c54595c99c | ||
|
|
aacebae373 | ||
|
|
5a363d70ff | ||
|
|
a405b3e53b | ||
|
|
93df766eb5 | ||
|
|
e35c422e89 | ||
|
|
70c1c416a2 | ||
|
|
ccefb37f20 | ||
|
|
c2c4a28868 | ||
|
|
92afbfb388 |
@@ -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 {
|
||||
@@ -58,6 +59,7 @@ import type {
|
||||
LimitCategories,
|
||||
PasswordSettings,
|
||||
} from "./types";
|
||||
import { UserData } from "../store/types";
|
||||
import { StatusCodes } from "./types";
|
||||
|
||||
/** Wrap all requests to normalize response handling */
|
||||
@@ -257,13 +259,21 @@ export const postApplication = (payload: Partial<Application>) => {
|
||||
};
|
||||
|
||||
export const postSpeechService = (
|
||||
user: UserData,
|
||||
sid: string,
|
||||
payload: Partial<SpeechCredential>
|
||||
) => {
|
||||
return postFetch<SidResponse, Partial<SpeechCredential>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials`,
|
||||
payload
|
||||
);
|
||||
if (user.scope === USER_ACCOUNT) {
|
||||
return postFetch<SidResponse, Partial<SpeechCredential>>(
|
||||
`${API_ACCOUNTS}/${user.account_sid}/SpeechCredentials`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
return postFetch<SidResponse, Partial<SpeechCredential>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const postMsTeamsTentant = (payload: Partial<MSTeamsTenant>) => {
|
||||
@@ -280,11 +290,22 @@ 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
|
||||
);
|
||||
export const postCarrier = (
|
||||
user: UserData,
|
||||
sid: string,
|
||||
payload: Partial<Carrier>
|
||||
) => {
|
||||
if (user.scope === USER_ACCOUNT) {
|
||||
return postFetch<SidResponse, Partial<SpeechCredential>>(
|
||||
`${API_ACCOUNTS}/${user.account_sid}/VoipCarriers/`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
return postFetch<SidResponse, Partial<Carrier>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers/`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const postPredefinedCarrierTemplate = (
|
||||
@@ -296,6 +317,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);
|
||||
};
|
||||
@@ -364,14 +394,22 @@ export const putApplication = (sid: string, payload: Partial<Application>) => {
|
||||
};
|
||||
|
||||
export const putSpeechService = (
|
||||
user: UserData,
|
||||
sid1: string,
|
||||
sid2: string,
|
||||
payload: Partial<SpeechCredential>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<SpeechCredential>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`,
|
||||
payload
|
||||
);
|
||||
if (user.scope === USER_ACCOUNT) {
|
||||
return putFetch<EmptyResponse, Partial<SpeechCredential>>(
|
||||
`${API_ACCOUNTS}/${user.account_sid}/SpeechCredentials/${sid2}`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
return putFetch<EmptyResponse, Partial<SpeechCredential>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const putMsTeamsTenant = (
|
||||
|
||||
@@ -106,7 +106,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[];
|
||||
}
|
||||
@@ -142,6 +144,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,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,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,7 @@ 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";
|
||||
|
||||
type CommonProps = {
|
||||
handleMenu: () => void;
|
||||
@@ -30,16 +31,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 +73,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 +137,7 @@ export const Navi = ({
|
||||
<Icon subStyle="white" onClick={handleMenu}>
|
||||
<Icons.X />
|
||||
</Icon>
|
||||
<UserMe />
|
||||
<Button
|
||||
small
|
||||
mainStyle="hollow"
|
||||
@@ -172,9 +189,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 +207,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 {
|
||||
|
||||
34
src/containers/internal/user-me/index.tsx
Normal file
34
src/containers/internal/user-me/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
42
src/containers/internal/user-me/styles.scss
Normal file
42
src/containers/internal/user-me/styles.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -6,20 +6,45 @@ import { useServiceProviderData, deleteAccount } from "src/api";
|
||||
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 { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
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";
|
||||
|
||||
export const Accounts = () => {
|
||||
const user = useSelectState("user");
|
||||
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,
|
||||
@@ -18,6 +22,7 @@ import {
|
||||
import type { Account, Alert, PageQuery } from "src/api/types";
|
||||
|
||||
export const Alerts = () => {
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [dateFilter, setDateFilter] = useState("today");
|
||||
@@ -69,7 +74,13 @@ export const Alerts = () => {
|
||||
<section className="filters filters--multi">
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
/>
|
||||
<SelectFilter
|
||||
id="date_filter"
|
||||
@@ -103,7 +114,7 @@ export const Alerts = () => {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<M>No data</M>
|
||||
<M>No data.</M>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -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,
|
||||
@@ -31,7 +31,11 @@ import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
} 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 type {
|
||||
RecognizerVendors,
|
||||
@@ -58,6 +62,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 +120,13 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (user?.scope === USER_ACCOUNT && user.account_sid !== accountSid) {
|
||||
toastError(
|
||||
"You do not have permissions to make changes to these Speech Credentials"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage("");
|
||||
|
||||
if (applications) {
|
||||
@@ -152,6 +164,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
.then(() => {
|
||||
application.refetch();
|
||||
toastSuccess("Application updated successfully");
|
||||
navigate(`${ROUTE_INTERNAL_APPLICATIONS}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
@@ -266,7 +279,13 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<AccountSelect
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
account={[accountSid, setAccountSid]}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { API_ACCOUNTS, USER_ACCOUNT } from "src/api/constants";
|
||||
import {
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
@@ -22,6 +22,7 @@ import { hasLength, hasValue, useFilteredResults } from "src/utils";
|
||||
import type { Application, Account } from "src/api/types";
|
||||
|
||||
export const Applications = () => {
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
@@ -44,6 +45,12 @@ export const Applications = () => {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (application) {
|
||||
if (user?.scope === USER_ACCOUNT && user.account_sid !== accountSid) {
|
||||
toastError(
|
||||
"You do not have permissions to make changes to this Application"
|
||||
);
|
||||
return;
|
||||
}
|
||||
deleteApplication(application.application_sid)
|
||||
.then(() => {
|
||||
getApplications();
|
||||
@@ -97,7 +104,13 @@ export const Applications = () => {
|
||||
/>
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredApplications) && { slim: true })}>
|
||||
|
||||
@@ -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 {
|
||||
@@ -63,6 +65,7 @@ export const CarrierForm = ({
|
||||
carrierSmppGateways,
|
||||
}: CarrierFormProps) => {
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
|
||||
const refSipIp = useRef<HTMLInputElement[]>([]);
|
||||
@@ -437,6 +440,14 @@ export const CarrierForm = ({
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
user?.scope === USER_ACCOUNT &&
|
||||
(user.account_sid !== accountSid || !accountSid)
|
||||
) {
|
||||
toastError("You do not have permissions to make changes to this Carrier");
|
||||
return;
|
||||
}
|
||||
|
||||
setSipMessage("");
|
||||
setSmppInboundMessage("");
|
||||
setSmppOutboundMessage("");
|
||||
@@ -485,7 +496,7 @@ export const CarrierForm = ({
|
||||
smpp_inbound_password: smppInboundPass.trim() || null,
|
||||
};
|
||||
|
||||
if (carrier && carrier.data) {
|
||||
if (carrier && carrier.data && user) {
|
||||
putCarrier(
|
||||
currentServiceProvider.service_provider_sid,
|
||||
carrier.data.voip_carrier_sid,
|
||||
@@ -499,25 +510,27 @@ export const CarrierForm = ({
|
||||
|
||||
toastSuccess("Carrier updated successfully");
|
||||
carrier.refetch();
|
||||
navigate(ROUTE_INTERNAL_CARRIERS);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else {
|
||||
postCarrier(currentServiceProvider.service_provider_sid, {
|
||||
...carrierPayload,
|
||||
service_provider_sid: currentServiceProvider.service_provider_sid,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
handleSipGatewayPutPost(json.sid);
|
||||
handleSmppGatewayPutPost(json.sid);
|
||||
|
||||
toastSuccess("Carrier created successfully");
|
||||
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
|
||||
if (user)
|
||||
postCarrier(user, currentServiceProvider.service_provider_sid, {
|
||||
...carrierPayload,
|
||||
service_provider_sid: currentServiceProvider.service_provider_sid,
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
.then(({ json }) => {
|
||||
handleSipGatewayPutPost(json.sid);
|
||||
handleSmppGatewayPutPost(json.sid);
|
||||
|
||||
toastSuccess("Carrier created successfully");
|
||||
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -529,16 +542,26 @@ export const CarrierForm = ({
|
||||
)?.predefined_carrier_sid;
|
||||
|
||||
if (currentServiceProvider && predefinedCarrierSid) {
|
||||
postPredefinedCarrierTemplate(
|
||||
currentServiceProvider.service_provider_sid,
|
||||
predefinedCarrierSid
|
||||
)
|
||||
.then(({ json }) => {
|
||||
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
if (user?.scope === USER_ACCOUNT) {
|
||||
postPredefinedCarrierTemplateAccount(accountSid, predefinedCarrierSid)
|
||||
.then(({ json }) => {
|
||||
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else {
|
||||
postPredefinedCarrierTemplate(
|
||||
currentServiceProvider.service_provider_sid,
|
||||
predefinedCarrierSid
|
||||
)
|
||||
.then(({ json }) => {
|
||||
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [predefinedName]);
|
||||
@@ -583,6 +606,21 @@ export const CarrierForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
const checkOptions = () => {
|
||||
if (user?.scope === USER_ACCOUNT) {
|
||||
if (carrier && carrier.data?.account_sid) {
|
||||
return false;
|
||||
}
|
||||
if (!carrier) {
|
||||
return false;
|
||||
}
|
||||
if (carrier && !carrier.data?.account_sid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
@@ -677,11 +715,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={checkOptions()}
|
||||
disabled={
|
||||
user?.scope !== USER_ACCOUNT
|
||||
? false
|
||||
: user.account_sid !== accountSid
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{accountSid && hasLength(applications) && (
|
||||
<>
|
||||
@@ -971,6 +1022,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 {
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
getFetch,
|
||||
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 +17,26 @@ import {
|
||||
Spinner,
|
||||
SearchFilter,
|
||||
} from "src/components";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Gateways } from "./gateways";
|
||||
import { hasLength, hasValue, useFilteredResults } from "src/utils";
|
||||
import { API_SIP_GATEWAY, API_SMPP_GATEWAY } from "src/api/constants";
|
||||
import {
|
||||
API_ACCOUNTS,
|
||||
API_SERVICE_PROVIDERS,
|
||||
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 [carrier, setCarrier] = useState<Carrier | null>(null);
|
||||
const [carriers, refetch] = useServiceProviderData<Carrier[]>("VoipCarriers");
|
||||
const [carriers, setCarriers] = useState<Carrier[]>();
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
@@ -39,15 +49,30 @@ export const Carriers = () => {
|
||||
: carrier.account_sid === null
|
||||
)
|
||||
: [];
|
||||
}, [accountSid, carriers]);
|
||||
}, [accountSid, carrier, carriers]);
|
||||
|
||||
const filteredCarriers = useFilteredResults<Carrier>(
|
||||
filter,
|
||||
carriersFiltered
|
||||
);
|
||||
|
||||
const getCarriers = (url: string) => {
|
||||
getFetch<Carrier[]>(url)
|
||||
.then(({ json }) => {
|
||||
setCarriers(json);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (carrier) {
|
||||
if (user?.scope === USER_ACCOUNT && user.account_sid !== accountSid) {
|
||||
toastError("You do not have permissions to delete this Carrier");
|
||||
return;
|
||||
}
|
||||
|
||||
deleteCarrier(carrier.voip_carrier_sid)
|
||||
.then(() => {
|
||||
Promise.all([
|
||||
@@ -77,7 +102,15 @@ export const Carriers = () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
refetch();
|
||||
if ((user && user?.scope === USER_ACCOUNT) || accountSid) {
|
||||
getCarriers(
|
||||
`${API_ACCOUNTS}/${user?.account_sid || accountSid}/VoipCarriers`
|
||||
);
|
||||
} else {
|
||||
getCarriers(
|
||||
`${API_SERVICE_PROVIDERS}/${currentServiceProvider?.service_provider_sid}/VoipCarriers`
|
||||
);
|
||||
}
|
||||
setCarrier(null);
|
||||
toastSuccess(
|
||||
<>
|
||||
@@ -91,6 +124,20 @@ export const Carriers = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (accountSid) {
|
||||
getCarriers(
|
||||
`${API_ACCOUNTS}/${user?.account_sid || accountSid}/VoipCarriers`
|
||||
);
|
||||
} else {
|
||||
if (currentServiceProvider) {
|
||||
getCarriers(
|
||||
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/VoipCarriers`
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [user, accountSid, currentServiceProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
@@ -109,7 +156,13 @@ export const Carriers = () => {
|
||||
/>
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
label="Used by"
|
||||
defaultOption
|
||||
/>
|
||||
@@ -123,14 +176,19 @@ 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={Scope.service_provider}>
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_CARRIERS}/${carrier.voip_carrier_sid}/edit`}
|
||||
title="Edit Carrier"
|
||||
className="i"
|
||||
>
|
||||
<strong>{carrier.name}</strong>
|
||||
<Icons.ArrowRight />
|
||||
</Link>
|
||||
</ScopedAccess>
|
||||
{user?.scope === USER_ACCOUNT && (
|
||||
<strong>{carrier.name}</strong>
|
||||
<Icons.ArrowRight />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="item__meta">
|
||||
<div>
|
||||
@@ -150,22 +208,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,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
|
||||
.then(() => {
|
||||
phoneNumber.refetch();
|
||||
toastSuccess("Phone number updated successfully");
|
||||
navigate(ROUTE_INTERNAL_PHONE_NUMBERS);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
|
||||
@@ -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,10 @@ import {
|
||||
import { DeletePhoneNumber } from "./delete";
|
||||
|
||||
import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
|
||||
export const PhoneNumbers = () => {
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [applications] = useServiceProviderData<Application[]>("Applications");
|
||||
const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
|
||||
@@ -123,8 +125,14 @@ export const PhoneNumbers = () => {
|
||||
/>
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
defaultOption
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
defaultOption={user?.scope !== USER_ACCOUNT}
|
||||
/>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredPhoneNumbers) && { slim: true })}>
|
||||
|
||||
@@ -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,
|
||||
@@ -30,6 +34,7 @@ const statusSelection = [
|
||||
];
|
||||
|
||||
export const RecentCalls = () => {
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [dateFilter, setDateFilter] = useState("today");
|
||||
@@ -86,7 +91,13 @@ export const RecentCalls = () => {
|
||||
<section className="filters filters--multi">
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
/>
|
||||
<SelectFilter
|
||||
id="date_filter"
|
||||
@@ -114,7 +125,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>
|
||||
|
||||
@@ -5,15 +5,21 @@ import { 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";
|
||||
|
||||
type SettingsProps = {
|
||||
currentServiceProvider: ServiceProvider;
|
||||
};
|
||||
|
||||
export const Settings = ({ currentServiceProvider }: SettingsProps) => {
|
||||
const user = useSelectState("user");
|
||||
const [activeTab, setActiveTab] = useState("");
|
||||
|
||||
return (
|
||||
@@ -24,14 +30,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>
|
||||
|
||||
|
||||
@@ -2,17 +2,25 @@ import React, { useEffect } 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, useServiceProviderData } 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";
|
||||
|
||||
export const EditSpeechService = () => {
|
||||
const params = useParams();
|
||||
const [data, refetch, error] = useServiceProviderData<SpeechCredential>(
|
||||
`SpeechCredentials/${params.speech_credential_sid}`
|
||||
);
|
||||
const user = useSelectState("user");
|
||||
|
||||
const [data, refetch, error] =
|
||||
user && user.scope !== USER_ACCOUNT
|
||||
? useServiceProviderData<SpeechCredential>(
|
||||
`SpeechCredentials/${params.speech_credential_sid}`
|
||||
)
|
||||
: useApiData<SpeechCredential>(
|
||||
`Accounts/${user?.account_sid}/SpeechCredentials/${params.speech_credential_sid}`
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CredentialStatus } from "./status";
|
||||
|
||||
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
|
||||
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
|
||||
type SpeechServiceFormProps = {
|
||||
credential?: UseApiDataMap<SpeechCredential>;
|
||||
@@ -41,6 +42,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 +100,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (user?.scope === USER_ACCOUNT && user.account_sid !== accountSid) {
|
||||
toastError(
|
||||
"You do not have permissions to make changes to these Speech Credentials"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentServiceProvider) {
|
||||
const payload: Partial<SpeechCredential> = {
|
||||
vendor,
|
||||
@@ -124,10 +133,11 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
}),
|
||||
};
|
||||
|
||||
if (credential && credential.data) {
|
||||
if (credential && credential.data && user) {
|
||||
/** The backend API returns obscured secrets now so we need to make sure we don't send them back */
|
||||
/** Fields not sent back via :PUT are `service_key`, `access_key_id`, `secret_access_key` and `api_key` */
|
||||
putSpeechService(
|
||||
user,
|
||||
currentServiceProvider.service_provider_sid,
|
||||
credential.data.speech_credential_sid,
|
||||
payload
|
||||
@@ -136,42 +146,61 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
if (credential && credential.data) {
|
||||
toastSuccess("Speech credential updated successfully");
|
||||
credential.refetch();
|
||||
navigate(ROUTE_INTERNAL_SPEECH);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else {
|
||||
postSpeechService(currentServiceProvider.service_provider_sid, {
|
||||
...payload,
|
||||
service_key:
|
||||
vendor === VENDOR_GOOGLE ? JSON.stringify(googleServiceKey) : null,
|
||||
access_key_id: vendor === VENDOR_AWS ? accessKeyId : null,
|
||||
secret_access_key: vendor === VENDOR_AWS ? secretAccessKey : null,
|
||||
api_key:
|
||||
vendor === VENDOR_MICROSOFT ||
|
||||
vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_DEEPGRAM
|
||||
? apiKey
|
||||
: 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 }) => {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(`${ROUTE_INTERNAL_SPEECH}/${json.sid}/edit`);
|
||||
if (user)
|
||||
postSpeechService(user, currentServiceProvider.service_provider_sid, {
|
||||
...payload,
|
||||
service_key:
|
||||
vendor === VENDOR_GOOGLE
|
||||
? JSON.stringify(googleServiceKey)
|
||||
: null,
|
||||
access_key_id: vendor === VENDOR_AWS ? accessKeyId : null,
|
||||
secret_access_key: vendor === VENDOR_AWS ? secretAccessKey : null,
|
||||
api_key:
|
||||
vendor === VENDOR_MICROSOFT ||
|
||||
vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_DEEPGRAM
|
||||
? apiKey
|
||||
: 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,
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
.then(({ json }) => {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(`${ROUTE_INTERNAL_SPEECH}/${json.sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkOptions = () => {
|
||||
if (user?.scope === USER_ACCOUNT) {
|
||||
if (credential && credential.data?.account_sid) {
|
||||
return false;
|
||||
}
|
||||
if (!credential) {
|
||||
return false;
|
||||
}
|
||||
if (credential && !credential.data?.account_sid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (credential && credential.data) {
|
||||
if (credential.data.vendor) {
|
||||
@@ -289,10 +318,16 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<AccountSelect
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
account={[accountSid, setAccountSid]}
|
||||
required={false}
|
||||
defaultOption
|
||||
defaultOption={checkOptions()}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
</fieldset>
|
||||
@@ -541,7 +576,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 +612,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,25 +1,53 @@
|
||||
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 {
|
||||
API_ACCOUNTS,
|
||||
API_SERVICE_PROVIDERS,
|
||||
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 { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
|
||||
import { getHumanDateTime, hasLength, hasValue } from "src/utils";
|
||||
import {
|
||||
getHumanDateTime,
|
||||
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 [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [credential, setCredential] = useState<SpeechCredential | null>(null);
|
||||
const [credentials, setCredentials] = useState<SpeechCredential[]>();
|
||||
const [filter] = useState("");
|
||||
|
||||
const credentialsFiltered = useMemo(() => {
|
||||
return credentials
|
||||
? credentials.filter((credentials) =>
|
||||
accountSid
|
||||
? credentials.account_sid === accountSid
|
||||
: credentials.account_sid === null
|
||||
)
|
||||
: [];
|
||||
}, [accountSid, accounts, credentials]);
|
||||
|
||||
const filteredCredentials = useFilteredResults<SpeechCredential>(
|
||||
filter,
|
||||
credentialsFiltered
|
||||
);
|
||||
|
||||
const getSpeechCredentials = (url: string) => {
|
||||
getFetch<SpeechCredential[]>(url)
|
||||
@@ -33,14 +61,22 @@ export const SpeechServices = () => {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (credential && currentServiceProvider) {
|
||||
if (user?.scope === USER_ACCOUNT && user.account_sid !== accountSid) {
|
||||
toastError(
|
||||
"You do not have permissions to delete these Speech Credentials"
|
||||
);
|
||||
return;
|
||||
}
|
||||
deleteSpeechService(
|
||||
currentServiceProvider.service_provider_sid,
|
||||
credential.speech_credential_sid
|
||||
)
|
||||
.then(() => {
|
||||
if (accountSid) {
|
||||
if ((user && user?.scope === USER_ACCOUNT) || accountSid) {
|
||||
getSpeechCredentials(
|
||||
`${API_ACCOUNTS}/${accountSid}/SpeechCredentials`
|
||||
`${API_ACCOUNTS}/${
|
||||
user?.account_sid || accountSid
|
||||
}/SpeechCredentials`
|
||||
);
|
||||
} else {
|
||||
getSpeechCredentials(
|
||||
@@ -62,13 +98,17 @@ export const SpeechServices = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (accountSid) {
|
||||
getSpeechCredentials(`${API_ACCOUNTS}/${accountSid}/SpeechCredentials`);
|
||||
} else if (currentServiceProvider) {
|
||||
getSpeechCredentials(
|
||||
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/SpeechCredentials`
|
||||
`${API_ACCOUNTS}/${user?.account_sid || accountSid}/SpeechCredentials`
|
||||
);
|
||||
} else {
|
||||
if (currentServiceProvider) {
|
||||
getSpeechCredentials(
|
||||
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/SpeechCredentials`
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [accountSid, currentServiceProvider]);
|
||||
}, [user, accountSid, currentServiceProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -83,29 +123,45 @@ export const SpeechServices = () => {
|
||||
<section className="filters filters--ender">
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
label="Used by"
|
||||
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>
|
||||
{user?.scope === USER_ACCOUNT && (
|
||||
<strong>Vendor: {credential.vendor}</strong>
|
||||
<Icons.ArrowRight />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="item__meta">
|
||||
<div>
|
||||
@@ -147,22 +203,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 { 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, useServiceProviderData } 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,15 @@ export const CredentialStatus = ({
|
||||
cred,
|
||||
showSummary = false,
|
||||
}: CredentialStatusProps) => {
|
||||
const user = useSelectState("user");
|
||||
const [testResult, testRefetch, testError] =
|
||||
useServiceProviderData<CredentialTestResult>(
|
||||
`SpeechCredentials/${cred.speech_credential_sid}/test`
|
||||
);
|
||||
user && user.scope !== USER_ACCOUNT
|
||||
? useServiceProviderData<CredentialTestResult>(
|
||||
`SpeechCredentials/${cred.speech_credential_sid}/test`
|
||||
)
|
||||
: useApiData<CredentialTestResult>(
|
||||
`Accounts/${user?.account_sid}/SpeechCredentials/${cred.speech_credential_sid}/test`
|
||||
);
|
||||
const notTestedTxt =
|
||||
"In order to test your credentials you need to enable TTS/STT.";
|
||||
|
||||
|
||||
@@ -52,7 +52,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);
|
||||
@@ -168,6 +170,7 @@ export const UserForm = ({ user }: UserFormProps) => {
|
||||
setIsActive(!!user.data.is_active);
|
||||
setEmail(user.data.email);
|
||||
setScope(getUserScope(user.data));
|
||||
user.data.account_sid ? setAccountSid(user.data.account_sid) : false;
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
@@ -184,7 +187,7 @@ export const UserForm = ({ user }: UserFormProps) => {
|
||||
<Selector
|
||||
id="scope"
|
||||
name="scope"
|
||||
value={scope}
|
||||
value={scope || currentUser?.scope}
|
||||
options={
|
||||
currentUser?.scope === USER_SP
|
||||
? USER_SCOPE_SELECTION.filter(
|
||||
@@ -203,7 +206,7 @@ export const UserForm = ({ user }: UserFormProps) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{scope !== USER_ACCOUNT && !hasLength(accounts) && (
|
||||
{scope === USER_ACCOUNT && !hasLength(accounts) && (
|
||||
<>
|
||||
<label htmlFor="account">
|
||||
Account:<span>*</span>
|
||||
|
||||
@@ -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,18 @@ import {
|
||||
AccountFilter,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import { hasLength, hasValue, useFilteredResults } from "src/utils";
|
||||
import {
|
||||
hasLength,
|
||||
hasValue,
|
||||
sortAlphabetically,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
|
||||
import type { Account, User } from "src/api/types";
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
export const Users = () => {
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [users] = useApiData<User[]>("Users");
|
||||
const [filter, setFilter] = useState("");
|
||||
@@ -28,6 +38,15 @@ export const Users = () => {
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
|
||||
const usersFiltered = useMemo(() => {
|
||||
//find and add account/sp names to user to improve fuzzy search
|
||||
users?.forEach((user) => {
|
||||
user.account_name =
|
||||
accounts?.find((acct) => acct.account_sid === user.account_sid)?.name ||
|
||||
null;
|
||||
user.service_provider_name =
|
||||
user.scope === USER_ADMIN ? null : currentServiceProvider?.name || null;
|
||||
});
|
||||
|
||||
const serviceProviderUsers = users?.filter((e) => {
|
||||
return (
|
||||
e.scope === USER_ADMIN ||
|
||||
@@ -55,7 +74,9 @@ export const Users = () => {
|
||||
return [];
|
||||
}, [accountSid, scopeFilter, users, accounts, currentServiceProvider]);
|
||||
|
||||
const filteredUsers = useFilteredResults<User>(filter, usersFiltered);
|
||||
const filteredUsers = useFilteredResults<User>(filter, usersFiltered)?.sort(
|
||||
(a, b) => sortAlphabetically(a, b)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,16 +103,21 @@ export const Users = () => {
|
||||
/>
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
defaultOption={true}
|
||||
accounts={
|
||||
user?.scope === USER_ACCOUNT
|
||||
? accounts?.filter(
|
||||
(acct) => acct.account_sid === user.account_sid
|
||||
)
|
||||
: accounts
|
||||
}
|
||||
defaultOption={user?.scope !== USER_ACCOUNT}
|
||||
/>
|
||||
</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 +127,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,7 @@ export const CreatePassword = () => {
|
||||
|
||||
const userSid = sessionStorage.getItem(SESS_USER_SID);
|
||||
const oldPassword = sessionStorage.getItem(SESS_OLD_PASSWORD);
|
||||
const token = getToken();
|
||||
|
||||
if (!oldPassword) {
|
||||
navigate(ROUTE_LOGIN);
|
||||
@@ -65,7 +72,11 @@ export const CreatePassword = () => {
|
||||
if (response.status === StatusCodes.NO_CONTENT) {
|
||||
sessionStorage.clear();
|
||||
|
||||
navigate(ROUTE_INTERNAL_ACCOUNTS);
|
||||
navigate(
|
||||
parseJwt(token).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,6 +10,7 @@ import {
|
||||
ROUTE_LOGIN,
|
||||
ROUTE_CREATE_PASSWORD,
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
} from "./routes";
|
||||
import {
|
||||
SESS_OLD_PASSWORD,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from "src/constants";
|
||||
|
||||
import type { UserLogin } from "src/api/types";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
|
||||
interface SignIn {
|
||||
(username: string, password: string): Promise<UserLogin>;
|
||||
@@ -101,7 +103,11 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
sessionStorage.setItem(SESS_OLD_PASSWORD, password);
|
||||
navigate(ROUTE_CREATE_PASSWORD);
|
||||
} else {
|
||||
navigate(ROUTE_INTERNAL_ACCOUNTS);
|
||||
navigate(
|
||||
parseJwt(token).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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -148,6 +149,20 @@ export const getUserScope = (user: User): UserScopes => {
|
||||
}
|
||||
};
|
||||
|
||||
export const sortAlphabetically = (a: User, b: User) => {
|
||||
const nameA = a.name.toUpperCase(); // ignore upper and lowercase
|
||||
const nameB = b.name.toUpperCase(); // ignore upper and lowercase
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// names must be equal
|
||||
return 0;
|
||||
};
|
||||
|
||||
export {
|
||||
withSuspense,
|
||||
useMobileMedia,
|
||||
@@ -155,4 +170,5 @@ export {
|
||||
withSelectState,
|
||||
useRedirect,
|
||||
useFilteredResults,
|
||||
useScopedRedirect,
|
||||
};
|
||||
|
||||
23
src/utils/use-scoped-redirect.ts
Normal file
23
src/utils/use-scoped-redirect.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { toastError } from "src/store";
|
||||
|
||||
import type { IMessage, Scope, UserData } from "src/store/types";
|
||||
|
||||
export const useScopedRedirect = (
|
||||
access: Scope,
|
||||
redirect: string,
|
||||
user?: UserData,
|
||||
message?: IMessage
|
||||
) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user && access >= user.access) {
|
||||
if (message) toastError(message);
|
||||
|
||||
navigate(redirect);
|
||||
}
|
||||
}, [user]);
|
||||
};
|
||||
Reference in New Issue
Block a user