Skip to content

Commit

Permalink
feat: implemet social users api
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Sep 13, 2024
1 parent acef34d commit 6187577
Show file tree
Hide file tree
Showing 18 changed files with 288 additions and 41 deletions.
3 changes: 2 additions & 1 deletion client/pages/oauth2/authorize.page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { OAuthConfig } from '@aws-amplify/core';
import word from '@fakerjs/word';
import { PROVIDER_LIST } from 'common/constants';
import type { MaybeId } from 'common/types/brandedId';
import { Spacer } from 'components/Spacer';
import { useRouter } from 'next/router';
Expand Down Expand Up @@ -84,7 +85,7 @@ const AddAccount = (props: { provider: string; onBack: () => void }) => {
const Authorize = () => {
const router = useRouter();
const provider = z
.enum(['Google', 'Apple', 'Amazon', 'Facebook'])
.enum(PROVIDER_LIST)
.parse((router.query.identity_provider as string).replace(/^.+([A-Z][a-z]+)$/, '$1'));
const [mode, setMode] = useState<'default' | 'add'>('default');

Expand Down
29 changes: 29 additions & 0 deletions server/api/public/socialUsers/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PROVIDER_LIST } from 'common/constants';
import { userQuery } from 'domain/user/repository/userQuery';
import { socialUseCase } from 'domain/user/useCase/socialUseCase';
import { brandedId } from 'service/brandedId';
import { prismaClient } from 'service/prismaClient';
import { z } from 'zod';
import { defineController } from './$relay';

export default defineController(() => ({
get: {
validators: { query: z.object({ userPoolClientId: brandedId.userPoolClient.maybe }) },
handler: async ({ query }) => ({
status: 200,
body: await userQuery.listSocials(prismaClient, query.userPoolClientId),
}),
},
post: {
validators: {
body: z.object({
provider: z.enum(PROVIDER_LIST),
name: z.string(),
email: z.string(),
photoUrl: z.string().optional(),
userPoolClientId: brandedId.userPoolClient.maybe,
}),
},
handler: async ({ body }) => ({ status: 200, body: await socialUseCase.createUser(body) }),
},
}));
14 changes: 14 additions & 0 deletions server/api/public/socialUsers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DefineMethods } from 'aspida';
import type { MaybeId } from 'common/types/brandedId';
import type { SocialUserCreateVal, SocialUserEntity } from 'common/types/user';

