Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
vlad-ignatov committed Oct 16, 2022
1 parent d12f499 commit 833e7c2
Show file tree
Hide file tree
Showing 131 changed files with 122,980 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {

// Specify "require" for CommonJS
require: "ts-node/register",

// Specify "loader" for native ESM
// loader: "ts-node/esm",

extensions: ["ts"],

// watch: true,

"watch-files": [
"backend",
"test",
"src/isomorphic"
],

spec: [
"./test/unit/**/*.test.ts",
"./test/integration/**/*.test.ts"
],


// ignore: ["tests/import.test.js"],
// parallel: true,
timeout: 5000, // defaults to 2000ms; increase if needed
checkLeaks: true
}
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"cSpell.words": [
"fhir",
"fhirclient",
"getpages",
"getpagesoffset",
"glyphicon"
]
}
64 changes: 64 additions & 0 deletions backend/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import FS from "fs"

const { env } = process


export default {

/**
* The port to listen on. If not set defaults to 8443
*/
port: env.LAUNCHER_PORT || env.PORT || 8443,

/**
* The host to listen on. If not set defaults to "localhost"
*/
host: env.HOST || "localhost",

/**
* We use this to sign our tokens
*/
jwtSecret: env.SECRET || "this is a secret",

/**
* The base URL of the R2 FHIR server (if any)
*/
fhirServerR2: env.FHIR_SERVER_R2 || "",

/**
* The base URL of the R3 FHIR server (if any)
*/
fhirServerR3: env.FHIR_SERVER_R3 || "",

/**
* The base URL of the R4 FHIR server (if any)
*/
fhirServerR4: env.FHIR_SERVER_R4 || "",

/**
* Default access token lifetime in minutes
*/
accessTokenLifetime: env.ACCESS_TOKEN_LIFETIME || 60,

/**
* Default refresh token lifetime in minutes
*/
refreshTokenLifeTime : env.REFRESH_TOKEN_LIFETIME || 60 * 24 * 365,

/**
* Accept JWKs using the following algorithms
*/
supportedAlgorithms: ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"],

/**
* Whether to include encounter in standalone launch context. Note that if
* this is false, encounter will not be included even if "launch/encounter"
* scope is requested
*/
includeEncounterContextInStandaloneLaunch: true,

/**
* Our private key as PEM (used to generate the JWKS at /keys)
*/
privateKeyAsPem: FS.readFileSync(__dirname + "/../private-key.pem", "utf8")
}
94 changes: 94 additions & 0 deletions backend/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Request, Response } from "express"
import { format } from "node:util"

export class HttpError extends Error
{
code = 400

constructor(message: string, ...args: any[]) {
super(format(message, ...args))
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
}

status(statusCode: number) {
this.code = statusCode
return this
}

render(req: Request, res: Response) {
res.status(this.code).type("text").end(this.message)
}
}

export class OAuthError extends HttpError
{
id: string = "invalid_request"

code = 302

constructor(message: string, ...args: any[]) {
super(message, ...args)
}

errorId(id: string) {
this.id = id
return this
}

render(req: Request, res: Response) {
const redirectUri = req.body ?
String(req.body.redirect_uri || "") :
String(req.query.redirect_uri || "");

const isRedirectCode = [301, 302, 303, 307, 308].includes(this.code)

if (redirectUri && isRedirectCode) {
// console.log("%o", redirectUri)
let redirectURL = new URL(redirectUri);
redirectURL.searchParams.set("error", this.id);
redirectURL.searchParams.set("error_description", this.message);
if (req.query.state) {
redirectURL.searchParams.set("state", req.query.state + "");
}
return res.redirect(this.code, redirectURL.href);
}

if (!redirectUri && isRedirectCode) {
this.code = 400
}

return res.status(this.code).json({
error: this.id,
error_description: this.message
});
}
}

