Skip to content

Commit

Permalink
feat: config file support
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Apr 12, 2020
1 parent 2f9ecfc commit ad0f247
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 34 deletions.
70 changes: 57 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ The goal for this project is to be _Good Enough_ for local development use, and
- [x] Initiate Auth (Login)
- [x] Forgot Password
- [x] Confirm Forgot Password
- [x] User Migration lambda trigger (Authentication)
- [ ] User Migration lambda trigger (Forgot Password)

## Installation

Expand Down Expand Up @@ -42,6 +44,61 @@ new CognitoUserPool({

You likely only want to do this when you're running locally on your development machine.

## Configuration

You do not need to supply a config unless you need to customise the behaviour of Congito Local. If you are using Lambda triggers, you will definitely need to override `LambdaClient.endpoint` at a minimum.

Before starting Cognito Local, create a config file:

mkdir .cognito && echo '{}' > .cognito/config.json

You can edit that `.cognito/config.json` and add any of the following settings:

| Setting | Type | Default | Description |
| ------------------------------------------ | ---------- | ----------- | ----------------------------------------------------------- |
| `LambdaClient` | `object` | | Any setting you would pass to the AWS.Lambda Node.js client |
| `LambdaClient.credentials.accessKeyId` | `string` | `local` | |
| `LambdaClient.credentials.secretAccessKey` | `string` | `local` | |
| `LambdaClient.endpoint` | `string` | `local` | |
| `LambdaClient.region` | `string` | `local` | |
| `TriggerFunctions` | `object` | `{}` | Trigger name to Function name mapping |
| `TriggerFunctions.UserMigration` | `string` | | User Migration lambda name |
| `UserPoolDefaults` | `object` | | Default behaviour to use for the User Pool |
| `UserPoolDefaults.UserPoolId` | `string` | `local` | Default User Pool Id |
| `UserPoolDefaults.UsernameAttributes` | `string[]` | `["email"]` | Username alias attributes |

The default config is:

```json
{
"LambdaClient": {
"credentials": {
"accessKeyId": "local",
"secretAccessKey": "local"
},
"region": "local"
},
"TriggerFunctions": {},
"UserPoolDefaults": {
"UserPoolId": "local",
"UsernameAttributes": ["email"]
}
}
```

## Known Limitations

Many. Cognito Local only works for my exact use-case.

Issues I know about:

- The database is shared for all User Pools, if you use different User Pool IDs they will all access the same database.
- Client IDs are ignored and all connect to the same User Pool.
- Users can't be disabled
- Only `USER_PASSWORD_AUTH` flow is supported
- You can't reset your password
- Not all Lambda triggers (yet, watch this space)

## Confirmation codes

If you register a new user and they need to confirm their account, Cognito Local will write a message to the console with their confirmation code instead of emailing it to the user.
Expand All @@ -59,16 +116,3 @@ For example:
│ │
╰───────────────────────────────────────────────────────╯
```

## Known Limitations

Many. Cognito Local only works for my exact use-case.

Issues I know about:

- The database is shared for all User Pools, if you use different User Pool IDs they will all access the same database.
- Client IDs are ignored and all connect to the same User Pool.
- Users can't be disabled
- Only `USER_PASSWORD_AUTH` flow is supported
- You can't reset your password
- No Lambda triggers (yet, watch this space)
14 changes: 14 additions & 0 deletions integration-tests/dataStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ describe("Data Store", () => {
});

describe("get", () => {
it("returns entire db if no key used", async () => {
const dataStore = await createDataStore(
"example",
{ DefaultValue: true },
path
);

await dataStore.set("key", "value");

const result = await dataStore.get();

expect(result).toEqual({ DefaultValue: true, key: "value" });
});

it("returns a default", async () => {
const dataStore = await createDataStore(
"example",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"body-parser": "^1.19.0",
"boxen": "^4.2.0",
"cors": "^2.8.5",
"deepmerge": "^4.2.2",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"stormdb": "^0.3.0",
Expand Down
33 changes: 33 additions & 0 deletions src/server/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import deepmerge from "deepmerge";
import { createDataStore } from "../services/dataStore";
import { FunctionConfig } from "../services/lambda";
import { UserPoolOptions } from "../services/userPool";

export interface Config {
LambdaClient: AWS.Lambda.ClientConfiguration;
TriggerFunctions: FunctionConfig;
UserPoolDefaults: UserPoolOptions;
}

const defaults: Config = {
LambdaClient: {
credentials: {
accessKeyId: "local",
secretAccessKey: "local",
},
region: "local",
},
TriggerFunctions: {},
UserPoolDefaults: {
UserPoolId: "local",
UsernameAttributes: ["email"],
},
};

export const loadConfig = async (): Promise<Config> => {
const dataStore = await createDataStore("config", defaults, ".cognito");

const config = await dataStore.get<Config>();

return deepmerge(defaults, config ?? {});
};
14 changes: 8 additions & 6 deletions src/server/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ import { createLambda } from "../services/lambda";
import { createTriggers } from "../services/triggers";
import { createUserPool } from "../services/userPool";
import { Router } from "../targets/router";
import { loadConfig } from "./config";
import { createServer, Server } from "./server";
import * as AWS from "aws-sdk";

export const createDefaultServer = async (): Promise<Server> => {
const config = await loadConfig();

console.log("Loaded config:", config);

const userPool = await createUserPool(
{
UserPoolId: "local",
UsernameAttributes: ["email"],
},
config.UserPoolDefaults,
createDataStore
);
const lambdaClient = new AWS.Lambda({});
const lambda = createLambda({}, lambdaClient);
const lambdaClient = new AWS.Lambda(config.LambdaClient);
const lambda = createLambda(config.TriggerFunctions, lambdaClient);
const triggers = createTriggers({
lambda,
userPool,
Expand Down
8 changes: 6 additions & 2 deletions src/services/dataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { promisify } from "util";
const mkdir = promisify(fs.mkdir);

export interface DataStore {
get<T>(key: string): Promise<T | null>;
get<T>(key?: string): Promise<T | null>;
set<T>(key: string, value: T): Promise<void>;
}

Expand All @@ -30,7 +30,11 @@ export const createDataStore: CreateDataStore = async (
db.default(defaults);

return {
async get(key: string) {
async get(key?: string) {
if (!key) {
return db.value();
}

const result = await db.get(key).value();

return result ?? null;
Expand Down
21 changes: 14 additions & 7 deletions src/services/lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CognitoUserPoolEvent } from "aws-lambda";
import * as AWS from "aws-sdk";
import { InvocationResponse } from "aws-sdk/clients/lambda";
import { UnexpectedLambdaExceptionError } from "../errors";

interface UserMigrationEvent {
Expand Down Expand Up @@ -57,13 +58,19 @@ export const createLambda: CreateLambda = (config, lambdaClient) => ({
`Invoking "${lambdaName}" with event`,
JSON.stringify(lambdaEvent, undefined, 2)
);
const result = await lambdaClient
.invoke({
FunctionName: lambdaName,
InvocationType: "RequestResponse",
Payload: JSON.stringify(lambdaEvent),
})
.promise();
let result: InvocationResponse;
try {
result = await lambdaClient
.invoke({
FunctionName: lambdaName,
InvocationType: "RequestResponse",
Payload: JSON.stringify(lambdaEvent),
})
.promise();
} catch (ex) {
console.log(ex);
throw new UnexpectedLambdaExceptionError();
}

console.log(
`Lambda completed with StatusCode=${result.StatusCode} and FunctionError=${result.FunctionError}`
Expand Down
12 changes: 6 additions & 6 deletions src/services/userPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ export interface UserPool {

type UsernameAttribute = "email" | "phone_number";

interface UserPoolOptions {
UserPoolId: string;
UsernameAttributes: UsernameAttribute[];
export interface UserPoolOptions {
UserPoolId?: string;
UsernameAttributes?: UsernameAttribute[];
}

export const createUserPool = async (
options: UserPoolOptions,
createDataStore: CreateDataStore
): Promise<UserPool> => {
const dataStore = await createDataStore(options.UserPoolId, {
const dataStore = await createDataStore(options.UserPoolId ?? "local", {
Users: {},
Options: options,
});
Expand Down Expand Up @@ -61,8 +61,8 @@ export const createUserPool = async (
console.log("getUserByUsername", username);

const options = await dataStore.get<UserPoolOptions>("Options");
const aliasEmailEnabled = options?.UsernameAttributes.includes("email");
const aliasPhoneNumberEnabled = options?.UsernameAttributes.includes(
const aliasEmailEnabled = options?.UsernameAttributes?.includes("email");
const aliasPhoneNumberEnabled = options?.UsernameAttributes?.includes(
"phone_number"
);

Expand Down

0 comments on commit ad0f247

Please sign in to comment.