Skip to content

Commit

Permalink
feat(api): basic listUsers support
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed May 3, 2020
1 parent b77bb74 commit 6e0c18f
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 9 deletions.
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ The goal for this project is to be _Good Enough_ for local development use, and

## Features

- [x] Sign Up
- [x] Confirm Sign Up
- [x] Initiate Auth (Login)
- [x] JWKs verification
- [x] Forgot Password
- [x] Confirm Forgot Password
- [x] User Migration lambda trigger (Authentication)
- [ ] User Migration lambda trigger (Forgot Password)
- [x] Post Confirmation lambda trigger (ConfirmSignUp & ConfirmForgotPassword)
> At this point in time, assume any features listed below are _partially implemented_ based on @jagregory's personal use-cases. If they don't work for you, please raise an issue.
- [ConfirmForgotPassword](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmForgotPassword.html)
- [ConfirmSignUp](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmSignUp.html)
- [ForgotPassword](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ForgotPassword.html)
- [InitiateAuth](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html)
- [ListUsers](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html)
- [SignUp](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html)

Additional supported features:

- JWKs verification
- User Migration lambda trigger
- Post Confirmation lambda trigger

## Installation

Expand Down
66 changes: 66 additions & 0 deletions integration-tests/userPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,70 @@ describe("User Pool", () => {
expect(user?.Username).toEqual("1");
});
});

