Skip to content

Commit

Permalink
feat: define user kinds
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Sep 12, 2024
1 parent b600539 commit acef34d
Show file tree
Hide file tree
Showing 20 changed files with 156 additions and 71 deletions.
2 changes: 1 addition & 1 deletion client/pages/_app.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function MyApp({ Component, pageProps }: AppProps) {
email: true,
oauth: {
domain: 'localhost:5052',
scopes: ['email', 'profile'],
scopes: ['openid'],
redirectSignIn: ['http://localhost:5051'],
redirectSignOut: ['http://localhost:5051'],
responseType: 'token',
Expand Down
2 changes: 1 addition & 1 deletion client/pages/index.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const Home = () => {
<Spacer axis="y" size={24} />
<Authenticator
signUpAttributes={['email']}
socialProviders={['google']}
socialProviders={['google', 'apple', 'amazon', 'facebook']}
services={{
handleSignUp: (input) =>
signUp({
Expand Down
12 changes: 7 additions & 5 deletions client/pages/oauth2/authorize.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ export type Query = {
redirect_uri: string;
response_type: OAuthConfig['responseType'];
client_id: MaybeId['userPoolClient'];
identity_provider: OAuthConfig['providers'];
identity_provider: string;
scope: OAuthConfig['scopes'];
state: string;
};

const AddAccount = (props: { onBack: () => void }) => {
const AddAccount = (props: { provider: string; onBack: () => void }) => {
const [email, setEmail] = useState('');
const [displayName, setDisplayName] = useState('');
const [photoUrl, setPhotoUrl] = useState('');
Expand All @@ -30,7 +30,7 @@ const AddAccount = (props: { onBack: () => void }) => {
const setFakeVals = () => {
const fakeWord = word({ length: 8 });

setEmail(`${fakeWord}@magnito.com`);
setEmail(`${fakeWord}@${props.provider.toLowerCase()}.com`);
setDisplayName(fakeWord);
};

Expand Down Expand Up @@ -83,7 +83,9 @@ const AddAccount = (props: { onBack: () => void }) => {

const Authorize = () => {
const router = useRouter();
const provider = router.query.identity_provider;
const provider = z
.enum(['Google', 'Apple', 'Amazon', 'Facebook'])
.parse((router.query.identity_provider as string).replace(/^.+([A-Z][a-z]+)$/, '$1'));
const [mode, setMode] = useState<'default' | 'add'>('default');

return (
Expand All @@ -97,7 +99,7 @@ const Authorize = () => {
+ Add new account
</button>
) : (
<AddAccount onBack={() => setMode('default')} />
<AddAccount provider={provider} onBack={() => setMode('default')} />
)}
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion server/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
export const APP_NAME = 'Magnito';

export const BRANDED_ID_NAMES = [
'user',
'cognitoUser',
'socialUser',
'userAttribute',
'deletableUser',
'userPool',
'userPoolClient',
] as const;

export const USER_KIND_LIST = ['social', 'cognito'] 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 });

export const USER_KINDS = listToDict(USER_KIND_LIST);
20 changes: 18 additions & 2 deletions server/common/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UserStatusType } from '@aws-sdk/client-cognito-identity-provider';
import type { USER_KINDS } from 'common/constants';
import type { EntityId } from './brandedId';

export type ChallengeVal = {
Expand All @@ -14,8 +15,21 @@ export type UserAttributeEntity = {
value: string;
};

export type UserEntity = {
id: EntityId['user'];
export type SocialUserEntity = {
id: EntityId['socialUser'];
kind: typeof USER_KINDS.social;
name: string;
email: string;
refreshToken: string;
userPoolId: EntityId['userPool'];
attributes: UserAttributeEntity[];
createdTime: number;
updatedTime: number;
};

export type CognitoUserEntity = {
id: EntityId['cognitoUser'];
kind: typeof USER_KINDS.cognito;
name: string;
enabled: boolean;
status: UserStatusType;
Expand All @@ -31,3 +45,5 @@ export type UserEntity = {
updatedTime: number;
challenge?: ChallengeVal;
};

export type UserEntity = SocialUserEntity | CognitoUserEntity;
14 changes: 10 additions & 4 deletions server/domain/user/model/adminMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AttributeType } from '@aws-sdk/client-cognito-identity-provider';
import assert from 'assert';
import type { AdminCreateUserTarget, AdminSetUserPasswordTarget } from 'common/types/auth';
import type { EntityId } from 'common/types/brandedId';
import type { UserEntity } from 'common/types/user';
import type { CognitoUserEntity, UserEntity } from 'common/types/user';
import { brandedId } from 'service/brandedId';
import { ulid } from 'ulid';
import { createAttributes } from '../service/createAttributes';
Expand All @@ -16,7 +16,7 @@ export const adminMethod = {
idCount: number,
req: AdminCreateUserTarget['reqBody'],
userPoolId: EntityId['userPool'],
): UserEntity => {
): CognitoUserEntity => {
assert(req.Username);

const password = req.TemporaryPassword ?? `TempPass-${Date.now()}`;
Expand All @@ -38,7 +38,10 @@ export const adminMethod = {

return brandedId.deletableUser.entity.parse(user.id);
},
setUserPassword: (user: UserEntity, req: AdminSetUserPasswordTarget['reqBody']): UserEntity => {
setUserPassword: (
user: CognitoUserEntity,
req: AdminSetUserPasswordTarget['reqBody'],
): CognitoUserEntity => {
assert(req.UserPoolId);
assert(req.Password);
validatePass(req.Password);
Expand All @@ -53,7 +56,10 @@ export const adminMethod = {
updatedTime: Date.now(),
};
},
updateAttributes: (user: UserEntity, attributes: AttributeType[] | undefined): UserEntity => {
updateAttributes: (
user: CognitoUserEntity,
attributes: AttributeType[] | undefined,
): CognitoUserEntity => {
assert(attributes);
const email = attributes.find((attr) => attr.Name === 'email')?.Value ?? user.email;

Expand Down
8 changes: 4 additions & 4 deletions server/domain/user/model/signInMethod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'assert';
import type { UserSrpAuthTarget } from 'common/types/signIn';
import type { ChallengeVal, UserEntity } from 'common/types/user';
import type { ChallengeVal, CognitoUserEntity } from 'common/types/user';
import type { Jwks, UserPoolClientEntity, UserPoolEntity } from 'common/types/userPool';
import crypto from 'crypto';
import { genTokens } from 'domain/user/service/genTokens';
Expand All @@ -15,10 +15,10 @@ import { cognitoAssert } from 'service/cognitoAssert';

export const signInMethod = {
createChallenge: (
user: UserEntity,
user: CognitoUserEntity,
params: UserSrpAuthTarget['reqBody']['AuthParameters'],
): {
userWithChallenge: UserEntity;
userWithChallenge: CognitoUserEntity;
ChallengeParameters: UserSrpAuthTarget['resBody']['ChallengeParameters'];
} => {
const { B, b } = calculateSrpB(user.verifier);
Expand All @@ -37,7 +37,7 @@ export const signInMethod = {
};
},
srpAuth: (params: {
user: UserEntity;
user: CognitoUserEntity;
timestamp: string;
clientSignature: string;
jwks: Jwks;
Expand Down
34 changes: 22 additions & 12 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AttributeType } from '@aws-sdk/client-cognito-identity-provider';
import assert from 'assert';
import type { ChangePasswordTarget, VerifyUserAttributeTarget } from 'common/types/auth';
import type { EntityId } from 'common/types/brandedId';
import type { UserEntity } from 'common/types/user';
import type { CognitoUserEntity } from 'common/types/user';
import { genConfirmationCode } from 'domain/user/service/genConfirmationCode';
import { brandedId } from 'service/brandedId';
import { cognitoAssert } from 'service/cognitoAssert';
Expand All @@ -22,7 +22,7 @@ export const userMethod = {
userPoolId: EntityId['userPool'];
attributes: AttributeType[] | undefined;
},
): UserEntity => {
): CognitoUserEntity => {
assert(params.attributes);
cognitoAssert(idCount === 0, 'User already exists');
cognitoAssert(
Expand All @@ -40,7 +40,8 @@ export const userMethod = {
username: params.name,
password: params.password,
}),
id: brandedId.user.entity.parse(ulid()),
id: brandedId.cognitoUser.entity.parse(ulid()),
kind: 'cognito',
email: params.email,
enabled: true,
status: 'UNCONFIRMED',
Expand All @@ -54,7 +55,7 @@ export const userMethod = {
updatedTime: now,
};
},
confirm: (user: UserEntity, confirmationCode: string): UserEntity => {
confirm: (user: CognitoUserEntity, confirmationCode: string): CognitoUserEntity => {
cognitoAssert(
user.confirmationCode === confirmationCode,
'Invalid verification code provided, please try again.',
Expand All @@ -63,9 +64,9 @@ export const userMethod = {
return { ...user, status: 'CONFIRMED', updatedTime: Date.now() };
},
changePassword: (params: {
user: UserEntity;
user: CognitoUserEntity;
req: ChangePasswordTarget['reqBody'];
}): UserEntity => {
}): CognitoUserEntity => {
cognitoAssert(
params.user.password === params.req.PreviousPassword,
'Incorrect username or password.',
Expand All @@ -85,16 +86,16 @@ export const userMethod = {
updatedTime: Date.now(),
};
},
forgotPassword: (user: UserEntity): UserEntity => {
forgotPassword: (user: CognitoUserEntity): CognitoUserEntity => {
const confirmationCode = genConfirmationCode();

return { ...user, status: 'RESET_REQUIRED', confirmationCode, updatedTime: Date.now() };
},
confirmForgotPassword: (params: {
user: UserEntity;
user: CognitoUserEntity;
confirmationCode: string;
password: string;
}): UserEntity => {
}): CognitoUserEntity => {
const { user, confirmationCode } = params;
cognitoAssert(
user.confirmationCode === confirmationCode,
Expand All @@ -114,7 +115,10 @@ export const userMethod = {
updatedTime: Date.now(),
};
},
updateAttributes: (user: UserEntity, attributes: AttributeType[] | undefined): UserEntity => {
updateAttributes: (
user: CognitoUserEntity,
attributes: AttributeType[] | undefined,
): CognitoUserEntity => {
assert(attributes);
const email = attributes.find((attr) => attr.Name === 'email')?.Value ?? user.email;
const verified = user.email === email;
Expand All @@ -128,7 +132,10 @@ export const userMethod = {
updatedTime: Date.now(),
};
},
verifyAttribute: (user: UserEntity, req: VerifyUserAttributeTarget['reqBody']): UserEntity => {
verifyAttribute: (
user: CognitoUserEntity,
req: VerifyUserAttributeTarget['reqBody'],
): CognitoUserEntity => {
assert(req.AttributeName === 'email');
cognitoAssert(
user.confirmationCode === req.Code,
Expand All @@ -137,7 +144,10 @@ export const userMethod = {

return { ...user, status: 'CONFIRMED', updatedTime: Date.now() };
},
deleteAttributes: (user: UserEntity, attributeNames: string[] | undefined): UserEntity => {
deleteAttributes: (
user: CognitoUserEntity,
attributeNames: string[] | undefined,
): CognitoUserEntity => {
assert(attributeNames);

return {
Expand Down
16 changes: 10 additions & 6 deletions server/domain/user/repository/toUserEntity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { UserStatusType } from '@aws-sdk/client-cognito-identity-provider';
import type { User, UserAttribute } from '@prisma/client';
import type { UserAttributeEntity, UserEntity } from 'common/types/user';
import { USER_KINDS } from 'common/constants';
import type { CognitoUserEntity, UserAttributeEntity } from 'common/types/user';
import { brandedId } from 'service/brandedId';
import { z } from 'zod';

const getChallenge = (prismaUser: User): UserEntity['challenge'] =>
const getChallenge = (prismaUser: User): CognitoUserEntity['challenge'] =>
prismaUser.secretBlock && prismaUser.pubA && prismaUser.pubB && prismaUser.secB
? {
secretBlock: prismaUser.secretBlock,
Expand All @@ -14,11 +15,14 @@ const getChallenge = (prismaUser: User): UserEntity['challenge'] =>
}
: undefined;

export const toUserEntity = (prismaUser: User & { attributes: UserAttribute[] }): UserEntity => {
export const toUserEntity = (
prismaUser: User & { attributes: UserAttribute[] },
): CognitoUserEntity => {
return {
id: brandedId.user.entity.parse(prismaUser.id),
id: brandedId.cognitoUser.entity.parse(prismaUser.id),
kind: z.literal(USER_KINDS.cognito).parse(prismaUser.kind),
name: prismaUser.name,
enabled: z.boolean().parse(prismaUser.enabled),
enabled: prismaUser.enabled,
status: z
.enum([
UserStatusType.UNCONFIRMED,
Expand All @@ -43,6 +47,6 @@ export const toUserEntity = (prismaUser: User & { attributes: UserAttribute[] })
}),
),
createdTime: prismaUser.createdAt.getTime(),
updatedTime: z.number().parse(prismaUser.updatedAt?.getTime()),
updatedTime: prismaUser.updatedAt.getTime(),
};
};
6 changes: 4 additions & 2 deletions server/domain/user/repository/userCommand.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { Prisma } from '@prisma/client';
import type { EntityId } from 'common/types/brandedId';
import type { UserAttributeEntity, UserEntity } from 'common/types/user';
import type { CognitoUserEntity, UserAttributeEntity } from 'common/types/user';

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

await tx.user.upsert({
where: { id: user.id },
update: {
kind: user.kind,
email: user.email,
name: user.name,
enabled: user.enabled,
Expand All @@ -27,6 +28,7 @@ export const userCommand = {
},
create: {
id: user.id,
kind: user.kind,
email: user.email,
name: user.name,
enabled: user.enabled,
Expand Down
16 changes: 11 additions & 5 deletions server/domain/user/repository/userQuery.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import type { Prisma } from '@prisma/client';
import type { EntityId } from 'common/types/brandedId';
import type { UserEntity } from 'common/types/user';
import type { CognitoUserEntity } from 'common/types/user';
import { toUserEntity } 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<UserEntity[]> =>
listByPoolId: (tx: Prisma.TransactionClient, userPoolId: string): Promise<CognitoUserEntity[]> =>
tx.user
.findMany({ where: { userPoolId }, include: { attributes: true } })
.then((users) => users.map(toUserEntity)),
findById: (tx: Prisma.TransactionClient, id: EntityId['user']): Promise<UserEntity> =>
findById: (
tx: Prisma.TransactionClient,
id: EntityId['cognitoUser'],
): Promise<CognitoUserEntity> =>
tx.user.findUniqueOrThrow({ where: { id }, include: { attributes: true } }).then(toUserEntity),
findByName: (tx: Prisma.TransactionClient, name: string): Promise<UserEntity> =>
findByName: (tx: Prisma.TransactionClient, name: string): Promise<CognitoUserEntity> =>
tx.user.findFirstOrThrow({ where: { name }, include: { attributes: true } }).then(toUserEntity),
findByRefreshToken: (tx: Prisma.TransactionClient, refreshToken: string): Promise<UserEntity> =>
findByRefreshToken: (
tx: Prisma.TransactionClient,
refreshToken: string,
): Promise<CognitoUserEntity> =>
tx.user
.findFirstOrThrow({ where: { refreshToken }, include: { attributes: true } })
.then(toUserEntity),
Expand Down
Loading

0 comments on commit acef34d

Please sign in to comment.