Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gcloudignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ node_modules
/src/jupyter
/src/server/public
/src/server/config
/src/app/firebase.json
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ yarn-error.log*
/website/.cache-loader
/website/build

/src/app/firebase-debug.log
/src/app/ui-debug.log
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"install-git-hooks": "cd .git/hooks && rm -f pre-commit && ln -s ../../scripts/pre-commit.hook ./pre-commit",
"lint": "yarn rust-lint && yarn workspaces run lint",
"rust-lint": "cargo clippy",
"start:firestore": "gcloud beta emulators firestore start --host-port=127.0.0.1:8092",
"start:firestore": "(gcloud beta emulators firestore start --host-port=127.0.0.1:8092 &) && yarn workspace @system-dynamics/app run firebase emulators:start",
"start:backend": "yarn workspace @system-dynamics/server start:backend",
"start:frontend": "yarn workspace @system-dynamics/app start:frontend",
"build:gen-protobufs": "protoc --plugin='protoc-gen-ts=node_modules/.bin/protoc-gen-ts' --js_out='import_style=commonjs_strict,binary:.' --ts_out=. $(find src -name '*.proto') && sed -i 's/goog.object.extend(exports, proto);/goog.object.extend(exports, proto.project_io);/g' src/simlin-engine/src/project_io_pb.js && mv src/simlin-engine/src/*.[jt]s src/core/pb/ && yarn format",
Expand Down
5 changes: 5 additions & 0 deletions src/app/.firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "net-systemdynamics"
}
}
110 changes: 101 additions & 9 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright 2019 The Model Authors. All rights reserved.
// Copyright 2021 The Model Authors. All rights reserved.
// Use of this source code is governed by the Apache License,
// Version 2.0, that can be found in the LICENSE file.

import * as React from 'react';

import firebase from 'firebase/app';
import 'firebase/auth';
import { BrowserRouter, Route, RouteComponentProps } from 'react-router-dom';

import { createGenerateClassName, StylesProvider } from '@material-ui/styles';
Expand All @@ -19,6 +21,12 @@ import { HostedWebEditor } from '@system-dynamics/diagram/HostedWebEditor';
import { NewUser } from './NewUser';
import { User } from './User';

const config = {
apiKey: 'AIzaSyB9pGSjgGJAOIZPH0HCuofOVDNMiX-XoGI',
authDomain: 'net-systemdynamics.firebaseapp.com',
};
firebase.initializeApp(config);

