Skip to content

Commit

Permalink
feat: implement CreateUserPoolCommand and CreateUserPoolClientCommand
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Dec 29, 2024
1 parent 827c3b7 commit 5ec2689
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 33 deletions.
6 changes: 3 additions & 3 deletions IMPLEMENTATION_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## cognito-idp

<details>
<summary> 24% implemented </summary>
<summary> 26% implemented </summary>

- [ ] AddCustomAttributes
- [ ] AdminAddUserToGroup
Expand Down Expand Up @@ -41,8 +41,8 @@
- [ ] CreateIdentityProvider
- [ ] CreateResourceServer
- [ ] CreateUserImportJob
- [ ] CreateUserPool
- [ ] CreateUserPoolClient
- [x] CreateUserPool
- [x] CreateUserPoolClient
- [ ] CreateUserPoolDomain
- [ ] DeleteGroup
- [ ] DeleteIdentityProvider
Expand Down
2 changes: 2 additions & 0 deletions server/api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const useCases: {
'AWSCognitoIdentityProviderService.RevokeToken': authUseCase.revokeToken,
'AWSCognitoIdentityProviderService.ResendConfirmationCode': signUpUseCase.resendConfirmationCode,
'AWSCognitoIdentityProviderService.ListUserPools': userPoolUseCase.listUserPools,
'AWSCognitoIdentityProviderService.CreateUserPool': userPoolUseCase.createUserPool,
'AWSCognitoIdentityProviderService.CreateUserPoolClient': userPoolUseCase.createUserPoolClient,
'AWSCognitoIdentityProviderService.ListUsers': authUseCase.listUsers,
'AWSCognitoIdentityProviderService.AdminGetUser': adminUseCase.getUser,
'AWSCognitoIdentityProviderService.AdminCreateUser': adminUseCase.createUser,
Expand Down
13 changes: 13 additions & 0 deletions server/common/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import type {
AssociateSoftwareTokenRequest,
AssociateSoftwareTokenResponse,
CodeDeliveryDetailsType,
CreateUserPoolClientRequest,
CreateUserPoolClientResponse,
CreateUserPoolRequest,
CreateUserPoolResponse,
DeleteUserAttributesRequest,
DeleteUserAttributesResponse,
GetUserResponse,
Expand Down Expand Up @@ -61,6 +65,13 @@ export type ListUsersTarget = TargetBody<ListUsersRequest, ListUsersResponse>;

export type ListUserPoolsTarget = TargetBody<ListUserPoolsRequest, ListUserPoolsResponse>;

export type CreateUserPoolTarget = TargetBody<CreateUserPoolRequest, CreateUserPoolResponse>;

export type CreateUserPoolClientTarget = TargetBody<
CreateUserPoolClientRequest,
CreateUserPoolClientResponse
>;

export type AdminGetUserTarget = TargetBody<AdminGetUserRequest, AdminGetUserResponse>;

export type AdminCreateUserTarget = TargetBody<AdminCreateUserRequest, AdminCreateUserResponse>;
Expand Down Expand Up @@ -147,6 +158,8 @@ export type AmzTargets = {
'AWSCognitoIdentityProviderService.ResendConfirmationCode': ResendConfirmationCodeTarget;
'AWSCognitoIdentityProviderService.ListUsers': ListUsersTarget;
'AWSCognitoIdentityProviderService.ListUserPools': ListUserPoolsTarget;
'AWSCognitoIdentityProviderService.CreateUserPool': CreateUserPoolTarget;
'AWSCognitoIdentityProviderService.CreateUserPoolClient': CreateUserPoolClientTarget;
'AWSCognitoIdentityProviderService.AdminGetUser': AdminGetUserTarget;
'AWSCognitoIdentityProviderService.AdminCreateUser': AdminCreateUserTarget;
'AWSCognitoIdentityProviderService.AdminDeleteUser': AdminDeleteUserTarget;
Expand Down
2 changes: 2 additions & 0 deletions server/common/types/userPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ export type Jwks = { keys: [{ kid: string; alg: string }] };

export type UserPoolEntity = {
id: EntityId['userPool'];
name: string;
privateKey: string;
createdTime: number;
};

export type UserPoolClientEntity = {
id: EntityId['userPoolClient'];
userPoolId: EntityId['userPool'];
name: string;
createdTime: number;
};
18 changes: 14 additions & 4 deletions server/domain/userPool/model/userPoolMethod.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import type { EntityId } from 'common/types/brandedId';
import type { UserPoolClientEntity, UserPoolEntity } from 'common/types/userPool';
import { randomUUID } from 'crypto';
import { brandedId } from 'service/brandedId';
import { createShortHash } from 'service/createShortHash';
import { REGION } from 'service/envValues';
import { genPrivatekey } from 'service/privateKey';

export const userPoolMethod = {
create: (val: { id: EntityId['userPool'] }): UserPoolEntity => ({
id: val.id,
create: (val: { id?: EntityId['userPool']; name: string }): UserPoolEntity => ({
id: brandedId.userPool.entity.parse(`${REGION}_${createShortHash(randomUUID())}`),
...val,
privateKey: genPrivatekey(),
createdTime: Date.now(),
}),
createClient: (val: {
id: EntityId['userPoolClient'];
id?: EntityId['userPoolClient'];
name: string;
userPoolId: EntityId['userPool'];
}): UserPoolClientEntity => ({ id: val.id, userPoolId: val.userPoolId, createdTime: Date.now() }),
}): UserPoolClientEntity => ({
id: brandedId.userPoolClient.entity.parse(randomUUID().replace(/-/g, '')),
...val,
createdTime: Date.now(),
}),
};
7 changes: 7 additions & 0 deletions server/domain/userPool/repository/toUserPoolEntity.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import type { UserPool, UserPoolClient } from '@prisma/client';
import assert from 'assert';
import type { UserPoolClientEntity, UserPoolEntity } from 'common/types/userPool';
import { brandedId } from 'service/brandedId';

export const toUserPoolEntity = (prismaPool: UserPool): UserPoolEntity => {
assert(prismaPool.name);

return {
id: brandedId.userPool.entity.parse(prismaPool.id),
name: prismaPool.name,
privateKey: prismaPool.privateKey,
createdTime: prismaPool.createdAt.getTime(),
};
};

export const toUserPoolClientEntity = (prismaPoolClient: UserPoolClient): UserPoolClientEntity => {
assert(prismaPoolClient.name);

return {
id: brandedId.userPoolClient.entity.parse(prismaPoolClient.id),
userPoolId: brandedId.userPool.entity.parse(prismaPoolClient.userPoolId),
name: prismaPoolClient.name,
createdTime: prismaPoolClient.createdAt.getTime(),
};
};
25 changes: 17 additions & 8 deletions server/domain/userPool/repository/userPoolCommand.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import type { Prisma } from '@prisma/client';
import type { UserPoolClientEntity, UserPoolEntity } from 'common/types/userPool';
import { prismaClient } from 'service/prismaClient';

export const userPoolCommand = {
save: async (pool: UserPoolEntity): Promise<void> => {
await prismaClient.userPool.upsert({
save: async (tx: Prisma.TransactionClient, pool: UserPoolEntity): Promise<void> => {
await tx.userPool.upsert({
where: { id: pool.id },
update: { privateKey: pool.privateKey },
create: { id: pool.id, privateKey: pool.privateKey, createdAt: new Date(pool.createdTime) },
update: { name: pool.name, privateKey: pool.privateKey },
create: {
id: pool.id,
name: pool.name,
privateKey: pool.privateKey,
createdAt: new Date(pool.createdTime),
},
});
},
saveClient: async (poolClient: UserPoolClientEntity): Promise<void> => {
await prismaClient.userPoolClient.upsert({
saveClient: async (
tx: Prisma.TransactionClient,
poolClient: UserPoolClientEntity,
): Promise<void> => {
await tx.userPoolClient.upsert({
where: { id: poolClient.id },
update: {},
update: { name: poolClient.name },
create: {
id: poolClient.id,
userPoolId: poolClient.userPoolId,
name: poolClient.name,
createdAt: new Date(poolClient.createdTime),
},
});
Expand Down
71 changes: 55 additions & 16 deletions server/domain/userPool/useCase/userPoolUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
import type { ListUserPoolsTarget } from 'common/types/auth';
import assert from 'assert';
import type {
CreateUserPoolClientTarget,
CreateUserPoolTarget,
ListUserPoolsTarget,
} from 'common/types/auth';
import { DEFAULT_USER_POOL_CLIENT_ID, DEFAULT_USER_POOL_ID } from 'service/envValues';
import { prismaClient } from 'service/prismaClient';
import { prismaClient, transaction } from 'service/prismaClient';
import { userPoolMethod } from '../model/userPoolMethod';
import { userPoolCommand } from '../repository/userPoolCommand';
import { userPoolQuery } from '../repository/userPoolQuery';

export const userPoolUseCase = {
initDefaults: async (): Promise<void> => {
await userPoolQuery
.findById(prismaClient, DEFAULT_USER_POOL_ID)
.catch(() => userPoolCommand.save(userPoolMethod.create({ id: DEFAULT_USER_POOL_ID })));
initDefaults: (): Promise<void> =>
transaction(async (tx) => {
await userPoolQuery
.findById(prismaClient, DEFAULT_USER_POOL_ID)
.catch(() =>
userPoolCommand.save(
tx,
userPoolMethod.create({ id: DEFAULT_USER_POOL_ID, name: 'defaultPool' }),
),
);

await userPoolQuery.findClientById(prismaClient, DEFAULT_USER_POOL_CLIENT_ID).catch(() =>
userPoolCommand.saveClient(
userPoolMethod.createClient({
id: DEFAULT_USER_POOL_CLIENT_ID,
userPoolId: DEFAULT_USER_POOL_ID,
}),
),
);
},
await userPoolQuery.findClientById(prismaClient, DEFAULT_USER_POOL_CLIENT_ID).catch(() =>
userPoolCommand.saveClient(
tx,
userPoolMethod.createClient({
id: DEFAULT_USER_POOL_CLIENT_ID,
userPoolId: DEFAULT_USER_POOL_ID,
name: 'defaultPoolClient',
}),
),
);
}),
listUserPools: async (
req: ListUserPoolsTarget['reqBody'],
): Promise<ListUserPoolsTarget['resBody']> => {
const pools = await userPoolQuery.listAll(prismaClient, req.MaxResults);

return { UserPools: pools.map((p) => ({ Id: p.id })) };
return { UserPools: pools.map((p) => ({ Id: p.id, Name: p.name })) };
},
createUserPool: (
req: CreateUserPoolTarget['reqBody'],
): Promise<CreateUserPoolTarget['resBody']> =>
transaction(async (tx) => {
assert(req.PoolName);

const pool = userPoolMethod.create({ name: req.PoolName });
await userPoolCommand.save(tx, pool);

return { UserPool: { Id: pool.id, Name: pool.name } };
}),
createUserPoolClient: (
req: CreateUserPoolClientTarget['reqBody'],
): Promise<CreateUserPoolClientTarget['resBody']> =>
transaction(async (tx) => {
assert(req.ClientName);
assert(req.UserPoolId);

const pool = await userPoolQuery.findById(tx, req.UserPoolId);
const client = userPoolMethod.createClient({ name: req.ClientName, userPoolId: pool.id });
await userPoolCommand.saveClient(tx, client);

return {
UserPoolClient: { ClientId: client.id, UserPoolId: pool.id, ClientName: client.name },
};
}),
};
5 changes: 5 additions & 0 deletions server/prisma/migrations/20241229145939_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "UserPool" ADD COLUMN "name" TEXT;

-- AlterTable
ALTER TABLE "UserPoolClient" ADD COLUMN "name" TEXT;
2 changes: 2 additions & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ model UserAttribute {

model UserPool {
id String @id
name String?
privateKey String
createdAt DateTime
users User[]
Expand All @@ -56,6 +57,7 @@ model UserPool {

model UserPoolClient {
id String @id
name String?
createdAt DateTime
UserPool UserPool @relation(fields: [userPoolId], references: [id])
userPoolId String
Expand Down
30 changes: 29 additions & 1 deletion server/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,35 @@ const migrateUser = async (tx: Prisma.TransactionClient): Promise<void> => {
await test();
};

transaction((tx) => Promise.all([migrateUser(tx)]))
const migrateUserPool = async (tx: Prisma.TransactionClient): Promise<void> => {
const pools = await tx.userPool.findMany({ where: { name: null } });

await tx.userPool.updateMany({
where: { id: { in: pools.map((p) => p.id) } },
data: { name: 'defaultPool' },
});

const poolClients = await tx.userPoolClient.findMany({ where: { name: null } });

await tx.userPoolClient.updateMany({
where: { id: { in: poolClients.map((p) => p.id) } },
data: { name: 'defaultPoolClient' },
});

const test = async (): Promise<void> => {
const pools = await tx.userPool.findMany();

pools.forEach((pool) => assert(pool.name !== null));

const poolClients = await tx.userPoolClient.findMany();

poolClients.forEach((client) => assert(client.name !== null));
};

await test();
};

transaction((tx) => Promise.all([migrateUser(tx), migrateUserPool(tx)]))
.catch((e) => {
console.error(e);
process.exit(1);
Expand Down
38 changes: 38 additions & 0 deletions server/service/createShortHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// MIT License
//
// Copyright (c) Sindre Sorhus <[email protected]> (sindresorhus.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

const OFFSET_BASIS_32 = 2166136261;

const fnv1a = (input: string): number => {
let hash = OFFSET_BASIS_32;

for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);

// 32-bit FNV prime: 2**24 + 2**8 + 0x93 = 16777619
// Using bitshift for accuracy and performance. Numbers in JS suck.
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}

return hash >>> 0;
};

export const createShortHash = (input: string): string => fnv1a(input).toString(36);
16 changes: 16 additions & 0 deletions server/tests/sdk/pool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
CreateUserPoolClientCommand,
CreateUserPoolCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import { cognitoClient } from 'service/cognito';
import { expect, test } from 'vitest';

test(CreateUserPoolCommand.name, async () => {
const pool = await cognitoClient.send(new CreateUserPoolCommand({ PoolName: 'testPool' }));
const client = await cognitoClient.send(
new CreateUserPoolClientCommand({ ClientName: 'testClient', UserPoolId: pool.UserPool?.Id }),
);

expect(pool.UserPool?.Name === 'testPool').toBeTruthy();
expect(client.UserPoolClient?.ClientName === 'testClient').toBeTruthy();
});
1 change: 0 additions & 1 deletion server/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default defineConfig({
coverage: {
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 },
include: ['api/**/{controller,hooks,validators}.ts', 'domain/**'],
exclude: ['domain/**/model/*Entity.ts'],
},
},
});

0 comments on commit 5ec2689

Please sign in to comment.