mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-10-04 02:04:17 +00:00
apply review comments
This commit is contained in:
@@ -44,7 +44,6 @@
|
|||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"jambonz-ui": "^0.0.19",
|
"jambonz-ui": "^0.0.19",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-blockies": "^1.4.1",
|
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-router-dom": "^6.3.0"
|
"react-router-dom": "^6.3.0"
|
||||||
@@ -54,7 +53,6 @@
|
|||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/node": "^18.6.1",
|
"@types/node": "^18.6.1",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-blockies": "^1.4.1",
|
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||||
"@typescript-eslint/parser": "^5.30.6",
|
"@typescript-eslint/parser": "^5.30.6",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Vendor } from "src/vendor/types";
|
import type { Vendor } from "src/vendor/types";
|
||||||
import type { Scope } from "src/store/types";
|
|
||||||
|
|
||||||
/** Simple types */
|
/** Simple types */
|
||||||
|
|
||||||
@@ -117,7 +116,6 @@ export interface User {
|
|||||||
service_provider_sid?: string | null;
|
service_provider_sid?: string | null;
|
||||||
initial_password?: string;
|
initial_password?: string;
|
||||||
permissions?: UserPermissions;
|
permissions?: UserPermissions;
|
||||||
enumScope: Scope;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserLogin extends User {
|
export interface UserLogin extends User {
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { H1, H2 } from "jambonz-ui";
|
import { H1 } from "jambonz-ui";
|
||||||
|
|
||||||
import { TestProvider } from "src/test";
|
import { TestProvider } from "src/test";
|
||||||
import { ScopedAccess } from "./scoped-access";
|
import { ScopedAccess } from "./scoped-access";
|
||||||
|
|
||||||
import type { ScopedAccessProps } from "./scoped-access";
|
import type { ScopedAccessProps } from "./scoped-access";
|
||||||
|
import { Scope } from "src/store/types";
|
||||||
|
|
||||||
/** Wrapper to pass different user scopes as enum values */
|
/** Wrapper to pass different user scopes as enum values */
|
||||||
const ScopedAccessTestWrapper = (props: Partial<ScopedAccessProps>) => {
|
const ScopedAccessTestWrapper = (props: Partial<ScopedAccessProps>) => {
|
||||||
return (
|
return (
|
||||||
<TestProvider>
|
<TestProvider>
|
||||||
<ScopedAccess
|
<ScopedAccess scope={props.scope!}>
|
||||||
enumScope={props.enumScope!}
|
|
||||||
noAccessRender={<H2>{"noAccessRender"}</H2>}
|
|
||||||
>
|
|
||||||
<div className="scope-div">
|
<div className="scope-div">
|
||||||
<H1>ScopedAccess: {props.enumScope}</H1>
|
<H1>ScopedAccess: {props.scope}</H1>
|
||||||
</div>
|
</div>
|
||||||
</ScopedAccess>
|
</ScopedAccess>
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
@@ -24,20 +22,20 @@ const ScopedAccessTestWrapper = (props: Partial<ScopedAccessProps>) => {
|
|||||||
|
|
||||||
describe("<ScopedAccess>", () => {
|
describe("<ScopedAccess>", () => {
|
||||||
it("has sufficient scope - admin", () => {
|
it("has sufficient scope - admin", () => {
|
||||||
cy.mount(<ScopedAccessTestWrapper enumScope={0} />);
|
cy.mount(<ScopedAccessTestWrapper scope={Scope.admin} />);
|
||||||
cy.get(".scope-div").should("exist");
|
cy.get(".scope-div").should("exist");
|
||||||
cy.get("h1").should("exist");
|
cy.get("h1").should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has insufficient scope - service_provider", () => {
|
it("has insufficient scope - service_provider", () => {
|
||||||
cy.mount(<ScopedAccessTestWrapper enumScope={1} />);
|
cy.mount(<ScopedAccessTestWrapper scope={Scope.service_provider} />);
|
||||||
cy.get(".scope-div").should("not.exist");
|
cy.get(".scope-div").should("not.exist");
|
||||||
cy.get("h2").should("exist");
|
cy.get("h1").should("not.exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has insufficient scope - account", () => {
|
it("has insufficient scope - account", () => {
|
||||||
cy.mount(<ScopedAccessTestWrapper enumScope={2} />);
|
cy.mount(<ScopedAccessTestWrapper scope={Scope.account} />);
|
||||||
cy.get(".scope-div").should("not.exist");
|
cy.get(".scope-div").should("not.exist");
|
||||||
cy.get("h2").should("exist");
|
cy.get("h1").should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,25 +5,16 @@ import { useSelectState } from "src/store";
|
|||||||
import type { Scope } from "src/store/types";
|
import type { Scope } from "src/store/types";
|
||||||
|
|
||||||
export type ScopedAccessProps = {
|
export type ScopedAccessProps = {
|
||||||
enumScope: Scope;
|
scope: Scope;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
noAccessRender?: React.ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScopedAccess = ({
|
export const ScopedAccess = ({ scope, children }: ScopedAccessProps) => {
|
||||||
enumScope,
|
|
||||||
children,
|
|
||||||
noAccessRender,
|
|
||||||
}: ScopedAccessProps) => {
|
|
||||||
const user = useSelectState("user");
|
const user = useSelectState("user");
|
||||||
|
|
||||||
if (user && user.enumScope <= enumScope) {
|
if (user && user.scopeAccess >= scope) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user && user.enumScope >= enumScope && noAccessRender) {
|
|
||||||
return <>{noAccessRender}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import Blockies from "react-blockies";
|
import { classNames, M, Icon, Button } from "jambonz-ui";
|
||||||
import { classNames, getCssVar, M, Icon, Button } from "jambonz-ui";
|
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { Icons, ModalForm, AccessControl } from "src/components";
|
import { Icons, ModalForm } from "src/components";
|
||||||
import { naviTop, naviByo } from "./items";
|
import { naviTop, naviByo } from "./items";
|
||||||
import {
|
import {
|
||||||
useSelectState,
|
useSelectState,
|
||||||
@@ -17,6 +16,7 @@ import type { NaviItem } from "./items";
|
|||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
import { ScopedAccess } from "src/components/scoped-access";
|
import { ScopedAccess } from "src/components/scoped-access";
|
||||||
|
import { Scope } from "src/store/types";
|
||||||
|
|
||||||
type CommonProps = {
|
type CommonProps = {
|
||||||
handleMenu: () => void;
|
handleMenu: () => void;
|
||||||
@@ -57,7 +57,6 @@ export const Navi = ({
|
|||||||
handleLogout,
|
handleLogout,
|
||||||
}: NaviProps) => {
|
}: NaviProps) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const user = useSelectState("user");
|
|
||||||
const accessControl = useSelectState("accessControl");
|
const accessControl = useSelectState("accessControl");
|
||||||
const serviceProviders = useSelectState("serviceProviders");
|
const serviceProviders = useSelectState("serviceProviders");
|
||||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||||
@@ -150,7 +149,7 @@ export const Navi = ({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<option>currentServiceProvider.name</option>
|
<option> </option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<span>
|
<span>
|
||||||
@@ -159,7 +158,7 @@ export const Navi = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScopedAccess enumScope={0}>
|
<ScopedAccess scope={Scope.admin}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModal(true)}
|
onClick={() => setModal(true)}
|
||||||
@@ -170,26 +169,6 @@ export const Navi = ({
|
|||||||
</button>
|
</button>
|
||||||
</ScopedAccess>
|
</ScopedAccess>
|
||||||
</div>
|
</div>
|
||||||
{/* Intentionally hiding this as we will need to work this out as we go... */}
|
|
||||||
{/* ACL component will need to be updated for new user scope/permissions handling */}
|
|
||||||
{false && user && (
|
|
||||||
<div className="navi__user">
|
|
||||||
<Blockies
|
|
||||||
seed={user!.user_sid}
|
|
||||||
size={6}
|
|
||||||
scale={6}
|
|
||||||
color={getCssVar("--jambonz")}
|
|
||||||
bgColor={getCssVar("--pink")}
|
|
||||||
spotColor={getCssVar("--teal")}
|
|
||||||
className="avatar"
|
|
||||||
/>
|
|
||||||
<AccessControl acl="hasAdminAuth">
|
|
||||||
<button type="button" className="btnty adduser" title="Add user">
|
|
||||||
<Icons.PlusCircle />
|
|
||||||
</button>
|
|
||||||
</AccessControl>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="navi__routes">
|
<div className="navi__routes">
|
||||||
<ul>
|
<ul>
|
||||||
{naviTop.map((item) => (
|
{naviTop.map((item) => (
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { getServiceProviders } from "src/api";
|
|||||||
import { sortLocaleName } from "src/utils";
|
import { sortLocaleName } from "src/utils";
|
||||||
import { getToken, parseJwt } from "src/router/auth";
|
import { getToken, parseJwt } from "src/router/auth";
|
||||||
|
|
||||||
import type { State, Action } from "./types";
|
import type { State, Action, ScopeAccess } from "./types";
|
||||||
import { Scope } from "./types";
|
import { Scope } from "./types";
|
||||||
import { ServiceProvider, User } from "src/api/types";
|
import { ServiceProvider } from "src/api/types";
|
||||||
|
|
||||||
/** A generic action assumes action.type is ALWAYS our state key */
|
/** A generic action assumes action.type is ALWAYS our state key */
|
||||||
/** Since this is how we're designing our state interface we cool */
|
/** Since this is how we're designing our state interface we cool */
|
||||||
@@ -62,10 +62,11 @@ export const currentServiceProviderAction = (
|
|||||||
return genericAction(state, action);
|
return genericAction(state, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAsyncAction = async (): Promise<User> => {
|
export const userAsyncAction = async (): Promise<ScopeAccess> => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const userData = parseJwt(token);
|
const userData = parseJwt(token);
|
||||||
return { ...userData, enumScope: Scope[userData.scope] };
|
|
||||||
|
return { ...userData, scopeAccess: Scope[userData.scope] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serviceProvidersAsyncAction = async (): Promise<
|
export const serviceProvidersAsyncAction = async (): Promise<
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ const middleware: MiddleWare = (dispatch) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const StateContext = React.createContext<AppStateContext>(null!);
|
export const StateContext = React.createContext<AppStateContext>(null!);
|
||||||
|
|
||||||
/** This will let us make a hook so dispatch is accessible anywhere */
|
/** This will let us make a hook so dispatch is accessible anywhere */
|
||||||
let globalDispatch: GlobalDispatch;
|
let globalDispatch: GlobalDispatch;
|
||||||
|
|||||||
+25
-8
@@ -1,6 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { User, ServiceProvider } from "src/api/types";
|
import type {
|
||||||
|
ServiceProvider,
|
||||||
|
UserScopes,
|
||||||
|
UserPermissions,
|
||||||
|
} from "src/api/types";
|
||||||
|
|
||||||
export type IMessage = string | JSX.Element;
|
export type IMessage = string | JSX.Element;
|
||||||
|
|
||||||
@@ -14,19 +18,32 @@ export interface ACL {
|
|||||||
hasMSTeamsFqdn: boolean;
|
hasMSTeamsFqdn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Scope {
|
|
||||||
"admin" = 0,
|
|
||||||
"service_provider" = 1,
|
|
||||||
"account" = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FeatureFlag {
|
export interface FeatureFlag {
|
||||||
development: boolean;
|
development: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Scope {
|
||||||
|
"admin" = 2,
|
||||||
|
"service_provider" = 1,
|
||||||
|
"account" = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserJWT {
|
||||||
|
scope: UserScopes;
|
||||||
|
user_sid: string;
|
||||||
|
force_change: boolean;
|
||||||
|
account_sid?: string | null;
|
||||||
|
service_provider_sid?: string | null;
|
||||||
|
permissions?: UserPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScopeAccess extends UserJWT {
|
||||||
|
scopeAccess: Scope;
|
||||||
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
/** logged in user */
|
/** logged in user */
|
||||||
user?: User;
|
user?: ScopeAccess;
|
||||||
/** global toast notifications */
|
/** global toast notifications */
|
||||||
toast?: Toast;
|
toast?: Toast;
|
||||||
/** feature flags from vite ENV */
|
/** feature flags from vite ENV */
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["@types/react", "@types/react-dom", "@types/react-blockies"],
|
"types": ["@types/react", "@types/react-dom"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
|||||||
Reference in New Issue
Block a user