export type Methods = DefineMethods<{
get: {
query: { userPoolClientId: MaybeId['userPoolClient'] };
resBody: SocialUserEntity[];
};
post: {
reqBody: SocialUserCreateVal;
resBody: SocialUserEntity;
};
}>;
2 changes: 2 additions & 0 deletions server/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const BRANDED_ID_NAMES = [

export const USER_KIND_LIST = ['social', 'cognito'] as const;

export const PROVIDER_LIST = ['Google', 'Apple', 'Amazon', 'Facebook'] as const;

const listToDict = <T extends readonly [string, ...string[]]>(list: T): { [U in T[number]]: U } =>
list.reduce((dict, type) => ({ ...dict, [type]: type }), {} as { [U in T[number]]: U });

Expand Down
21 changes: 19 additions & 2 deletions server/common/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { UserStatusType } from '@aws-sdk/client-cognito-identity-provider';
import type { USER_KINDS } from 'common/constants';
import type { EntityId } from './brandedId';
import type { PROVIDER_LIST, USER_KINDS } from 'common/constants';
import type { EntityId, MaybeId } from './brandedId';

export type ChallengeVal = {
secretBlock: string;
Expand All @@ -19,12 +19,20 @@ export type SocialUserEntity = {
id: EntityId['socialUser'];
kind: typeof USER_KINDS.social;
name: string;
enabled: boolean;
status: typeof UserStatusType.EXTERNAL_PROVIDER;
email: string;
provider: (typeof PROVIDER_LIST)[number];
password?: undefined;
confirmationCode?: undefined;
salt?: undefined;
verifier?: undefined;
refreshToken: string;
userPoolId: EntityId['userPool'];
attributes: UserAttributeEntity[];
createdTime: number;
updatedTime: number;
challenge?: undefined;
};

export type CognitoUserEntity = {
Expand All @@ -34,6 +42,7 @@ export type CognitoUserEntity = {
enabled: boolean;
status: UserStatusType;
email: string;
provider?: undefined;
password: string;
confirmationCode: string;
salt: string;
Expand All @@ -47,3 +56,11 @@ export type CognitoUserEntity = {
};

export type UserEntity = SocialUserEntity | CognitoUserEntity;

export type SocialUserCreateVal = {
provider: (typeof PROVIDER_LIST)[number];
name: string;
email: string;
photoUrl?: string;
userPoolClientId: MaybeId['userPoolClient'];
};
4 changes: 2 additions & 2 deletions server/domain/user/model/adminMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createAttributes } from '../service/createAttributes';
import { findEmail } from '../service/findEmail';
import { genCredentials } from '../service/genCredentials';
import { validatePass } from '../service/validatePass';
import { userMethod } from './userMethod';
import { cognitoUserMethod } from './cognitoUserMethod';

export const adminMethod = {
createVerifiedUser: (
Expand All @@ -23,7 +23,7 @@ export const adminMethod = {
const email = findEmail(req.UserAttributes);

return {
...userMethod.create(idCount, {
...cognitoUserMethod.create(idCount, {
name: req.Username,
password,
email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { createAttributes } from '../service/createAttributes';
import { genCredentials } from '../service/genCredentials';
import { validatePass } from '../service/validatePass';

export const userMethod = {
export const cognitoUserMethod = {
create: (
idCount: number,
params: {
Expand Down
38 changes: 38 additions & 0 deletions server/domain/user/model/socialUserMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import assert from 'assert';
import type { EntityId } from 'common/types/brandedId';
import type { SocialUserCreateVal, SocialUserEntity } from 'common/types/user';
import { brandedId } from 'service/brandedId';
import { cognitoAssert } from 'service/cognitoAssert';
import { ulid } from 'ulid';
import { z } from 'zod';
import { createAttributes } from '../service/createAttributes';

export const socialUserMethod = {
create: (userPoolId: EntityId['userPool'], val: SocialUserCreateVal): SocialUserEntity => {
cognitoAssert(
/^[a-z][a-z\d_-]/.test(val.name),
"1 validation error detected: Value at 'username' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{M}\\p{S}\\p{N}\\p{P}]+",
);
cognitoAssert(z.string().email().parse(val.email), 'Invalid email address format.');
assert(z.string().url().optional().safeParse(val.photoUrl).success, 'Invalid photoUrl format.');

const now = Date.now();

return {
id: brandedId.socialUser.entity.parse(ulid()),
kind: 'social',
email: val.email,
provider: val.provider,
enabled: true,
status: 'EXTERNAL_PROVIDER',
name: val.name,
refreshToken: ulid(),
userPoolId,
attributes: val.photoUrl
? createAttributes([{ Name: 'picture', Value: val.photoUrl }], [])
: [],
createdTime: now,
updatedTime: now,
};
},
};
53 changes: 46 additions & 7 deletions server/domain/user/repository/toUserEntity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserStatusType } from '@aws-sdk/client-cognito-identity-provider';
import type { User, UserAttribute } from '@prisma/client';
import { USER_KINDS } from 'common/constants';
import type { CognitoUserEntity, UserAttributeEntity } from 'common/types/user';
import { PROVIDER_LIST, USER_KINDS } from 'common/constants';
import type { CognitoUserEntity, SocialUserEntity, UserAttributeEntity } from 'common/types/user';
import { brandedId } from 'service/brandedId';
import { z } from 'zod';

Expand All @@ -15,7 +15,7 @@ const getChallenge = (prismaUser: User): CognitoUserEntity['challenge'] =>
}
: undefined;

export const toUserEntity = (
export const toCognitoUserEntity = (
prismaUser: User & { attributes: UserAttribute[] },
): CognitoUserEntity => {
return {
Expand All @@ -32,11 +32,11 @@ export const toUserEntity = (
])
.parse(prismaUser.status),
email: prismaUser.email,
password: prismaUser.password,
salt: prismaUser.salt,
verifier: prismaUser.verifier,
password: z.string().parse(prismaUser.password),
salt: z.string().parse(prismaUser.salt),
verifier: z.string().parse(prismaUser.verifier),
refreshToken: prismaUser.refreshToken,
confirmationCode: prismaUser.confirmationCode,
confirmationCode: z.string().parse(prismaUser.confirmationCode),
challenge: getChallenge(prismaUser),
userPoolId: brandedId.userPool.entity.parse(prismaUser.userPoolId),
attributes: prismaUser.attributes.map(
Expand All @@ -50,3 +50,42 @@ export const toUserEntity = (
updatedTime: prismaUser.updatedAt.getTime(),
};
};

export const toSocialUserEntity = (
prismaUser: User & { attributes: UserAttribute[] },
): SocialUserEntity => {
return {
id: brandedId.socialUser.entity.parse(prismaUser.id),
kind: z.literal(USER_KINDS.social).parse(prismaUser.kind),
name: prismaUser.name,
enabled: prismaUser.enabled,
status: z.literal(UserStatusType.EXTERNAL_PROVIDER).parse(prismaUser.status),
email: prismaUser.email,
provider: z.enum(PROVIDER_LIST).parse(prismaUser.provider),
refreshToken: prismaUser.refreshToken,
userPoolId: brandedId.userPool.entity.parse(prismaUser.userPoolId),
attributes: prismaUser.attributes.map(
(attr): UserAttributeEntity => ({
id: brandedId.userAttribute.entity.parse(attr.id),
name: attr.name,
value: attr.value,
}),
),
createdTime: prismaUser.createdAt.getTime(),
updatedTime: prismaUser.updatedAt.getTime(),
};
};

// export const toUserEntity = (prismaUser: User & { attributes: UserAttribute[] }): UserEntity => {
// const kind = z.enum(USER_KIND_LIST).parse(prismaUser.kind);

// switch (kind) {
// case 'cognito':
// return toCognitoUserEntity(prismaUser);
// case 'social':
// return toSocialUserEntity(prismaUser);
// /* v8 ignore next 2 */
// default:
// throw new Error(kind satisfies never);
// }
// };
6 changes: 4 additions & 2 deletions server/domain/user/repository/userCommand.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Prisma } from '@prisma/client';
import type { EntityId } from 'common/types/brandedId';
import type { CognitoUserEntity, UserAttributeEntity } from 'common/types/user';
import type { UserAttributeEntity, UserEntity } from 'common/types/user';

export const userCommand = {
save: async (tx: Prisma.TransactionClient, user: CognitoUserEntity): Promise<void> => {
save: async (tx: Prisma.TransactionClient, user: UserEntity): Promise<void> => {
await tx.userAttribute.deleteMany({ where: { userId: user.id } });

await tx.user.upsert({
Expand All @@ -13,6 +13,7 @@ export const userCommand = {
email: user.email,
name: user.name,
enabled: user.enabled,
provider: user.provider,
status: user.status,
password: user.password,
salt: user.salt,
Expand All @@ -32,6 +33,7 @@ export const userCommand = {
email: user.email,
name: user.name,
enabled: user.enabled,
provider: user.provider,
status: user.status,
password: user.password,
salt: user.salt,
Expand Down
36 changes: 27 additions & 9 deletions server/domain/user/repository/userQuery.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
import type { Prisma } from '@prisma/client';
import type { EntityId } from 'common/types/brandedId';
import type { CognitoUserEntity } from 'common/types/user';
import { toUserEntity } from './toUserEntity';
import { USER_KINDS } from 'common/constants';
import type { EntityId, MaybeId } from 'common/types/brandedId';
import type { CognitoUserEntity, SocialUserEntity } from 'common/types/user';
import { toCognitoUserEntity, toSocialUserEntity } from './toUserEntity';

export const userQuery = {
countId: (tx: Prisma.TransactionClient, id: string): Promise<number> =>
tx.user.count({ where: { id } }),
listByPoolId: (tx: Prisma.TransactionClient, userPoolId: string): Promise<CognitoUserEntity[]> =>
listSocials: (
tx: Prisma.TransactionClient,
userPoolClientId: MaybeId['userPoolClient'],
): Promise<SocialUserEntity[]> =>
tx.user
.findMany({
where: {
UserPool: { userPoolClients: { some: { id: userPoolClientId } } },
kind: USER_KINDS.social,
},
include: { attributes: true },
})
.then((users) => users.map(toSocialUserEntity)),
listCognitos: (tx: Prisma.TransactionClient, userPoolId: string): Promise<CognitoUserEntity[]> =>
tx.user
.findMany({ where: { userPoolId }, include: { attributes: true } })
.then((users) => users.map(toUserEntity)),
.findMany({ where: { userPoolId, kind: USER_KINDS.cognito }, include: { attributes: true } })
.then((users) => users.map(toCognitoUserEntity)),
findById: (
tx: Prisma.TransactionClient,
id: EntityId['cognitoUser'],
): Promise<CognitoUserEntity> =>
tx.user.findUniqueOrThrow({ where: { id }, include: { attributes: true } }).then(toUserEntity),
tx.user
.findUniqueOrThrow({ where: { id }, include: { attributes: true } })
.then(toCognitoUserEntity),
findByName: (tx: Prisma.TransactionClient, name: string): Promise<CognitoUserEntity> =>
tx.user.findFirstOrThrow({ where: { name }, include: { attributes: true } }).then(toUserEntity),
tx.user
.findFirstOrThrow({ where: { name }, include: { attributes: true } })
.then(toCognitoUserEntity),
findByRefreshToken: (
tx: Prisma.TransactionClient,
refreshToken: string,
): Promise<CognitoUserEntity> =>
tx.user
.findFirstOrThrow({ where: { refreshToken }, include: { attributes: true } })
.then(toUserEntity),
.then(toCognitoUserEntity),
};
4 changes: 2 additions & 2 deletions server/domain/user/useCase/adminUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { brandedId } from 'service/brandedId';
import { prismaClient, transaction } from 'service/prismaClient';
import { genJwks } from 'service/privateKey';
import { adminMethod } from '../model/adminMethod';
import { userMethod } from '../model/userMethod';
import { cognitoUserMethod } from '../model/cognitoUserMethod';
import { userCommand } from '../repository/userCommand';
import { userQuery } from '../repository/userQuery';
import { toAttributeTypes } from '../service/createAttributes';
Expand Down Expand Up @@ -139,7 +139,7 @@ export const adminUseCase = {

const user = await userQuery.findByName(tx, req.Username);

await userCommand.save(tx, userMethod.deleteAttributes(user, req.UserAttributeNames));
await userCommand.save(tx, cognitoUserMethod.deleteAttributes(user, req.UserAttributeNames));

return {};
}),
Expand Down
Loading

0 comments on commit 6187577

Please sign in to comment.