Skip to content

Commit ad495a5

Browse files
committed
#108 authentication and error refactor
1 parent b17c7ae commit ad495a5

File tree

11 files changed

+165
-56
lines changed

11 files changed

+165
-56
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
This changelog covers all three packages, as they are (for now) updated as a whole
44

5+
## v0.29.0
6+
7+
- Add authentication: sign requests, so the server knows who sent it. This allows for better authorization. #108
8+
- Refactor Error type, improve Error page / views
9+
- Automatically retry unauthorized resources (but I want a prettier solution, see #110)
10+
511
## v0.28.2
612

713
- Added server-side full text search #106

data-browser/src/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { isDev } from './config';
1313
import { handleError, initBugsnag } from './helpers/handlers';
1414
import HotKeysWrapper from './components/HotKeyWrapper';
1515
import { AppSettingsContextProvider } from './helpers/AppSettings';
16-
import ErrorPage from './views/ErrorPage';
16+
import CrashPage from './views/CrashPage';
1717
import toast from 'react-hot-toast';
1818

1919
/** Initialize the store */
@@ -51,7 +51,7 @@ function App(): JSX.Element {
5151
<QueryParamProvider ReactRouterRoute={Route}>
5252
<HotKeysWrapper>
5353
<ThemeWrapper>
54-
<ErrorBoundary FallbackComponent={ErrorPage}>
54+
<ErrorBoundary FallbackComponent={CrashPage}>
5555
<GlobalStyle />
5656
<Toaster />
5757
<MetaSetter />

data-browser/src/components/SideBar.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styled from 'styled-components';
22
import * as React from 'react';
3-
import { useArray, useResource, useTitle } from '@tomic/react';
3+
import { useArray, useCurrentAgent, useResource, useTitle } from '@tomic/react';
44
import { properties } from '@tomic/lib';
55
import { useHover } from '../helpers/useHover';
66
import { useSettings } from '../helpers/AppSettings';
@@ -22,6 +22,7 @@ import {
2222
import { paths } from '../routes/paths';
2323
import { ErrorLook } from '../views/ResourceInline';
2424
import { openURL } from '../helpers/navigation';
25+
import { isUnauthorized } from '@tomic/lib/src/error';
2526

2627
/** Amount of pixels where the sidebar automatically shows */
2728
export const SIDEBAR_TOGGLE_WIDTH = 600;
@@ -177,6 +178,7 @@ const SideBarDrive = React.memo(function SBD({
177178
handleClickItem,
178179
}: SideBarDriveProps): JSX.Element {
179180
const { baseURL } = useSettings();
181+
const [agent] = useCurrentAgent();
180182
const [drive] = useResource(baseURL);
181183
const [children] = useArray(drive, properties.children);
182184
const title = useTitle(drive);
@@ -218,7 +220,13 @@ const SideBarDrive = React.memo(function SBD({
218220
})
219221
) : drive.loading ? null : (
220222
<SideBarErr>
221-
{drive.getError()?.message || 'Could not load this baseURL'}
223+
{drive.error
224+
? isUnauthorized(drive.error)
225+
? agent
226+
? 'unauthorized'
227+
: 'Sign in to get access'
228+
: drive.error.message
229+
: 'this should not happen'}
222230
</SideBarErr>
223231
)}
224232
</>

data-browser/src/views/CrashPage.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as React from 'react';
2+
import { Resource } from '@tomic/lib';
3+
4+
import { ContainerNarrow } from '../components/Containers';
5+
import { ErrorLook } from './ResourceInline';
6+
import { Button } from '../components/Button';
7+
8+
type ErrorPageProps = {
9+
resource?: Resource;
10+
children?: React.ReactNode;
11+
error: Error;
12+
info: React.ErrorInfo;
13+
clearError: () => void;
14+
};
15+
16+
/** If the entire app crashes, show this page */
17+
function CrashPage({
18+
resource,
19+
children,
20+
error,
21+
clearError,
22+
}: ErrorPageProps): JSX.Element {
23+
return (
24+
<ContainerNarrow resource={resource?.getSubject()}>
25+
<ErrorLook>
26+
{children ? children : JSON.stringify(error?.message)}
27+
</ErrorLook>
28+
<div>
29+
<Button onClick={clearError}>Clear error</Button>
30+
<Button
31+
onClick={() =>
32+
window.setTimeout(window.location.reload.bind(window.location), 200)
33+
}
34+
>
35+
Reload page
36+
</Button>
37+
</div>
38+
</ContainerNarrow>
39+
);
40+
}
41+
42+
export default CrashPage;

data-browser/src/views/DocumentPage.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ import { ErrorLook } from './ResourceInline';
1515
import { ElementEdit, ElementEditPropsBase, ElementShow } from './Element';
1616
import { Button } from '../components/Button';
1717

18-
type DrivePageProps = {
18+
type DocumentPageProps = {
1919
resource: Resource;
20-
setEditMode: (arg: boolean) => void;
2120
};
2221

2322
/** A full page, editable document, consisting of Elements */
24-
function DocumentPage({ resource }: DrivePageProps): JSX.Element {
23+
function DocumentPage({ resource }: DocumentPageProps): JSX.Element {
2524
const [canWrite, canWriteMessage] = useCanWrite(resource);
2625
const [editMode, setEditMode] = useState(canWrite);
2726

@@ -40,10 +39,15 @@ function DocumentPage({ resource }: DrivePageProps): JSX.Element {
4039
);
4140
}
4241

42+
type DocumentSubPageProps = {
43+
resource: Resource;
44+
setEditMode: (arg: boolean) => void;
45+
};
46+
4347
function DocumentPageEdit({
4448
resource,
4549
setEditMode,
46-
}: DrivePageProps): JSX.Element {
50+
}: DocumentSubPageProps): JSX.Element {
4751
const [elements, setElements] = useArray(
4852
resource,
4953
properties.document.elements,
@@ -279,7 +283,7 @@ function DocumentPageEdit({
279283
function DocumentPageShow({
280284
resource,
281285
setEditMode,
282-
}: DrivePageProps): JSX.Element {
286+
}: DocumentSubPageProps): JSX.Element {
283287
const [elements] = useArray(resource, properties.document.elements);
284288
const [title] = useString(resource, properties.name);
285289
return (

data-browser/src/views/ErrorPage.tsx

+33-26
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,46 @@
11
import * as React from 'react';
2+
import { useCurrentAgent, useStore } from '@tomic/react';
23
import { Resource } from '@tomic/lib';
3-
44
import { ContainerNarrow } from '../components/Containers';
55
import { ErrorLook } from './ResourceInline';
66
import { Button } from '../components/Button';
7+
import { isUnauthorized } from '@tomic/lib/src/error';
78

89
type ErrorPageProps = {
9-
resource?: Resource;
10-
children?: React.ReactNode;
11-
error: Error;
12-
info: React.ErrorInfo;
13-
clearError: () => void;
10+
resource: Resource;
1411
};
1512

16-
function ErrorPage({
17-
resource,
18-
children,
19-
error,
20-
clearError,
21-
}: ErrorPageProps): JSX.Element {
13+
/**
14+
* A View for Resource Errors. Not to be confused with the CrashPage, which is
15+
* for App wide errors.
16+
*/
17+
function ErrorPage({ resource }: ErrorPageProps): JSX.Element {
18+
const [agent] = useCurrentAgent();
19+
const store = useStore();
20+
const subject = resource.getSubject();
21+
22+
if (isUnauthorized(resource.error)) {
23+
return (
24+
<ContainerNarrow>
25+
<h1>Unauthorized</h1>
26+
{agent ? null : <p>Try signing in</p>}
27+
<p>{resource.error.message}</p>
28+
<Button onClick={() => store.fetchResource(subject)}>Retry</Button>
29+
</ContainerNarrow>
30+
);
31+
}
2232
return (
23-
<ContainerNarrow resource={resource?.getSubject()}>
24-
<ErrorLook>
25-
{children ? children : JSON.stringify(error?.message)}
26-
</ErrorLook>
27-
<div>
28-
<Button onClick={clearError}>Clear error</Button>
29-
<Button
30-
onClick={() =>
31-
window.setTimeout(window.location.reload.bind(window.location), 200)
32-
}
33-
>
34-
Reload page
35-
</Button>
36-
</div>
33+
<ContainerNarrow>
34+
<h1>⚠️ Error opening {resource.getSubject()}</h1>
35+
<ErrorLook>{resource.getError().message}</ErrorLook>
36+
<br />
37+
<Button onClick={() => store.fetchResource(subject)}>Retry</Button>
38+
<Button
39+
onClick={() => store.fetchResource(subject, { fromProxy: true })}
40+
title={`Fetches the URL from your current Atomic-Server (${store.getBaseUrl()}), instead of from the actual URL itself. Can be useful if the URL is down, but the resource is cached in your server.`}
41+
>
42+
Use proxy
43+
</Button>
3744
</ContainerNarrow>
3845
);
3946
}

data-browser/src/views/ResourcePage.tsx

+3-18
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import React from 'react';
2-
import { useString, useResource, useTitle, useStore } from '@tomic/react';
2+
import { useString, useResource, useTitle } from '@tomic/react';
33
import { properties, urls } from '@tomic/lib';
44
import AllProps from '../components/AllProps';
55
import { ContainerNarrow } from '../components/Containers';
66
import Collection from '../views/CollectionPage';
77
import ClassDetail from '../components/ClassDetail';
88
import NewInstanceButton from '../components/NewInstanceButton';
9-
import { Button } from '../components/Button';
10-
import { ErrorLook } from './ResourceInline';
119
import EndpointPage from './EndpointPage';
1210
import { ValueForm } from '../components/forms/ValueForm';
1311
import Parent from '../components/Parent';
1412
import DrivePage from './DrivePage';
1513
import RedirectPage from './RedirectPage';
1614
import InvitePage from './InvitePage';
1715
import DocumentPage from './DocumentPage';
16+
import ErrorPage from './ErrorPage';
1817

1918
type Props = {
2019
subject: string;
@@ -40,26 +39,12 @@ function ResourcePage({ subject }: Props): JSX.Element {
4039
const [resource] = useResource(subject);
4140
const title = useTitle(resource);
4241
const [klass] = useString(resource, properties.isA);
43-
const store = useStore();
4442

4543
if (resource.loading) {
4644
return <ContainerNarrow>Loading...</ContainerNarrow>;
4745
}
4846
if (resource.error) {
49-
return (
50-
<ContainerNarrow>
51-
<h1>⚠️ {title}</h1>
52-
<ErrorLook>{resource.getError().message}</ErrorLook>
53-
<br />
54-
<Button onClick={() => store.fetchResource(subject)}>Retry</Button>
55-
<Button
56-
onClick={() => store.fetchResource(subject, { fromProxy: true })}
57-
title={`Fetches the URL from your current Atomic-Server (${store.getBaseUrl()}), instead of from the actual URL itself. Can be useful if the URL is down, but the resource is cached in your server.`}
58-
>
59-
Use proxy
60-
</Button>
61-
</ContainerNarrow>
62-
);
47+
return <ErrorPage resource={resource} />;
6348
}
6449

6550
// TODO: Make these registerable, so users can easily extend these

lib/src/client.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Commit, serializeDeterministically, signToBase64 } from './commit';
33
import { parseJsonADResource } from './parse';
44
import { Resource } from './resource';
55
import { Agent } from '@tomic/lib';
6+
import { AtomicError, ErrorType } from './error';
67

78
/**
89
* Fetches and Parses a Resource. Can fetch through another atomic server if you
@@ -38,7 +39,7 @@ export async function fetchResource(
3839
url = newURL.href;
3940
}
4041
if (window.fetch == undefined) {
41-
throw new Error(
42+
throw new AtomicError(
4243
`No window object available this lib currently requires the DOM for fetching`,
4344
);
4445
}
@@ -51,12 +52,17 @@ export async function fetchResource(
5152
const json = JSON.parse(body);
5253
resource = parseJsonADResource(json, resource, store);
5354
} catch (e) {
54-
throw new Error(
55+
throw new AtomicError(
5556
`Could not parse JSON from fetching ${subject}. Is it an Atomic Data resource? Error message: ${e.message}`,
5657
);
5758
}
59+
} else if (response.status == 401) {
60+
throw new AtomicError(
61+
`You don't have the rights to do view ${subject}. Are you signed in with the right Agent? More detailed error from server: ${body}`,
62+
ErrorType.Unauthorized,
63+
);
5864
} else {
59-
const error = new Error(`${response.status} error: ${body}`);
65+
const error = new AtomicError(`${response.status} error: ${body}`);
6066
resource.setError(error);
6167
}
6268
} catch (e) {

lib/src/error.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export enum ErrorType {
2+
Unauthorized = 'Unauthorized',
3+
NotFound = 'NotFound',
4+
Server = 'Server',
5+
Client = 'Client',
6+
}
7+
8+
/** Pass any error. If the error is an AtomicError and it's Unauthorized, return true */
9+
export function isUnauthorized(error: Error): boolean {
10+
if (error instanceof AtomicError) {
11+
if ((error.type = ErrorType.Unauthorized)) {
12+
return true;
13+
}
14+
}
15+
return false;
16+
}
17+
18+
/**
19+
* Atomic Data Errors have an additional Type, which tells the client what kind
20+
* of error to render.
21+
*/
22+
export class AtomicError extends Error {
23+
type: ErrorType;
24+
25+
constructor(message, type = ErrorType.Client) {
26+
super(message);
27+
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript
28+
Object.setPrototypeOf(this, AtomicError.prototype);
29+
this.type = type;
30+
}
31+
}

lib/src/urls.ts

+5
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,13 @@ export const datatypes = {
9595
timestamp: 'https://atomicdata.dev/datatypes/timestamp',
9696
};
9797

98+
export const instances = {
99+
publicAgent: 'https://atomicdata.dev/agents/publicAgent',
100+
};
101+
98102
export const urls = {
99103
properties,
100104
classes,
101105
datatypes,
106+
instances,
102107
};

0 commit comments

Comments
 (0)