From 08a0a6748b6434b6296480f353c3747e35907f88 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 15 Sep 2023 11:16:47 +0800 Subject: [PATCH] feat(schemas): add user verifications column (#4480) --- packages/core/src/__mocks__/user.ts | 7 +++ packages/core/src/queries/user.test.ts | 2 + .../next-1694746763-user-verifications.ts | 20 +++++++ .../schemas/src/foundations/jsonb-types.ts | 57 +++++++++++++++---- packages/schemas/tables/users.sql | 1 + 5 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 packages/schemas/alterations/next-1694746763-user-verifications.ts diff --git a/packages/core/src/__mocks__/user.ts b/packages/core/src/__mocks__/user.ts index 195eacc53..67ed0ffe1 100644 --- a/packages/core/src/__mocks__/user.ts +++ b/packages/core/src/__mocks__/user.ts @@ -15,6 +15,7 @@ export const mockUser: User = { identities: { connector1: { userId: 'connector1', details: {} }, }, + mfaVerifications: [], customData: {}, applicationId: 'bar', lastSignInAt: 1_650_969_465_789, @@ -39,6 +40,7 @@ export const mockUserWithPassword: User = { connector1: { userId: 'connector1', details: {} }, }, customData: {}, + mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_789, createdAt: 1_650_969_000_000, @@ -58,6 +60,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, createdAt: 1_650_969_000_000, @@ -75,6 +78,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, createdAt: 1_650_969_000_000, @@ -92,6 +96,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, createdAt: 1_650_969_000_000, @@ -109,6 +114,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, createdAt: 1_650_969_000_000, @@ -126,6 +132,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, createdAt: 1_650_969_000_000, diff --git a/packages/core/src/queries/user.test.ts b/packages/core/src/queries/user.test.ts index 4c5f68559..6cd7daeef 100644 --- a/packages/core/src/queries/user.test.ts +++ b/packages/core/src/queries/user.test.ts @@ -39,6 +39,7 @@ describe('user query', () => { ...mockUser, identities: JSON.stringify(mockUser.identities), customData: JSON.stringify(mockUser.customData), + mfaVerifications: JSON.stringify(mockUser.mfaVerifications), }; it('findUserByUsername', async () => { @@ -271,6 +272,7 @@ describe('user query', () => { ...mockUser, identities: JSON.stringify(restIdentities), customData: JSON.stringify(mockUser.customData), + mfaVerifications: JSON.stringify(mockUser.mfaVerifications), }; const expectSql = sql` diff --git a/packages/schemas/alterations/next-1694746763-user-verifications.ts b/packages/schemas/alterations/next-1694746763-user-verifications.ts new file mode 100644 index 000000000..e0b8038b5 --- /dev/null +++ b/packages/schemas/alterations/next-1694746763-user-verifications.ts @@ -0,0 +1,20 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table users + add column if not exists mfa_verifications jsonb not null default '[]'::jsonb; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table users + drop column mfa_verifications; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 83033b9e1..a367cc7cb 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -92,18 +92,6 @@ export const customClientMetadataGuard = z.object({ */ export type CustomClientMetadata = z.infer; -/* === Users === */ -export const roleNamesGuard = z.string().array(); - -const identityGuard = z.object({ - userId: z.string(), - details: z.object({}).optional(), // Connector's userinfo details, schemaless -}); -export const identitiesGuard = z.record(identityGuard); - -export type Identity = z.infer; -export type Identities = z.infer; - /* === SignIn Experiences === */ export const colorGuard = z.object({ @@ -186,6 +174,51 @@ export const mfaGuard = z.object({ export type Mfa = z.infer; +/* === Users === */ +export const roleNamesGuard = z.string().array(); + +const identityGuard = z.object({ + userId: z.string(), + details: z.object({}).optional(), // Connector's userinfo details, schemaless +}); +export const identitiesGuard = z.record(identityGuard); + +export type Identity = z.infer; +export type Identities = z.infer; + +const baseMfaVerification = { + id: z.string(), + createdAt: z.date(), +}; + +const mfaVerificationGuard = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(MfaFactor.TOTP), + ...baseMfaVerification, + key: z.string(), + }), + z.object({ + type: z.literal(MfaFactor.WebAuthn), + ...baseMfaVerification, + credentialId: z.string(), + publicKey: z.string(), + counter: z.number(), + agent: z.string(), + }), + z.object({ + type: z.literal(MfaFactor.BackupCode), + ...baseMfaVerification, + code: z.string(), + usedAt: z.date().optional(), + }), +]); + +export type MfaVerification = z.infer; + +export const mfaVerificationsGuard = mfaVerificationGuard.array(); + +export type MfaVerifications = z.infer; + /* === Phrases === */ export type Translation = { diff --git a/packages/schemas/tables/users.sql b/packages/schemas/tables/users.sql index 50a106607..b72c95325 100644 --- a/packages/schemas/tables/users.sql +++ b/packages/schemas/tables/users.sql @@ -16,6 +16,7 @@ create table users ( application_id varchar(21), identities jsonb /* @use Identities */ not null default '{}'::jsonb, custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb, + mfa_verifications jsonb /* @use MfaVerifications */ not null default '[]'::jsonb, is_suspended boolean not null default false, last_sign_in_at timestamptz, created_at timestamptz not null default (now()),