mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-19 05:37:43 +00:00
ScopeAccess component implementation (#154)
* component based on enumScope
* apply review comments
* add store props
* Revert "add store props"
This reverts commit 0e0978c5f3.
* Tests for ScopedAccess (#156)
* Tests for ScopedAccess
* Create cypress mountTestProvider
Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Brandon Lee Kitajchuk <bk@kitajchuk.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3NpZCI6IjFjNTc4MWQyLTY5MGItNDIwYy1iZDUzLTVkN2Y1NjMwMDVjOCIsInNjb3BlIjoiYWRtaW4iLCJmb3JjZV9jaGFuZ2UiOnRydWUsInBlcm1pc3Npb25zIjpbIlBST1ZJU0lPTl9VU0VSUyIsIlBST1ZJU0lPTl9TRVJWSUNFUyIsIlZJRVdfT05MWSJdLCJpYXQiOjE2NjY3OTgzMTEsImV4cCI6MTY2NjgwMTkxMX0.ZV3KnRit8WGpipfiiMAZ2AVLQ25csWje1-K6hdqxktE",
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3NpZCI6IjFjNTc4MWQyLTY5MGItNDIwYy1iZDUzLTVkN2Y1NjMwMDVjOCIsInNjb3BlIjoiYWRtaW4iLCJmb3JjZV9jaGFuZ2UiOnRydWUsInBlcm1pc3Npb25zIjpbIlBST1ZJU0lPTl9VU0VSUyIsIlBST1ZJU0lPTl9TRVJWSUNFUyIsIlZJRVdfT05MWSJdLCJpYXQiOjE2NjY3OTgzMTEsImV4cCI6MTY2NjgwMTkxMX0.ZV3KnRit8WGpipfiiMAZ2AVLQ25csWje1-K6hdqxktE",
|
||||||
"user_sid": "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
"user_sid": "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
||||||
"force_change": false
|
"force_change": false,
|
||||||
|
"scope": "admin",
|
||||||
|
"permissions": ["VIEW_ONLY", "PROVISION_SERVICES", "PROVISION_USERS"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,23 +19,47 @@ import "src/styles/index.scss";
|
|||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import "./commands";
|
import "./commands";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "cypress/react18";
|
import { mount } from "cypress/react18";
|
||||||
|
|
||||||
|
import { TestProvider } from "src/test";
|
||||||
|
|
||||||
|
import type { TestProviderProps } from "src/test";
|
||||||
|
import type { MountOptions, MountReturn } from "cypress/react";
|
||||||
|
|
||||||
// Augment the Cypress namespace to include type definitions for
|
// Augment the Cypress namespace to include type definitions for
|
||||||
// your custom command.
|
// your custom command.
|
||||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||||
// with a <reference path="./component" /> at the top of your spec.
|
|
||||||
declare global {
|
declare global {
|
||||||
// Disabling, but: https://typescript-eslint.io/rules/no-namespace/...
|
// Disabling, but: https://typescript-eslint.io/rules/no-namespace/...
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
mount: typeof mount;
|
mount: typeof mount;
|
||||||
|
/**
|
||||||
|
* Mounts a React node
|
||||||
|
* @param component React Node to mount
|
||||||
|
* @param options Additional options to pass into mount
|
||||||
|
*/
|
||||||
|
mountTestProvider(
|
||||||
|
component: React.ReactNode,
|
||||||
|
options?: MountOptions & { authProps?: TestProviderProps["authProps"] }
|
||||||
|
): Cypress.Chainable<MountReturn>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("mount", mount);
|
Cypress.Commands.add("mount", mount);
|
||||||
|
|
||||||
|
// This gives us access to dispatch inside of our test wrappers...
|
||||||
|
Cypress.Commands.add("mountTestProvider", (component, options = {}) => {
|
||||||
|
const { authProps, ...mountOptions } = options;
|
||||||
|
const wrapper = (
|
||||||
|
<TestProvider authProps={authProps}>{component}</TestProvider>
|
||||||
|
);
|
||||||
|
return mount(wrapper, mountOptions);
|
||||||
|
});
|
||||||
|
|
||||||
// Example use:
|
// Example use:
|
||||||
// cy.mount(<MyComponent />)
|
// cy.mount(<MyComponent />)
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ export type UserPermissions =
|
|||||||
| "PROVISION_SERVICES"
|
| "PROVISION_SERVICES"
|
||||||
| "PROVISION_USERS";
|
| "PROVISION_USERS";
|
||||||
|
|
||||||
// We'll want something like this for actual permissions implementation...
|
|
||||||
// export enum UserPermissions {
|
|
||||||
// VIEW_ONLY = 0,
|
|
||||||
// PROVISION_SERVICES = 1,
|
|
||||||
// PROVISION_USERS = 2,
|
|
||||||
// }
|
|
||||||
|
|
||||||
/** Status codes */
|
/** Status codes */
|
||||||
|
|
||||||
export enum StatusCodes {
|
export enum StatusCodes {
|
||||||
@@ -106,21 +99,24 @@ export interface PasswordSettings {
|
|||||||
/** API responses/payloads */
|
/** API responses/payloads */
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
scope?: UserScopes;
|
scope: UserScopes;
|
||||||
user_sid: string;
|
user_sid: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
force_change: boolean;
|
force_change: boolean;
|
||||||
account_sid?: string | null;
|
account_sid: string | null;
|
||||||
service_provider_sid?: string | null;
|
service_provider_sid: string | null;
|
||||||
initial_password?: string;
|
initial_password?: string;
|
||||||
permissions?: UserPermissions;
|
permissions?: UserPermissions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserLogin extends User {
|
export interface UserLogin {
|
||||||
token: string;
|
token: string;
|
||||||
force_change: boolean;
|
force_change: boolean;
|
||||||
|
scope: UserScopes;
|
||||||
|
user_sid: string;
|
||||||
|
permissions: UserPermissions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserLoginPayload {
|
export interface UserLoginPayload {
|
||||||
@@ -140,6 +136,14 @@ export interface UserUpdatePayload {
|
|||||||
account_sid: string | null;
|
account_sid: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserJWT {
|
||||||
|
scope: UserScopes;
|
||||||
|
user_sid: string;
|
||||||
|
account_sid?: string | null;
|
||||||
|
service_provider_sid?: string | null;
|
||||||
|
permissions: UserPermissions[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServiceProvider {
|
export interface ServiceProvider {
|
||||||
name: string;
|
name: string;
|
||||||
ms_teams_fqdn: null | string;
|
ms_teams_fqdn: null | string;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { H1 } from "jambonz-ui";
|
import { H1 } from "jambonz-ui";
|
||||||
|
|
||||||
import { TestProvider } from "src/test";
|
|
||||||
import { AccessControl } from "./access-control";
|
import { AccessControl } from "./access-control";
|
||||||
|
|
||||||
import type { ACLProps } from "./access-control";
|
import type { ACLProps } from "./access-control";
|
||||||
@@ -9,30 +8,28 @@ import type { ACLProps } from "./access-control";
|
|||||||
/** Wrapper to pass different ACLs */
|
/** Wrapper to pass different ACLs */
|
||||||
const AccessControlTestWrapper = (props: Partial<ACLProps>) => {
|
const AccessControlTestWrapper = (props: Partial<ACLProps>) => {
|
||||||
return (
|
return (
|
||||||
<TestProvider>
|
|
||||||
<AccessControl acl={props.acl!}>
|
<AccessControl acl={props.acl!}>
|
||||||
<div className="acl-div">
|
<div className="acl-div">
|
||||||
<H1>ACL: {props.acl}</H1>
|
<H1>ACL: {props.acl}</H1>
|
||||||
</div>
|
</div>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
</TestProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<AccessControl>", () => {
|
describe("<AccessControl>", () => {
|
||||||
it("mounts", () => {
|
it("mounts", () => {
|
||||||
cy.mount(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
cy.mountTestProvider(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't have teams fqdn ACL", () => {
|
it("doesn't have teams fqdn ACL", () => {
|
||||||
cy.mount(<AccessControlTestWrapper acl="hasMSTeamsFqdn" />);
|
cy.mountTestProvider(<AccessControlTestWrapper acl="hasMSTeamsFqdn" />);
|
||||||
|
|
||||||
/** Default ACL disables MS Teams FQDN */
|
/** Default ACL disables MS Teams FQDN */
|
||||||
cy.get(".acl-div").should("not.exist");
|
cy.get(".acl-div").should("not.exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has admin ACL", () => {
|
it("has admin ACL", () => {
|
||||||
cy.mount(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
cy.mountTestProvider(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
||||||
|
|
||||||
/** Default ACL applies admin auth -- the singleton admin user */
|
/** Default ACL applies admin auth -- the singleton admin user */
|
||||||
cy.get(".acl-div").should("exist");
|
cy.get(".acl-div").should("exist");
|
||||||
|
|||||||
@@ -1,37 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { H1 } from "jambonz-ui";
|
import { H1 } from "jambonz-ui";
|
||||||
|
|
||||||
import { TestProvider } from "src/test";
|
|
||||||
import { RequireAuth } from "./require-auth";
|
import { RequireAuth } from "./require-auth";
|
||||||
|
|
||||||
import type { AuthStateContext } from "src/router/auth";
|
|
||||||
|
|
||||||
/** Wrapper to pass different auth contexts */
|
/** Wrapper to pass different auth contexts */
|
||||||
const RequireAuthTestWrapper = (props: Partial<AuthStateContext>) => {
|
const RequireAuthTestWrapper = () => {
|
||||||
return (
|
return (
|
||||||
<TestProvider {...(props as AuthStateContext)}>
|
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<div className="auth-div">
|
<div className="auth-div">
|
||||||
<H1>Protected Route</H1>
|
<H1>Protected Route</H1>
|
||||||
</div>
|
</div>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
</TestProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<RequireAuth>", () => {
|
describe("<RequireAuth>", () => {
|
||||||
it("mounts", () => {
|
it("mounts", () => {
|
||||||
cy.mount(<RequireAuthTestWrapper />);
|
cy.mountTestProvider(<RequireAuthTestWrapper />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is not authorized", () => {
|
it("is not authorized", () => {
|
||||||
cy.mount(<RequireAuthTestWrapper />);
|
cy.mountTestProvider(<RequireAuthTestWrapper />);
|
||||||
|
|
||||||
cy.get(".auth-div").should("not.exist");
|
cy.get(".auth-div").should("not.exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is authorized", () => {
|
it("is authorized", () => {
|
||||||
cy.mount(<RequireAuthTestWrapper authorized />);
|
cy.mountTestProvider(<RequireAuthTestWrapper />, {
|
||||||
|
authProps: { authorized: true },
|
||||||
|
});
|
||||||
|
|
||||||
cy.get(".auth-div").should("exist");
|
cy.get(".auth-div").should("exist");
|
||||||
});
|
});
|
||||||
|
|||||||
66
src/components/scoped-access.cy.tsx
Normal file
66
src/components/scoped-access.cy.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { H1 } from "jambonz-ui";
|
||||||
|
|
||||||
|
import { ScopedAccess } from "./scoped-access";
|
||||||
|
import { USER_SP, USER_ADMIN, USER_ACCOUNT } from "src/api/constants";
|
||||||
|
|
||||||
|
import type { ScopedAccessProps } from "./scoped-access";
|
||||||
|
import type { UserData } from "src/store/types";
|
||||||
|
|
||||||
|
import { Scope } from "src/store/types";
|
||||||
|
|
||||||
|
/** Wrapper to pass different user scopes as enum values */
|
||||||
|
const ScopedAccessTestWrapper = (props: ScopedAccessProps) => {
|
||||||
|
return (
|
||||||
|
<ScopedAccess {...props}>
|
||||||
|
<div className="scope-div">{props.children}</div>
|
||||||
|
</ScopedAccess>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("<ScopedAccess>", () => {
|
||||||
|
it("has sufficient scope - admin", () => {
|
||||||
|
const user = {
|
||||||
|
user_sid: "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
||||||
|
scope: USER_ADMIN,
|
||||||
|
access: Scope.admin,
|
||||||
|
} as UserData;
|
||||||
|
|
||||||
|
cy.mountTestProvider(
|
||||||
|
<ScopedAccessTestWrapper scope={Scope.admin} user={user}>
|
||||||
|
<H1>ScopedAccess: admin</H1>
|
||||||
|
</ScopedAccessTestWrapper>
|
||||||
|
);
|
||||||
|
cy.get(".scope-div").should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has insufficient scope - service_provider", () => {
|
||||||
|
const user = {
|
||||||
|
user_sid: "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
||||||
|
scope: USER_SP,
|
||||||
|
access: Scope.service_provider,
|
||||||
|
} as UserData;
|
||||||
|
|
||||||
|
cy.mountTestProvider(
|
||||||
|
<ScopedAccessTestWrapper scope={Scope.admin} user={user}>
|
||||||
|
<H1>ScopedAccess: service_provider</H1>
|
||||||
|
</ScopedAccessTestWrapper>
|
||||||
|
);
|
||||||
|
cy.get(".scope-div").should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has insufficient scope - account", () => {
|
||||||
|
const user = {
|
||||||
|
user_sid: "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
||||||
|
scope: USER_ACCOUNT,
|
||||||
|
access: Scope.account,
|
||||||
|
} as UserData;
|
||||||
|
|
||||||
|
cy.mountTestProvider(
|
||||||
|
<ScopedAccessTestWrapper scope={Scope.admin} user={user}>
|
||||||
|
<H1>ScopedAccess: account</H1>
|
||||||
|
</ScopedAccessTestWrapper>
|
||||||
|
);
|
||||||
|
cy.get(".scope-div").should("not.exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/components/scoped-access.tsx
Normal file
17
src/components/scoped-access.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { UserData, Scope } from "src/store/types";
|
||||||
|
|
||||||
|
export type ScopedAccessProps = {
|
||||||
|
user?: UserData;
|
||||||
|
scope: Scope;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScopedAccess = ({ user, scope, children }: ScopedAccessProps) => {
|
||||||
|
if (user && user.access >= scope) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -16,6 +15,8 @@ import { postServiceProviders } from "src/api";
|
|||||||
import type { NaviItem } from "./items";
|
import type { NaviItem } from "./items";
|
||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
|
import { ScopedAccess } from "src/components/scoped-access";
|
||||||
|
import { Scope } from "src/store/types";
|
||||||
|
|
||||||
type CommonProps = {
|
type CommonProps = {
|
||||||
handleMenu: () => void;
|
handleMenu: () => void;
|
||||||
@@ -158,6 +159,7 @@ export const Navi = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ScopedAccess scope={Scope.admin} user={user}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModal(true)}
|
onClick={() => setModal(true)}
|
||||||
@@ -166,27 +168,8 @@ export const Navi = ({
|
|||||||
>
|
>
|
||||||
<Icons.PlusCircle />
|
<Icons.PlusCircle />
|
||||||
</button>
|
</button>
|
||||||
|
</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,8 +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, UserData } from "./types";
|
||||||
import type { ServiceProvider, User } from "src/api/types";
|
import { Scope } from "./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 */
|
||||||
@@ -61,9 +62,12 @@ export const currentServiceProviderAction = (
|
|||||||
return genericAction(state, action);
|
return genericAction(state, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAsyncAction = async (): Promise<User> => {
|
export const userAsyncAction = async (): Promise<UserData> => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
return parseJwt(token);
|
const userData = parseJwt(token);
|
||||||
|
|
||||||
|
// Add `access` as enum for client-side usage
|
||||||
|
return { ...userData, access: Scope[userData.scope] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serviceProvidersAsyncAction = async (): Promise<
|
export const serviceProvidersAsyncAction = async (): Promise<
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type {
|
|||||||
ACL,
|
ACL,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const initialState: State = {
|
export const initialState: State = {
|
||||||
featureFlags: {
|
featureFlags: {
|
||||||
/** Placeholder since we may need feature-flags in the future... */
|
/** Placeholder since we may need feature-flags in the future... */
|
||||||
development: import.meta.env.DEV,
|
development: import.meta.env.DEV,
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { User, ServiceProvider } from "src/api/types";
|
import type { UserJWT, ServiceProvider } from "src/api/types";
|
||||||
|
|
||||||
export type IMessage = string | JSX.Element;
|
export type IMessage = string | JSX.Element;
|
||||||
|
|
||||||
@@ -18,9 +18,19 @@ export interface FeatureFlag {
|
|||||||
development: boolean;
|
development: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Scope {
|
||||||
|
"account" = 0,
|
||||||
|
"service_provider" = 1,
|
||||||
|
"admin" = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserData extends UserJWT {
|
||||||
|
access: Scope;
|
||||||
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
/** logged in user */
|
/** logged in user */
|
||||||
user?: User;
|
user?: UserData;
|
||||||
/** global toast notifications */
|
/** global toast notifications */
|
||||||
toast?: Toast;
|
toast?: Toast;
|
||||||
/** feature flags from vite ENV */
|
/** feature flags from vite ENV */
|
||||||
@@ -53,6 +63,6 @@ export interface MiddleWare {
|
|||||||
export interface AppStateContext {
|
export interface AppStateContext {
|
||||||
/** Global data store */
|
/** Global data store */
|
||||||
state: State;
|
state: State;
|
||||||
/** Globsl dispatch method */
|
/** Global dispatch method */
|
||||||
dispatch: GlobalDispatch;
|
dispatch: GlobalDispatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,10 +101,16 @@
|
|||||||
|
|
||||||
&--col4 {
|
&--col4 {
|
||||||
.grid__row {
|
.grid__row {
|
||||||
grid-template-columns: [col] 37% [col] 35% [col] 20% [col] 3%;
|
grid-template-columns: [col] 30% [col] 30% [col] 30% [col] 10%;
|
||||||
grid-template-rows: [row] auto [row] auto [row] [row] auto;
|
grid-template-rows: [row] auto [row] auto [row] [row] auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> div:last-child {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,21 @@ import type { UserLogin } from "src/api/types";
|
|||||||
|
|
||||||
import userLogin from "../../cypress/fixtures/userLogin.json";
|
import userLogin from "../../cypress/fixtures/userLogin.json";
|
||||||
|
|
||||||
type TestProviderProps = Partial<AuthStateContext> & {
|
export type TestProviderProps = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
authProps?: Partial<AuthStateContext>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LayoutProviderProps = TestProviderProps & {
|
export type LayoutProviderProps = TestProviderProps & {
|
||||||
outlet: JSX.Element;
|
outlet: JSX.Element;
|
||||||
Layout: React.ElementType;
|
Layout: React.ElementType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signinError = () => Promise.reject(MSG_SOMETHING_WRONG);
|
export const signinError = () => Promise.reject(MSG_SOMETHING_WRONG);
|
||||||
export const signinSuccess = () => Promise.resolve(userLogin as UserLogin);
|
export const signinSuccess = () =>
|
||||||
|
Promise.resolve(userLogin as unknown as UserLogin);
|
||||||
export const signout = () => undefined;
|
export const signout = () => undefined;
|
||||||
export const authProps: AuthStateContext = {
|
export const defaultAuthProps: AuthStateContext = {
|
||||||
token: "",
|
token: "",
|
||||||
signin: signinSuccess,
|
signin: signinSuccess,
|
||||||
signout,
|
signout,
|
||||||
@@ -32,13 +34,13 @@ export const authProps: AuthStateContext = {
|
|||||||
/**
|
/**
|
||||||
* Use this when you simply need to wrap with state and auth
|
* Use this when you simply need to wrap with state and auth
|
||||||
*/
|
*/
|
||||||
export const TestProvider = ({ children, ...restProps }: TestProviderProps) => {
|
export const TestProvider = ({ children, authProps }: TestProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<StateProvider>
|
<StateProvider>
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
...defaultAuthProps,
|
||||||
...authProps,
|
...authProps,
|
||||||
...restProps,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -57,14 +59,14 @@ export const TestProvider = ({ children, ...restProps }: TestProviderProps) => {
|
|||||||
export const LayoutProvider = ({
|
export const LayoutProvider = ({
|
||||||
Layout,
|
Layout,
|
||||||
outlet,
|
outlet,
|
||||||
...restProps
|
authProps,
|
||||||
}: LayoutProviderProps) => {
|
}: LayoutProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<StateProvider>
|
<StateProvider>
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
...defaultAuthProps,
|
||||||
...authProps,
|
...authProps,
|
||||||
...restProps,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@@ -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