1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

feat: forbidden user interface (#4523)

* feat: forbidden user interface

* Delete App.stories.tsx

* Update package.json

* Delete package.svg

* fix
This commit is contained in:
Juan Picado 2024-03-03 19:06:39 +01:00 committed by GitHub
parent 4a81ed791a
commit c9962fe1d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 292 additions and 195 deletions

@ -0,0 +1,7 @@
---
'@verdaccio/ui-theme': minor
'@verdaccio/ui-components': minor
'@verdaccio/config': minor
---
feat: forbidden user interface

@ -137,7 +137,7 @@
"docker": "docker build -t verdaccio/verdaccio:local . --no-cache", "docker": "docker build -t verdaccio/verdaccio:local . --no-cache",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"", "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
"lint": "eslint --max-warnings 100 \"**/*.{js,jsx,ts,tsx}\"", "lint": "eslint --max-warnings 70 \"**/*.{js,jsx,ts,tsx}\"",
"test": "pnpm --filter \"./packages/**\" test", "test": "pnpm --filter \"./packages/**\" test",
"test:e2e:cli": "pnpm --filter ...@verdaccio/e2e-cli-* test -- --coverage=false", "test:e2e:cli": "pnpm --filter ...@verdaccio/e2e-cli-* test -- --coverage=false",
"test:e2e:ui": "pnpm --filter ...@verdaccio/e2e-ui test", "test:e2e:ui": "pnpm --filter ...@verdaccio/e2e-ui test",

@ -10,7 +10,7 @@ auth:
uplinks: uplinks:
ver: ver:
url: https://registry.verdaccio.org url: https://registry.npmjs.org
security: security:
api: api:

@ -7,7 +7,7 @@ web:
uplinks: uplinks:
ver: ver:
url: https://registry.verdaccio.org url: https://registry.npmjs.org
log: { type: stdout, format: pretty, level: trace } log: { type: stdout, format: pretty, level: trace }

@ -1,9 +1,12 @@
import assert from 'assert'; import assert from 'assert';
import buildDebug from 'debug';
import _ from 'lodash'; import _ from 'lodash';
import { errorUtils } from '@verdaccio/core'; import { errorUtils } from '@verdaccio/core';
import { PackageAccess } from '@verdaccio/types'; import { PackageAccess } from '@verdaccio/types';
const debug = buildDebug('verdaccio:config:utils');
export interface LegacyPackageList { export interface LegacyPackageList {
[key: string]: PackageAccess; [key: string]: PackageAccess;
} }
@ -61,6 +64,7 @@ export function normalisePackageAccess(packages: LegacyPackageList): LegacyPacka
for (const pkg in packages) { for (const pkg in packages) {
if (Object.prototype.hasOwnProperty.call(packages, pkg)) { if (Object.prototype.hasOwnProperty.call(packages, pkg)) {
const packageAccess = packages[pkg]; const packageAccess = packages[pkg];
debug('package access %s for %s ', packageAccess, pkg);
const isInvalid = _.isObject(packageAccess) && _.isArray(packageAccess) === false; const isInvalid = _.isObject(packageAccess) && _.isArray(packageAccess) === false;
assert(isInvalid, `CONFIG: bad "'${pkg}'" package description (object expected)`); assert(isInvalid, `CONFIG: bad "'${pkg}'" package description (object expected)`);

@ -147,9 +147,11 @@
"error": { "error": {
"unspecific": "Something went wrong.", "unspecific": "Something went wrong.",
"404": { "404": {
"page-not-found": "404 - Page not found",
"sorry-we-could-not-find-it": "Sorry, we couldn't find it..." "sorry-we-could-not-find-it": "Sorry, we couldn't find it..."
}, },
"401": {
"sorry-no-access": "Sorry, you need credentials access to see this page."
},
"app-context-not-correct-used": "The app context was not used correctly", "app-context-not-correct-used": "The app context was not used correctly",
"theme-context-not-correct-used": "The theme context was not used correctly", "theme-context-not-correct-used": "The theme context was not used correctly",
"package-meta-is-required-at-detail-context": "packageMeta is required at DetailContext" "package-meta-is-required-at-detail-context": "packageMeta is required at DetailContext"

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Loading, NotFound, RootState, VersionLayout } from '@verdaccio/ui-components'; import { Forbidden, Loading, NotFound, RootState, VersionLayout } from '@verdaccio/ui-components';
const Version: React.FC = () => { const Version: React.FC = () => {
const manifestStore = useSelector((state: RootState) => state.manifest); const manifestStore = useSelector((state: RootState) => state.manifest);
@ -11,10 +11,13 @@ const Version: React.FC = () => {
return <Loading />; return <Loading />;
} }
if (manifestStore.forbidden) {
return <Forbidden />;
}
if (manifestStore.hasNotBeenFound) { if (manifestStore.hasNotBeenFound) {
return <NotFound />; return <NotFound />;
} }
return <VersionLayout />; return <VersionLayout />;
}; };

@ -37,7 +37,7 @@ module.exports = Object.assign({}, config, {
branches: 70, branches: 70,
functions: 76, functions: 76,
lines: 80, lines: 80,
statements: 82, statements: 81,
}, },
}, },
}); });