describe("listUsers", () => {
let userPool: UserPool;
let now: Date;

beforeAll(async () => {
now = new Date();

userPool = await createUserPool(
{ UserPoolId: "local", UsernameAttributes: [] },
tmpCreateDataStore
);

await userPool.saveUser({
Username: "1",
Password: "hunter2",
UserStatus: "UNCONFIRMED",
Attributes: [
{ Name: "email", Value: "[email protected]" },
{ Name: "phone_number", Value: "0411000111" },
],
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
Enabled: true,
});

await userPool.saveUser({
Username: "2",
Password: "password1",
UserStatus: "UNCONFIRMED",
Attributes: [],
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
Enabled: true,
});
});

it("returns all users", async () => {
const users = await userPool.listUsers();

expect(users).toEqual([
{
Username: "1",
Password: "hunter2",
UserStatus: "UNCONFIRMED",
Attributes: [
{ Name: "sub", Value: "1" },
{ Name: "email", Value: "[email protected]" },
{ Name: "phone_number", Value: "0411000111" },
],
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
Enabled: true,
},
{
Username: "2",
Password: "password1",
UserStatus: "UNCONFIRMED",
Attributes: [{ Name: "sub", Value: "2" }],
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
Enabled: true,
},
]);
});
});
});
1 change: 1 addition & 0 deletions src/services/triggers/postConfirmation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("PostConfirmation trigger", () => {
mockUserPool = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};

Expand Down
1 change: 1 addition & 0 deletions src/services/triggers/userMigration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("UserMigration trigger", () => {
mockUserPool = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};

Expand Down
8 changes: 8 additions & 0 deletions src/services/userPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface User {
export interface UserPool {
getUserByUsername(username: string): Promise<User | null>;
getUserPoolIdForClientId(clientId: string): Promise<string | null>;
listUsers(): Promise<readonly User[]>;
saveUser(user: User): Promise<void>;
}

Expand Down Expand Up @@ -106,6 +107,13 @@ export const createUserPool = async (
return null;
},

async listUsers(): Promise<readonly User[]> {
console.log("listUsers");
const users = (await dataStore.get<Record<string, User>>("Users")) ?? {};

return Object.values(users);
},

async saveUser(user) {
console.log("saveUser", user);

Expand Down
1 change: 1 addition & 0 deletions src/targets/confirmForgotPassword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("ConfirmForgotPassword target", () => {
mockDataStore = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();
Expand Down
1 change: 1 addition & 0 deletions src/targets/confirmSignUp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe("ConfirmSignUp target", () => {
mockDataStore = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();
Expand Down
1 change: 1 addition & 0 deletions src/targets/forgotPassword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("ForgotPassword target", () => {
mockDataStore = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();
Expand Down
1 change: 1 addition & 0 deletions src/targets/initiateAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe("InitiateAuth target", () => {
mockDataStore = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();
Expand Down
91 changes: 91 additions & 0 deletions src/targets/listUsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { advanceTo } from "jest-date-mock";
import { UserPool } from "../services";
import { Triggers } from "../services/triggers";
import { ListUsers, ListUsersTarget } from "./listUsers";

describe("ListUsers target", () => {
let listUsers: ListUsersTarget;
let mockDataStore: jest.Mocked<UserPool>;
let mockCodeDelivery: jest.Mock;
let mockTriggers: jest.Mocked<Triggers>;
let now: Date;

beforeEach(() => {
now = new Date(2020, 1, 2, 3, 4, 5);
advanceTo(now);

mockDataStore = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();
mockTriggers = {
enabled: jest.fn(),
postConfirmation: jest.fn(),
userMigration: jest.fn(),
};

listUsers = ListUsers({
userPool: mockDataStore,
codeDelivery: mockCodeDelivery,
triggers: mockTriggers,
});
});

it.todo("should validate UserPoolId parameter");

it("lists users and removes Cognito Local fields", async () => {
mockDataStore.listUsers.mockResolvedValue([
{
Attributes: [],
UserStatus: "CONFIRMED",
Password: "hunter2",
Username: "0000-0000",
Enabled: true,
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
ConfirmationCode: "1234",
},
{
Attributes: [],
UserStatus: "CONFIRMED",
Password: "password1",
Username: "1111-1111",
Enabled: true,
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
},
]);

const output = await listUsers({
UserPoolId: "userPoolId",
});

expect(output).toBeDefined();
expect(output.Users).toEqual([
{
Attributes: [],
UserStatus: "CONFIRMED",
Username: "0000-0000",
Enabled: true,
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
},
{
Attributes: [],
UserStatus: "CONFIRMED",
Username: "1111-1111",
Enabled: true,
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
},
]);
});

it.todo("supports AttributesToGet to specify which attributes to return");
it.todo("supports Filter to filter users before returning");
it.todo("supports Limit to specify the number of users to return");
it.todo("supports PaginationToken to paginate results");
});
43 changes: 43 additions & 0 deletions src/targets/listUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Services } from "../services";
import { UserAttribute } from "../services/userPool";

interface Input {
UserPoolId: string;
AttributesToGet?: string[]; // TODO: filter returned attributes
Filter?: string; // TODO: filter users before returning
Limit?: number; // TODO: limit number of returned users
PaginationToken?: string; // TODO: support pagination
}

export interface DynamoDBUserRecord {
Username: string;
UserCreateDate: number;
UserLastModifiedDate: number;
Enabled: boolean;
UserStatus: "CONFIRMED" | "UNCONFIRMED" | "RESET_REQUIRED";
Attributes: readonly UserAttribute[];
}

interface Output {
PaginationToken?: string;
Users: readonly DynamoDBUserRecord[];
}

export type ListUsersTarget = (body: Input) => Promise<Output>;

export const ListUsers = ({
userPool,
}: Services): ListUsersTarget => async () => {
const users = await userPool.listUsers();

return {
Users: users.map((user) => ({
Username: user.Username,
UserCreateDate: user.UserCreateDate,
UserLastModifiedDate: user.UserLastModifiedDate,
Enabled: user.Enabled,
UserStatus: user.UserStatus,
Attributes: user.Attributes,
})),
};
};
2 changes: 2 additions & 0 deletions src/targets/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { ConfirmForgotPassword } from "./confirmForgotPassword";
import { ConfirmSignUp } from "./confirmSignUp";
import { ForgotPassword } from "./forgotPassword";
import { InitiateAuth } from "./initiateAuth";
import { ListUsers } from "./listUsers";
import { SignUp } from "./signUp";

export const Targets = {
ConfirmForgotPassword,
ConfirmSignUp,
ForgotPassword,
InitiateAuth,
ListUsers,
SignUp,
};

Expand Down
1 change: 1 addition & 0 deletions src/targets/signUp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe("SignUp target", () => {
mockDataStore = {
getUserByUsername: jest.fn(),
getUserPoolIdForClientId: jest.fn(),
listUsers: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();
Expand Down

0 comments on commit 6e0c18f

Please sign in to comment.