// export class InvalidParamError extends HttpError
// {
// constructor(paramName: string, paramValue: string, ...args: any[]) {
// super(`Invalid parameter value "%s" for parameter "%s"`, paramValue, paramName, ...args)
// }
// }

// export class MissingParamError extends HttpError
// {
// constructor(paramName: string, ...args: any[]) {
// super(`Missing parameter "%s"`, paramName, ...args)
// }
// }

export class InvalidRequestError extends OAuthError
{
id = "invalid_request"
}

export class InvalidScopeError extends OAuthError
{
id = "invalid_scope"
}

export class InvalidClientError extends OAuthError
{
id = "invalid_client"
}
99 changes: 99 additions & 0 deletions backend/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Path from "path"
import FS from "fs"
import express from "express"
import cors from "cors"
import jose from "node-jose"
import config from "./config"
import fhirServer from "./routes/fhir"
import authServer from "./routes/auth"
import launcher from "./routes/launcher"
import generator from "./routes/generator"
import { bool } from "./lib"
import { globalErrorHandler } from "./middlewares"


const app = express()

// CORS everywhere :)
app.use(cors({ origin: true, credentials: true }))

app.use(express.static(Path.join(__dirname, '../build/')));

// Auth server
app.use(["/v/:fhir_release/sim/:sim/auth", "/v/:fhir_release/auth"], authServer)

// FHIR servers
app.use(["/v/:fhir_release/sim/:sim/fhir", "/v/:fhir_release/fhir"], fhirServer)

// The launcher endpoint
app.get("/launcher", launcher);

// Host public keys for backend services JWKS auth
app.get("/keys", async (req, res) => {
const key = await jose.JWK.asKey(config.privateKeyAsPem, "pem", { alg: "RS256", key_ops: ["verify"] })
res.json(key.keystore.toJSON(false));
});

// Also host the public key as PEM
app.get("/public_key", (req, res) => {
FS.readFile(__dirname + "/../public-key.pem", "utf8", (err, key) => {
if (err) {
return res.status(500).end("Failed to read public key");
}
res.type("text").send(key);
});
});

// generate random strings or RS384 JWKs
app.use("/generator", generator);

// Provide some env variables to the frontend
app.use("/env.js", (req, res) => {
const out = {
NODE_ENV : process.env.NODE_ENV || "production",
PICKER_ORIGIN : process.env.PICKER_ORIGIN || "https://patient-browser.smarthealthit.org",

DISABLE_BACKEND_SERVICES: bool(process.env.DISABLE_BACKEND_SERVICES),
GOOGLE_ANALYTICS_ID : process.env.GOOGLE_ANALYTICS_ID,
CDS_SANDBOX_URL : process.env.CDS_SANDBOX_URL,

FHIR_SERVER_R2 : process.env.FHIR_SERVER_R2 || "",
FHIR_SERVER_R3 : process.env.FHIR_SERVER_R3 || "",
FHIR_SERVER_R4 : process.env.FHIR_SERVER_R4 || "",
};

res.type("application/javascript").send(`var ENV = ${JSON.stringify(out, null, 4)};`);
});

// React app - redirect all to ./build/index.html
app.get("*", (req, res) => res.sendFile("index.html", { root: "./build" }));

// Catch all errors
app.use(globalErrorHandler)

// Start the server if ran directly (tests import it and start it manually)
/* istanbul ignore if */
if (require.main?.filename === __filename) {
app.listen(+config.port, config.host, () => {
console.log(`SMART launcher listening on port ${config.port}!`)
});

if (process.env.SSL_PORT) {
require('pem').createCertificate({
days: 100,
selfSigned: true
}, (err: Error, keys: any) => {
if (err) {
throw err
}
require("https").createServer({
key : keys.serviceKey,
cert: keys.certificate
}, app).listen(process.env.SSL_PORT, config.host, () => {
console.log(`SMART launcher listening on port ${process.env.SSL_PORT}!`)
});
});
}
}

export default app
Loading

0 comments on commit 833e7c2

Please sign in to comment.