mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-19 05:37:43 +00:00
Feature/users dashboard (#150)
* Add user list into nav, add user form with basic functionality * add password check for temp password, add search filter * apply review comments * fix filters, apply review comments * fix button placement * handle SelfDelete * apply review comments * add conditional class * creating scoped users * apply review comments * apply review comments * apply review comments Co-authored-by: eglehelms <e.helms@cognigy.com>
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3NpZCI6IjFjNTc4MWQyLTY5MGItNDIwYy1iZDUzLTVkN2Y1NjMwMDVjOCIsInNjb3BlIjoiYWRtaW4iLCJmb3JjZV9jaGFuZ2UiOnRydWUsInBlcm1pc3Npb25zIjpbIlBST1ZJU0lPTl9VU0VSUyIsIlBST1ZJU0lPTl9TRVJWSUNFUyIsIlZJRVdfT05MWSJdLCJpYXQiOjE2NjY3OTgzMTEsImV4cCI6MTY2NjgwMTkxMX0.ZV3KnRit8WGpipfiiMAZ2AVLQ25csWje1-K6hdqxktE",
|
||||
"scope": "admin",
|
||||
"user_sid": "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
||||
"force_change": false,
|
||||
"permissions": ["VIEW_ONLY", "PROVISION_SERVICES", "PROVISION_USERS"]
|
||||
"force_change": false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
LimitField,
|
||||
PasswordSettings,
|
||||
SipGateway,
|
||||
SmppGateway,
|
||||
WebHook,
|
||||
@@ -102,6 +103,13 @@ export const PER_PAGE_SELECTION = [
|
||||
{ name: "100 / page", value: "100" },
|
||||
];
|
||||
|
||||
export const USER_SCOPE_SELECTION = [
|
||||
{ name: "All Scopes", value: "all" },
|
||||
{ name: "Admin", value: "admin" },
|
||||
{ name: "Service Provider", value: "service_provider" },
|
||||
{ name: "Account", value: "account" },
|
||||
];
|
||||
|
||||
/** Available webhook methods */
|
||||
export const WEBHOOK_METHODS: WebhookOption[] = [
|
||||
{
|
||||
@@ -131,6 +139,17 @@ export const LIMITS: LimitField[] = [
|
||||
// },
|
||||
];
|
||||
|
||||
export const DEFAULT_PSWD_SETTINGS: PasswordSettings = {
|
||||
min_password_length: 6,
|
||||
require_digit: 0,
|
||||
require_special_character: 0,
|
||||
};
|
||||
|
||||
/** User scope values values */
|
||||
export const USER_ADMIN = "admin";
|
||||
export const USER_SP = "service_provider";
|
||||
export const USER_ACCOUNT = "account";
|
||||
|
||||
/** Speech credential test result status values */
|
||||
export const CRED_OK = "ok";
|
||||
export const CRED_FAIL = "fail";
|
||||
|
||||
@@ -332,8 +332,8 @@ export const postPasswordSettings = (payload: Partial<PasswordSettings>) => {
|
||||
};
|
||||
/** Named wrappers for `putFetch` */
|
||||
|
||||
export const putUser = (sid: string, payload: UserUpdatePayload) => {
|
||||
return putFetch<EmptyResponse, UserUpdatePayload>(
|
||||
export const putUser = (sid: string, payload: Partial<UserUpdatePayload>) => {
|
||||
return putFetch<EmptyResponse, Partial<UserUpdatePayload>>(
|
||||
`${API_USERS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
@@ -418,6 +418,10 @@ export const putSmppGateway = (sid: string, payload: Partial<SmppGateway>) => {
|
||||
|
||||
/** Named wrappers for `deleteFetch` */
|
||||
|
||||
export const deleteUser = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_USERS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteServiceProvider = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_SERVICE_PROVIDERS}/${sid}`);
|
||||
};
|
||||
@@ -477,7 +481,6 @@ export const deleteAccountLimit = (sid: string, cat: LimitCategories) => {
|
||||
|
||||
/** Named wrappers for `getFetch` */
|
||||
|
||||
/** This is not a functioning API at the moment... */
|
||||
export const getUser = (sid: string) => {
|
||||
return getFetch<User>(`${API_USERS}/${sid}`);
|
||||
};
|
||||
|
||||
@@ -106,9 +106,16 @@ export interface PasswordSettings {
|
||||
/** API responses/payloads */
|
||||
|
||||
export interface User {
|
||||
scope: UserScopes;
|
||||
scope?: UserScopes;
|
||||
user_sid: string;
|
||||
permissions: UserPermissions[];
|
||||
name: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
force_change: boolean;
|
||||
account_sid?: string | null;
|
||||
service_provider_sid?: string | null;
|
||||
initial_password?: string;
|
||||
permissions?: UserPermissions;
|
||||
}
|
||||
|
||||
export interface UserLogin extends User {
|
||||
@@ -122,8 +129,15 @@ export interface UserLoginPayload {
|
||||
}
|
||||
|
||||
export interface UserUpdatePayload {
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
old_password?: string;
|
||||
new_password?: string;
|
||||
initial_password: string | null;
|
||||
email: string;
|
||||
name: string;
|
||||
force_change: boolean;
|
||||
is_active: boolean;
|
||||
service_provider_sid: string | null;
|
||||
account_sid: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceProvider {
|
||||
@@ -345,6 +359,10 @@ export interface SidResponse {
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface UserSidResponse {
|
||||
user_sid: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse extends SidResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Settings,
|
||||
FilePlus,
|
||||
Activity,
|
||||
UserCheck,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
RefreshCw,
|
||||
@@ -69,6 +70,7 @@ export const Icons: IconMap = {
|
||||
Settings,
|
||||
FilePlus,
|
||||
Activity,
|
||||
UserCheck,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
RefreshCw,
|
||||
|
||||
@@ -8,8 +8,6 @@ import { getHumanDateTime, hasLength } from "src/utils";
|
||||
|
||||
import type { ApiKey, TokenResponse } from "src/api/types";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type ApiKeyProps = {
|
||||
path: string;
|
||||
post: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_SETTINGS,
|
||||
ROUTE_INTERNAL_USERS,
|
||||
ROUTE_INTERNAL_ALERTS,
|
||||
ROUTE_INTERNAL_RECENT_CALLS,
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
@@ -22,6 +23,11 @@ export interface NaviItem {
|
||||
}
|
||||
|
||||
export const naviTop: NaviItem[] = [
|
||||
{
|
||||
label: "Users",
|
||||
icon: Icons.UserCheck,
|
||||
route: ROUTE_INTERNAL_USERS,
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
icon: Icons.Settings,
|
||||
|
||||
15
src/containers/internal/views/users/add.tsx
Normal file
15
src/containers/internal/views/users/add.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { H1 } from "jambonz-ui";
|
||||
|
||||
import { UserForm } from "./form";
|
||||
|
||||
export const AddUser = () => {
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Add new user</H1>
|
||||
<UserForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUser;
|
||||
26
src/containers/internal/views/users/delete.tsx
Normal file
26
src/containers/internal/views/users/delete.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { P } from "jambonz-ui";
|
||||
import { Modal } from "src/components";
|
||||
import type { User } from "src/api/types";
|
||||
|
||||
type DeleteProps = {
|
||||
user: User;
|
||||
handleCancel: () => void;
|
||||
handleSubmit: () => void;
|
||||
};
|
||||
|
||||
export const DeleteUser = ({
|
||||
user,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
}: DeleteProps) => {
|
||||
return (
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete the user <strong>{user.name}</strong>?
|
||||
</P>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteUser;
|
||||
29
src/containers/internal/views/users/edit.tsx
Normal file
29
src/containers/internal/views/users/edit.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { H1 } from "jambonz-ui";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { UserForm } from "./form";
|
||||
import { useApiData } from "src/api";
|
||||
import { User } from "src/api/types";
|
||||
import { toastError } from "src/store";
|
||||
|
||||
export const EditUser = () => {
|
||||
const params = useParams();
|
||||
const [data, refetch, error] = useApiData<User>(`Users/${params.user_sid}`);
|
||||
|
||||
/** Handle error toast at top level... */
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toastError(error.msg);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Edit user</H1>
|
||||
<UserForm user={{ data, refetch, error }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUser;
|
||||
326
src/containers/internal/views/users/form.tsx
Normal file
326
src/containers/internal/views/users/form.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, ButtonGroup, MS } from "jambonz-ui";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import {
|
||||
deleteUser,
|
||||
postFetch,
|
||||
putUser,
|
||||
useApiData,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
import { ROUTE_INTERNAL_USERS } from "src/router/routes";
|
||||
import { useAuth } from "src/router/auth";
|
||||
|
||||
import { ClipBoard, Section } from "src/components";
|
||||
import { DeleteUser } from "./delete";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import {
|
||||
API_USERS,
|
||||
DEFAULT_PSWD_SETTINGS,
|
||||
USER_SCOPE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
USER_ADMIN,
|
||||
USER_SP,
|
||||
} from "src/api/constants";
|
||||
import { isValidPasswd, getUserScope, hasLength } from "src/utils";
|
||||
|
||||
import type {
|
||||
UserSidResponse,
|
||||
User,
|
||||
PasswordSettings,
|
||||
UserScopes,
|
||||
UseApiDataMap,
|
||||
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>;
|
||||
};
|
||||
|
||||
export const UserForm = ({ user }: UserFormProps) => {
|
||||
const { signout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const currentUser = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [pwdSettings] =
|
||||
useApiData<PasswordSettings>("PasswordSettings") || DEFAULT_PSWD_SETTINGS;
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [initialPassword, setInitialPassword] = useState("");
|
||||
const [scope, setScope] = useState<UserScopes>();
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [forceChange, setForceChange] = useState(true);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
|
||||
const handleCancel = () => {
|
||||
setModal(false);
|
||||
};
|
||||
|
||||
const handleSelfDetete = () => {
|
||||
if (user?.data?.user_sid === currentUser?.user_sid) {
|
||||
signout();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (user && user.data) {
|
||||
deleteUser(user.data.user_sid)
|
||||
.then(() => {
|
||||
navigate(ROUTE_INTERNAL_USERS);
|
||||
toastSuccess(
|
||||
<>
|
||||
Deleted user <strong>{user?.data?.name}</strong>
|
||||
</>
|
||||
);
|
||||
handleSelfDetete();
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const passwdCheck = () => {
|
||||
if (pwdSettings && !isValidPasswd(initialPassword, pwdSettings)) {
|
||||
toastError("Invalid password.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (scope === USER_ACCOUNT && !accounts?.length) {
|
||||
toastError("Cannot create an account. Service Provider has no accounts.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
if (!passwdCheck()) return;
|
||||
|
||||
postFetch<UserSidResponse, Partial<User>>(API_USERS, {
|
||||
name: name,
|
||||
email: email,
|
||||
initial_password: initialPassword,
|
||||
force_change: forceChange,
|
||||
is_active: isActive,
|
||||
service_provider_sid:
|
||||
scope === USER_ADMIN && currentUser?.scope === USER_ADMIN
|
||||
? null
|
||||
: currentServiceProvider?.service_provider_sid,
|
||||
account_sid:
|
||||
scope !== USER_ACCOUNT && currentUser?.scope !== USER_ACCOUNT
|
||||
? null
|
||||
: accountSid,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
toastSuccess("User created successfully");
|
||||
navigate(`${ROUTE_INTERNAL_USERS}/${json.user_sid}/edit`);
|
||||
})
|
||||
.catch((error: { msg: IMessage }) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
|
||||
if (user && user.data) {
|
||||
if (initialPassword && !passwdCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
putUser(user.data.user_sid, {
|
||||
name: name || user.data.name,
|
||||
email: email || user.data.email,
|
||||
initial_password: initialPassword || null,
|
||||
force_change: forceChange || !!user.data.force_change,
|
||||
is_active: isActive || !!user.data.is_active,
|
||||
service_provider_sid:
|
||||
scope === USER_ADMIN && currentUser?.scope === USER_ADMIN
|
||||
? null
|
||||
: currentServiceProvider?.service_provider_sid,
|
||||
account_sid:
|
||||
scope !== USER_ACCOUNT && currentUser?.scope !== USER_ACCOUNT
|
||||
? null
|
||||
: accountSid,
|
||||
})
|
||||
.then(() => {
|
||||
user.refetch();
|
||||
toastSuccess("User updated successfully");
|
||||
navigate(ROUTE_INTERNAL_USERS);
|
||||
})
|
||||
.catch((error: { msg: IMessage }) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Set current user data values if applicable -- e.g. "edit mode" */
|
||||
useEffect(() => {
|
||||
if (user && user.data) {
|
||||
setName(user.data.name);
|
||||
setForceChange(!!user.data.force_change);
|
||||
setIsActive(!!user.data.is_active);
|
||||
setEmail(user.data.email);
|
||||
setScope(getUserScope(user.data));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
<fieldset>
|
||||
<MS>{MSG_REQUIRED_FIELDS}</MS>
|
||||
</fieldset>
|
||||
{currentUser?.scope !== USER_ACCOUNT && (
|
||||
<fieldset>
|
||||
<label htmlFor="scope">Scope:</label>
|
||||
<Selector
|
||||
id="scope"
|
||||
name="scope"
|
||||
value={scope}
|
||||
options={
|
||||
currentUser?.scope === USER_SP
|
||||
? USER_SCOPE_SELECTION.filter(
|
||||
(e) => e.value !== USER_ADMIN && e.value !== "all"
|
||||
)
|
||||
: USER_SCOPE_SELECTION.filter((e) => e.value !== "all")
|
||||
}
|
||||
onChange={(e) => setScope(e.target.value as UserScopes)}
|
||||
/>
|
||||
|
||||
{hasLength(accounts) && scope === USER_ACCOUNT && (
|
||||
<>
|
||||
<AccountSelect
|
||||
accounts={accounts}
|
||||
account={[accountSid, setAccountSid]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{scope !== USER_ACCOUNT && !hasLength(accounts) && (
|
||||
<>
|
||||
<label htmlFor="account">
|
||||
Account:<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="account"
|
||||
required
|
||||
type="text"
|
||||
disabled
|
||||
name="account"
|
||||
value="No accounts."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
{user && user.data && (
|
||||
<fieldset>
|
||||
<label htmlFor="user_sid">User SID</label>
|
||||
<ClipBoard
|
||||
id="user_sid"
|
||||
name="user_sid"
|
||||
text={user.data.user_sid}
|
||||
/>
|
||||
<label htmlFor="is_active" className="chk">
|
||||
<input
|
||||
id="is_active"
|
||||
name="is_active"
|
||||
type="checkbox"
|
||||
checked={isActive}
|
||||
onChange={(e) => setIsActive(e.target.checked)}
|
||||
/>
|
||||
<div>User is active</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
<fieldset>
|
||||
<label htmlFor="name">
|
||||
User name<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
required
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="User Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="email">
|
||||
User email<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
required
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="User Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="initial_password">
|
||||
Temporary password
|
||||
{!user && <span>*</span>}
|
||||
</label>
|
||||
<Passwd
|
||||
id="initial_password"
|
||||
required={!user}
|
||||
name="initial_password"
|
||||
value={initialPassword}
|
||||
placeholder="Temporary password"
|
||||
setValue={setInitialPassword}
|
||||
/>
|
||||
<label htmlFor="force_change" className="chk">
|
||||
<input
|
||||
id="force_change"
|
||||
name="force_change"
|
||||
type="checkbox"
|
||||
checked={forceChange}
|
||||
onChange={(e) => setForceChange(e.target.checked)}
|
||||
/>
|
||||
<div>Force change of password</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<ButtonGroup left className={user && "btns--spaced"}>
|
||||
<Button small subStyle="grey" as={Link} to={ROUTE_INTERNAL_USERS}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" small>
|
||||
Save
|
||||
</Button>
|
||||
{user && user.data && (
|
||||
<Button
|
||||
small
|
||||
type="button"
|
||||
subStyle="grey"
|
||||
onClick={() => setModal(true)}
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Section>
|
||||
{user && user.data && modal && (
|
||||
<DeleteUser
|
||||
user={user.data}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
134
src/containers/internal/views/users/index.tsx
Normal file
134
src/containers/internal/views/users/index.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { H1, Button, Icon } from "jambonz-ui";
|
||||
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 {
|
||||
Section,
|
||||
Icons,
|
||||
Spinner,
|
||||
SearchFilter,
|
||||
AccountFilter,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import { hasLength, hasValue, useFilteredResults } from "src/utils";
|
||||
|
||||
import type { Account, User } from "src/api/types";
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
export const Users = () => {
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [users] = useApiData<User[]>("Users");
|
||||
const [filter, setFilter] = useState("");
|
||||
const [scopeFilter, setScopeFilter] = useState("all");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
|
||||
const usersFiltered = useMemo(() => {
|
||||
const serviceProviderUsers = users?.filter((e) => {
|
||||
return (
|
||||
e.scope === USER_ADMIN ||
|
||||
e.service_provider_sid === currentServiceProvider?.service_provider_sid
|
||||
);
|
||||
});
|
||||
|
||||
if (scopeFilter === "all" && !accountSid) {
|
||||
return serviceProviderUsers;
|
||||
}
|
||||
|
||||
if (scopeFilter !== "all" && !accountSid) {
|
||||
return serviceProviderUsers?.filter((e) => e.scope === scopeFilter);
|
||||
}
|
||||
|
||||
if (scopeFilter !== "all" && accountSid) {
|
||||
return serviceProviderUsers?.filter(
|
||||
(e) => e.scope === scopeFilter && accountSid === e.account_sid
|
||||
);
|
||||
}
|
||||
|
||||
if (scopeFilter === "all" && accountSid) {
|
||||
return serviceProviderUsers?.filter((e) => e.account_sid === accountSid);
|
||||
}
|
||||
return [];
|
||||
}, [accountSid, scopeFilter, users, accounts, currentServiceProvider]);
|
||||
|
||||
const filteredUsers = useFilteredResults<User>(filter, usersFiltered);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
<H1 className="h2">Users</H1>
|
||||
<Link to={`${ROUTE_INTERNAL_USERS}/add`} title="Add user">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="filters filters--mix">
|
||||
<section>
|
||||
<SearchFilter
|
||||
placeholder="Filter 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}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Section slim>
|
||||
<div className="grid grid--col4">
|
||||
<div className="grid__row grid__th">
|
||||
<div>User Name</div>
|
||||
<div>Email</div>
|
||||
<div>Scope</div>
|
||||
<div> </div>
|
||||
</div>
|
||||
{!hasValue(users) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredUsers) ? (
|
||||
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 className="item__actions">
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_USERS}/${user.user_sid}/edit`}
|
||||
title="Edit user"
|
||||
>
|
||||
<Icons.Edit3 />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="grid__row grid__empty">
|
||||
<div>No users.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
<Section clean>
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_USERS}/add`}>
|
||||
Add user
|
||||
</Button>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
@@ -3,8 +3,8 @@ import { Button, H1, M } from "jambonz-ui";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { isValidPasswd } from "src/utils";
|
||||
import { putUser } from "src/api";
|
||||
import { StatusCodes } from "src/api/types";
|
||||
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 {
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import type { IMessage } from "src/store/types";
|
||||
|
||||
export const CreatePassword = () => {
|
||||
const [passwdSettings] = useApiData<PasswordSettings>("PasswordSettings");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [message, setMessage] = useState<IMessage>("");
|
||||
@@ -42,7 +43,7 @@ export const CreatePassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidPasswd(password)) {
|
||||
if (passwdSettings && !isValidPasswd(password, passwdSettings)) {
|
||||
setMessage(MSG_PASSWD_CRITERIA);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import { NotFound } from "src/containers/notfound";
|
||||
import CreatePassword from "src/containers/login/create-password";
|
||||
|
||||
/** Top navi */
|
||||
import Users from "src/containers/internal/views/users";
|
||||
import UserAdd from "src/containers/internal/views/users/add";
|
||||
import UserEdit from "src/containers/internal/views/users/edit";
|
||||
import Settings from "src/containers/internal/views/settings";
|
||||
import Accounts from "src/containers/internal/views/accounts";
|
||||
import AccountAdd from "src/containers/internal/views/accounts/add";
|
||||
@@ -66,6 +69,9 @@ export const Router = () => {
|
||||
<Routes>
|
||||
<Route path="*" element={<InternalLayout />}>
|
||||
{/* Top navi */}
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="users/add" element={<UserAdd />} />
|
||||
<Route path="users/:user_sid/edit" element={<UserEdit />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="accounts" element={<Accounts />} />
|
||||
<Route path="accounts/add" element={<AccountAdd />} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const ROUTE_LOGIN = "/";
|
||||
export const ROUTE_CREATE_PASSWORD = "/create-password";
|
||||
export const ROUTE_INTERNAL_USERS = "/internal/users";
|
||||
export const ROUTE_INTERNAL_SETTINGS = "/internal/settings";
|
||||
export const ROUTE_INTERNAL_ACCOUNTS = "/internal/accounts";
|
||||
export const ROUTE_INTERNAL_APPLICATIONS = "/internal/applications";
|
||||
|
||||
@@ -63,8 +63,7 @@ export const currentServiceProviderAction = (
|
||||
|
||||
export const userAsyncAction = async (): Promise<User> => {
|
||||
const token = getToken();
|
||||
const { user_sid, permissions, scope } = parseJwt(token);
|
||||
return { user_sid, permissions, scope };
|
||||
return parseJwt(token);
|
||||
};
|
||||
|
||||
export const serviceProvidersAsyncAction = async (): Promise<
|
||||
|
||||
@@ -30,6 +30,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--mix {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
grid-gap: ui-vars.$px02;
|
||||
justify-content: flex-end;
|
||||
|
||||
> :nth-child(2) {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
+ * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="password"] {
|
||||
@include ui-mixins.m();
|
||||
@@ -154,6 +155,7 @@ fieldset {
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
|
||||
@@ -98,4 +98,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--col4 {
|
||||
.grid__row {
|
||||
grid-template-columns: [col] 37% [col] 35% [col] 20% [col] 3%;
|
||||
grid-template-rows: [row] auto [row] auto [row] [row] auto;
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
@use "./smsel";
|
||||
@use "./inpbtn";
|
||||
@use "./filters";
|
||||
@use "./grid";
|
||||
@use "jambonz-ui/src/styles/index";
|
||||
|
||||
@use "./vars";
|
||||
@@ -113,6 +114,12 @@ details {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.btns--spaced {
|
||||
> :last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/** 404 page (login & internal) */
|
||||
.notfound {
|
||||
.btn {
|
||||
|
||||
@@ -10,9 +10,12 @@ import {
|
||||
INVALID,
|
||||
IP,
|
||||
TCP_MAX_PORT,
|
||||
USER_ACCOUNT,
|
||||
USER_ADMIN,
|
||||
USER_SP,
|
||||
} from "src/api/constants";
|
||||
|
||||
import type { IpType } from "src/api/types";
|
||||
import type { IpType, PasswordSettings, User, UserScopes } from "src/api/types";
|
||||
|
||||
export const hasValue = <Type>(
|
||||
variable: Type | null | undefined
|
||||
@@ -32,10 +35,20 @@ export const isObject = (obj: unknown) => {
|
||||
return typeof obj === "object" && hasValue(obj) && !Array.isArray(obj);
|
||||
};
|
||||
|
||||
export const isValidPasswd = (password: string) => {
|
||||
return (
|
||||
password.length >= 6 && /\d/.test(password) && /[a-zA-Z]/.test(password)
|
||||
);
|
||||
export const isValidPasswd = (
|
||||
password: string,
|
||||
passwordSettings: PasswordSettings
|
||||
) => {
|
||||
if (passwordSettings) {
|
||||
return (
|
||||
password.length >= passwordSettings?.min_password_length &&
|
||||
(passwordSettings?.require_digit ? /\d/.test(password) : true) &&
|
||||
(passwordSettings?.require_special_character
|
||||
? /[!@#$%^&*(),.?"';:{}|<>+~]/.test(password)
|
||||
: true)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isValidPort = (port: number) => {
|
||||
@@ -125,6 +138,16 @@ export const sortLocaleName = (
|
||||
b: Required<{ name: string }>
|
||||
) => a.name.localeCompare(b.name);
|
||||
|
||||
export const getUserScope = (user: User): UserScopes => {
|
||||
if (user.account_sid) {
|
||||
return USER_ACCOUNT;
|
||||
} else if (user.service_provider_sid) {
|
||||
return USER_SP;
|
||||
} else {
|
||||
return USER_ADMIN;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
withSuspense,
|
||||
useMobileMedia,
|
||||
|
||||
Reference in New Issue
Block a user