@ -31,6 +31,15 @@ export const handlers = [
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => { rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => {
return res(ctx.json(require('./api/jquery-sidebar.json'))); return res(ctx.json(require('./api/jquery-sidebar.json')));
}), }),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/JSONStream', (req, res, ctx) => {
return res(ctx.status(401));
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/semver', (req, res, ctx) => {
return res(ctx.status(500));
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/kleur', (req, res, ctx) => {
return res(ctx.status(404));
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/glob', (req, res, ctx) => { rest.get('http://localhost:9000/-/verdaccio/data/sidebar/glob', (req, res, ctx) => {
return res(ctx.json(require('./api/glob-sidebar.json'))); return res(ctx.json(require('./api/glob-sidebar.json')));
}), }),

@ -0,0 +1,32 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import AppRoute from './AppRoute';
export default {
title: 'App/Main',
};
export const ApplicationStoryBook: any = () => (
<MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
<AppRoute />
</MemoryRouter>
);
export const ApplicationjQuery: any = () => (
<MemoryRouter initialEntries={[`/-/web/detail/jquery`]}>
<AppRoute />
</MemoryRouter>
);
export const ApplicationForbidden: any = () => (
<MemoryRouter initialEntries={[`/-/web/detail/JSONStream`]}>
<AppRoute />
</MemoryRouter>
);
export const ApplicationNotFound: any = () => (
<MemoryRouter initialEntries={[`/-/web/detail/kleur`]}>
<AppRoute />
</MemoryRouter>
);

@ -0,0 +1,42 @@
import React from 'react';
import { Route as ReactRouterDomRoute, Switch } from 'react-router-dom';
import { NotFound, Route, VersionProvider, loadable } from '../index';
const VersionPage = loadable(() => import(/* webpackChunkName: "Version" */ './pages/Version'));
const Front = loadable(() => import(/* webpackChunkName: "Home" */ './pages/Front'));
const AppRoute: React.FC = () => {
return (
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<Front />
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionProvider>
<VersionPage />
</VersionProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE_VERSION}>
<VersionProvider>
<VersionPage />
</VersionProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE_VERSION}>
<VersionProvider>
<VersionPage />
</VersionProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE}>
<VersionProvider>
<VersionPage />
</VersionProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute>
<NotFound />
</ReactRouterDomRoute>
</Switch>
);
};
export default AppRoute;

@ -0,0 +1,3 @@
import { Home } from '../../../index';
export default Home;

@ -0,0 +1 @@
export { default } from './Home';

