Deleted .idea/.gitignore, .idea/clion.iml, .idea/misc.xml, .idea/modules.xml, .idea/platformio.iml, .idea/serialmonitor_settings.xml, .idea/vcs.xml, .idea/watcherTasks.xml files
This commit is contained in:
42
interface/src/authentication/AuthenticatedRoute.tsx
Normal file
42
interface/src/authentication/AuthenticatedRoute.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
|
||||
import * as Authentication from './Authentication';
|
||||
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext } from './AuthenticationContext';
|
||||
|
||||
type ChildComponent = React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||
|
||||
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
|
||||
component: ChildComponent;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||
|
||||
render() {
|
||||
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 AuthenticatedContext}>
|
||||
<Component {...props} />
|
||||
</AuthenticatedContext.Provider>
|
||||
);
|
||||
}
|
||||
Authentication.storeLoginRedirect(location);
|
||||
enqueueSnackbar("Please sign in to continue.", { variant: 'info' });
|
||||
return (
|
||||
<Redirect to='/' />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Route {...rest} render={renderComponent} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
114
interface/src/authentication/Authentication.ts
Normal file
114
interface/src/authentication/Authentication.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import * as H from 'history';
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
|
||||
*/
|
||||
export function getStorage() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearLoginRedirect() {
|
||||
getStorage().removeItem(SIGN_IN_PATHNAME);
|
||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||
}
|
||||
|
||||
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
|
||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
return {
|
||||
pathname: signInPathname || getDefaultRoute(features),
|
||||
search: (signInPathname && signInSearch) || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
||||
*/
|
||||
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
params = params || {};
|
||||
params.credentials = 'include';
|
||||
params.headers = {
|
||||
...params.headers,
|
||||
"Authorization": 'Bearer ' + accessToken
|
||||
};
|
||||
}
|
||||
return fetch(url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
||||
* for a single file upload and takes care of adding the Authroization header and redirecting on
|
||||
* authroization errors as we do for normal fetch operations.
|
||||
*/
|
||||
export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, file: File, onProgress: (event: ProgressEvent<EventTarget>) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.open("POST", url, true);
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
|
||||
}
|
||||
xhr.upload.onprogress = onProgress;
|
||||
xhr.onload = function () {
|
||||
if (xhr.status === 401 || xhr.status === 403) {
|
||||
history.push("/unauthorized");
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
||||
reject(new DOMException('Error', 'UploadError'));
|
||||
};
|
||||
xhr.onabort = function () {
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the normal fetch routene which redirects on 401 response.
|
||||
*/
|
||||
export function redirectingAuthorizedFetch(url: RequestInfo, 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");
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function addAccessTokenParameter(url: string) {
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (!accessToken) {
|
||||
return url;
|
||||
}
|
||||
const parsedUrl = new URL(url);
|
||||
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
|
||||
return parsedUrl.toString();
|
||||
}
|
59
interface/src/authentication/AuthenticationContext.tsx
Normal file
59
interface/src/authentication/AuthenticationContext.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface Me {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
export interface AuthenticationContext {
|
||||
refresh: () => void;
|
||||
signIn: (accessToken: string) => void;
|
||||
signOut: () => void;
|
||||
me?: Me;
|
||||
}
|
||||
|
||||
const AuthenticationContextDefaultValue = {} as AuthenticationContext
|
||||
export const AuthenticationContext = React.createContext(
|
||||
AuthenticationContextDefaultValue
|
||||
);
|
||||
|
||||
export interface AuthenticationContextProps {
|
||||
authenticationContext: AuthenticationContext;
|
||||
}
|
||||
|
||||
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.Consumer>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthenticatedContext extends AuthenticationContext {
|
||||
me: Me;
|
||||
}
|
||||
|
||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContext
|
||||
export const AuthenticatedContext = React.createContext(
|
||||
AuthenticatedContextDefaultValue
|
||||
);
|
||||
|
||||
export interface AuthenticatedContextProps {
|
||||
authenticatedContext: AuthenticatedContext;
|
||||
}
|
||||
|
||||
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.Consumer>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
109
interface/src/authentication/AuthenticationWrapper.tsx
Normal file
109
interface/src/authentication/AuthenticationWrapper.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import * as React from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import history from '../history'
|
||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||
import { AuthenticationContext, Me } from './AuthenticationContext';
|
||||
import FullScreenLoading from '../components/FullScreenLoading';
|
||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||
|
||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
|
||||
|
||||
interface AuthenticationWrapperState {
|
||||
context: AuthenticationContext;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||
|
||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
||||
|
||||
constructor(props: AuthenticationWrapperProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
context: {
|
||||
refresh: this.refresh,
|
||||
signIn: this.signIn,
|
||||
signOut: this.signOut,
|
||||
},
|
||||
initialized: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<AuthenticationContext.Provider value={this.state.context}>
|
||||
{this.props.children}
|
||||
</AuthenticationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderContentLoading() {
|
||||
return (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
if (!this.props.features.security) {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||
return;
|
||||
}
|
||||
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',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
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' });
|
||||
} catch (err) {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
throw new Error("Failed to parse JWT " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
signOut = () => {
|
||||
getStorage().removeItem(ACCESS_TOKEN);
|
||||
this.setState({
|
||||
context: {
|
||||
...this.state.context,
|
||||
me: undefined
|
||||
}
|
||||
});
|
||||
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
30
interface/src/authentication/UnauthenticatedRoute.tsx
Normal file
30
interface/src/authentication/UnauthenticatedRoute.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||
|
||||
public render() {
|
||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
if (authenticationContext.me) {
|
||||
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
||||
}
|
||||
return (<Component {...props} />);
|
||||
}
|
||||
return (
|
||||
<Route {...rest} render={renderComponent} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withFeatures(withAuthenticationContext(UnauthenticatedRoute));
|
6
interface/src/authentication/index.ts
Normal file
6
interface/src/authentication/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
|
||||
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
|
||||
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
|
||||
|
||||
export * from './Authentication';
|
||||
export * from './AuthenticationContext';
|
Reference in New Issue
Block a user