const styles = createStyles({
modelApp: {
height: '100%',
Expand Down Expand Up @@ -50,7 +58,7 @@ class UserInfoSingleton {
private result?: [User | undefined, number];
constructor() {
// store this promise; we might race calling get() below, but all racers will
// await thins single fetch result.
// await this single fetch result.
this.fetch();
}

Expand All @@ -73,13 +81,15 @@ class UserInfoSingleton {
}

async invalidate(): Promise<void> {
if (this.resultPromise) {
await this.resultPromise;
this.resultPromise = undefined;
}

this.result = undefined;

const resultPromise = this.resultPromise;
this.fetch();

if (resultPromise) {
// don't leave the promise un-awaited
await resultPromise;
}
}
}

Expand All @@ -89,6 +99,8 @@ interface AppState {
authUnknown: boolean;
isNewUser?: boolean;
user?: User;
auth: firebase.auth.Auth;
firebaseIdToken?: string | null;
}
type AppProps = WithStyles<typeof styles>;

Expand All @@ -99,20 +111,89 @@ const InnerApp = withStyles(styles)(
constructor(props: AppProps) {
super(props);

const isDevServer = process.env.NODE_ENV === 'development';
const auth = firebase.auth();
if (isDevServer) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
auth.useEmulator('http://localhost:9099', { disableWarnings: true });
}

this.state = {
authUnknown: true,
auth,
};

// notify our app when a user logs in
firebase.auth().onAuthStateChanged(this.authStateChanged);

// eslint-disable-next-line @typescript-eslint/no-misused-promises
setTimeout(this.getUserInfo);
}

authStateChanged = (user: firebase.User | null) => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
setTimeout(this.asyncAuthStateChanged, undefined, user);
};

asyncAuthStateChanged = async (user: firebase.User | null) => {
if (!user) {
this.setState({ firebaseIdToken: null });
return;
}

const firebaseIdToken = await user.getIdToken();
this.setState({ firebaseIdToken });
await this.maybeLogin(undefined, firebaseIdToken);
};

async maybeLogin(authIsKnown = false, firebaseIdToken?: string): Promise<void> {
if (!(authIsKnown || !this.state.authUnknown)) {
console.log(`maybeLogin exit auth`);
return;
}

const idToken = firebaseIdToken ?? this.state.firebaseIdToken;
if (idToken === null || idToken === undefined) {
console.log(`maybeLogin exit no id`);
return;
}

const bodyContents = {
idToken,
};

const base = this.getBaseURL();
const apiPath = `${base}/session`;
const response = await fetch(apiPath, {
credentials: 'same-origin',
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(bodyContents),
});

const status = response.status;
if (!(status >= 200 && status < 400)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body = await response.json();
const errorMsg =
body && body.error ? (body.error as string) : `HTTP ${status}; maybe try a different username ¯\\_(ツ)_/¯`;
// this.appendModelError(errorMsg);
console.log(`session error: ${errorMsg}`);
return undefined;
}
}

getUserInfo = async (): Promise<void> => {
const [user, status] = await userInfo.get();
if (!(status >= 200 && status < 400) || !user) {
this.setState({
authUnknown: false,
});
await this.maybeLogin(true);
return;
}
const isNewUser = user.id.startsWith(`temp-`);
Expand All @@ -131,9 +212,20 @@ const InnerApp = withStyles(styles)(
});
};

getBaseURL(): string {
return '';
}

editor = (props: RouteComponentProps<EditorMatchParams>) => {
const { username, projectName } = props.match.params;
return <HostedWebEditor username={username} projectName={projectName} baseURL="" history={props.history} />;
return (
<HostedWebEditor
username={username}
projectName={projectName}
baseURL={this.getBaseURL()}
history={props.history}
/>
);
};

home = (props: RouteComponentProps) => {
Expand All @@ -143,7 +235,7 @@ const InnerApp = withStyles(styles)(

render() {
if (!this.state.user) {
return <Login disabled={this.state.authUnknown} />;
return <Login disabled={this.state.authUnknown} auth={this.state.auth} />;
}

if (this.state.isNewUser) {
Expand Down
46 changes: 30 additions & 16 deletions src/app/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// Copyright 2019 The Model Authors. All rights reserved.
// Copyright 2021 The Model Authors. All rights reserved.
// Use of this source code is governed by the Apache License,
// Version 2.0, that can be found in the LICENSE file.

import * as React from 'react';

import Button from '@material-ui/core/Button';
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';
import firebase from 'firebase/app';
import 'firebase/auth';

import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles';

import { ModelIcon } from '@system-dynamics/diagram/ModelIcon';

import { exists } from '@system-dynamics/core/common';

const styles = createStyles({
loginOuter: {
display: 'table',
Expand Down Expand Up @@ -39,32 +40,45 @@ const styles = createStyles({

interface LoginPropsFull extends WithStyles<typeof styles> {
disabled: boolean;
auth: firebase.auth.Auth;
}

export type LoginProps = Pick<LoginPropsFull, 'disabled' | 'auth'>;

function appleProvider(): string {
const provider = new firebase.auth.OAuthProvider('apple.com');
provider.addScope('email');
provider.addScope('name');
return provider.providerId;
}

export type LoginProps = Pick<LoginPropsFull, 'disabled'>;
const uiConfig = {
signInFlow: 'redirect',
signInSuccessUrl: '/',
signInOptions: [
appleProvider(),
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
};

export const Login = withStyles(styles)(
class Login extends React.Component<LoginPropsFull> {
loginClick = (): void => {
// eslint-disable-next-line
const location = exists(/http(s?):\/\/[^\/]*/.exec(window.location.href))[0];
window.location.href = `${location}/auth/google`;
};

render() {
const { classes } = this.props;
const disabledClass = this.props.disabled ? classes.loginDisabled : '';

const loginUI = !this.props.disabled ? (
<StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={this.props.auth} />
) : undefined;

return (
<div className={classes.loginOuter}>
<div className={classes.loginMiddle}>
<div className={classes.loginInner}>
<ModelIcon className={classes.logo} />
<br />
<div className={disabledClass}>
<Button variant="contained" color="primary" onClick={this.loginClick}>
Sign in with Google
</Button>
</div>
<div className={disabledClass}>{loginUI}</div>
</div>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/app/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"emulators": {
"auth": {
"port": 9099
},
"ui": {
"enabled": true
}
}
}
4 changes: 3 additions & 1 deletion src/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-webpack-plugin": "^2.4.1",
"file-loader": "^6.2.0",
"firebase": "^8.3.1",
"html-webpack-plugin": "^4.5.0",
"prettier": "^2.0.1",
"react-dev-utils": "^11.0.1",
"react-refresh": "^0.9.0",
"react-firebaseui": "^4.1.0",
"react-refresh": "^0.10.0",
"resolve": "^1.19.0",
"resolve-url-loader": "^3.1.2",
"terser-webpack-plugin": "^4.2.3",
Expand Down
6 changes: 3 additions & 3 deletions src/diagram/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
"react-dom": "^17.0.1",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"slate": "^0.59.0",
"slate-history": "^0.59.0",
"slate-react": "^0.59.0"
"slate": "~0.59.0",
"slate-history": "~0.59.0",
"slate-react": "~0.59.0"
},
"resolutions": {
"@types/slate*/**/immutable": "4.0.0-rc.12",
Expand Down
8 changes: 7 additions & 1 deletion src/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { IncomingMessage, ServerResponse } from 'http';

import * as admin from 'firebase-admin';
import * as bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import cors from 'cors';
Expand Down Expand Up @@ -48,9 +49,14 @@ export async function createApp(): Promise<App> {

class App {
private readonly app: Application;
private readonly authn: admin.auth.Auth;

constructor() {
this.app = (express() as any) as Application;

// initialize firebase
admin.initializeApp();
this.authn = admin.auth();
}

listen(): void {
Expand Down Expand Up @@ -196,7 +202,7 @@ class App {

this.app.use(favicon(path.join(this.app.get('public'), 'favicon.ico')));

authn(this.app);
authn(this.app, this.authn);

// authenticated:
// /api is for API requests
Expand Down
2 changes: 1 addition & 1 deletion src/server/application.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2019 The Model Authors. All rights reserved.
// Copyright 2021 The Model Authors. All rights reserved.
// Use of this source code is governed by the Apache License,
// Version 2.0, that can be found in the LICENSE file.

Expand Down
Loading