This commit is contained in:
proddy
2021-05-14 12:45:57 +02:00
parent 15df0c0552
commit fec5ff3132
108 changed files with 3508 additions and 2455 deletions

View File

@@ -1,40 +1,56 @@
import * as React from 'react';
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
import {
Redirect,
Route,
RouteProps,
RouteComponentProps
} from 'react-router-dom';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import * as Authentication from './Authentication';
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps,
AuthenticatedContext,
AuthenticatedContextValue
} from './AuthenticationContext';
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface AuthenticatedRouteProps
extends RouteProps,
WithSnackbarProps,
AuthenticationContextProps {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
render() {
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
const {
enqueueSnackbar,
authenticationContext,
component: Component,
...rest
} = this.props;
const { location } = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return (
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
<AuthenticatedContext.Provider
value={authenticationContext as AuthenticatedContextValue}
>
<Component {...props} />
</AuthenticatedContext.Provider>
);
}
Authentication.storeLoginRedirect(location);
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
return (
<Redirect to='/' />
);
}
return (
<Route {...rest} render={renderComponent} />
);
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
return <Redirect to="/" />;
};
return <Route {...rest} render={renderComponent} />;
}
}
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));

View File

@@ -1,42 +1,42 @@
import * as H from 'history'
import * as H from 'history';
import history from '../history'
import { Features } from '../features/types'
import { getDefaultRoute } from '../AppRouting'
import history from '../history';
import { Features } from '../features/types';
import { getDefaultRoute } from '../AppRouting';
export const ACCESS_TOKEN = 'access_token'
export const SIGN_IN_PATHNAME = 'signInPathname'
export const SIGN_IN_SEARCH = 'signInSearch'
export const ACCESS_TOKEN = 'access_token';
export const SIGN_IN_PATHNAME = 'signInPathname';
export const SIGN_IN_SEARCH = 'signInSearch';
/**
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
*/
export function getStorage() {
return localStorage || sessionStorage
return localStorage || sessionStorage;
}
export function storeLoginRedirect(location?: H.Location) {
if (location) {
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname)
getStorage().setItem(SIGN_IN_SEARCH, location.search)
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
getStorage().setItem(SIGN_IN_SEARCH, location.search);
}
}
export function clearLoginRedirect() {
getStorage().removeItem(SIGN_IN_PATHNAME)
getStorage().removeItem(SIGN_IN_SEARCH)
getStorage().removeItem(SIGN_IN_PATHNAME);
getStorage().removeItem(SIGN_IN_SEARCH);
}
export function fetchLoginRedirect(
features: Features,
features: Features
): H.LocationDescriptorObject {
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME)
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH)
clearLoginRedirect()
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
clearLoginRedirect();
return {
pathname: signInPathname || getDefaultRoute(features),
search: (signInPathname && signInSearch) || undefined,
}
search: (signInPathname && signInSearch) || undefined
};
}
/**
@@ -44,18 +44,18 @@ export function fetchLoginRedirect(
*/
export function authorizedFetch(
url: RequestInfo,
params?: RequestInit,
params?: RequestInit
): Promise<Response> {
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
params = params || {}
params.credentials = 'include'
params = params || {};
params.credentials = 'include';
params.headers = {
...params.headers,
Authorization: 'Bearer ' + accessToken,
}
Authorization: 'Bearer ' + accessToken
};
}
return fetch(url, params)
return fetch(url, params);
}
/**
@@ -67,33 +67,33 @@ export function redirectingAuthorizedUpload(
xhr: XMLHttpRequest,
url: string,
file: File,
onProgress: (event: ProgressEvent<EventTarget>) => void,
onProgress: (event: ProgressEvent<EventTarget>) => void
): Promise<void> {
return new Promise((resolve, reject) => {
xhr.open('POST', url, true)
const accessToken = getStorage().getItem(ACCESS_TOKEN)
xhr.open('POST', url, true);
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
xhr.withCredentials = true
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken)
xhr.withCredentials = true;
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
}
xhr.upload.onprogress = onProgress
xhr.upload.onprogress = onProgress;
xhr.onload = function () {
if (xhr.status === 401 || xhr.status === 403) {
history.push('/unauthorized')
history.push('/unauthorized');
} else {
resolve()
resolve();
}
}
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
reject(new DOMException('Error', 'UploadError'))
}
};
xhr.onerror = function () {
reject(new DOMException('Error', 'UploadError'));
};
xhr.onabort = function () {
reject(new DOMException('Aborted', 'AbortError'))
}
const formData = new FormData()
formData.append('file', file)
xhr.send(formData)
})
reject(new DOMException('Aborted', 'AbortError'));
};
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
});
}
/**
@@ -101,29 +101,29 @@ export function redirectingAuthorizedUpload(
*/
export function redirectingAuthorizedFetch(
url: RequestInfo,
params?: RequestInit,
params?: RequestInit
): Promise<Response> {
return new Promise<Response>((resolve, reject) => {
authorizedFetch(url, params)
.then((response) => {
if (response.status === 401 || response.status === 403) {
history.push('/unauthorized')
history.push('/unauthorized');
} else {
resolve(response)
resolve(response);
}
})
.catch((error) => {
reject(error)
})
})
reject(error);
});
});
}
export function addAccessTokenParameter(url: string) {
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (!accessToken) {
return url
return url;
}
const parsedUrl = new URL(url)
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken)
return parsedUrl.toString()
const parsedUrl = new URL(url);
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
return parsedUrl.toString();
}