@ -0,0 +1,28 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Forbidden from '../../../components/Forbidden';
import { Loading, NotFound, RootState, VersionLayout } from '../../../index';
const Version: React.FC = () => {
const manifestStore = useSelector((state: RootState) => state.manifest);
const isLoading = useSelector((state: RootState) => state?.loading?.models.manifest);
if (isLoading) {
return <Loading />;
}
// @ts-expect-error
if (manifestStore.forbidden) {
return <Forbidden />;
}
// @ts-expect-error
if (manifestStore.hasNotBeenFound) {
return <NotFound />;
}
return <VersionLayout />;
};
export default Version;

@ -0,0 +1 @@
export { default } from './Version';

@ -0,0 +1,38 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
import Forbidden from './Forbidden';
const mockHistory = jest.fn();
jest.mock('react-router-dom', () => ({
useHistory: () => ({
push: mockHistory,
}),
}));
describe('<Forbidden /> component', () => {
test('should load the component in default state', () => {
render(
<MemoryRouter>
<Forbidden />
</MemoryRouter>
);
expect(screen.getByTestId('LockIcon')).toBeInTheDocument();
expect(screen.getByText('error.401.sorry-no-access')).toBeInTheDocument();
expect(screen.getByText('button.go-to-the-home-page')).toBeInTheDocument();
});
test('go to Home Page button click', async () => {
const { getByTestId } = render(
<MemoryRouter>
<Forbidden />
</MemoryRouter>
);
const node = getByTestId('not-found-go-to-home-button');
fireEvent.click(node);
expect(mockHistory).toHaveBeenCalled();
});
});

@ -0,0 +1,56 @@
/* eslint-disable react/forbid-component-props */
/* eslint-disable verdaccio/jsx-no-style */
import styled from '@emotion/styled';
import LockIcon from '@mui/icons-material/Lock';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { Theme } from '../..';
import Heading from '../Heading';
const Foebidden: React.FC = () => {
const history = useHistory();
const { t } = useTranslation();
const handleGoHome = useCallback(() => {
history.push('/');
}, [history]);
return (
<Box
alignItems="center"
data-testid="404"
display="flex"
flexDirection="column"
flexGrow={1}
justifyContent="center"
p={2}
>
<Container>
<LockIcon color="primary" style={{ fontSize: 236 }} />
</Container>
<StyledHeading className="not-found-text" variant="h4">
{t('error.401.sorry-no-access')}
</StyledHeading>
<Button data-testid="not-found-go-to-home-button" onClick={handleGoHome} variant="contained">
{t('button.go-to-the-home-page')}
</Button>
</Box>
);
};
export default Foebidden;
const Container = styled('div')({
margin: '0 auto',
});
const StyledHeading = styled(Heading)<{ theme?: Theme }>(({ theme }) => ({
color: theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.white,
marginBottom: 16,
}));

@ -0,0 +1 @@
export { default } from './Forbidden';

