diff --git a/.gitignore b/.gitignore index 025e8b1..c48d1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,10 @@ node_modules/ -server/src/http/**/*.js -server/src/lib/**/*.js -server/test/**/*.js *.jic .arc-env sam.json sam.yaml -/server/coverage/ /client/coverage/ # react-app compiled assets (these are regengerated by re-building src/react-app/ -/server/public/ -/server/preferences.arc -!/server/public/readme.md /client/src/**/*.js /client/public/static/vendor/svg-injector/ /client/public/static/vendor/bootstrap/ diff --git a/client/package.json b/client/package.json index 01bdb0d..284acb3 100644 --- a/client/package.json +++ b/client/package.json @@ -28,7 +28,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "postbuild": "mkdir -p ../server/public ; cp -R ./build/* ../server/public/", + "postbuild": "rm -rf ../server/public ; mkdir -p ../server/public ; cp -R ./build/* ../server/public/", "test": "react-scripts test --watchAll=false", "dev": "react-scripts test --watchAll=true" }, diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..4aff00d --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,6 @@ +/src/**/*.js +/test/**/*.js +/coverage/ +/public/ +/preferences.arc +!/public/readme.md \ No newline at end of file diff --git a/server/jest.config.js b/server/jest.config.js index ba5d4b9..df796b9 100644 --- a/server/jest.config.js +++ b/server/jest.config.js @@ -7,7 +7,7 @@ module.exports = { globalTeardown: "./test/support/globalTeardown.ts", globals: { "ts-jest": { - tsconfig: "tsconfig.json", + tsconfig: "tsconfig.prod.json", }, }, collectCoverageFrom: ["src/**/*.ts", "!src/react-app/**"], diff --git a/server/package-lock.json b/server/package-lock.json index ecfe41b..3f62816 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1749,6 +1749,12 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" }, + "@tsconfig/node12": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.7.tgz", + "integrity": "sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==", + "dev": true + }, "@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", diff --git a/server/package.json b/server/package.json index 4473509..f7804db 100644 --- a/server/package.json +++ b/server/package.json @@ -8,16 +8,16 @@ "build-production": "npm run -s build-react-production && npm run -s build-server", "build-staging": "npm run -s build-react-staging && npm run -s build-server", "build-testing": "npm run -s build-production", - "build-react-staging": "pushd . && cd ../client && PUBLIC_URL=/staging npm run build && popd", + "build-react-staging": "pushd . && cd ../client && PUBLIC_URL='' npm run build && popd", "build-react-production": "pushd . && cd ../client && PUBLIC_URL='' npm run build && popd", "build-react-testing": "npm run -s build-react-production", - "build-server": "./node_modules/.bin/tsc --project tsconfig.prod.json", + "build-server": "./node_modules/.bin/arc hydrate && ./node_modules/.bin/tsc --project tsconfig.prod.json", "start": "npm run build-testing && ./node_modules/.bin/arc sandbox", "clean": "find ./src -type f -name '*.js' -delete; find ./test -type f -name '*.js' -delete", "//deploy": "PUBLIC_URL for react-app static assets. If more variables are needed, switch to using env-cmd", "deploy": "echo 'Please execute `npm run deploy-production` or `npm run deploy-staging`\n'", - "deploy-production": "PUBLIC_URL='' npm run build && ./node_modules/.bin/arc deploy", - "deploy-staging": "PUBLIC_URL='/staging' npm run build-staging && ./node_modules/.bin/arc deploy", + "deploy-production": "run build-production && ./node_modules/.bin/arc deploy production", + "deploy-staging": "npm run build-staging && ./node_modules/.bin/arc deploy staging --verbose", "test": "./node_modules/.bin/jest --coverage", "dev": "./node_modules/.bin/jest --watch" }, @@ -25,6 +25,7 @@ "license": "ISC", "devDependencies": { "@architect/architect": "^8.4.5", + "@tsconfig/node12": "^1.0.7", "@types/cookie": "^0.4.0", "@types/jest": "^26.0.19", "@types/node": "^14.14.19", diff --git a/server/src/http/get-api-echo/index.ts b/server/src/http/get-api-echo/index.ts index 405cc06..953e65d 100644 --- a/server/src/http/get-api-echo/index.ts +++ b/server/src/http/get-api-echo/index.ts @@ -1,7 +1,7 @@ import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, -} from "../../types/http" +} from "@architect/shared/types/http" import * as arc from "@architect/functions" const handlerImp = async function handlerImp( diff --git a/server/src/http/get-auth-login-000provider/index.ts b/server/src/http/get-auth-login-000provider/index.ts index d97a0ed..a78c437 100644 --- a/server/src/http/get-auth-login-000provider/index.ts +++ b/server/src/http/get-auth-login-000provider/index.ts @@ -1,4 +1,4 @@ import * as arc from "@architect/functions" -import login from "../../lib/architect/oauth/handlers/login" +import login from "@architect/shared/architect/oauth/handlers/login" export const handler = arc.http.async(login) diff --git a/server/src/http/get-auth-me/index.ts b/server/src/http/get-auth-me/index.ts index a865985..753f804 100644 --- a/server/src/http/get-auth-me/index.ts +++ b/server/src/http/get-auth-me/index.ts @@ -1,6 +1,6 @@ import * as arc from "@architect/functions" -import userRepositoryFactory from "../../lib/architect/oauth/repository/UserRepository" -import meHandlerFactory from "../../lib/architect/oauth/handlers/me" +import userRepositoryFactory from "@architect/shared/architect/oauth/repository/UserRepository" +import meHandlerFactory from "@architect/shared/architect/oauth/handlers/me" const handlerImp = meHandlerFactory(userRepositoryFactory()) export const handler = arc.http.async(handlerImp) diff --git a/server/src/http/get-auth-redirect-000provider/index.ts b/server/src/http/get-auth-redirect-000provider/index.ts index e443bcb..69e56f3 100644 --- a/server/src/http/get-auth-redirect-000provider/index.ts +++ b/server/src/http/get-auth-redirect-000provider/index.ts @@ -1,8 +1,8 @@ import * as arc from "@architect/functions" -import oAuthRedirectHandlerFactory from "../../lib/architect/oauth/handlers/redirect" -import { tokenRepositoryFactory } from "../../lib/architect/oauth/repository/TokenRepository" -import userRepositoryFactory from "../../lib/architect/oauth/repository/UserRepository" -import { fetchJson } from "../../lib/fetch" +import oAuthRedirectHandlerFactory from "@architect/shared/architect/oauth/handlers/redirect" +import { tokenRepositoryFactory } from "@architect/shared/architect/oauth/repository/TokenRepository" +import userRepositoryFactory from "@architect/shared/architect/oauth/repository/UserRepository" +import { fetchJson } from "@architect/shared/fetch" const impl = oAuthRedirectHandlerFactory( fetchJson, diff --git a/server/src/shared/README.md b/server/src/shared/README.md new file mode 100644 index 0000000..f6d0724 --- /dev/null +++ b/server/src/shared/README.md @@ -0,0 +1,4 @@ +NOTE: This is specific to Architect to allow each http handler to be bundled (since in essence each http handler is it's own completely isolated module). +See https://arc.codes/docs/en/guides/developer-experience/sharing-code + +NOTE: A potentially cleaner and less-specific approach to architect would be to put all code from shared into it's own package that is referenced by the web project itself (this is essentially what architect is doing but it kinda hides it). diff --git a/server/src/lib/Tokenater.spec.ts b/server/src/shared/Tokenater.spec.ts similarity index 96% rename from server/src/lib/Tokenater.spec.ts rename to server/src/shared/Tokenater.spec.ts index 4281db1..48846d7 100644 --- a/server/src/lib/Tokenater.spec.ts +++ b/server/src/shared/Tokenater.spec.ts @@ -61,8 +61,8 @@ describe("Tokenater", () => { describe("isValid", () => { it("should reject null/undefined tokens", async () => { - expect(ater.isValid(undefined)).toBeFalsy() - expect(ater.isValid(null)).toBeFalsy() + expect(ater.isValid((undefined as any) as string)).toBeFalsy() + expect(ater.isValid((null as any) as string)).toBeFalsy() }) it("should accept valid signature + unexpired", async () => { diff --git a/server/src/lib/Tokenater.ts b/server/src/shared/Tokenater.ts similarity index 100% rename from server/src/lib/Tokenater.ts rename to server/src/shared/Tokenater.ts diff --git a/server/src/lib/architect/middleware/csrf.spec.ts b/server/src/shared/architect/middleware/csrf.spec.ts similarity index 90% rename from server/src/lib/architect/middleware/csrf.spec.ts rename to server/src/shared/architect/middleware/csrf.spec.ts index 133a6e6..1448c0e 100644 --- a/server/src/lib/architect/middleware/csrf.spec.ts +++ b/server/src/shared/architect/middleware/csrf.spec.ts @@ -2,7 +2,7 @@ import { createMockRequest } from "../../../../test/support/architect" import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, -} from "../../../types/http" +} from "../../types/http" import { addCsrfTokenToResponse, expectCsrfTokenWithRequest, @@ -34,9 +34,11 @@ describe("csrf", () => { // add token to request: req.headers = {} - req.headers[CSRF_HEADER_NAME] = tempResponse.headers[CSRF_HEADER_NAME] + req.headers[CSRF_HEADER_NAME] = tempResponse.headers + ? tempResponse.headers[CSRF_HEADER_NAME] + : "" - // not ensure that it is accepted (request middleware will return no response if all is well): + // ensure that it is accepted (request middleware will return no response if all is well): const res = expectCsrfTokenWithRequest(req) expect(res).toBeUndefined() }) diff --git a/server/src/lib/architect/middleware/csrf.ts b/server/src/shared/architect/middleware/csrf.ts similarity index 99% rename from server/src/lib/architect/middleware/csrf.ts rename to server/src/shared/architect/middleware/csrf.ts index 3ecab94..6f6de52 100644 --- a/server/src/lib/architect/middleware/csrf.ts +++ b/server/src/shared/architect/middleware/csrf.ts @@ -2,7 +2,7 @@ import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, HttpHandler, -} from "../../../types/http" +} from "../../types/http" import Tokenater from "../../Tokenater" import { readSessionID } from "./session" diff --git a/server/src/lib/architect/middleware/session.spec.ts b/server/src/shared/architect/middleware/session.spec.ts similarity index 100% rename from server/src/lib/architect/middleware/session.spec.ts rename to server/src/shared/architect/middleware/session.spec.ts diff --git a/server/src/lib/architect/middleware/session.ts b/server/src/shared/architect/middleware/session.ts similarity index 98% rename from server/src/lib/architect/middleware/session.ts rename to server/src/shared/architect/middleware/session.ts index 3639c4b..ec7cfd9 100644 --- a/server/src/lib/architect/middleware/session.ts +++ b/server/src/shared/architect/middleware/session.ts @@ -25,7 +25,7 @@ export function writeSessionID( /** Returns the session id for the given request. Assumes the request already has a session id */ export function readSessionID(req: HttpRequestLike): string { if (!req.session || !req.session[SESSION_ID_KEY]) { - return null + return "" } return req.session[SESSION_ID_KEY] } diff --git a/server/src/lib/architect/oauth/OAuth Notes.md b/server/src/shared/architect/oauth/OAuth Notes.md similarity index 100% rename from server/src/lib/architect/oauth/OAuth Notes.md rename to server/src/shared/architect/oauth/OAuth Notes.md diff --git a/server/src/lib/architect/oauth/OAuthProviderConfig.ts b/server/src/shared/architect/oauth/OAuthProviderConfig.ts similarity index 94% rename from server/src/lib/architect/oauth/OAuthProviderConfig.ts rename to server/src/shared/architect/oauth/OAuthProviderConfig.ts index fe45308..58a3288 100644 --- a/server/src/lib/architect/oauth/OAuthProviderConfig.ts +++ b/server/src/shared/architect/oauth/OAuthProviderConfig.ts @@ -17,7 +17,7 @@ export class OAuthProviderConfig { * Returns the value of the config setting for this provider. */ public value(template: Config): string { - return process.env[this.name(template)] + return process.env[this.name(template)] || "" } /** @@ -27,7 +27,7 @@ export class OAuthProviderConfig { */ public validate(): string { const missingConfigs = this.getMissingConfigNames() - if (missingConfigs) { + if (missingConfigs.length > 0) { return ( `Provider "${this.providerName}" is not configured properly. Missing configuration: ` + missingConfigs.join(", ") @@ -54,7 +54,7 @@ export class OAuthProviderConfig { } } if (missing.length > 0) return missing - else return null + else return [] } } diff --git a/server/src/lib/architect/oauth/README.md b/server/src/shared/architect/oauth/README.md similarity index 100% rename from server/src/lib/architect/oauth/README.md rename to server/src/shared/architect/oauth/README.md diff --git a/server/src/lib/architect/oauth/handlers/common.ts b/server/src/shared/architect/oauth/handlers/common.ts similarity index 94% rename from server/src/lib/architect/oauth/handlers/common.ts rename to server/src/shared/architect/oauth/handlers/common.ts index 7ba36f6..9e05142 100644 --- a/server/src/lib/architect/oauth/handlers/common.ts +++ b/server/src/shared/architect/oauth/handlers/common.ts @@ -1,7 +1,7 @@ import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, -} from "../../../../types/http" +} from "../../../types/http" import { writeSessionID } from "../../middleware/session" import { BAD_REQUEST, INTERNAL_SERVER_ERROR } from "./httpStatus" @@ -13,14 +13,14 @@ export function getProviderName( ): [string, ArchitectHttpResponsePayload | null] { const PROVIDER_NAME_PARAM = "provider" const provider = req.pathParameters[PROVIDER_NAME_PARAM] - let err: ArchitectHttpResponsePayload = null if (!provider) { - err = errorResponse( + const err = errorResponse( BAD_REQUEST, "provider path parameter must be specified" ) + return ["", err] } - return [provider, err] + return [provider, null] } /** diff --git a/server/src/lib/architect/oauth/handlers/httpStatus.ts b/server/src/shared/architect/oauth/handlers/httpStatus.ts similarity index 100% rename from server/src/lib/architect/oauth/handlers/httpStatus.ts rename to server/src/shared/architect/oauth/handlers/httpStatus.ts diff --git a/server/src/lib/architect/oauth/handlers/login.spec.ts b/server/src/shared/architect/oauth/handlers/login.spec.ts similarity index 94% rename from server/src/lib/architect/oauth/handlers/login.spec.ts rename to server/src/shared/architect/oauth/handlers/login.spec.ts index 91ae57e..70cd257 100644 --- a/server/src/lib/architect/oauth/handlers/login.spec.ts +++ b/server/src/shared/architect/oauth/handlers/login.spec.ts @@ -1,7 +1,9 @@ import { createMockRequest } from "../../../../../test/support/architect" -import { ArchitectHttpRequestPayload } from "../../../../types/http" +import { ArchitectHttpRequestPayload } from "../../../types/http" import { readSessionID } from "../../middleware/session" import login from "./login" +import { URL } from "url" +import assert from "assert" describe("login handler", () => { // preserve environment @@ -97,9 +99,10 @@ describe("login handler", () => { const res = await login(req) expect(res).toHaveProperty("statusCode", 302) expect(res).toHaveProperty("headers.location") + assert(res.headers) const location = new URL(res.headers.location) expect(location.searchParams.get("response_type")).toEqual("code") - expect(location.searchParams.get("scope").split(" ")).toContain("openid") + expect(location.searchParams.get("scope")?.split(" ")).toContain("openid") expect(location.searchParams.get("client_id")).toEqual( process.env.OAUTH_GOO_CLIENT_ID ) @@ -120,6 +123,7 @@ describe("login handler", () => { const res = await login(req) // make sure it created a session + expect(res).toHaveProperty("statusCode", 302) const foundSession = readSessionID(res) expect(typeof foundSession).toStrictEqual("string") expect(foundSession.length).toBeGreaterThan(0) diff --git a/server/src/lib/architect/oauth/handlers/login.ts b/server/src/shared/architect/oauth/handlers/login.ts similarity index 97% rename from server/src/lib/architect/oauth/handlers/login.ts rename to server/src/shared/architect/oauth/handlers/login.ts index 1da3487..fbf5642 100644 --- a/server/src/lib/architect/oauth/handlers/login.ts +++ b/server/src/shared/architect/oauth/handlers/login.ts @@ -1,7 +1,7 @@ import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, -} from "../../../../types/http" +} from "../../../types/http" import { createCSRFToken } from "../../middleware/csrf" import { createAnonymousSessionID, @@ -10,6 +10,7 @@ import { import { OAuthProviderConfig, Config } from "../OAuthProviderConfig" import { addResponseSession, errorResponse, getProviderName } from "./common" import { BAD_REQUEST } from "./httpStatus" +import { URL } from "url" export default async function login( req: ArchitectHttpRequestPayload diff --git a/server/src/lib/architect/oauth/handlers/me.ts b/server/src/shared/architect/oauth/handlers/me.ts similarity index 93% rename from server/src/lib/architect/oauth/handlers/me.ts rename to server/src/shared/architect/oauth/handlers/me.ts index e6833ca..18308da 100644 --- a/server/src/lib/architect/oauth/handlers/me.ts +++ b/server/src/shared/architect/oauth/handlers/me.ts @@ -2,7 +2,7 @@ import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, HttpHandler, -} from "../../../../types/http" +} from "../../../types/http" import { readSessionID } from "../../middleware/session" import { StoredUser, UserRepository } from "../repository/UserRepository" @@ -24,7 +24,7 @@ export default function meHandlerFactory( statusCode: STATUS.UNAUTHENTICATED, } } - const user: StoredUser = await userRepository.get(sessionID) + const user = await userRepository.get(sessionID) if (!user) { return { statusCode: STATUS.NOT_FOUND, diff --git a/server/src/lib/architect/oauth/handlers/redirect.spec.ts b/server/src/shared/architect/oauth/handlers/redirect.spec.ts similarity index 97% rename from server/src/lib/architect/oauth/handlers/redirect.spec.ts rename to server/src/shared/architect/oauth/handlers/redirect.spec.ts index cba9b21..7a734d1 100644 --- a/server/src/lib/architect/oauth/handlers/redirect.spec.ts +++ b/server/src/shared/architect/oauth/handlers/redirect.spec.ts @@ -1,6 +1,6 @@ import { randomBytes } from "crypto" import { createMockRequest } from "../../../../../test/support/architect" -import { ArchitectHttpRequestPayload } from "../../../../types/http" +import { ArchitectHttpRequestPayload } from "../../../types/http" import { createCSRFToken } from "../../middleware/csrf" import { createAnonymousSessionID, @@ -13,6 +13,7 @@ import oAuthRedirectHandlerFactory from "./redirect" import * as jwt from "node-webtokens" import { randomEmail, randomInt } from "../../../../../test/support" import sinon from "sinon" +import { URL } from "url" // note to self: Jest's auto-mocking voodoo wastes more time than it saves. Just inject dependencies (e.g. w/ oAuthRedirectHandlerFactory) @@ -222,6 +223,7 @@ describe("redirect", () => { // NOTE: could mock the userRepo and just return a new user with an ID, to not need a functioning userRepo, but this works for now. const newUser = await userRepo.getFromEmail(email) + if (!newUser) throw new Error("newUser must not be null") expect(tokenRepoUpsert.callCount).toEqual(1) const actualToken = tokenRepoUpsert.firstCall.args[0] @@ -273,11 +275,11 @@ describe("redirect", () => { expect(res).toHaveProperty("statusCode", 302) expect(res).toHaveProperty("headers.location", "/") - // invoke handler for staging (/staging) + // invoke handler for staging (it used to be deployed to /staging, but we deploy everything to / now. Consider removing this test) process.env.NODE_ENV = "staging" res = await oauthRedirectHandler(req) expect(res).toHaveProperty("statusCode", 302) - expect(res).toHaveProperty("headers.location", "/staging") + expect(res).toHaveProperty("headers.location", "/") }) }) diff --git a/server/src/lib/architect/oauth/handlers/redirect.ts b/server/src/shared/architect/oauth/handlers/redirect.ts similarity index 91% rename from server/src/lib/architect/oauth/handlers/redirect.ts rename to server/src/shared/architect/oauth/handlers/redirect.ts index 8ec8bf5..934f08e 100644 --- a/server/src/lib/architect/oauth/handlers/redirect.ts +++ b/server/src/shared/architect/oauth/handlers/redirect.ts @@ -2,7 +2,7 @@ import { ArchitectHttpRequestPayload, ArchitectHttpResponsePayload, HttpHandler, -} from "../../../../types/http" +} from "../../../types/http" import { fetchJson as fetchJsonImpl, FetchJsonFunc } from "../../../fetch" import { isTokenValid } from "../../middleware/csrf" import { readSessionID } from "../../middleware/session" @@ -16,7 +16,8 @@ import { } from "./httpStatus" import * as jwt from "node-webtokens" import { assert } from "console" -import { addResponseSession, errorResponse } from "./common" +import { addResponseSession, errorResponse, getProviderName } from "./common" +import { URL } from "url" /** * Factory to create a handler for the [Authorization Response](https://tools.ietf.org/html/rfc6749#section-4.1.2) when the user is directed with a `code` from the OAuth Authorization Server back to the OAuth client application. @@ -97,7 +98,7 @@ export default function oAuthRedirectHandlerFactory( //TODO: consider looking for `email_verified: true` in response. Is that OIDC standard claim? // create user (if they don't exist already): - let user: StoredUser = await userRepository.getFromEmail( + let user: StoredUser | null = await userRepository.getFromEmail( parsed.payload.email ) if (!user) { @@ -125,21 +126,6 @@ export default function oAuthRedirectHandlerFactory( return oauthRedirectHandler } -function getProviderName( - req: ArchitectHttpRequestPayload -): [string, ArchitectHttpResponsePayload | null] { - const PROVIDER_NAME_PARAM = "provider" - const provider = req.pathParameters[PROVIDER_NAME_PARAM] - let err: ArchitectHttpResponsePayload = null - if (!provider) { - err = errorResponse( - BAD_REQUEST, - "provider path parameter must be specified" - ) - } - return [provider, err] -} - function addResponseHeaders( res: ArchitectHttpResponsePayload ): ArchitectHttpResponsePayload { @@ -147,7 +133,7 @@ function addResponseHeaders( ...res, headers: { ...res.headers, - location: process.env.NODE_ENV === "staging" ? "/staging" : "/", + location: "/", }, } } @@ -180,7 +166,7 @@ function handleProviderErrors( return null } // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - const unauthorizedErrorMap = { + const unauthorizedErrorMap: Record = { access_denied: "The resource owner or authorization server denied the request (access_denied).", unauthorized_client: diff --git a/server/src/lib/architect/oauth/repository/Repository.ts b/server/src/shared/architect/oauth/repository/Repository.ts similarity index 84% rename from server/src/lib/architect/oauth/repository/Repository.ts rename to server/src/shared/architect/oauth/repository/Repository.ts index 946c8f6..813fc2b 100644 --- a/server/src/lib/architect/oauth/repository/Repository.ts +++ b/server/src/shared/architect/oauth/repository/Repository.ts @@ -5,8 +5,8 @@ import { v4 as uuidv4 } from "uuid" import assert from "assert" export default abstract class Repository { - private _ddb: DocumentClient - private _tableName: string + private _ddb?: DocumentClient = undefined + private _tableName?: string = undefined private _didInit: boolean = false protected constructor(protected readonly tableNickname: string) { @@ -17,11 +17,13 @@ export default abstract class Repository { protected async getTableName(): Promise { await this.ensureInitialized() + assert(this._tableName, "_tableName not initialized") return this._tableName } protected async getDDB(): Promise { await this.ensureInitialized() + assert(this._ddb, "_ddb not initialized") return this._ddb } @@ -48,10 +50,10 @@ export default abstract class Repository { } as T // NOTE: We're trusting the caller to make sure that the proposedItem has every item of T except the ones omitted in the type definition const putParams = { - TableName: this._tableName, + TableName: await this.getTableName(), Item: storedItem, } - await this._ddb.put(putParams).promise() + await (await this.getDDB()).put(putParams).promise() return storedItem } catch (err) { throw new Error("Repository.addItem error: " + err) @@ -77,9 +79,9 @@ export default abstract class Repository { protected async listItems(): Promise> { await this.ensureInitialized() try { - const scanned = await this._ddb + const scanned = await (await this.getDDB()) .scan({ - TableName: this._tableName, + TableName: await this.getTableName(), }) .promise() // TODO: need to fix this. See Alert Genie for some examples of doing this more cleanly with an Iterable. @@ -97,10 +99,10 @@ export default abstract class Repository { await this.ensureInitialized() try { const params = { - TableName: this._tableName, + TableName: await this.getTableName(), Key: { id: id }, } - await this._ddb.delete(params).promise() + await (await this.getDDB()).delete(params).promise() } catch (err) { throw new Error("Repository.delete error: " + err) } @@ -108,8 +110,8 @@ export default abstract class Repository { protected async scan(): Promise { await this.ensureInitialized() - const scanned = await this._ddb - .scan({ TableName: this._tableName }) + const scanned = await (await this.getDDB()) + .scan({ TableName: await this.getTableName() }) .promise() return scanned.Items as T[] } diff --git a/server/src/lib/architect/oauth/repository/StoredItem.ts b/server/src/shared/architect/oauth/repository/StoredItem.ts similarity index 100% rename from server/src/lib/architect/oauth/repository/StoredItem.ts rename to server/src/shared/architect/oauth/repository/StoredItem.ts diff --git a/server/src/lib/architect/oauth/repository/TokenRepository.spec.ts b/server/src/shared/architect/oauth/repository/TokenRepository.spec.ts similarity index 73% rename from server/src/lib/architect/oauth/repository/TokenRepository.spec.ts rename to server/src/shared/architect/oauth/repository/TokenRepository.spec.ts index ba6cc07..4d5e22b 100644 --- a/server/src/lib/architect/oauth/repository/TokenRepository.spec.ts +++ b/server/src/shared/architect/oauth/repository/TokenRepository.spec.ts @@ -6,7 +6,7 @@ import { tokenRepositoryFactory, } from "./TokenRepository" -let repo: TokenRepository = null +let repo: TokenRepository beforeEach(() => { repo = tokenRepositoryFactory() @@ -55,17 +55,17 @@ describe("get", () => { }) it("should reject if missing args", async () => { - await expect(repo.get(null, "something")).rejects.toThrowError( - /userID must be provided/ - ) - await expect(repo.get("something", null)).rejects.toThrowError( - /provider must be provided/ - ) + await expect( + repo.get((null as any) as string, "something") + ).rejects.toThrowError(/userID must be provided/) + await expect( + repo.get("something", (null as any) as string) + ).rejects.toThrowError(/provider must be provided/) }) }) -function expectStrictTokenProps(user: StoredToken): void { - const expectedUserProps = { +function expectStrictTokenProps(actual: StoredToken): void { + const expectedProps: Record = { id: "string", userID: "string", provider: "string", @@ -76,12 +76,16 @@ function expectStrictTokenProps(user: StoredToken): void { updatedAt: "number", } // make sure the returned object has exactly these props: - expect(new Set(Reflect.ownKeys(expectedUserProps))).toEqual( - new Set(Reflect.ownKeys(user)) + expect(new Set(Reflect.ownKeys(expectedProps))).toEqual( + new Set(Reflect.ownKeys(actual)) ) - for (const propName in expectedUserProps) { - expect(typeof user[propName]).toEqual(expectedUserProps[propName]) + for (const propName in expectedProps) { + const rec: Record = (actual as any) as Record< + string, + string + > + expect(typeof rec[propName]).toEqual(expectedProps[propName]) } } diff --git a/server/src/lib/architect/oauth/repository/TokenRepository.ts b/server/src/shared/architect/oauth/repository/TokenRepository.ts similarity index 100% rename from server/src/lib/architect/oauth/repository/TokenRepository.ts rename to server/src/shared/architect/oauth/repository/TokenRepository.ts diff --git a/server/src/lib/architect/oauth/repository/UserRepository.spec.ts b/server/src/shared/architect/oauth/repository/UserRepository.spec.ts similarity index 79% rename from server/src/lib/architect/oauth/repository/UserRepository.spec.ts rename to server/src/shared/architect/oauth/repository/UserRepository.spec.ts index bf0c68a..11721be 100644 --- a/server/src/lib/architect/oauth/repository/UserRepository.spec.ts +++ b/server/src/shared/architect/oauth/repository/UserRepository.spec.ts @@ -5,7 +5,7 @@ import userRepositoryFactory, { } from "./UserRepository" import { randomEmail } from "../../../../../test/support" -let users: UserRepository = null +let users: UserRepository beforeEach(() => { users = userRepositoryFactory() @@ -41,7 +41,7 @@ describe("getUserFromEmail", () => { await users.add(user) const found = await users.getFromEmail(user.email) expect(found).toHaveProperty("email", user.email) - expectStrictUserProps(found) + expectStrictUserProps(found as StoredUser) }) it("should return null when user not found", async () => { @@ -60,18 +60,22 @@ describe("listUsers", () => { }) }) -function expectStrictUserProps(user: StoredUser): void { - const expectedUserProps = { +function expectStrictUserProps(actual: StoredUser): void { + const expectedProps: Record = { email: "string", id: "string", createdAt: "number", updatedAt: "number", } // make sure the returned object has exactly these props: - expect(Reflect.ownKeys(expectedUserProps)).toEqual(Reflect.ownKeys(user)) + expect(Reflect.ownKeys(expectedProps)).toEqual(Reflect.ownKeys(actual)) - for (const propName in expectedUserProps) { - expect(typeof user[propName]).toEqual(expectedUserProps[propName]) + for (const propName in expectedProps) { + const rec: Record = (actual as any) as Record< + string, + string + > + expect(typeof rec[propName]).toEqual(expectedProps[propName]) } } diff --git a/server/src/lib/architect/oauth/repository/UserRepository.ts b/server/src/shared/architect/oauth/repository/UserRepository.ts similarity index 95% rename from server/src/lib/architect/oauth/repository/UserRepository.ts rename to server/src/shared/architect/oauth/repository/UserRepository.ts index 4e109cd..d0430d0 100644 --- a/server/src/lib/architect/oauth/repository/UserRepository.ts +++ b/server/src/shared/architect/oauth/repository/UserRepository.ts @@ -12,8 +12,8 @@ export interface UserRepository { * @param user The user to store or update. */ add(user: StoredUserProposal): Promise - getFromEmail(email: string): Promise - get(email: string): Promise + getFromEmail(email: string): Promise + get(email: string): Promise list(): Promise> delete(userID: string): Promise } diff --git a/server/src/lib/fetch.ts b/server/src/shared/fetch.ts similarity index 100% rename from server/src/lib/fetch.ts rename to server/src/shared/fetch.ts diff --git a/server/src/shared/types/architect__functions.d.ts b/server/src/shared/types/architect__functions.d.ts new file mode 100644 index 0000000..c97d163 --- /dev/null +++ b/server/src/shared/types/architect__functions.d.ts @@ -0,0 +1 @@ +declare module "@architect/functions" diff --git a/server/src/shared/types/architect__sandbox.d.ts b/server/src/shared/types/architect__sandbox.d.ts new file mode 100644 index 0000000..5df06b4 --- /dev/null +++ b/server/src/shared/types/architect__sandbox.d.ts @@ -0,0 +1 @@ +declare module "@architect/sandbox" diff --git a/server/src/types/http.d.ts b/server/src/shared/types/http.d.ts similarity index 100% rename from server/src/types/http.d.ts rename to server/src/shared/types/http.d.ts diff --git a/server/src/shared/types/node-webtokens.d.ts b/server/src/shared/types/node-webtokens.d.ts new file mode 100644 index 0000000..ec1d7d3 --- /dev/null +++ b/server/src/shared/types/node-webtokens.d.ts @@ -0,0 +1 @@ +declare module "node-webtokens" diff --git a/server/tsc b/server/tsc new file mode 120000 index 0000000..b64257c --- /dev/null +++ b/server/tsc @@ -0,0 +1 @@ +./node_modules/.bin/tsc \ No newline at end of file diff --git a/server/tsconfig.base.json b/server/tsconfig.base.json index 6a90794..80f306f 100644 --- a/server/tsconfig.base.json +++ b/server/tsconfig.base.json @@ -1,9 +1,5 @@ { - "compilerOptions": { - "target": "esnext", - "module": "commonjs", - "moduleResolution": "node", - "esModuleInterop": true - }, + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node12/tsconfig.json", "include": ["src/**/*.ts"] } diff --git a/server/tsconfig.json b/server/tsconfig.json index ffcbb94..57bb67f 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,3 +1,4 @@ { + "$schema": "https://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.json" } diff --git a/server/tsconfig.prod.json b/server/tsconfig.prod.json index 15cc508..5b11f66 100644 --- a/server/tsconfig.prod.json +++ b/server/tsconfig.prod.json @@ -1,6 +1,7 @@ { + "$schema": "https://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.json", - // We include this here because the build for tsconfig is dumping all the *.spec.js files too. B ut if you remove them here then VSCode doesn't parse spec files right anymore. + // We include this here because the build for tsconfig is dumping all the *.spec.js files too. But if you remove them in tsconfig.json then VSCode doesn't parse spec files right anymore. // TODO: probably worth trying to build into a dist/ directory and deploy that! "exclude": ["node_modules", "src/**/*.spec.ts"] }