View File

@@ -1,9 +1,8 @@
import * as React from "react";
import * as React from 'react';
export interface Me {
username: string;
admin: boolean;
version: string; // proddy added
}
export interface AuthenticationContextValue {
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
me?: Me;
}
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
export const AuthenticationContext = React.createContext(
AuthenticationContextDefaultValue
);
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
authenticationContext: AuthenticationContextValue;
}
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
export function withAuthenticationContext<T extends AuthenticationContextProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<
Omit<T, keyof AuthenticationContextProps>
> {
render() {
return (
<AuthenticationContext.Consumer>
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
{(authenticationContext) => (
<Component
{...(this.props as T)}
authenticationContext={authenticationContext}
/>
)}
</AuthenticationContext.Consumer>
);
}
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
me: Me;
}
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
export const AuthenticatedContext = React.createContext(
AuthenticatedContextDefaultValue
);
@@ -47,14 +55,23 @@ export interface AuthenticatedContextProps {
authenticatedContext: AuthenticatedContextValue;
}
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<
Omit<T, keyof AuthenticatedContextProps>
> {
render() {
return (
<AuthenticatedContext.Consumer>
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
{(authenticatedContext) => (
<Component
{...(this.props as T)}
authenticatedContext={authenticatedContext}
/>
)}
</AuthenticatedContext.Consumer>
);
}
};
}
}

View File

@@ -2,14 +2,19 @@ import * as React from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import jwtDecode from 'jwt-decode';
import history from '../history'
import history from '../history';
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
import {
AuthenticationContext,
AuthenticationContextValue,
Me
} from './AuthenticationContext';
import FullScreenLoading from '../components/FullScreenLoading';
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
export const decodeMeJWT = (accessToken: string): Me =>
jwtDecode(accessToken) as Me;
interface AuthenticationWrapperState {
context: AuthenticationContextValue;
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
class AuthenticationWrapper extends React.Component<
AuthenticationWrapperProps,
AuthenticationWrapperState
> {
constructor(props: AuthenticationWrapperProps) {
super(props);
this.state = {
context: {
refresh: this.refresh,
signIn: this.signIn,
signOut: this.signOut,
signOut: this.signOut
},
initialized: false
};
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
render() {
return (
<React.Fragment>
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
{this.state.initialized
? this.renderContent()
: this.renderContentLoading()}
</React.Fragment>
);
}
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
}
renderContentLoading() {
return (
<FullScreenLoading />
);
return <FullScreenLoading />;
}
refresh = () => {
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
// return;
// }
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
.then(response => {
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
this.setState({ initialized: true, context: { ...this.state.context, me } });
}).catch(error => {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
variant: 'error',
.then((response) => {
const me =
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
this.setState({
initialized: true,
context: { ...this.state.context, me }
});
})
.catch((error) => {
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
this.props.enqueueSnackbar(
'Error verifying authorization: ' + error.message,
{
variant: 'error'
}
);
});
} else {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
}
}
};
signIn = (accessToken: string) => {
try {
getStorage().setItem(ACCESS_TOKEN, accessToken);
const me: Me = decodeMeJWT(accessToken);
this.setState({ context: { ...this.state.context, me } });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, {
variant: 'success'
});
} catch (err) {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
throw new Error("Failed to parse JWT " + err.message);
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
throw new Error('Failed to parse JWT ' + err.message);
}
}
};
signOut = () => {
getStorage().removeItem(ACCESS_TOKEN);
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
me: undefined
}
});
this.props.enqueueSnackbar("You have signed out", { variant: 'success', });
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
history.push('/');
}
};
}
export default withFeatures(withSnackbar(AuthenticationWrapper))
export default withFeatures(withSnackbar(AuthenticationWrapper));

View File

@@ -1,31 +1,46 @@
import * as React from 'react';
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
import {
Redirect,
Route,
RouteProps,
RouteComponentProps
} from 'react-router-dom';
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps
} from './AuthenticationContext';
import * as Authentication from './Authentication';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface UnauthenticatedRouteProps
extends RouteProps,
AuthenticationContextProps,
WithFeaturesProps {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
public render() {
const { authenticationContext, component: Component, features, ...rest } = this.props;
const {
authenticationContext,
component: Component,
features,
...rest
} = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
}
if (Component) {
return (<Component {...props} />);
return <Component {...props} />;
}
}
return (
<Route {...rest} render={renderComponent} />
);
};
return <Route {...rest} render={renderComponent} />;
}
}

View File

@@ -1,6 +1,6 @@
export { default as AuthenticatedRoute } from './AuthenticatedRoute'
export { default as AuthenticationWrapper } from './AuthenticationWrapper'
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute'
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
export * from './Authentication'
export * from './AuthenticationContext'
export * from './Authentication';
export * from './AuthenticationContext';