From 508c58908a1c73706369a49ff2da415aef09ece9 Mon Sep 17 00:00:00 2001 From: Killian Challeau Date: Tue, 21 Jun 2022 16:35:10 -0400 Subject: [PATCH 1/4] feat: 2FA WIP --- apps/api/src/app/app.module.ts | 2 + hapify-models.json | 816 ++++------ .../src/authentication.module.ts | 15 +- .../src/constants/authentication.contants.ts | 2 + .../authentication/src/controllers/index.ts | 1 + .../two-factor-authentication.controller.ts | 59 + .../src/dtos/authentication-options.dto.ts | 14 +- libs/nestjs/authentication/src/dtos/index.ts | 1 + .../src/dtos/jwt-token-payload.dto.ts | 2 + .../two-factor-authentication-code.dto.ts | 6 + .../nestjs/authentication/src/guards/index.ts | 1 + .../src/guards/jwt-two-factor.guard.ts | 5 + .../authentication-module.interface.ts | 5 + .../src/services/authentication.service.ts | 7 +- .../authentication/src/services/index.ts | 1 + .../two-factor-authentification.service.ts | 50 + .../src/strategies/jwt.strategy.ts | 8 +- package-lock.json | 1379 ++++++++++++++++- package.json | 1 + 19 files changed, 1770 insertions(+), 605 deletions(-) create mode 100644 libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts create mode 100644 libs/nestjs/authentication/src/dtos/two-factor-authentication-code.dto.ts create mode 100644 libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts create mode 100644 libs/nestjs/authentication/src/services/two-factor-authentification.service.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index c0017390e..df4d52a84 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -51,9 +51,11 @@ import { MailerModule } from '@tractr/nestjs-mailer'; passwordField: 'password', emailField: 'email', customSelect: getSelectPrismaUserQuery(), + otpField: 'otp', formatUser: ({ ...user }) => user, }, userService: USER_SERVICE, + otp: true, }), }), FileStorageModule.registerAsync({ diff --git a/hapify-models.json b/hapify-models.json index f03537846..39741edf9 100644 --- a/hapify-models.json +++ b/hapify-models.json @@ -1,10 +1,6 @@ { - "version": "3", - "name": "New project", "models": [ { - "id": "ccebcf02-4b82-923f-114e-252c9455899d", - "name": "Answer", "accesses": { "count": "owner", "create": "owner", @@ -16,77 +12,63 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ], - "notes": "Answer UUID" + "notes": "Answer UUID", + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { - "name": "user", - "type": "entity", - "subtype": "oneMany", - "value": "a7d0308a-49f0-3458-0975-1dce106136a1", - "properties": [ - "ownership" - ], - "notes": "User that owns the answer", "meta": { "backRelation": "answers", - "ownerStringPath": "user.id", + "onDelete": "Cascade", "onUpdate": "Cascade", - "onDelete": "Cascade" - } + "ownerStringPath": "user.id" + }, + "name": "user", + "notes": "User that owns the answer", + "properties": ["ownership"], + "subtype": "oneMany", + "type": "entity", + "value": "a7d0308a-49f0-3458-0975-1dce106136a1" }, { - "name": "question", - "type": "entity", - "subtype": "oneMany", - "value": "6894a782-2e83-cd6a-52cc-f62967165636", - "properties": [], - "notes": "Question associated to the answer", "meta": { "backRelation": "answers" - } + }, + "name": "question", + "notes": "Question associated to the answer", + "properties": [], + "subtype": "oneMany", + "type": "entity", + "value": "6894a782-2e83-cd6a-52cc-f62967165636" }, { - "name": "tags", - "type": "entity", - "subtype": "manyMany", - "value": "1342e64d-6880-cc39-ecf0-372e8ffddc17", - "properties": [ - "nullable", - "multiple" - ], - "notes": "Tags associated to the answer", "meta": { "backRelation": "answers" - } + }, + "name": "tags", + "notes": "Tags associated to the answer", + "properties": ["nullable", "multiple"], + "subtype": "manyMany", + "type": "entity", + "value": "1342e64d-6880-cc39-ecf0-372e8ffddc17" }, { "name": "answerDate", - "type": "datetime", + "notes": "Date of the answer creation", "properties": [], - "notes": "Date of the answer creation" + "type": "datetime" } ], + "id": "ccebcf02-4b82-923f-114e-252c9455899d", + "name": "Answer", "notes": "The answer model" }, { - "id": "2a523149-31f3-80bf-d32b-3b9174cc6f2c", - "name": "Department", "accesses": { "count": "owner", "create": "owner", @@ -97,34 +79,27 @@ }, "fields": [ { + "meta": {}, "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ], - "meta": {} + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { - "name": "enterprise", - "type": "entity", - "subtype": "oneMany", - "value": "c069cc55-f28f-dcc1-cad8-0df044631ef8", - "properties": [ - "ownership" - ], "meta": { "backRelation": "departments", "ownerStringPath": "user.enterprises.id" - } + }, + "name": "enterprise", + "properties": ["ownership"], + "subtype": "oneMany", + "type": "entity", + "value": "c069cc55-f28f-dcc1-cad8-0df044631ef8" } - ] + ], + "id": "2a523149-31f3-80bf-d32b-3b9174cc6f2c", + "name": "Department" }, { - "id": "c069cc55-f28f-dcc1-cad8-0df044631ef8", - "name": "Enterprise", "accesses": { "count": "owner", "create": "owner", @@ -136,40 +111,30 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "name", - "type": "string", - "properties": [ - "unique" - ] + "properties": ["unique"], + "type": "string" }, { - "name": "employee", - "type": "entity", - "subtype": "manyMany", - "value": "a7d0308a-49f0-3458-0975-1dce106136a1", - "properties": [ - "multiple", - "ownership" - ], "meta": { "backRelation": "enterprises", "ownerStringPath": "user.id" - } + }, + "name": "employee", + "properties": ["multiple", "ownership"], + "subtype": "manyMany", + "type": "entity", + "value": "a7d0308a-49f0-3458-0975-1dce106136a1" } - ] + ], + "id": "c069cc55-f28f-dcc1-cad8-0df044631ef8", + "name": "Enterprise" }, { - "id": "2019dfe5-3b43-4eea-82e5-2533fb1f9c85", - "name": "InternalField", "accesses": { "count": "guest", "create": "guest", @@ -181,154 +146,98 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "defaultNullable", - "type": "string", - "properties": [ - "nullable", - "searchable", - "sortable", - "internal" - ], - "notes": "Test nullable system field with no default metadata" + "notes": "Test nullable system field with no default metadata", + "properties": ["nullable", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "defaultString", - "type": "string", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test string system field with no default metadata" + "notes": "Test string system field with no default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "string" }, { "name": "defaultNumber", - "type": "number", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test number system field with no default metadata" + "notes": "Test number system field with no default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "number" }, { "name": "defaultBoolean", - "type": "boolean", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test boolean system field with no default metadata" + "notes": "Test boolean system field with no default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "boolean" }, { "name": "defaultDatetime", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test datetime system field with no default metadata" + "notes": "Test datetime system field with no default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "defaultEnum", + "notes": "Test enum system field with no default metadata", + "properties": ["searchable", "sortable", "internal"], "type": "enum", - "value": [ - "Enum1", - "Enum2" - ], - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test enum system field with no default metadata" + "value": ["Enum1", "Enum2"] }, { - "name": "customString", - "type": "string", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test string system field with default metadata", "meta": { "default": "'custom string'" - } + }, + "name": "customString", + "notes": "Test string system field with default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "string" }, { - "name": "customNumber", - "type": "number", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test number system field with default metadata", "meta": { "default": "666" - } + }, + "name": "customNumber", + "notes": "Test number system field with default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "number" }, { - "name": "customBoolean", - "type": "boolean", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test boolean system field with default metadata", "meta": { "default": "true" - } + }, + "name": "customBoolean", + "notes": "Test boolean system field with default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "boolean" }, { - "name": "customDatetime", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test datetime system field with default metadata", "meta": { "default": "new Date(1)" - } + }, + "name": "customDatetime", + "notes": "Test datetime system field with default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { - "name": "customEnum", - "type": "enum", - "value": [ - "Enmu1", - "Enum2" - ], - "properties": [ - "searchable", - "sortable", - "internal" - ], - "notes": "Test enum system field with default metadata", "meta": { "default": "InternalFieldCustomEnum.Enum2" - } + }, + "name": "customEnum", + "notes": "Test enum system field with default metadata", + "properties": ["searchable", "sortable", "internal"], + "type": "enum", + "value": ["Enmu1", "Enum2"] } ], + "id": "2019dfe5-3b43-4eea-82e5-2533fb1f9c85", + "name": "InternalField", "notes": "Table to test system fields and default metadata" }, { - "id": "b0ecc3a7-9f28-a140-d505-91f598e547a5", - "name": "Message", "accesses": { "count": "owner", "create": "auth", @@ -340,71 +249,50 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "text", - "type": "string", - "properties": [ - "label", - "searchable", - "sortable" - ] + "properties": ["label", "searchable", "sortable"], + "type": "string" }, { "name": "hour", - "type": "datetime", + "properties": ["nullable"], "subtype": "time", - "properties": [ - "nullable" - ] + "type": "datetime" }, { - "name": "tags", - "type": "entity", - "subtype": "manyMany", - "value": "1342e64d-6880-cc39-ecf0-372e8ffddc17", - "properties": [ - "multiple" - ], "meta": { "backRelation": "messages" - } + }, + "name": "tags", + "properties": ["multiple"], + "subtype": "manyMany", + "type": "entity", + "value": "1342e64d-6880-cc39-ecf0-372e8ffddc17" }, { - "name": "questions", - "type": "entity", - "subtype": "manyMany", - "value": "6894a782-2e83-cd6a-52cc-f62967165636", - "properties": [ - "multiple", - "searchable", - "sortable" - ], "meta": { "backRelation": "messages" - } + }, + "name": "questions", + "properties": ["multiple", "searchable", "sortable"], + "subtype": "manyMany", + "type": "entity", + "value": "6894a782-2e83-cd6a-52cc-f62967165636" } - ] + ], + "id": "b0ecc3a7-9f28-a140-d505-91f598e547a5", + "name": "Message" }, { - "id": "0e6ab116-cf35-caaa-f40d-0a6a2d124a2c", - "name": "OpenQuestion", "accesses": { "count": "owner", "create": "auth", @@ -416,56 +304,39 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "text", - "type": "string", - "properties": [ - "searchable", - "sortable" - ] + "properties": ["searchable", "sortable"], + "type": "string" }, { "name": "key", - "type": "string", - "properties": [ - "unique", - "label", - "searchable", - "sortable" - ] + "properties": ["unique", "label", "searchable", "sortable"], + "type": "string" }, { - "name": "question", - "type": "entity", - "subtype": "oneMany", - "value": "6894a782-2e83-cd6a-52cc-f62967165636", - "properties": [], "meta": { "backRelation": "openQuestions" - } + }, + "name": "question", + "properties": [], + "subtype": "oneMany", + "type": "entity", + "value": "6894a782-2e83-cd6a-52cc-f62967165636" } - ] + ], + "id": "0e6ab116-cf35-caaa-f40d-0a6a2d124a2c", + "name": "OpenQuestion" }, { - "id": "6894a782-2e83-cd6a-52cc-f62967165636", - "name": "Question", "accesses": { "count": "owner", "create": "auth", @@ -477,70 +348,49 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "title", - "type": "string", - "properties": [ - "label", - "searchable", - "sortable" - ] + "properties": ["label", "searchable", "sortable"], + "type": "string" }, { "name": "text", - "type": "string", - "properties": [ - "searchable", - "sortable" - ] + "properties": ["searchable", "sortable"], + "type": "string" }, { - "name": "parentQuestion", - "type": "entity", - "subtype": "oneMany", - "value": "6894a782-2e83-cd6a-52cc-f62967165636", - "properties": [ - "nullable" - ], "meta": { "backRelation": "questions" - } + }, + "name": "parentQuestion", + "properties": ["nullable"], + "subtype": "oneMany", + "type": "entity", + "value": "6894a782-2e83-cd6a-52cc-f62967165636" }, { - "name": "tags", - "type": "entity", - "subtype": "manyMany", - "value": "1342e64d-6880-cc39-ecf0-372e8ffddc17", - "properties": [ - "nullable", - "multiple" - ], "meta": { "backRelation": "questions" - } + }, + "name": "tags", + "properties": ["nullable", "multiple"], + "subtype": "manyMany", + "type": "entity", + "value": "1342e64d-6880-cc39-ecf0-372e8ffddc17" } - ] + ], + "id": "6894a782-2e83-cd6a-52cc-f62967165636", + "name": "Question" }, { - "id": "1342e64d-6880-cc39-ecf0-372e8ffddc17", - "name": "Tag", "accesses": { "count": "owner", "create": "auth", @@ -552,50 +402,35 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "label", - "type": "string", - "properties": [ - "label", - "searchable", - "sortable" - ] + "properties": ["label", "searchable", "sortable"], + "type": "string" }, { - "name": "owner", - "type": "entity", - "subtype": "oneMany", - "value": "a7d0308a-49f0-3458-0975-1dce106136a1", - "properties": [ - "ownership" - ], "meta": { "backRelation": "tags", "ownerStringPath": "user.id" - } + }, + "name": "owner", + "properties": ["ownership"], + "subtype": "oneMany", + "type": "entity", + "value": "a7d0308a-49f0-3458-0975-1dce106136a1" } - ] + ], + "id": "1342e64d-6880-cc39-ecf0-372e8ffddc17", + "name": "Tag" }, { - "id": "d49a961e-c506-6869-3e1a-405fb52f109c", - "name": "UniqueValueField", "accesses": { "count": "guest", "create": "guest", @@ -607,170 +442,96 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "uniqueString", - "type": "string", - "properties": [ - "unique", - "searchable", - "sortable" - ] + "properties": ["unique", "searchable", "sortable"], + "type": "string" }, { "name": "uniqueNumber", - "type": "number", - "properties": [ - "unique", - "searchable", - "sortable" - ] + "properties": ["unique", "searchable", "sortable"], + "type": "number" }, { "name": "uniqueBoolean", - "type": "boolean", - "properties": [ - "unique", - "searchable", - "sortable" - ] + "properties": ["unique", "searchable", "sortable"], + "type": "boolean" }, { "name": "uniqueDatetime", - "type": "datetime", - "properties": [ - "unique", - "searchable", - "sortable" - ] + "properties": ["unique", "searchable", "sortable"], + "type": "datetime" }, { "name": "uniqueEnum", + "properties": ["unique", "searchable", "sortable"], "type": "enum", - "value": [ - "value1", - "value2" - ], - "properties": [ - "unique", - "searchable", - "sortable" - ] + "value": ["value1", "value2"] }, { "name": "uniqueObject", - "type": "object", - "properties": [ - "unique", - "searchable", - "sortable" - ] + "properties": ["unique", "searchable", "sortable"], + "type": "object" }, { - "name": "uniqueEntity", - "type": "entity", - "subtype": "oneOne", - "value": "a7d0308a-49f0-3458-0975-1dce106136a1", - "properties": [ - "unique", - "searchable", - "sortable" - ], "meta": { "backRelation": "uniqueValue" - } + }, + "name": "uniqueEntity", + "properties": ["unique", "searchable", "sortable"], + "subtype": "oneOne", + "type": "entity", + "value": "a7d0308a-49f0-3458-0975-1dce106136a1" }, { "name": "uniqueStringNullable", - "type": "string", - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ] + "properties": ["unique", "nullable", "searchable", "sortable"], + "type": "string" }, { "name": "uniqueNumberNullable", - "type": "number", - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ] + "properties": ["unique", "nullable", "searchable", "sortable"], + "type": "number" }, { "name": "uniqueBooleanNullable", - "type": "boolean", - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ] + "properties": ["unique", "nullable", "searchable", "sortable"], + "type": "boolean" }, { "name": "uniqueDatetimeNullable", - "type": "datetime", - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ] + "properties": ["unique", "nullable", "searchable", "sortable"], + "type": "datetime" }, { "name": "uniqueEnumNullable", + "properties": ["unique", "nullable", "searchable", "sortable"], "type": "enum", - "value": [ - "val1", - "val2" - ], - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ] + "value": ["val1", "val2"] }, { "name": "uniqueObjectNullable", - "type": "object", - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ] + "properties": ["unique", "nullable", "searchable", "sortable"], + "type": "object" }, { - "name": "uniqueEntityNullable", - "type": "entity", - "subtype": "oneOne", - "value": "a7d0308a-49f0-3458-0975-1dce106136a1", - "properties": [ - "unique", - "nullable", - "searchable", - "sortable" - ], "meta": { "backRelation": "uniqueEntityNullable" - } + }, + "name": "uniqueEntityNullable", + "properties": ["unique", "nullable", "searchable", "sortable"], + "subtype": "oneOne", + "type": "entity", + "value": "a7d0308a-49f0-3458-0975-1dce106136a1" } - ] + ], + "id": "d49a961e-c506-6869-3e1a-405fb52f109c", + "name": "UniqueValueField" }, { - "id": "a7d0308a-49f0-3458-0975-1dce106136a1", - "name": "User", "accesses": { "count": "admin", "create": "guest", @@ -781,8 +542,10 @@ }, "fields": [ { + "meta": { + "ownerStringPath": "user.id" + }, "name": "id", - "type": "string", "properties": [ "primary", "searchable", @@ -790,80 +553,56 @@ "internal", "ownership" ], - "meta": { - "ownerStringPath": "user.id" - } + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "name", - "type": "string", - "properties": [ - "label", - "searchable", - "sortable" - ] + "properties": ["label", "searchable", "sortable"], + "type": "string" }, { "name": "email", - "type": "string", + "properties": ["unique", "searchable", "sortable"], "subtype": "email", - "properties": [ - "unique", - "searchable", - "sortable" - ] + "type": "string" }, { "name": "password", - "type": "string", + "properties": ["nullable", "hidden"], "subtype": "password", - "properties": [ - "nullable", - "hidden" - ] + "type": "string" }, { "name": "listObject", - "type": "object", - "properties": [ - "multiple", - "searchable", - "sortable" - ] + "properties": ["multiple", "searchable", "sortable"], + "type": "object" }, { "name": "roles", + "properties": ["multiple", "searchable", "sortable"], "type": "enum", - "value": [ - "admin", - "user", - "guest" - ], - "properties": [ - "multiple", - "searchable", - "sortable" - ] + "value": ["admin", "user", "guest"] }, { "name": "Object", - "type": "object", - "properties": [] + "properties": [], + "type": "object" + }, + { + "name": "otp", + "properties": ["nullable"], + "type": "string" } - ] + ], + "id": "a7d0308a-49f0-3458-0975-1dce106136a1", + "name": "User" }, { - "id": "5d791308-4e9d-aa7d-3d36-d45d46b04c6a", - "name": "Variable", "accesses": { "count": "owner", "create": "auth", @@ -875,49 +614,44 @@ "fields": [ { "name": "id", - "type": "string", - "properties": [ - "primary", - "searchable", - "sortable", - "internal" - ] + "properties": ["primary", "searchable", "sortable", "internal"], + "type": "string" }, { "name": "createdAt", - "type": "datetime", - "properties": [ - "searchable", - "sortable", - "internal" - ] + "properties": ["searchable", "sortable", "internal"], + "type": "datetime" }, { "name": "value", - "type": "string", - "properties": [] + "properties": [], + "type": "string" }, { - "name": "openQuestion", - "type": "entity", - "subtype": "oneMany", - "value": "0e6ab116-cf35-caaa-f40d-0a6a2d124a2c", - "properties": [], "meta": { "backRelation": "variables" - } + }, + "name": "openQuestion", + "properties": [], + "subtype": "oneMany", + "type": "entity", + "value": "0e6ab116-cf35-caaa-f40d-0a6a2d124a2c" }, { - "name": "answer", - "type": "entity", - "subtype": "oneMany", - "value": "ccebcf02-4b82-923f-114e-252c9455899d", - "properties": [], "meta": { "backRelation": "variables" - } + }, + "name": "answer", + "properties": [], + "subtype": "oneMany", + "type": "entity", + "value": "ccebcf02-4b82-923f-114e-252c9455899d" } - ] + ], + "id": "5d791308-4e9d-aa7d-3d36-d45d46b04c6a", + "name": "Variable" } - ] -} \ No newline at end of file + ], + "name": "New project", + "version": "3" +} diff --git a/libs/nestjs/authentication/src/authentication.module.ts b/libs/nestjs/authentication/src/authentication.module.ts index 7b02ca7f7..1d1cc6042 100644 --- a/libs/nestjs/authentication/src/authentication.module.ts +++ b/libs/nestjs/authentication/src/authentication.module.ts @@ -6,13 +6,18 @@ import { AUTHENTICATION_MODULE_OPTIONS, AUTHENTICATION_USER_SERVICE, } from './constants'; -import { LoginController, PasswordController } from './controllers'; +import { + LoginController, + PasswordController, + TwoFactorAuthenticationController, +} from './controllers'; import { AuthenticationOptions } from './dtos'; import { AuthenticationPublicOptions } from './interfaces'; import { AuthenticationService, PasswordService, StrategyOptionsService, + TwoFactorAuthenticationService, } from './services'; import { AuthenticationUserService } from './services/authentication-user.service'; import { JwtStrategy, LocalStrategy } from './strategies'; @@ -96,6 +101,7 @@ export class AuthenticationModule extends ModuleOptionsFactory< ], exports: [ AuthenticationService, + TwoFactorAuthenticationService, JwtStrategy, LocalStrategy, PasswordService, @@ -106,6 +112,7 @@ export class AuthenticationModule extends ModuleOptionsFactory< ], providers: [ AuthenticationService, + TwoFactorAuthenticationService, PasswordService, StrategyOptionsService, JwtStrategy, @@ -115,7 +122,11 @@ export class AuthenticationModule extends ModuleOptionsFactory< useClass: AuthenticationUserService, }, ], - controllers: [LoginController, PasswordController], + controllers: [ + LoginController, + PasswordController, + TwoFactorAuthenticationController, + ], }; } } diff --git a/libs/nestjs/authentication/src/constants/authentication.contants.ts b/libs/nestjs/authentication/src/constants/authentication.contants.ts index 57b069476..130bcf553 100644 --- a/libs/nestjs/authentication/src/constants/authentication.contants.ts +++ b/libs/nestjs/authentication/src/constants/authentication.contants.ts @@ -10,6 +10,8 @@ export const DEFAULT_EMAIL_FIELD = 'email'; export const AUTHENTICATION_USER_SERVICE = 'AUTHENTICATION_USER_SERVICE'; +export const TWO_FACTOR_AUTHENTICATION = 'TWO_FACTOR_AUTHENTICATION'; + export const DEFAULT_RESET_HTML = ` diff --git a/libs/nestjs/authentication/src/controllers/index.ts b/libs/nestjs/authentication/src/controllers/index.ts index 99a4ab2d3..3ba5ca4d3 100644 --- a/libs/nestjs/authentication/src/controllers/index.ts +++ b/libs/nestjs/authentication/src/controllers/index.ts @@ -1,2 +1,3 @@ export * from './login.controller'; export * from './password.controller'; +export * from './two-factor-authentication.controller'; diff --git a/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts new file mode 100644 index 000000000..cfee034dc --- /dev/null +++ b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts @@ -0,0 +1,59 @@ +import { + Body, + ClassSerializerInterceptor, + Controller, + HttpCode, + Post, + UnauthorizedException, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; + +import { CurrentUser } from '../decorators'; +import { TwoFactorAuthenticationCodeDto } from '../dtos'; +import { JwtAuthGuard, JwtTwoFactorGuard, LocalAuthGuard } from '../guards'; +import { + AuthenticationService, + TwoFactorAuthenticationService, +} from '../services'; + +@Controller('2fa') +@UseInterceptors(ClassSerializerInterceptor) +export class TwoFactorAuthenticationController { + constructor( + private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService, + private readonly authenticationService: AuthenticationService, + ) {} + + @Post('generate') + @UseGuards(JwtAuthGuard) + async register(@CurrentUser() user: { id: string; email: string }) { + const otpauthUrl = + await this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret( + user, + ); + + return otpauthUrl; + } + + @Post('authenticate') + @UseGuards(JwtAuthGuard) + @HttpCode(200) + async authenticate( + @CurrentUser() user: { id: string; otp: string }, + @Body() { code }: TwoFactorAuthenticationCodeDto, + ) { + const isCodeValid = + this.twoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid( + code, + user, + ); + if (!isCodeValid) { + throw new UnauthorizedException('Wrong authentication code'); + } + + return { + accessToken: await this.authenticationService.createUserJWT(user, true), + }; + } +} diff --git a/libs/nestjs/authentication/src/dtos/authentication-options.dto.ts b/libs/nestjs/authentication/src/dtos/authentication-options.dto.ts index 351b92f1d..a378143ab 100644 --- a/libs/nestjs/authentication/src/dtos/authentication-options.dto.ts +++ b/libs/nestjs/authentication/src/dtos/authentication-options.dto.ts @@ -1,6 +1,11 @@ import { JwtModuleOptions } from '@nestjs/jwt'; import { IAuthModuleOptions } from '@nestjs/passport'; -import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { + IsBoolean, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import { AuthenticationOptionsCookies } from './authentication-options-cookies.dto'; import { AuthenticationOptionsMailer } from './authentication-options-mailer.dto'; @@ -72,4 +77,11 @@ export class AuthenticationOptions { @IsOptional() @ValidateNested() mailer?: AuthenticationOptionsMailer; + + /** + * Options to enable 2FA with OTP. + */ + @IsOptional() + @IsBoolean() + otp?: boolean; } diff --git a/libs/nestjs/authentication/src/dtos/index.ts b/libs/nestjs/authentication/src/dtos/index.ts index 8429d9686..8f0eca0e4 100644 --- a/libs/nestjs/authentication/src/dtos/index.ts +++ b/libs/nestjs/authentication/src/dtos/index.ts @@ -9,3 +9,4 @@ export * from './authentication-options.dto'; export * from './jwt-token-payload.dto'; export * from './password-reset.dto'; export * from './password-reset-requested.dto'; +export * from './two-factor-authentication-code.dto'; diff --git a/libs/nestjs/authentication/src/dtos/jwt-token-payload.dto.ts b/libs/nestjs/authentication/src/dtos/jwt-token-payload.dto.ts index 2402ba67d..5cdf6ee0f 100644 --- a/libs/nestjs/authentication/src/dtos/jwt-token-payload.dto.ts +++ b/libs/nestjs/authentication/src/dtos/jwt-token-payload.dto.ts @@ -13,4 +13,6 @@ export interface JwtTokenPayload { azp?: string; /** Token scope (what the token has access to) */ scope?: string; + /** Check if 2FA is enabled */ + isSecondFactorAuthenticated?: boolean; } diff --git a/libs/nestjs/authentication/src/dtos/two-factor-authentication-code.dto.ts b/libs/nestjs/authentication/src/dtos/two-factor-authentication-code.dto.ts new file mode 100644 index 000000000..cabd20b6e --- /dev/null +++ b/libs/nestjs/authentication/src/dtos/two-factor-authentication-code.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class TwoFactorAuthenticationCodeDto { + @IsString() + code!: string; +} diff --git a/libs/nestjs/authentication/src/guards/index.ts b/libs/nestjs/authentication/src/guards/index.ts index d606b05ef..9aef54d89 100644 --- a/libs/nestjs/authentication/src/guards/index.ts +++ b/libs/nestjs/authentication/src/guards/index.ts @@ -2,3 +2,4 @@ export * from './jwt-auth.guard'; export * from './jwt-global-auth.guard'; export * from './local-auth.guard'; export * from './public-global-auth.guard'; +export * from './jwt-two-factor.guard'; diff --git a/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts b/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts new file mode 100644 index 000000000..1b0587ac2 --- /dev/null +++ b/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtTwoFactorGuard extends AuthGuard('jwt-two-factor') {} diff --git a/libs/nestjs/authentication/src/interfaces/authentication-module.interface.ts b/libs/nestjs/authentication/src/interfaces/authentication-module.interface.ts index 9811653f2..b608b06e6 100644 --- a/libs/nestjs/authentication/src/interfaces/authentication-module.interface.ts +++ b/libs/nestjs/authentication/src/interfaces/authentication-module.interface.ts @@ -52,4 +52,9 @@ export interface AuthenticationPublicOptions { * Options to configure the mailer. */ mailer?: AuthenticationOptionsMailer; + + /** + * Options to enable 2FA with OTP. + */ + otp?: boolean; } diff --git a/libs/nestjs/authentication/src/services/authentication.service.ts b/libs/nestjs/authentication/src/services/authentication.service.ts index 15b128419..1e252681f 100644 --- a/libs/nestjs/authentication/src/services/authentication.service.ts +++ b/libs/nestjs/authentication/src/services/authentication.service.ts @@ -87,8 +87,11 @@ export class AuthenticationService { return bcrypt.compare(password, hash); } - async createUserJWT(user: UserType): Promise { - return this.jwtService.sign({ sub: user.id }); + async createUserJWT( + user: UserType, + isSecondFactorAuthenticated = false, + ): Promise { + return this.jwtService.sign({ sub: user.id, isSecondFactorAuthenticated }); } async login(user: UserType): Promise { diff --git a/libs/nestjs/authentication/src/services/index.ts b/libs/nestjs/authentication/src/services/index.ts index 1aefa8b3c..3c2702336 100644 --- a/libs/nestjs/authentication/src/services/index.ts +++ b/libs/nestjs/authentication/src/services/index.ts @@ -2,3 +2,4 @@ export * from './authentication-user.service'; export * from './authentication.service'; export * from './password.service'; export * from './strategy-options.service'; +export * from './two-factor-authentification.service'; diff --git a/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts b/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts new file mode 100644 index 000000000..0820aa2bd --- /dev/null +++ b/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts @@ -0,0 +1,50 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { authenticator } from 'otplib'; + +import { + AUTHENTICATION_USER_SERVICE, + TWO_FACTOR_AUTHENTICATION, +} from '../constants'; +import { UserService } from '../interfaces/user.service.interface'; + +@Injectable() +export class TwoFactorAuthenticationService { + constructor( + @Inject(AUTHENTICATION_USER_SERVICE) + private readonly userService: UserService, + ) {} + + public async generateTwoFactorAuthenticationSecret(user: { + id: string; + email: string; + }) { + const secret = authenticator.generateSecret(); + + const otpauthUrl = authenticator.keyuri( + user.email, + TWO_FACTOR_AUTHENTICATION, + secret, + ); + const args = { + where: { + id: user.id, + }, + data: { + otp: secret, + }, + }; + await this.userService.update(args); + + return otpauthUrl; + } + + public isTwoFactorAuthenticationCodeValid( + twoFactorAuthenticationCode: string, + user: { otp: string }, + ) { + return authenticator.verify({ + token: twoFactorAuthenticationCode, + secret: user.otp, + }); + } +} diff --git a/libs/nestjs/authentication/src/strategies/jwt.strategy.ts b/libs/nestjs/authentication/src/strategies/jwt.strategy.ts index 55c64b833..e2a8a1b90 100644 --- a/libs/nestjs/authentication/src/strategies/jwt.strategy.ts +++ b/libs/nestjs/authentication/src/strategies/jwt.strategy.ts @@ -28,11 +28,15 @@ export class JwtStrategy extends PassportStrategy(Strategy) { // Use select clause provided by the module consumer select: this.authenticationOptions.userConfig.customSelect, }); - if (!user) { throw new BadRequestException(); } - + if (!user.otp) { + return user; + } + if (!payload.isSecondFactorAuthenticated) { + throw new BadRequestException(); + } return user; } } diff --git a/package-lock.json b/package-lock.json index 763ada276..9e1ff8c14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "ng-zorro-antd": "^13.0.1", "node-fetch": "^2.6.1", "node-mailjet": "^3.3.7", + "otplib": "^12.0.1", "passport": "^0.5.2", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", @@ -6605,6 +6606,18 @@ "rxjs": "^6.5.3" } }, + "node_modules/@nrwl/angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "peer": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, "node_modules/@nrwl/angular/node_modules/rxjs-for-await": { "version": "0.0.2", "license": "MIT", @@ -6612,6 +6625,12 @@ "rxjs": "^6.0.0" } }, + "node_modules/@nrwl/angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "peer": true + }, "node_modules/@nrwl/angular/node_modules/semver": { "version": "7.3.4", "license": "ISC", @@ -8033,6 +8052,13 @@ "webpack-subresource-integrity": "^1.5.2" } }, + "node_modules/@nrwl/web/node_modules/@types/html-minifier-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", + "optional": true, + "peer": true + }, "node_modules/@nrwl/web/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -8092,6 +8118,16 @@ "ajv": "^6.9.1" } }, + "node_modules/@nrwl/web/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@nrwl/web/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8126,6 +8162,29 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@nrwl/web/node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "optional": true, + "peer": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/@nrwl/web/node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@nrwl/web/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8142,6 +8201,16 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/@nrwl/web/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@nrwl/web/node_modules/copy-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", @@ -8214,6 +8283,52 @@ "node": ">=8" } }, + "node_modules/@nrwl/web/node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "optional": true, + "peer": true, + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nrwl/web/node_modules/html-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "optional": true, + "peer": true, + "dependencies": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "engines": { + "node": ">=6.9" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/@nrwl/web/node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -8416,6 +8531,31 @@ "node": ">=6" } }, + "node_modules/@nrwl/web/node_modules/pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "optional": true, + "peer": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "node_modules/@nrwl/web/node_modules/renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "optional": true, + "peer": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + } + }, "node_modules/@nrwl/web/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -8471,6 +8611,19 @@ "node": ">=10" } }, + "node_modules/@nrwl/web/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@nrwl/web/node_modules/stylus": { "version": "0.55.0", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.55.0.tgz", @@ -8511,6 +8664,51 @@ "node": ">=8" } }, + "node_modules/@nrwl/web/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nrwl/web/node_modules/terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@nrwl/web/node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true, + "peer": true + }, + "node_modules/@nrwl/web/node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@nrwl/web/node_modules/ts-node": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", @@ -9272,6 +9470,48 @@ "node": ">=8" } }, + "node_modules/@otplib/core": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", + "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==" + }, + "node_modules/@otplib/plugin-crypto": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", + "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", + "dependencies": { + "@otplib/core": "^12.0.1" + } + }, + "node_modules/@otplib/plugin-thirty-two": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", + "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", + "dependencies": { + "@otplib/core": "^12.0.1", + "thirty-two": "^1.0.2" + } + }, + "node_modules/@otplib/preset-default": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", + "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@otplib/preset-v11": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", + "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, "node_modules/@paljs/plugins": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@paljs/plugins/-/plugins-4.1.0.tgz", @@ -10816,6 +11056,12 @@ "@types/node": "*" } }, + "node_modules/@types/bluebird": { + "version": "3.5.36", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.36.tgz", + "integrity": "sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==", + "peer": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "license": "MIT", @@ -11311,6 +11557,13 @@ "@types/node": "*" } }, + "node_modules/@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "optional": true, + "peer": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "license": "MIT" @@ -11332,12 +11585,39 @@ "@types/superagent": "*" } }, + "node_modules/@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "optional": true, + "peer": true + }, "node_modules/@types/triple-beam": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==", "dev": true }, + "node_modules/@types/uglify-js": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-0yeUr92L3r0GLRnBOvtYK1v2SjqMIqQDHMl7GLb+l2L8+6LSFWEEWEIgVsPdMn5ImLM8qzWT8xFPtQYpp8co0g==", + "optional": true, + "peer": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/uglify-js/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@types/unist": { "version": "2.0.6", "license": "MIT" @@ -11347,6 +11627,43 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/webpack": { + "version": "4.41.32", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", + "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@types/ws": { "version": "8.2.2", "license": "MIT", @@ -11374,6 +11691,13 @@ "@types/node": "*" } }, + "node_modules/@types/yoga-layout": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", + "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==", + "dev": true, + "peer": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.0.tgz", @@ -12765,6 +13089,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.reduce": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz", + "integrity": "sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==", + "optional": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arrify": { "version": "1.0.1", "dev": true, @@ -12871,6 +13215,19 @@ "node": ">=4" } }, + "node_modules/auto-bind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", + "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/autoprefixer": { "version": "10.4.2", "license": "MIT", @@ -15105,6 +15462,19 @@ "tslib": "2.3.1" } }, + "node_modules/code-excerpt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-3.0.0.tgz", + "integrity": "sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw==", + "dev": true, + "peer": true, + "dependencies": { + "convert-to-spaces": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -16210,6 +16580,16 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/convert-to-spaces": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz", + "integrity": "sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/cookie": { "version": "0.4.2", "license": "MIT", @@ -17512,13 +17892,18 @@ } }, "node_modules/define-properties": { - "version": "1.1.3", - "license": "MIT", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", "dependencies": { - "object-keys": "^1.0.12" + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/defined": { @@ -18325,29 +18710,33 @@ } }, "node_modules/es-abstract": { - "version": "1.19.1", - "license": "MIT", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", "dependencies": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.2", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", + "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", + "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -20928,10 +21317,35 @@ "version": "1.1.1", "license": "MIT" }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "license": "MIT" }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "3.0.2", "license": "ISC", @@ -21363,6 +21777,13 @@ "graphology-types": ">=0.24.0" } }, + "node_modules/graphology-types": { + "version": "0.24.4", + "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.4.tgz", + "integrity": "sha512-CSgmycWiviCctMFO86YoUTJN1t4/PLKC5Pos2Hite+7kCUXTr+mGlDUAOgpcKG1IfFaeL9VDmTjFpzs2rTnPWw==", + "dev": true, + "peer": true + }, "node_modules/graphql": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.4.0.tgz", @@ -21508,8 +21929,9 @@ } }, "node_modules/has-bigints": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -21522,9 +21944,21 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { - "version": "1.0.2", - "license": "MIT", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "engines": { "node": ">= 0.4" }, @@ -22415,6 +22849,163 @@ "tslib": "^2.0.0" } }, + "node_modules/ink": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-3.2.0.tgz", + "integrity": "sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "auto-bind": "4.0.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.0", + "cli-cursor": "^3.1.0", + "cli-truncate": "^2.1.0", + "code-excerpt": "^3.0.0", + "indent-string": "^4.0.0", + "is-ci": "^2.0.0", + "lodash": "^4.17.20", + "patch-console": "^1.0.0", + "react-devtools-core": "^4.19.1", + "react-reconciler": "^0.26.2", + "scheduler": "^0.20.2", + "signal-exit": "^3.0.2", + "slice-ansi": "^3.0.0", + "stack-utils": "^2.0.2", + "string-width": "^4.2.2", + "type-fest": "^0.12.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0", + "ws": "^7.5.5", + "yoga-layout-prebuilt": "^1.9.6" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "peer": true + }, + "node_modules/ink/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ink/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "peer": true + }, + "node_modules/ink/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "peer": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/ink/node_modules/react-reconciler": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.26.2.tgz", + "integrity": "sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^17.0.2" + } + }, + "node_modules/ink/node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/ink/node_modules/type-fest": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", + "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.1.1", "license": "MIT" @@ -22862,8 +23453,12 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -27959,6 +28554,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz", + "integrity": "sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==", + "optional": true, + "peer": true, + "dependencies": { + "array.prototype.reduce": "^1.0.4", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.hasown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", @@ -28126,6 +28740,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/otplib": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", + "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/preset-default": "^12.0.1", + "@otplib/preset-v11": "^12.0.1" + } + }, "node_modules/outmatch": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/outmatch/-/outmatch-0.7.0.tgz", @@ -28486,6 +29110,16 @@ "node": ">= 0.4.0" } }, + "node_modules/patch-console": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-1.0.0.tgz", + "integrity": "sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -30991,6 +31625,17 @@ "node": ">=6" } }, + "node_modules/react-devtools-core": { + "version": "4.24.7", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.7.tgz", + "integrity": "sha512-OFB1cp8bsh5Kc6oOJ3ZzH++zMBtydwD53yBYa50FKEGyOOdgdbJ4VsCsZhN/6F5T4gJfrZraU6EKda8P+tMLtg==", + "dev": true, + "peer": true, + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, "node_modules/react-dom": { "version": "16.14.0", "license": "MIT", @@ -31496,11 +32141,13 @@ "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.4.1", - "license": "MIT", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -33625,22 +34272,26 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "license": "MIT", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "license": "MIT", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -34330,6 +34981,14 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "engines": { + "node": ">=0.2.6" + } + }, "node_modules/throat": { "version": "6.0.1", "license": "MIT" @@ -34660,6 +35319,12 @@ "integrity": "sha512-eHqR/7A6fcw05vCOfnL6RwgGJbVi9G/YHTdYdjYmElhDdJ1SMn7pWs+6+YuxygaFwQS/g+cIDlu+UD8IVpur1A==", "devOptional": true }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "peer": true + }, "node_modules/ts-type": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/ts-type/-/ts-type-2.1.4.tgz", @@ -34878,12 +35543,13 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" }, "funding": { @@ -35360,6 +36026,17 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "optional": true, + "peer": true, + "dependencies": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, "node_modules/utila": { "version": "0.4.0", "license": "MIT" @@ -36505,6 +37182,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout-prebuilt": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz", + "integrity": "sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yoga-layout": "1.9.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zod": { "version": "1.11.17", "resolved": "https://registry.npmjs.org/zod/-/zod-1.11.17.tgz", @@ -40893,6 +41583,23 @@ "lodash": "^4.17.20" } }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "peer": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "peer": true + } + } + }, "rxjs-for-await": { "version": "0.0.2", "requires": {} @@ -41881,6 +42588,13 @@ "webpack-subresource-integrity": "^1.5.2" }, "dependencies": { + "@types/html-minifier-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", + "optional": true, + "peer": true + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -41924,6 +42638,13 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "requires": {} }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "optional": true, + "peer": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -41946,6 +42667,25 @@ "supports-color": "^7.1.0" } }, + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "optional": true, + "peer": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -41959,6 +42699,13 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "optional": true, + "peer": true + }, "copy-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", @@ -42011,6 +42758,40 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "optional": true, + "peer": true, + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + } + }, + "html-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "optional": true, + "peer": true, + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + } + }, "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -42155,6 +42936,31 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "optional": true }, + "pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "optional": true, + "peer": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "optional": true, + "peer": true, + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + } + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -42194,6 +43000,16 @@ "lru-cache": "^6.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "optional": true, + "peer": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "stylus": { "version": "0.55.0", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.55.0.tgz", @@ -42224,6 +43040,41 @@ "has-flag": "^4.0.0" } }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "optional": true, + "peer": true + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "optional": true, + "peer": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true, + "peer": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true + } + } + }, "ts-node": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", @@ -42739,6 +43590,48 @@ } } }, + "@otplib/core": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", + "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==" + }, + "@otplib/plugin-crypto": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", + "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", + "requires": { + "@otplib/core": "^12.0.1" + } + }, + "@otplib/plugin-thirty-two": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", + "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", + "requires": { + "@otplib/core": "^12.0.1", + "thirty-two": "^1.0.2" + } + }, + "@otplib/preset-default": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", + "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", + "requires": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "@otplib/preset-v11": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", + "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "requires": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, "@paljs/plugins": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@paljs/plugins/-/plugins-4.1.0.tgz", @@ -43757,6 +44650,12 @@ "@types/node": "*" } }, + "@types/bluebird": { + "version": "3.5.36", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.36.tgz", + "integrity": "sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==", + "peer": true + }, "@types/body-parser": { "version": "1.19.2", "requires": { @@ -44189,6 +45088,13 @@ "@types/node": "*" } }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "optional": true, + "peer": true + }, "@types/stack-utils": { "version": "2.0.1" }, @@ -44207,12 +45113,38 @@ "@types/superagent": "*" } }, + "@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "optional": true, + "peer": true + }, "@types/triple-beam": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==", "dev": true }, + "@types/uglify-js": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-0yeUr92L3r0GLRnBOvtYK1v2SjqMIqQDHMl7GLb+l2L8+6LSFWEEWEIgVsPdMn5ImLM8qzWT8xFPtQYpp8co0g==", + "optional": true, + "peer": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true + } + } + }, "@types/unist": { "version": "2.0.6" }, @@ -44220,6 +45152,42 @@ "version": "8.3.4", "dev": true }, + "@types/webpack": { + "version": "4.41.32", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", + "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", + "optional": true, + "peer": true, + "requires": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "peer": true + } + } + }, + "@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "optional": true, + "peer": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, "@types/ws": { "version": "8.2.2", "requires": { @@ -44243,6 +45211,13 @@ "@types/node": "*" } }, + "@types/yoga-layout": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", + "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==", + "dev": true, + "peer": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.0.tgz", @@ -45118,6 +46093,20 @@ "is-string": "^1.0.7" } }, + "array.prototype.reduce": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz", + "integrity": "sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==", + "optional": true, + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, "arrify": { "version": "1.0.1", "dev": true @@ -45190,6 +46179,13 @@ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" }, + "auto-bind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", + "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", + "dev": true, + "peer": true + }, "autoprefixer": { "version": "10.4.2", "requires": { @@ -46710,6 +47706,16 @@ "tslib": "2.3.1" } }, + "code-excerpt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-3.0.0.tgz", + "integrity": "sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw==", + "dev": true, + "peer": true, + "requires": { + "convert-to-spaces": "^1.0.1" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -47474,6 +48480,13 @@ "safe-buffer": "~5.1.1" } }, + "convert-to-spaces": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz", + "integrity": "sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==", + "dev": true, + "peer": true + }, "cookie": { "version": "0.4.2" }, @@ -48303,9 +49316,12 @@ "version": "2.0.0" }, "define-properties": { - "version": "1.1.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", "requires": { - "object-keys": "^1.0.12" + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, "defined": { @@ -48856,28 +49872,33 @@ } }, "es-abstract": { - "version": "1.19.1", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.2", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", + "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", + "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" } }, "es-array-method-boxes-properly": { @@ -50562,9 +51583,25 @@ "function-bind": { "version": "1.1.1" }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, "functional-red-black-tree": { "version": "1.0.1" }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, "gauge": { "version": "3.0.2", "requires": { @@ -50858,6 +51895,13 @@ "obliterator": "^2.0.2" } }, + "graphology-types": { + "version": "0.24.4", + "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.4.tgz", + "integrity": "sha512-CSgmycWiviCctMFO86YoUTJN1t4/PLKC5Pos2Hite+7kCUXTr+mGlDUAOgpcKG1IfFaeL9VDmTjFpzs2rTnPWw==", + "dev": true, + "peer": true + }, "graphql": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.4.0.tgz", @@ -50952,15 +51996,27 @@ } }, "has-bigints": { - "version": "1.0.1" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, "has-symbols": { - "version": "1.0.2" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { "version": "1.0.0", @@ -51531,6 +52587,126 @@ "tslib": "^2.0.0" } }, + "ink": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-3.2.0.tgz", + "integrity": "sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg==", + "dev": true, + "peer": true, + "requires": { + "ansi-escapes": "^4.2.1", + "auto-bind": "4.0.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.0", + "cli-cursor": "^3.1.0", + "cli-truncate": "^2.1.0", + "code-excerpt": "^3.0.0", + "indent-string": "^4.0.0", + "is-ci": "^2.0.0", + "lodash": "^4.17.20", + "patch-console": "^1.0.0", + "react-devtools-core": "^4.19.1", + "react-reconciler": "^0.26.2", + "scheduler": "^0.20.2", + "signal-exit": "^3.0.2", + "slice-ansi": "^3.0.0", + "stack-utils": "^2.0.2", + "string-width": "^4.2.2", + "type-fest": "^0.12.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0", + "ws": "^7.5.5", + "yoga-layout-prebuilt": "^1.9.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "peer": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "peer": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "peer": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "react-reconciler": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.26.2.tgz", + "integrity": "sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "type-fest": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", + "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", + "dev": true, + "peer": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "inline-style-parser": { "version": "0.1.1" }, @@ -51778,7 +52954,12 @@ "dev": true }, "is-shared-array-buffer": { - "version": "1.0.1" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } }, "is-stream": { "version": "2.0.1" @@ -55463,6 +56644,19 @@ "es-abstract": "^1.19.1" } }, + "object.getownpropertydescriptors": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz", + "integrity": "sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==", + "optional": true, + "peer": true, + "requires": { + "array.prototype.reduce": "^1.0.4", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.1" + } + }, "object.hasown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", @@ -55572,6 +56766,16 @@ "version": "1.2.2", "devOptional": true }, + "otplib": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", + "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "requires": { + "@otplib/core": "^12.0.1", + "@otplib/preset-default": "^12.0.1", + "@otplib/preset-v11": "^12.0.1" + } + }, "outmatch": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/outmatch/-/outmatch-0.7.0.tgz", @@ -55817,6 +57021,13 @@ "passport-strategy": { "version": "1.0.0" }, + "patch-console": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-1.0.0.tgz", + "integrity": "sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA==", + "dev": true, + "peer": true + }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -57364,6 +58575,17 @@ } } }, + "react-devtools-core": { + "version": "4.24.7", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.7.tgz", + "integrity": "sha512-OFB1cp8bsh5Kc6oOJ3ZzH++zMBtydwD53yBYa50FKEGyOOdgdbJ4VsCsZhN/6F5T4gJfrZraU6EKda8P+tMLtg==", + "dev": true, + "peer": true, + "requires": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, "react-dom": { "version": "16.14.0", "requires": { @@ -57704,10 +58926,13 @@ "dev": true }, "regexp.prototype.flags": { - "version": "1.4.1", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" } }, "regexpp": { @@ -59171,17 +60396,23 @@ } }, "string.prototype.trimend": { - "version": "1.0.4", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" } }, "string.prototype.trimstart": { - "version": "1.0.4", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" } }, "stringify-object": { @@ -59598,6 +60829,11 @@ "text-table": { "version": "0.2.0" }, + "thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==" + }, "throat": { "version": "6.0.1" }, @@ -59801,6 +61037,12 @@ "integrity": "sha512-eHqR/7A6fcw05vCOfnL6RwgGJbVi9G/YHTdYdjYmElhDdJ1SMn7pWs+6+YuxygaFwQS/g+cIDlu+UD8IVpur1A==", "devOptional": true }, + "ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "peer": true + }, "ts-type": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/ts-type/-/ts-type-2.1.4.tgz", @@ -59940,11 +61182,13 @@ "optional": true }, "unbox-primitive": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" } }, @@ -60225,6 +61469,17 @@ "util-deprecate": { "version": "1.0.2" }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "optional": true, + "peer": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, "utila": { "version": "0.4.0" }, @@ -60937,6 +62192,16 @@ "yocto-queue": { "version": "0.1.0" }, + "yoga-layout-prebuilt": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz", + "integrity": "sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==", + "dev": true, + "peer": true, + "requires": { + "@types/yoga-layout": "1.9.2" + } + }, "zod": { "version": "1.11.17", "resolved": "https://registry.npmjs.org/zod/-/zod-1.11.17.tgz", diff --git a/package.json b/package.json index a90e89752..aeccdb875 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "ng-zorro-antd": "^13.0.1", "node-fetch": "^2.6.1", "node-mailjet": "^3.3.7", + "otplib": "^12.0.1", "passport": "^0.5.2", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", From 04bb5a1374cace2152825666f2d8ff2f95f526d3 Mon Sep 17 00:00:00 2001 From: Killian Challeau Date: Thu, 23 Jun 2022 10:24:20 -0400 Subject: [PATCH 2/4] feat: implement OTP feature WIP --- apps/api/src/app/app.module.ts | 4 +- .../src/config/get-prisma-user-query.ts.hpf | 11 +- .../src/authentication.module.ts | 9 +- .../src/constants/authentication.contants.ts | 1 + .../two-factor-authentication.controller.ts | 41 ++- .../dtos/authentication-options-user.dto.ts | 7 + .../src/guards/jwt-two-factor.guard.ts | 31 +- .../two-factor-authentication.service.spec.ts | 131 +++++++ .../two-factor-authentification.service.ts | 10 +- .../authentication/src/strategies/index.ts | 1 + .../src/strategies/jwt-two-factor.strategy.ts | 55 +++ .../src/strategies/jwt.strategy.ts | 7 +- package-lock.json | 341 ++++++++++++++++++ package.json | 2 + 14 files changed, 625 insertions(+), 26 deletions(-) create mode 100644 libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts create mode 100644 libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index df4d52a84..dcc535bca 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -13,7 +13,7 @@ import { ModelsModule } from '@tractr/generated-nestjs-models'; import { USER_SERVICE } from '@tractr/generated-nestjs-models-common'; import { AuthenticationModule, - JwtGlobalAuthGuard, + JwtTwoFactorGuard, } from '@tractr/nestjs-authentication'; import { CaslExceptionInterceptor, @@ -85,7 +85,7 @@ import { MailerModule } from '@tractr/nestjs-mailer'; ], controllers: [FileStorageController], providers: [ - { provide: APP_GUARD, useClass: JwtGlobalAuthGuard }, + { provide: APP_GUARD, useClass: JwtTwoFactorGuard }, { provide: APP_GUARD, useClass: PoliciesGuard }, { provide: APP_INTERCEPTOR, useClass: CaslExceptionInterceptor }, { provide: APP_INTERCEPTOR, useClass: PrismaExceptionInterceptor }, diff --git a/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf b/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf index a94829c85..ebd27ef73 100644 --- a/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf +++ b/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf @@ -11,14 +11,14 @@ export type UserWithIds = <<=getUserFindUniqueReturnTypeFromHapify()>>; * Get the select configuration for the prisma user query to be able * to construct the user with the correct ids */ -export function getSelectPrismaUserQuery(): Prisma.UserSelect { - return <<=getSelectPrismaUserQueryFromHapify()>>; +export function getSelectPrismaUserQuery(user:Partial = {}): Prisma.UserSelect { + return <<=getSelectPrismaUserQueryFromHapify(user)>>; } <<< - function getSelectPrismaUserQueryFromHapify() { - const info = getOwnershipInformationsFromHapify(); + function getSelectPrismaUserQueryFromHapify(user) { + const info = getOwnershipInformationsFromHapify(user); return JSON.stringify(info.select, null, 2); } @@ -41,7 +41,7 @@ export function getSelectPrismaUserQuery(): Prisma.UserSelect { : `, ${info.modelListToImport.join(', ')} `; } - function getOwnershipInformationsFromHapify() { + function getOwnershipInformationsFromHapify(user:Partial = {}) { const models = root; if (!models) return JSON.stringify({}, null, 2); @@ -203,6 +203,7 @@ export function getSelectPrismaUserQuery(): Prisma.UserSelect { { select: { roles: true, + ...user }, types: { roles: "User['roles'];", diff --git a/libs/nestjs/authentication/src/authentication.module.ts b/libs/nestjs/authentication/src/authentication.module.ts index 1d1cc6042..6ed0b2e4d 100644 --- a/libs/nestjs/authentication/src/authentication.module.ts +++ b/libs/nestjs/authentication/src/authentication.module.ts @@ -20,7 +20,7 @@ import { TwoFactorAuthenticationService, } from './services'; import { AuthenticationUserService } from './services/authentication-user.service'; -import { JwtStrategy, LocalStrategy } from './strategies'; +import { JwtStrategy, JwtTwoFactorStrategy, LocalStrategy } from './strategies'; import { transformAndValidate } from '@tractr/common'; import { @@ -101,7 +101,7 @@ export class AuthenticationModule extends ModuleOptionsFactory< ], exports: [ AuthenticationService, - TwoFactorAuthenticationService, + ...(otp ? [TwoFactorAuthenticationService] : []), JwtStrategy, LocalStrategy, PasswordService, @@ -112,10 +112,11 @@ export class AuthenticationModule extends ModuleOptionsFactory< ], providers: [ AuthenticationService, - TwoFactorAuthenticationService, + ...(otp ? [TwoFactorAuthenticationService] : []), PasswordService, StrategyOptionsService, JwtStrategy, + ...(otp ? [JwtTwoFactorStrategy] : []), LocalStrategy, { provide: AUTHENTICATION_USER_SERVICE, @@ -125,7 +126,7 @@ export class AuthenticationModule extends ModuleOptionsFactory< controllers: [ LoginController, PasswordController, - TwoFactorAuthenticationController, + ...(otp ? [TwoFactorAuthenticationController] : []), ], }; } diff --git a/libs/nestjs/authentication/src/constants/authentication.contants.ts b/libs/nestjs/authentication/src/constants/authentication.contants.ts index 130bcf553..f9c413616 100644 --- a/libs/nestjs/authentication/src/constants/authentication.contants.ts +++ b/libs/nestjs/authentication/src/constants/authentication.contants.ts @@ -6,6 +6,7 @@ export const AUTHENTICATION_DEFAULT_QUERY_PARAM_NAME = 'authToken'; export const DEFAULT_ID_FIELD = 'id'; export const DEFAULT_LOGIN_FIELD = 'email'; export const DEFAULT_PASSWORD_FIELD = 'password'; +export const DEFAULT_OTP_FIELD = 'otp'; export const DEFAULT_EMAIL_FIELD = 'email'; export const AUTHENTICATION_USER_SERVICE = 'AUTHENTICATION_USER_SERVICE'; diff --git a/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts index cfee034dc..46e882a54 100644 --- a/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts +++ b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts @@ -3,15 +3,20 @@ import { ClassSerializerInterceptor, Controller, HttpCode, + Inject, Post, + Req, + Res, UnauthorizedException, UseGuards, UseInterceptors, } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { AUTHENTICATION_MODULE_OPTIONS } from '../constants'; import { CurrentUser } from '../decorators'; -import { TwoFactorAuthenticationCodeDto } from '../dtos'; -import { JwtAuthGuard, JwtTwoFactorGuard, LocalAuthGuard } from '../guards'; +import { AuthenticationOptions, TwoFactorAuthenticationCodeDto } from '../dtos'; +import { JwtAuthGuard } from '../guards'; import { AuthenticationService, TwoFactorAuthenticationService, @@ -23,17 +28,25 @@ export class TwoFactorAuthenticationController { constructor( private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService, private readonly authenticationService: AuthenticationService, + @Inject(AUTHENTICATION_MODULE_OPTIONS) + private readonly authenticationOptions: AuthenticationOptions, ) {} @Post('generate') @UseGuards(JwtAuthGuard) - async register(@CurrentUser() user: { id: string; email: string }) { + async register( + @Res() response: Response, + @CurrentUser() user: { id: string; email: string }, + ) { const otpauthUrl = await this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret( user, ); - return otpauthUrl; + return this.twoFactorAuthenticationService.pipeQrCodeStream( + response, + otpauthUrl, + ); } @Post('authenticate') @@ -42,18 +55,36 @@ export class TwoFactorAuthenticationController { async authenticate( @CurrentUser() user: { id: string; otp: string }, @Body() { code }: TwoFactorAuthenticationCodeDto, + @Req() req: Request & { secret: string }, + @Res({ passthrough: true }) res: Response, ) { const isCodeValid = this.twoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid( code, user, ); + if (!isCodeValid) { throw new UnauthorizedException('Wrong authentication code'); } + const { options: cookieOptions } = this.authenticationOptions.cookies; + + const { formatUser } = this.authenticationOptions.userConfig; + + const accessToken = await this.authenticationService.createUserJWT( + user, + true, + ); + + res.cookie(this.authenticationOptions.cookies.cookieName, accessToken, { + signed: !!req.secret, + ...cookieOptions, + }); + return { - accessToken: await this.authenticationService.createUserJWT(user, true), + accessToken, + user: formatUser(user), }; } } diff --git a/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts b/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts index 631af46c4..13763628e 100644 --- a/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts +++ b/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts @@ -4,6 +4,7 @@ import { DEFAULT_EMAIL_FIELD, DEFAULT_ID_FIELD, DEFAULT_LOGIN_FIELD, + DEFAULT_OTP_FIELD, DEFAULT_PASSWORD_FIELD, } from '../constants'; @@ -42,6 +43,12 @@ export class AuthenticationOptionsUser { // eslint-disable-next-line @typescript-eslint/no-explicit-any customSelect?: Record; + /** + * Specify the email field of the user entity + */ + @IsString() + otpField: string = DEFAULT_OTP_FIELD; + /** * Allows to specify a filter function to remove some fields * from the user before returning it to the frontend */ diff --git a/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts b/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts index 1b0587ac2..6529c14b7 100644 --- a/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts +++ b/libs/nestjs/authentication/src/guards/jwt-two-factor.guard.ts @@ -1,5 +1,32 @@ -import { Injectable } from '@nestjs/common'; +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { GUARDS_METADATA } from '@nestjs/common/constants'; +import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { Observable } from 'rxjs'; @Injectable() -export class JwtTwoFactorGuard extends AuthGuard('jwt-two-factor') {} +export class JwtTwoFactorGuard extends AuthGuard('jwt-two-factor') { + constructor(protected readonly reflector: Reflector) { + super(reflector); + } + + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const contextType: string = context.getType(); + + // Skip the guard for rabbitmq requests + if (contextType === 'rmq') return true; + + const useGuardOverriding = this.reflector.getAllAndOverride( + GUARDS_METADATA, + [context.getHandler(), context.getClass()], + ); + + // If we have other guard in the called method we get to them directly + if (useGuardOverriding && useGuardOverriding.length > 0) return true; + + // Check request authentication by using the JwtAuthGuard + return super.canActivate(context); + } +} diff --git a/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts b/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts new file mode 100644 index 000000000..514883b20 --- /dev/null +++ b/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts @@ -0,0 +1,131 @@ +/* eslint-disable @typescript-eslint/unbound-method */ + +import { JwtService } from '@nestjs/jwt'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as bcrypt from 'bcrypt'; +import { mockDeep, MockProxy, mockReset } from 'jest-mock-extended'; + +import { + AUTHENTICATION_MODULE_OPTIONS, + AUTHENTICATION_USER_SERVICE, +} from '../constants'; +import { AuthenticationOptions } from '../dtos'; +import { UserNotFoundError } from '../errors'; +import { UserType } from '../interfaces'; +import { AuthenticationUserService } from './authentication-user.service'; +import { AuthenticationService } from './authentication.service'; +import { TwoFactorAuthenticationService } from './two-factor-authentification.service'; + +describe('TwoFactorAuthenticationService', () => { + let twoFactorAuthService: TwoFactorAuthenticationService; + + let mockUserService: MockProxy; + let mockUser: MockProxy<{ id: string; otp: string; email: string }>; + + beforeEach(async () => { + mockUserService = mockDeep(); + mockUser = mockDeep<{ id: string; otp: string; email: string }>(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { provide: AUTHENTICATION_USER_SERVICE, useValue: mockUserService }, + ], + }).compile(); + + twoFactorAuthService = module.get( + TwoFactorAuthenticationService, + ); + }); + + afterEach(() => { + mockReset(mockUserService); + mockReset(mockUser); + }); + + it('should be defined', () => { + expect(twoFactorAuthService).toBeDefined(); + }); + + describe('generateTwoFactorAuthenticationSecret', () => { + it('should return otpAuthUrl', async () => { + const otpAuthUrl = + await twoFactorAuthService.generateTwoFactorAuthenticationSecret( + mockUser, + ); + expect( + twoFactorAuthService.generateTwoFactorAuthenticationSecret, + ).toHaveBeenCalledTimes(1); + expect( + twoFactorAuthService.generateTwoFactorAuthenticationSecret, + ).toHaveBeenCalledWith(mockUser); + expect( + twoFactorAuthService.generateTwoFactorAuthenticationSecret, + ).toEqual(otpAuthUrl); + }); + }); + + describe('generateTwoFactorAuthenticationSecret', () => { + it('should return otpAuthUrl and update otp props from user', async () => { + const compare = await authService.verifyPassword('test', 'hash'); + + expect(mockBcryptCompare).toHaveBeenCalledTimes(1); + expect(mockBcryptCompare).toHaveBeenCalledWith('test', 'hash'); + expect(mockBcryptCompare).toHaveReturnedWith(true); + + expect(compare).toEqual(true); + }); + }); + + describe('createUserJWT', () => { + it('should create a User JWT', async () => { + mockUser.id = 'id'; + mockJwtService.sign.mockReturnValue('jwt'); + const compare = await authService.createUserJWT(mockUser); + + expect(mockJwtService.sign).toHaveBeenCalledTimes(1); + expect(mockJwtService.sign).toHaveBeenCalledWith({ sub: mockUser.id }); + + expect(compare).toEqual('jwt'); + }); + }); + + describe('login', () => { + it('should login a user and return an access token', async () => { + const { createUserJWT } = authService; + const mockCreateUserJWT = jest.fn().mockReturnValue('jwt'); + authService.createUserJWT = mockCreateUserJWT; + const loggedIn = await authService.login(mockUser); + authService.createUserJWT = createUserJWT; + + expect(mockCreateUserJWT).toHaveBeenCalledTimes(1); + expect(mockCreateUserJWT).toHaveBeenCalledWith(mockUser); + + expect(loggedIn).toEqual({ accessToken: 'jwt' }); + }); + }); + + describe('authenticateLoginCredentials', () => { + it('should throw a UserNotFoundError if no user has been found by the login field', async () => { + mockAuthenticationOptions.userConfig = { + ...mockAuthenticationOptions.userConfig, + loginField: 'loginFieldTest', + emailField: 'emailFieldTest', + passwordField: 'passwordFieldTest', + idField: 'idFieldTest', + }; + + mockUserService.findUnique.mockReturnValueOnce(Promise.resolve(null)); + + await expect(async () => + authService.authenticateLoginCredentials('login', 'password'), + ).rejects.toThrow(UserNotFoundError); + + expect(mockUserService.findUnique).toHaveBeenCalledTimes(1); + expect(mockUserService.findUnique).toHaveBeenCalledWith({ + where: { + loginFieldTest: 'login', + }, + }); + }); + }); +}); diff --git a/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts b/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts index 0820aa2bd..a2ec4a061 100644 --- a/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts +++ b/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts @@ -1,5 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Response } from 'express'; import { authenticator } from 'otplib'; +import { toFileStream } from 'qrcode'; import { AUTHENTICATION_USER_SERVICE, @@ -20,7 +22,7 @@ export class TwoFactorAuthenticationService { }) { const secret = authenticator.generateSecret(); - const otpauthUrl = authenticator.keyuri( + const otpAuthUrl = authenticator.keyuri( user.email, TWO_FACTOR_AUTHENTICATION, secret, @@ -35,7 +37,7 @@ export class TwoFactorAuthenticationService { }; await this.userService.update(args); - return otpauthUrl; + return otpAuthUrl; } public isTwoFactorAuthenticationCodeValid( @@ -47,4 +49,8 @@ export class TwoFactorAuthenticationService { secret: user.otp, }); } + + public async pipeQrCodeStream(stream: Response, otpauthUrl: string) { + return toFileStream(stream, otpauthUrl); + } } diff --git a/libs/nestjs/authentication/src/strategies/index.ts b/libs/nestjs/authentication/src/strategies/index.ts index c1a1dbef5..4d040c09c 100644 --- a/libs/nestjs/authentication/src/strategies/index.ts +++ b/libs/nestjs/authentication/src/strategies/index.ts @@ -1,2 +1,3 @@ export * from './jwt.strategy'; export * from './local.strategy'; +export * from './jwt-two-factor.strategy'; diff --git a/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts b/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts new file mode 100644 index 000000000..7436b1ae3 --- /dev/null +++ b/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts @@ -0,0 +1,55 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-jwt'; + +import { + AUTHENTICATION_MODULE_OPTIONS, + AUTHENTICATION_USER_SERVICE, +} from '../constants'; +import { AuthenticationOptions, JwtTokenPayload } from '../dtos'; +import { UserService } from '../interfaces'; +import { StrategyOptionsService } from '../services'; + +@Injectable() +export class JwtTwoFactorStrategy extends PassportStrategy( + Strategy, + 'jwt-two-factor', +) { + constructor( + @Inject(AUTHENTICATION_MODULE_OPTIONS) + private readonly authenticationOptions: AuthenticationOptions, + @Inject(AUTHENTICATION_USER_SERVICE) + private readonly userService: UserService, + protected readonly strategyOptionsService: StrategyOptionsService, + ) { + super(strategyOptionsService.createJwtStrategyOptions()); + } + + async validate(payload: JwtTokenPayload) { + const { + otp, + userConfig: { otpField }, + } = this.authenticationOptions; + + const user = await this.userService.findUnique({ + where: { id: payload.sub }, + // Use select clause provided by the module consumer + select: { + ...this.authenticationOptions.userConfig.customSelect, + [otpField]: otp, + }, + }); + + if (!user) { + throw new BadRequestException(); + } + if (otp && !user[otpField]) { + return user; + } + if (!payload.isSecondFactorAuthenticated) { + throw new BadRequestException(); + } + + return user; + } +} diff --git a/libs/nestjs/authentication/src/strategies/jwt.strategy.ts b/libs/nestjs/authentication/src/strategies/jwt.strategy.ts index e2a8a1b90..e4eb13b7c 100644 --- a/libs/nestjs/authentication/src/strategies/jwt.strategy.ts +++ b/libs/nestjs/authentication/src/strategies/jwt.strategy.ts @@ -31,12 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { if (!user) { throw new BadRequestException(); } - if (!user.otp) { - return user; - } - if (!payload.isSecondFactorAuthenticated) { - throw new BadRequestException(); - } + return user; } } diff --git a/package-lock.json b/package-lock.json index 9e1ff8c14..20041ae28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,6 +99,7 @@ "pkg-dir": "^6.0.1", "prisma-graphql-type-decimal": "^2.0.0", "prisma-nestjs-graphql": "^16.0.1", + "qrcode": "^1.5.0", "query-string": "^7.1.1", "ra-core": "^3.19.10", "react": "^16.8.4", @@ -174,6 +175,7 @@ "@types/passport-jwt": "^3.0.6", "@types/passport-local": "^1.0.34", "@types/pg": "^8.6.4", + "@types/qrcode": "^1.4.2", "@types/react": "^17.0.39", "@types/react-helmet": "^6.1.5", "@types/react-router-dom": "^5.3.3", @@ -11425,6 +11427,15 @@ "version": "15.7.4", "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.2.tgz", + "integrity": "sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "license": "MIT" @@ -18247,6 +18258,11 @@ "version": "4.12.0", "license": "MIT" }, + "node_modules/dijkstrajs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", + "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" + }, "node_modules/dir-glob": { "version": "3.0.1", "license": "MIT", @@ -18601,6 +18617,11 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -29491,6 +29512,14 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/popper.js": { "version": "1.16.1-lts", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -31073,6 +31102,170 @@ "teleport": ">=0.2.0" } }, + "node_modules/qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/qrcode/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/qrcode/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -44972,6 +45165,15 @@ "@types/prop-types": { "version": "15.7.4" }, + "@types/qrcode": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.2.tgz", + "integrity": "sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/qs": { "version": "6.9.7" }, @@ -49547,6 +49749,11 @@ } } }, + "dijkstrajs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", + "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" + }, "dir-glob": { "version": "3.0.1", "requires": { @@ -49791,6 +49998,11 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -57263,6 +57475,11 @@ "pluralize": { "version": "8.0.0" }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "popper.js": { "version": "1.16.1-lts", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -58203,6 +58420,130 @@ "version": "1.5.1", "dev": true }, + "qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==", + "requires": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", diff --git a/package.json b/package.json index aeccdb875..4e06c5116 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "pkg-dir": "^6.0.1", "prisma-graphql-type-decimal": "^2.0.0", "prisma-nestjs-graphql": "^16.0.1", + "qrcode": "^1.5.0", "query-string": "^7.1.1", "ra-core": "^3.19.10", "react": "^16.8.4", @@ -207,6 +208,7 @@ "@types/passport-jwt": "^3.0.6", "@types/passport-local": "^1.0.34", "@types/pg": "^8.6.4", + "@types/qrcode": "^1.4.2", "@types/react": "^17.0.39", "@types/react-helmet": "^6.1.5", "@types/react-router-dom": "^5.3.3", From 085de189482e46be3de7709fdd9d529b693eade1 Mon Sep 17 00:00:00 2001 From: Killian Challeau Date: Mon, 27 Jun 2022 12:07:27 -0400 Subject: [PATCH 3/4] test: add test to controllers and services linked to the OTP --- apps/api/src/app/app.module.ts | 2 +- .../src/config/get-prisma-user-query.ts.hpf | 17 +- .../src/authentication.module.ts | 8 +- ...o-factor-authentication.controller.spec.ts | 344 ++++++++++++++++++ .../two-factor-authentication.controller.ts | 11 +- .../dtos/authentication-options-user.dto.ts | 5 +- .../two-factor-authentication.service.spec.ts | 217 ++++++----- .../two-factor-authentification.service.ts | 13 + .../src/strategies/jwt-two-factor.strategy.ts | 2 + package-lock.json | 13 + package.json | 1 + 11 files changed, 517 insertions(+), 116 deletions(-) create mode 100644 libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.spec.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index dcc535bca..58bb3c9d1 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -50,7 +50,7 @@ import { MailerModule } from '@tractr/nestjs-mailer'; loginField: 'email', passwordField: 'password', emailField: 'email', - customSelect: getSelectPrismaUserQuery(), + customSelect: getSelectPrismaUserQuery({ otp: true }), otpField: 'otp', formatUser: ({ ...user }) => user, }, diff --git a/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf b/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf index ebd27ef73..16f04b4aa 100644 --- a/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf +++ b/libs/hapify/templates/casl/hapify/generated/casl/src/config/get-prisma-user-query.ts.hpf @@ -11,14 +11,20 @@ export type UserWithIds = <<=getUserFindUniqueReturnTypeFromHapify()>>; * Get the select configuration for the prisma user query to be able * to construct the user with the correct ids */ -export function getSelectPrismaUserQuery(user:Partial = {}): Prisma.UserSelect { - return <<=getSelectPrismaUserQueryFromHapify(user)>>; +export function getSelectPrismaUserQuery(user:Partial = {}): Prisma.UserSelect { + + const result = <<=getSelectPrismaUserQueryFromHapify()>>; + + return { + ...result, + ...user, + } } <<< - function getSelectPrismaUserQueryFromHapify(user) { - const info = getOwnershipInformationsFromHapify(user); + function getSelectPrismaUserQueryFromHapify() { + const info = getOwnershipInformationsFromHapify(); return JSON.stringify(info.select, null, 2); } @@ -41,7 +47,7 @@ export function getSelectPrismaUserQuery(user:Partial = {}): Prisma.UserSe : `, ${info.modelListToImport.join(', ')} `; } - function getOwnershipInformationsFromHapify(user:Partial = {}) { + function getOwnershipInformationsFromHapify() { const models = root; if (!models) return JSON.stringify({}, null, 2); @@ -203,7 +209,6 @@ export function getSelectPrismaUserQuery(user:Partial = {}): Prisma.UserSe { select: { roles: true, - ...user }, types: { roles: "User['roles'];", diff --git a/libs/nestjs/authentication/src/authentication.module.ts b/libs/nestjs/authentication/src/authentication.module.ts index 6ed0b2e4d..6943beabe 100644 --- a/libs/nestjs/authentication/src/authentication.module.ts +++ b/libs/nestjs/authentication/src/authentication.module.ts @@ -101,7 +101,7 @@ export class AuthenticationModule extends ModuleOptionsFactory< ], exports: [ AuthenticationService, - ...(otp ? [TwoFactorAuthenticationService] : []), + TwoFactorAuthenticationService, JwtStrategy, LocalStrategy, PasswordService, @@ -112,11 +112,11 @@ export class AuthenticationModule extends ModuleOptionsFactory< ], providers: [ AuthenticationService, - ...(otp ? [TwoFactorAuthenticationService] : []), + TwoFactorAuthenticationService, PasswordService, StrategyOptionsService, JwtStrategy, - ...(otp ? [JwtTwoFactorStrategy] : []), + JwtTwoFactorStrategy, LocalStrategy, { provide: AUTHENTICATION_USER_SERVICE, @@ -126,7 +126,7 @@ export class AuthenticationModule extends ModuleOptionsFactory< controllers: [ LoginController, PasswordController, - ...(otp ? [TwoFactorAuthenticationController] : []), + TwoFactorAuthenticationController, ], }; } diff --git a/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.spec.ts b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.spec.ts new file mode 100644 index 000000000..c711f485c --- /dev/null +++ b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.spec.ts @@ -0,0 +1,344 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; +import { Test, TestingModule } from '@nestjs/testing'; +import { mockDeep, MockProxy } from 'jest-mock-extended'; +import * as request from 'supertest'; + +import { AuthenticationModule } from '../authentication.module'; +import { AUTHENTICATION_MODULE_OPTIONS } from '../constants'; +import { AuthenticationOptions } from '../dtos'; +import { JwtGlobalAuthGuard } from '../guards'; +import { UserType } from '../interfaces'; +import { + AuthenticationService, + AuthenticationUserService, + TwoFactorAuthenticationService, +} from '../services'; + +const AUTHENTICATION_MOCK_USER_SERVICE = 'AUTHENTICATION_MOCK_USER_SERVICE'; + +describe('Authentication Module with OTP mode enabled', () => { + let app: INestApplication; + let mockUserService: MockProxy; + let mockAuthenticationOptions: MockProxy; + let mockTwoFactorAuthenticationService: MockProxy; + + let mockUser: UserType; + const mockFormatUser = jest.fn(); + + beforeAll(async () => { + mockUserService = mockDeep(); + mockAuthenticationOptions = mockDeep(); + mockTwoFactorAuthenticationService = + mockDeep(); + + mockUser = { + id: '1', + email: 'test@email.com', + password: 'password', + otp: '123456', + }; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: TwoFactorAuthenticationService, + useValue: mockTwoFactorAuthenticationService, + }, + + { + provide: APP_GUARD, + useClass: JwtGlobalAuthGuard, + }, + { + provide: AUTHENTICATION_MOCK_USER_SERVICE, + useValue: mockUserService, + }, + { + provide: AUTHENTICATION_MODULE_OPTIONS, + useValue: mockAuthenticationOptions, + }, + ], + imports: [ + AuthenticationModule.register({ + userConfig: { + idField: 'id', + emailField: 'email', + loginField: 'email', + passwordField: 'password', + otpField: 'otp', + formatUser: mockFormatUser, + }, + jwtModuleOptions: { + secret: 'integration-tests', + }, + userService: AUTHENTICATION_MOCK_USER_SERVICE, + otp: true, + }), + ], + }).compile(); + + mockUserService.findUnique.mockReturnValue(Promise.resolve(mockUser)); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + transformOptions: { + enableImplicitConversion: false, + exposeDefaultValues: true, + }, + }), + ); + + await app.init(); + }); + + beforeEach(() => { + mockFormatUser.mockImplementation((user) => user); + }); + + afterEach(async () => { + mockFormatUser.mockReset(); + mockUserService.findUnique.mockReset(); + if (app) await app.close(); + }); + + describe('Route without token in header', () => { + it('/2fa/generate should return 401', async () => { + await request(app.getHttpServer()).post('/2fa/generate').expect(401); + }); + it('/2fa/authenticate should return 401', async () => { + await request(app.getHttpServer()).post('/2fa/authenticate').expect(401); + }); + }); + + describe('Route with token set in header', () => { + it('/2fa/generate should return 201', async () => { + const authenticationService = app.get( + AuthenticationService, + ); + const accessToken = await authenticationService.createUserJWT(mockUser); + + mockUserService.findUnique.mockResolvedValue(Promise.resolve(mockUser)); + + const response = await request(app.getHttpServer()) + .post('/2fa/generate') + .set('Authorization', `bearer ${accessToken}`); + + expect(response.status).toBe(201); + }); + it('/2fa/authenticate without body should return 400', async () => { + const authenticationService = app.get( + AuthenticationService, + ); + const accessToken = await authenticationService.createUserJWT(mockUser); + + mockUserService.findUnique.mockResolvedValue(Promise.resolve(mockUser)); + + const response = await request(app.getHttpServer()) + .post('/2fa/authenticate') + .set('Authorization', `bearer ${accessToken}`); + + expect(response.status).toBe(400); + expect(response.body).toEqual( + JSON.parse( + JSON.stringify({ + error: 'Bad Request', + message: ['code must be a string'], + statusCode: 400, + }), + ), + ); + }); + it('/2fa/authenticate with body should return 401 with wrong authorization code', async () => { + const authenticationService = app.get( + AuthenticationService, + ); + const accessToken = await authenticationService.createUserJWT(mockUser); + + mockUserService.findUnique.mockResolvedValue(Promise.resolve(mockUser)); + + mockTwoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid.mockReturnValueOnce( + false, + ); + + const response = await request(app.getHttpServer()) + .post('/2fa/authenticate') + .send({ + code: '1234', + }) + .set('Authorization', `bearer ${accessToken}`); + + expect(response.status).toBe(401); + expect(response.body).toEqual( + JSON.parse( + JSON.stringify({ + error: 'Unauthorized', + message: 'Wrong authentication code', + statusCode: 401, + }), + ), + ); + }); + it('/2fa/authenticate with body should return 200 with good authorization code', async () => { + const authenticationService = app.get( + AuthenticationService, + ); + const accessToken = await authenticationService.createUserJWT(mockUser); + + mockUserService.findUnique.mockResolvedValue(Promise.resolve(mockUser)); + + mockTwoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid.mockReturnValueOnce( + true, + ); + + const response = await request(app.getHttpServer()) + .post('/2fa/authenticate') + .send({ + code: '1234', + }) + .set('Authorization', `bearer ${accessToken}`); + + expect(response.status).toBe(401); + expect(response.body).toEqual( + JSON.parse( + JSON.stringify({ + error: 'Unauthorized', + message: 'Wrong authentication code', + statusCode: 401, + }), + ), + ); + }); + }); +}); +describe('Authentication Module with OTP mode not enabled', () => { + let app: INestApplication; + let mockUserService: MockProxy; + let mockAuthenticationOptions: MockProxy; + let mockTwoFactorAuthenticationService: MockProxy; + + let mockUser: UserType; + const mockFormatUser = jest.fn(); + + beforeAll(async () => { + mockUserService = mockDeep(); + mockAuthenticationOptions = mockDeep(); + mockTwoFactorAuthenticationService = + mockDeep(); + + mockUser = { + id: '1', + email: 'test@email.com', + password: 'password', + otp: '123456', + }; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: TwoFactorAuthenticationService, + useValue: mockTwoFactorAuthenticationService, + }, + + { + provide: APP_GUARD, + useClass: JwtGlobalAuthGuard, + }, + { + provide: AUTHENTICATION_MOCK_USER_SERVICE, + useValue: mockUserService, + }, + { + provide: AUTHENTICATION_MODULE_OPTIONS, + useValue: mockAuthenticationOptions, + }, + ], + imports: [ + AuthenticationModule.register({ + userConfig: { + idField: 'id', + emailField: 'email', + loginField: 'email', + passwordField: 'password', + otpField: 'otp', + formatUser: mockFormatUser, + }, + jwtModuleOptions: { + secret: 'integration-tests', + }, + userService: AUTHENTICATION_MOCK_USER_SERVICE, + }), + ], + }).compile(); + + mockUserService.findUnique.mockReturnValue(Promise.resolve(mockUser)); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + transformOptions: { + enableImplicitConversion: false, + exposeDefaultValues: true, + }, + }), + ); + + await app.init(); + }); + + beforeEach(() => { + mockFormatUser.mockImplementation((user) => user); + }); + + afterEach(async () => { + mockFormatUser.mockReset(); + mockUserService.findUnique.mockReset(); + if (app) await app.close(); + }); + + describe('Route without token in header', () => { + it('/2fa/generate should return 401', async () => { + await request(app.getHttpServer()).post('/2fa/generate').expect(401); + }); + it('/2fa/authenticate should return 401', async () => { + await request(app.getHttpServer()).post('/2fa/authenticate').expect(401); + }); + }); + + describe('Route with token set in header', () => { + it('/2fa/generate should return 404', async () => { + const authenticationService = app.get( + AuthenticationService, + ); + const accessToken = await authenticationService.createUserJWT(mockUser); + + mockUserService.findUnique.mockResolvedValue(Promise.resolve(mockUser)); + + const response = await request(app.getHttpServer()) + .post('/2fa/generate') + .set('Authorization', `bearer ${accessToken}`); + + expect(response.status).toBe(404); + }); + it('/2fa/authenticate without body should return 404', async () => { + const authenticationService = app.get( + AuthenticationService, + ); + const accessToken = await authenticationService.createUserJWT(mockUser); + + mockUserService.findUnique.mockResolvedValue(Promise.resolve(mockUser)); + + const response = await request(app.getHttpServer()) + .post('/2fa/authenticate') + .send({ + code: '1234', + }) + .set('Authorization', `bearer ${accessToken}`); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts index 46e882a54..23486171d 100644 --- a/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts +++ b/libs/nestjs/authentication/src/controllers/two-factor-authentication.controller.ts @@ -2,7 +2,8 @@ import { Body, ClassSerializerInterceptor, Controller, - HttpCode, + HttpException, + HttpStatus, Inject, Post, Req, @@ -38,6 +39,9 @@ export class TwoFactorAuthenticationController { @Res() response: Response, @CurrentUser() user: { id: string; email: string }, ) { + if (!this.authenticationOptions.otp) { + throw new HttpException('OTP mode is not enabled', HttpStatus.NOT_FOUND); + } const otpauthUrl = await this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret( user, @@ -51,13 +55,16 @@ export class TwoFactorAuthenticationController { @Post('authenticate') @UseGuards(JwtAuthGuard) - @HttpCode(200) async authenticate( @CurrentUser() user: { id: string; otp: string }, @Body() { code }: TwoFactorAuthenticationCodeDto, @Req() req: Request & { secret: string }, @Res({ passthrough: true }) res: Response, ) { + if (!this.authenticationOptions.otp) { + throw new HttpException('OTP mode is not enabled', HttpStatus.NOT_FOUND); + } + const isCodeValid = this.twoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid( code, diff --git a/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts b/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts index 13763628e..5d6b49031 100644 --- a/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts +++ b/libs/nestjs/authentication/src/dtos/authentication-options-user.dto.ts @@ -44,10 +44,11 @@ export class AuthenticationOptionsUser { customSelect?: Record; /** - * Specify the email field of the user entity + * Specify the otp field of the user entity */ @IsString() - otpField: string = DEFAULT_OTP_FIELD; + @IsOptional() + otpField?: string = DEFAULT_OTP_FIELD; /** * Allows to specify a filter function to remove some fields diff --git a/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts b/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts index 514883b20..875dcc93e 100644 --- a/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts +++ b/libs/nestjs/authentication/src/services/two-factor-authentication.service.spec.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import { JwtService } from '@nestjs/jwt'; +import { getMockRes } from '@jest-mock/express'; import { Test, TestingModule } from '@nestjs/testing'; -import * as bcrypt from 'bcrypt'; import { mockDeep, MockProxy, mockReset } from 'jest-mock-extended'; import { @@ -10,121 +9,137 @@ import { AUTHENTICATION_USER_SERVICE, } from '../constants'; import { AuthenticationOptions } from '../dtos'; -import { UserNotFoundError } from '../errors'; -import { UserType } from '../interfaces'; import { AuthenticationUserService } from './authentication-user.service'; -import { AuthenticationService } from './authentication.service'; import { TwoFactorAuthenticationService } from './two-factor-authentification.service'; describe('TwoFactorAuthenticationService', () => { let twoFactorAuthService: TwoFactorAuthenticationService; - let mockUserService: MockProxy; + let mockAuthenticationOptions: MockProxy; let mockUser: MockProxy<{ id: string; otp: string; email: string }>; - beforeEach(async () => { - mockUserService = mockDeep(); - mockUser = mockDeep<{ id: string; otp: string; email: string }>(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { provide: AUTHENTICATION_USER_SERVICE, useValue: mockUserService }, - ], - }).compile(); - - twoFactorAuthService = module.get( - TwoFactorAuthenticationService, - ); - }); - - afterEach(() => { - mockReset(mockUserService); - mockReset(mockUser); - }); - - it('should be defined', () => { - expect(twoFactorAuthService).toBeDefined(); - }); + describe('WHEN OTP IS NOT ENABLED', () => { + beforeEach(async () => { + mockUserService = mockDeep(); + mockUser = mockDeep<{ id: string; otp: string; email: string }>(); + mockAuthenticationOptions = mockDeep(); + mockAuthenticationOptions.otp = false; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TwoFactorAuthenticationService, + { provide: AUTHENTICATION_USER_SERVICE, useValue: mockUserService }, + { + provide: AUTHENTICATION_MODULE_OPTIONS, + useValue: mockAuthenticationOptions, + }, + ], + }).compile(); + + twoFactorAuthService = module.get( + TwoFactorAuthenticationService, + ); + }); - describe('generateTwoFactorAuthenticationSecret', () => { - it('should return otpAuthUrl', async () => { - const otpAuthUrl = - await twoFactorAuthService.generateTwoFactorAuthenticationSecret( + it('generateTwoFactorAuthenticationSecret should throw error if otp mode is not activated', async () => { + await expect(async () => + twoFactorAuthService.generateTwoFactorAuthenticationSecret(mockUser), + ).rejects.toThrow('OTP mode is not enabled'); + }); + it('isTwoFactorAuthenticationCodeValid should throw error if otp mode is not activated', async () => { + await expect(async () => + twoFactorAuthService.isTwoFactorAuthenticationCodeValid( + 'test', mockUser, - ); - expect( - twoFactorAuthService.generateTwoFactorAuthenticationSecret, - ).toHaveBeenCalledTimes(1); - expect( - twoFactorAuthService.generateTwoFactorAuthenticationSecret, - ).toHaveBeenCalledWith(mockUser); - expect( - twoFactorAuthService.generateTwoFactorAuthenticationSecret, - ).toEqual(otpAuthUrl); + ), + ).rejects.toThrow('OTP mode is not enabled'); }); - }); - - describe('generateTwoFactorAuthenticationSecret', () => { - it('should return otpAuthUrl and update otp props from user', async () => { - const compare = await authService.verifyPassword('test', 'hash'); - - expect(mockBcryptCompare).toHaveBeenCalledTimes(1); - expect(mockBcryptCompare).toHaveBeenCalledWith('test', 'hash'); - expect(mockBcryptCompare).toHaveReturnedWith(true); - - expect(compare).toEqual(true); + it('pipeQrCodeStream should throw error if otp mode is not activated', async () => { + const { res } = getMockRes(); + await expect(async () => + twoFactorAuthService.pipeQrCodeStream(res, 'test'), + ).rejects.toThrow('OTP mode is not enabled'); }); }); - describe('createUserJWT', () => { - it('should create a User JWT', async () => { - mockUser.id = 'id'; - mockJwtService.sign.mockReturnValue('jwt'); - const compare = await authService.createUserJWT(mockUser); - - expect(mockJwtService.sign).toHaveBeenCalledTimes(1); - expect(mockJwtService.sign).toHaveBeenCalledWith({ sub: mockUser.id }); - - expect(compare).toEqual('jwt'); + describe('WHEN OTP IS ENABLED', () => { + beforeEach(async () => { + mockUserService = mockDeep(); + mockUser = mockDeep<{ id: string; otp: string; email: string }>(); + mockAuthenticationOptions = mockDeep(); + mockAuthenticationOptions.otp = false; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TwoFactorAuthenticationService, + { provide: AUTHENTICATION_USER_SERVICE, useValue: mockUserService }, + { + provide: AUTHENTICATION_MODULE_OPTIONS, + useValue: mockAuthenticationOptions, + }, + ], + }).compile(); + + twoFactorAuthService = module.get( + TwoFactorAuthenticationService, + ); }); - }); - - describe('login', () => { - it('should login a user and return an access token', async () => { - const { createUserJWT } = authService; - const mockCreateUserJWT = jest.fn().mockReturnValue('jwt'); - authService.createUserJWT = mockCreateUserJWT; - const loggedIn = await authService.login(mockUser); - authService.createUserJWT = createUserJWT; - - expect(mockCreateUserJWT).toHaveBeenCalledTimes(1); - expect(mockCreateUserJWT).toHaveBeenCalledWith(mockUser); - - expect(loggedIn).toEqual({ accessToken: 'jwt' }); + afterEach(() => { + mockReset(mockUserService); + mockReset(mockAuthenticationOptions); + mockReset(mockUser); }); - }); - - describe('authenticateLoginCredentials', () => { - it('should throw a UserNotFoundError if no user has been found by the login field', async () => { - mockAuthenticationOptions.userConfig = { - ...mockAuthenticationOptions.userConfig, - loginField: 'loginFieldTest', - emailField: 'emailFieldTest', - passwordField: 'passwordFieldTest', - idField: 'idFieldTest', - }; - - mockUserService.findUnique.mockReturnValueOnce(Promise.resolve(null)); - - await expect(async () => - authService.authenticateLoginCredentials('login', 'password'), - ).rejects.toThrow(UserNotFoundError); - - expect(mockUserService.findUnique).toHaveBeenCalledTimes(1); - expect(mockUserService.findUnique).toHaveBeenCalledWith({ - where: { - loginFieldTest: 'login', - }, + it('should be defined', () => { + expect(twoFactorAuthService).toBeDefined(); + }); + describe('generateTwoFactorAuthenticationSecret', () => { + it('should return otpAuthUrl', async () => { + const mockAuthenticatorKeyUri = jest.fn().mockReturnValue('otpAuthUrl'); + twoFactorAuthService.generateTwoFactorAuthenticationSecret = + mockAuthenticatorKeyUri; + + const result = + await twoFactorAuthService.generateTwoFactorAuthenticationSecret( + mockUser, + ); + expect( + twoFactorAuthService.generateTwoFactorAuthenticationSecret, + ).toHaveBeenCalledTimes(1); + expect( + twoFactorAuthService.generateTwoFactorAuthenticationSecret, + ).toHaveBeenCalledWith(mockUser); + expect(result).toEqual('otpAuthUrl'); + }); + }); + describe('isTwoFactorAuthenticationCodeValid', () => { + it('should return a boolean value', async () => { + const mockBoolean = jest.fn().mockReturnValue(true); + twoFactorAuthService.isTwoFactorAuthenticationCodeValid = mockBoolean; + const result = twoFactorAuthService.isTwoFactorAuthenticationCodeValid( + 'test', + mockUser, + ); + expect( + twoFactorAuthService.isTwoFactorAuthenticationCodeValid, + ).toHaveBeenCalledTimes(1); + expect( + twoFactorAuthService.isTwoFactorAuthenticationCodeValid, + ).toHaveBeenCalledWith('test', mockUser); + expect(result).toEqual(true); + }); + }); + describe('pipeQrCodeStream', () => { + it('should return otpAuthUrl', async () => { + const mockFileStream = jest.fn().mockResolvedValueOnce('promise'); + twoFactorAuthService.pipeQrCodeStream = mockFileStream; + const { res } = getMockRes(); + const result = await twoFactorAuthService.pipeQrCodeStream(res, 'test'); + expect(twoFactorAuthService.pipeQrCodeStream).toHaveBeenCalledTimes(1); + expect(twoFactorAuthService.pipeQrCodeStream).toHaveBeenCalledWith( + res, + 'test', + ); + expect(result).toEqual('promise'); }); }); }); diff --git a/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts b/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts index a2ec4a061..3033d8bb5 100644 --- a/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts +++ b/libs/nestjs/authentication/src/services/two-factor-authentification.service.ts @@ -4,9 +4,11 @@ import { authenticator } from 'otplib'; import { toFileStream } from 'qrcode'; import { + AUTHENTICATION_MODULE_OPTIONS, AUTHENTICATION_USER_SERVICE, TWO_FACTOR_AUTHENTICATION, } from '../constants'; +import { AuthenticationOptions } from '../dtos'; import { UserService } from '../interfaces/user.service.interface'; @Injectable() @@ -14,12 +16,15 @@ export class TwoFactorAuthenticationService { constructor( @Inject(AUTHENTICATION_USER_SERVICE) private readonly userService: UserService, + @Inject(AUTHENTICATION_MODULE_OPTIONS) + private readonly authenticationOptions: AuthenticationOptions, ) {} public async generateTwoFactorAuthenticationSecret(user: { id: string; email: string; }) { + this.checkIfOtpModeIsEnabled(); const secret = authenticator.generateSecret(); const otpAuthUrl = authenticator.keyuri( @@ -44,6 +49,7 @@ export class TwoFactorAuthenticationService { twoFactorAuthenticationCode: string, user: { otp: string }, ) { + this.checkIfOtpModeIsEnabled(); return authenticator.verify({ token: twoFactorAuthenticationCode, secret: user.otp, @@ -51,6 +57,13 @@ export class TwoFactorAuthenticationService { } public async pipeQrCodeStream(stream: Response, otpauthUrl: string) { + this.checkIfOtpModeIsEnabled(); return toFileStream(stream, otpauthUrl); } + + checkIfOtpModeIsEnabled() { + if (!this.authenticationOptions.otp) { + throw new Error('OTP mode is not enabled'); + } + } } diff --git a/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts b/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts index 7436b1ae3..131ac3fd4 100644 --- a/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts +++ b/libs/nestjs/authentication/src/strategies/jwt-two-factor.strategy.ts @@ -31,6 +31,8 @@ export class JwtTwoFactorStrategy extends PassportStrategy( userConfig: { otpField }, } = this.authenticationOptions; + if (!otpField) throw new BadRequestException('OTP field is not configured'); + const user = await this.userService.findUnique({ where: { id: payload.sub }, // Use select clause provided by the module consumer diff --git a/package-lock.json b/package-lock.json index 20041ae28..1d09633f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,6 +136,7 @@ "@docusaurus/mdx-loader": "^2.0.0-beta.15", "@docusaurus/module-type-aliases": "^2.0.0-beta.15", "@hapify/cli": "1.3.2", + "@jest-mock/express": "^2.0.0", "@jscutlery/semver": "^2.23.4", "@nestjs/schematics": "^8.0.11", "@nestjs/testing": "^8.4.5", @@ -5062,6 +5063,12 @@ "node": ">=8" } }, + "node_modules/@jest-mock/express": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@jest-mock/express/-/express-2.0.0.tgz", + "integrity": "sha512-8nBAHPkwH6aU6POBNK/M8ny4M28HykvlV6fhmmhidhOsZps24zbKq3dyJi1CiZ7NyyQfN3XpZP+OJoo5ZTtaUQ==", + "dev": true + }, "node_modules/@jest/console": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", @@ -40719,6 +40726,12 @@ "@istanbuljs/schema": { "version": "0.1.3" }, + "@jest-mock/express": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@jest-mock/express/-/express-2.0.0.tgz", + "integrity": "sha512-8nBAHPkwH6aU6POBNK/M8ny4M28HykvlV6fhmmhidhOsZps24zbKq3dyJi1CiZ7NyyQfN3XpZP+OJoo5ZTtaUQ==", + "dev": true + }, "@jest/console": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", diff --git a/package.json b/package.json index 4e06c5116..ea1708338 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,7 @@ "@docusaurus/mdx-loader": "^2.0.0-beta.15", "@docusaurus/module-type-aliases": "^2.0.0-beta.15", "@hapify/cli": "1.3.2", + "@jest-mock/express": "^2.0.0", "@jscutlery/semver": "^2.23.4", "@nestjs/schematics": "^8.0.11", "@nestjs/testing": "^8.4.5", From 2561633bbd54a75b71d6e198899637592401acb7 Mon Sep 17 00:00:00 2001 From: Killian Challeau Date: Mon, 27 Jun 2022 14:35:06 -0400 Subject: [PATCH 4/4] fix: fix merge --- hapify-models.json | 2 +- package.json | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/hapify-models.json b/hapify-models.json index 39741edf9..d058229c5 100644 --- a/hapify-models.json +++ b/hapify-models.json @@ -30,7 +30,7 @@ }, "name": "user", "notes": "User that owns the answer", - "properties": ["ownership"], + "properties": ["internal", "ownership"], "subtype": "oneMany", "type": "entity", "value": "a7d0308a-49f0-3458-0975-1dce106136a1" diff --git a/package.json b/package.json index 523b0d33e..e2035537b 100644 --- a/package.json +++ b/package.json @@ -266,5 +266,13 @@ "@swc/core-darwin-arm64": "1.2.204", "@swc/core-darwin-x64": "1.2.204", "@swc/core-linux-x64-gnu": "^1.2.204" + }, + "publishConfig": { + "access": "restricted", + "registry": "https://npm.pkg.github.com" + }, + "prisma": { + "schema": "libs/generated/prisma/prisma/schema.prisma", + "seed": "ts-node -r tsconfig-paths/register --project libs/generated/prisma/tsconfig.lib.json libs/generated/prisma/prisma/seed.ts" } }