@ -1,4 +1,6 @@
/* eslint-disable verdaccio/jsx-no-style */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import FolderOffIcon from '@mui/icons-material/FolderOff';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@ -7,8 +9,6 @@ import { useHistory } from 'react-router-dom';
import { Theme } from '../../'; import { Theme } from '../../';
import Heading from '../Heading'; import Heading from '../Heading';
// @ts-ignore
import PackageImg from './img/package.svg';
const NotFound: React.FC = () => { const NotFound: React.FC = () => {
const history = useHistory(); const history = useHistory();
@ -28,7 +28,9 @@ const NotFound: React.FC = () => {
justifyContent="center" justifyContent="center"
p={2} p={2}
> >
<EmptyPackage alt={t('error.404.page-not-found')} src={PackageImg} /> <Container>
<FolderOffIcon color="primary" style={{ fontSize: 236 }} />
</Container>
<StyledHeading className="not-found-text" variant="h4"> <StyledHeading className="not-found-text" variant="h4">
{t('error.404.sorry-we-could-not-find-it')} {t('error.404.sorry-we-could-not-find-it')}
</StyledHeading> </StyledHeading>
@ -41,8 +43,7 @@ const NotFound: React.FC = () => {
export default NotFound; export default NotFound;
const EmptyPackage = styled('img')({ const Container = styled('div')({
width: '150px',
margin: '0 auto', margin: '0 auto',
}); });

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { fireEvent, render } from '../../test/test-react-testing-library'; import { fireEvent, render, screen } from '../../test/test-react-testing-library';
import NotFound from './NotFound'; import NotFound from './NotFound';
const mockHistory = jest.fn(); const mockHistory = jest.fn();
@ -14,12 +14,15 @@ jest.mock('react-router-dom', () => ({
describe('<NotFound /> component', () => { describe('<NotFound /> component', () => {
test('should load the component in default state', () => { test('should load the component in default state', () => {
const { container } = render( render(
<MemoryRouter> <MemoryRouter>
<NotFound /> <NotFound />
</MemoryRouter> </MemoryRouter>
); );
expect(container.firstChild).toMatchSnapshot();
expect(screen.getByTestId('FolderOffIcon')).toBeInTheDocument();
expect(screen.getByText('button.go-to-the-home-page')).toBeInTheDocument();
expect(screen.getByText('error.404.sorry-we-could-not-find-it')).toBeInTheDocument();
}); });
test('go to Home Page button click', async () => { test('go to Home Page button click', async () => {

@ -1,171 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NotFound /> component should load the component in default state 1`] = `
.emotion-0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
padding: 16px;
}
.emotion-1 {
width: 150px;
margin: 0 auto;
}
.emotion-4 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
font-size: 2.125rem;
line-height: 1.235;
color: #4b5e40;
margin-bottom: 16px;
}
.emotion-5 {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
position: relative;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
background-color: transparent;
outline: 0;
border: 0;
margin: 0;
border-radius: 0;
padding: 0;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
-moz-appearance: none;
-webkit-appearance: none;
-webkit-text-decoration: none;
text-decoration: none;
color: inherit;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.75;
text-transform: uppercase;
min-width: 64px;
padding: 6px 16px;
border-radius: 4px;
-webkit-transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
color: #fff;
background-color: #4b5e40;
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);
}
.emotion-5::-moz-focus-inner {
border-style: none;
}
.emotion-5.Mui-disabled {
pointer-events: none;
cursor: default;
}
@media print {
.emotion-5 {
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
}
.emotion-5:hover {
-webkit-text-decoration: none;
text-decoration: none;
background-color: rgb(52, 65, 44);
box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12);
}
@media (hover: none) {
.emotion-5:hover {
background-color: #4b5e40;
}
}
.emotion-5:active {
box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12);
}
.emotion-5.Mui-focusVisible {
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12);
}
.emotion-5.Mui-disabled {
color: rgba(0, 0, 0, 0.26);
box-shadow: none;
background-color: rgba(0, 0, 0, 0.12);
}
.emotion-6 {
overflow: hidden;
pointer-events: none;
position: absolute;
z-index: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: inherit;
}
<div
class="MuiBox-root emotion-0"
data-testid="404"
>
<img
alt="error.404.page-not-found"
class="emotion-1 emotion-2"
src="[object Object]"
/>
<h4
class="MuiTypography-root MuiTypography-h4 not-found-text emotion-3 emotion-4"
>
error.404.sorry-we-could-not-find-it
</h4>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium emotion-5"
data-testid="not-found-go-to-home-button"
tabindex="0"
type="button"
>
button.go-to-the-home-page
<span
class="MuiTouchRipple-root emotion-6"
/>
</button>
</div>
`;

@ -22,6 +22,7 @@ export { default as Label } from './components/Label';
export { default as Logo } from './components/Logo'; export { default as Logo } from './components/Logo';
export { default as MenuItem } from './components/MenuItem'; export { default as MenuItem } from './components/MenuItem';
export { default as NotFound } from './components/NotFound'; export { default as NotFound } from './components/NotFound';
export { default as Forbidden } from './components/Forbidden';
export { default as LoginDialog } from './components/LoginDialog'; export { default as LoginDialog } from './components/LoginDialog';
export { default as Search } from './components/Search'; export { default as Search } from './components/Search';
export { default as HeaderInfoDialog } from './components/HeaderInfoDialog'; export { default as HeaderInfoDialog } from './components/HeaderInfoDialog';

@ -27,3 +27,13 @@ export const DetailJquery: any = () => (
</Route> </Route>
</MemoryRouter> </MemoryRouter>
); );
export const DetailForbidden: any = () => (
<MemoryRouter initialEntries={[`/-/web/detail/JSONStream`]}>
<Route exact={true} path="/-/web/detail/:package">
<VersionProvider>
<Detail />
</VersionProvider>
</Route>
</MemoryRouter>
);

@ -14,7 +14,8 @@ describe('api', () => {
const handled = await handleResponseType(response); const handled = await handleResponseType(response);
expect(handled).toEqual([ok, responseText]); expect(handled[0]).toBeFalsy();
expect(handled[1]).toBeDefined();
}); });
test('should test tgz scenario', async () => { test('should test tgz scenario', async () => {
@ -123,7 +124,7 @@ describe('api', () => {
}) })
); );
await expect(api.request('/resource')).rejects.toThrow(new Error('something went wrong')); await expect(api.request('/resource')).rejects.toThrow(new Error('Unknown error'));
}); });
test('when api returns an error 5.x.x', async () => { test('when api returns an error 5.x.x', async () => {

@ -1,5 +1,10 @@
import storage from './storage'; import storage from './storage';
class CustomError extends Error {
// @ts-ignore
code: number;
}
/** /**
* Handles response according to content type * Handles response according to content type
* @param {object} response * @param {object} response
@ -25,7 +30,8 @@ export function handleResponseType(response: Response): Promise<[boolean, any]>
} }
} }
return Promise.all([response.ok, response.text()]); // error handling
return Promise.all([response.ok, response]);
} }
const AuthHeader = 'Authorization'; const AuthHeader = 'Authorization';
@ -55,12 +61,13 @@ class API {
}) })
.then(handleResponseType) .then(handleResponseType)
.then((response) => { .then((response) => {
if (response[0]) { const [ok, data] = response;
resolve(response[1]); if (ok === true) {
resolve(data);
} else { } else {
// eslint-disable-next-line no-console const error = new CustomError(data?.statusText ?? 'Unknown error');
console.error(response); error.code = data?.status ?? 500;
reject(new Error('something went wrong')); reject(error);
} }
}) })
.catch((error) => { .catch((error) => {

@ -33,6 +33,18 @@ export const manifest = createModel<RootModel>()({
return { return {
...state, ...state,
hasNotBeenFound: true, hasNotBeenFound: true,
forbidden: false,
manifest: undefined,
packageName: undefined,
packageVersion: undefined,
readme: undefined,
};
},
forbidden(state) {
return {
...state,
forbidden: true,
hasNotBeenFound: false,
manifest: undefined, manifest: undefined,
packageName: undefined, packageName: undefined,
packageVersion: undefined, packageVersion: undefined,
@ -50,6 +62,7 @@ export const manifest = createModel<RootModel>()({
...state, ...state,
isError: true, isError: true,
hasNotBeenFound: false, hasNotBeenFound: false,
forbidden: false,
manifest: undefined, manifest: undefined,
packageName: undefined, packageName: undefined,
packageVersion: undefined, packageVersion: undefined,
@ -64,6 +77,7 @@ export const manifest = createModel<RootModel>()({
packageVersion, packageVersion,
readme, readme,
hasNotBeenFound: false, hasNotBeenFound: false,
forbidden: false,
}; };
}, },
}, },
@ -91,7 +105,11 @@ export const manifest = createModel<RootModel>()({
); );
dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme }); dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme });
} catch (error: any) { } catch (error: any) {
if (error.code === 404) {
dispatch.manifest.notFound(); dispatch.manifest.notFound();
} else {
dispatch.manifest.forbidden();
}
} }
}, },
}), }),