From e4df9d0f89b015598e8524a2cfe9b87fad89ee08 Mon Sep 17 00:00:00 2001 From: sulnong Date: Tue, 14 Mar 2023 10:35:44 +0800 Subject: [PATCH 1/4] feat(authentication): new authentication implements --- server/package-lock.json | 343 +++++++++++++++++- server/package.json | 3 + server/prisma/schema.prisma | 49 ++- server/src/auth/auth.module.ts | 25 +- server/src/auth/authentication.controller.ts | 31 ++ server/src/auth/authentication.service.ts | 65 ++++ server/src/auth/dto/passwd-reset.dto.ts | 37 ++ server/src/auth/dto/passwd-signin.dto.ts | 22 ++ server/src/auth/dto/passwd-signup.dto.ts | 47 +++ server/src/auth/dto/phone-signin.dto.ts | 36 ++ server/src/auth/dto/send-phone-code.dto.ts | 22 ++ server/src/auth/phone/phone.controller.ts | 88 +++++ server/src/auth/phone/phone.service.ts | 128 +++++++ server/src/auth/phone/sms.service.ts | 163 +++++++++ server/src/auth/types.ts | 31 ++ .../user-passwd/user-password.controller.ts | 121 ++++++ .../auth/user-passwd/user-password.service.ts | 92 +++++ server/src/constants.ts | 12 + server/src/initializer/initializer.service.ts | 53 +++ server/src/main.ts | 1 + server/src/user/dto/user.response.ts | 3 + server/src/user/user.service.ts | 19 + server/src/utils/crypto.ts | 8 + 23 files changed, 1386 insertions(+), 13 deletions(-) create mode 100644 server/src/auth/authentication.controller.ts create mode 100644 server/src/auth/authentication.service.ts create mode 100644 server/src/auth/dto/passwd-reset.dto.ts create mode 100644 server/src/auth/dto/passwd-signin.dto.ts create mode 100644 server/src/auth/dto/passwd-signup.dto.ts create mode 100644 server/src/auth/dto/phone-signin.dto.ts create mode 100644 server/src/auth/dto/send-phone-code.dto.ts create mode 100644 server/src/auth/phone/phone.controller.ts create mode 100644 server/src/auth/phone/phone.service.ts create mode 100644 server/src/auth/phone/sms.service.ts create mode 100644 server/src/auth/types.ts create mode 100644 server/src/auth/user-passwd/user-password.controller.ts create mode 100644 server/src/auth/user-passwd/user-password.service.ts create mode 100644 server/src/utils/crypto.ts diff --git a/server/package-lock.json b/server/package-lock.json index 4c2244fb0d..f90d1b2be3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0-beta.4", "license": "UNLICENSED", "dependencies": { + "@alicloud/dysmsapi20170525": "^2.0.23", + "@alicloud/openapi-client": "^0.4.5", + "@alicloud/tea-util": "^1.4.5", "@aws-sdk/client-s3": "^3.245.0", "@aws-sdk/client-sts": "^3.226.0", "@kubernetes/client-node": "^0.17.1", @@ -79,6 +82,104 @@ "integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==", "dev": true }, + "node_modules/@alicloud/credentials": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@alicloud/credentials/-/credentials-2.2.6.tgz", + "integrity": "sha512-jG+msY77dHmAF3x+8VTy7fEgORyXLHmDci8t92HeipBdCHsPptDegA++GEwKgR7f6G4wvafYt+aqMZ1iligdrQ==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.3", + "httpx": "^2.2.0", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/dysmsapi20170525": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/@alicloud/dysmsapi20170525/-/dysmsapi20170525-2.0.23.tgz", + "integrity": "sha512-C02xj9S2ZPL13SciChlIY3s5+PiOM13jEGZSn+L92aiWYCBqTlpx9UMwNKBNWImMSOlG71IOSYfsQggaoIY+4Q==", + "dependencies": { + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/openapi-client": "^0.4.4", + "@alicloud/openapi-util": "^0.3.0", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.5" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-client": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-client/-/openapi-client-0.4.5.tgz", + "integrity": "sha512-x1blwhfPOVkH/JCLWFssFRWDL0C75RToun9AwhNV+84gqJB2/GUipm3quHGLon8JiQ0DQ9YBUho2rukSoAvhJQ==", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.1", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.5", + "@alicloud/tea-xml": "0.0.2" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-util/-/openapi-util-0.3.1.tgz", + "integrity": "sha512-6mGT+hs+SXismZi/CEkjPhhbn2U3qTT/Qv/RXAYFA1DC3Jk4/YaX3N7RtpgdzOhdD7uI8XtNkaULKHZY3BrtxQ==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-typescript/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@alicloud/tea-util/-/tea-util-1.4.5.tgz", + "integrity": "sha512-7NuThYUi90/ivT/ORKusm0NVKlc1khPTtlzTR77xEqSBt7d24Ee/Lo70hx9PWP28nHpIZ1gM0NKYBtpq7HUDlg==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/tea-xml": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@alicloud/tea-xml/-/tea-xml-0.0.2.tgz", + "integrity": "sha512-Xs7v5y7YSNSDDYmiDWAC0/013VWPjS3dQU4KezSLva9VGiTVPaL3S7Nk4NrTmAYCG6MKcrRj/nGEDIWL5KRoPg==", + "dependencies": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.4.22" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -7308,6 +7409,14 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/xml2js": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz", + "integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -9277,7 +9386,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -9293,8 +9401,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decache": { "version": "4.6.1", @@ -11178,6 +11285,20 @@ "npm": ">=1.3.7" } }, + "node_modules/httpx": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/httpx/-/httpx-2.2.7.tgz", + "integrity": "sha512-Wjh2JOAah0pdczfqL8NC5378G7jMt0Zcpn8U+yyxAiejjlagzSTQgJHuVvka2VNPQlKfoGehYRc79WKq9E4gDw==", + "dependencies": { + "@types/node": "^14", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "14.18.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.37.tgz", + "integrity": "sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -11306,6 +11427,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -12659,6 +12785,19 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kitx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/kitx/-/kitx-2.1.0.tgz", + "integrity": "sha512-C/5v9MtIX7aHGOjwn5BmrrbNkJSf7i0R5mRzmh13GSAdRqQ7bYQo/Su2pTYNylFicqKNTVX3HML9k1u8k51+pQ==", + "dependencies": { + "@types/node": "^12.0.2" + } + }, + "node_modules/kitx/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -14751,8 +14890,7 @@ "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/schema-utils": { "version": "3.1.1", @@ -15053,6 +15191,11 @@ "node": ">=8" } }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -16709,6 +16852,26 @@ } } }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmldoc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-1.2.0.tgz", @@ -16822,6 +16985,106 @@ "integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==", "dev": true }, + "@alicloud/credentials": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@alicloud/credentials/-/credentials-2.2.6.tgz", + "integrity": "sha512-jG+msY77dHmAF3x+8VTy7fEgORyXLHmDci8t92HeipBdCHsPptDegA++GEwKgR7f6G4wvafYt+aqMZ1iligdrQ==", + "requires": { + "@alicloud/tea-typescript": "^1.5.3", + "httpx": "^2.2.0", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "@alicloud/dysmsapi20170525": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/@alicloud/dysmsapi20170525/-/dysmsapi20170525-2.0.23.tgz", + "integrity": "sha512-C02xj9S2ZPL13SciChlIY3s5+PiOM13jEGZSn+L92aiWYCBqTlpx9UMwNKBNWImMSOlG71IOSYfsQggaoIY+4Q==", + "requires": { + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/openapi-client": "^0.4.4", + "@alicloud/openapi-util": "^0.3.0", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.5" + } + }, + "@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "requires": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "requires": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "@alicloud/openapi-client": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-client/-/openapi-client-0.4.5.tgz", + "integrity": "sha512-x1blwhfPOVkH/JCLWFssFRWDL0C75RToun9AwhNV+84gqJB2/GUipm3quHGLon8JiQ0DQ9YBUho2rukSoAvhJQ==", + "requires": { + "@alicloud/credentials": "^2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.1", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.5", + "@alicloud/tea-xml": "0.0.2" + } + }, + "@alicloud/openapi-util": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-util/-/openapi-util-0.3.1.tgz", + "integrity": "sha512-6mGT+hs+SXismZi/CEkjPhhbn2U3qTT/Qv/RXAYFA1DC3Jk4/YaX3N7RtpgdzOhdD7uI8XtNkaULKHZY3BrtxQ==", + "requires": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "requires": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + }, + "dependencies": { + "@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + } + } + }, + "@alicloud/tea-util": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@alicloud/tea-util/-/tea-util-1.4.5.tgz", + "integrity": "sha512-7NuThYUi90/ivT/ORKusm0NVKlc1khPTtlzTR77xEqSBt7d24Ee/Lo70hx9PWP28nHpIZ1gM0NKYBtpq7HUDlg==", + "requires": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "@alicloud/tea-xml": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@alicloud/tea-xml/-/tea-xml-0.0.2.tgz", + "integrity": "sha512-Xs7v5y7YSNSDDYmiDWAC0/013VWPjS3dQU4KezSLva9VGiTVPaL3S7Nk4NrTmAYCG6MKcrRj/nGEDIWL5KRoPg==", + "requires": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.4.22" + } + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -22547,6 +22810,14 @@ "@types/webidl-conversions": "*" } }, + "@types/xml2js": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz", + "integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==", + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -24065,7 +24336,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" }, @@ -24073,8 +24343,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -25509,6 +25778,22 @@ "sshpk": "^1.7.0" } }, + "httpx": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/httpx/-/httpx-2.2.7.tgz", + "integrity": "sha512-Wjh2JOAah0pdczfqL8NC5378G7jMt0Zcpn8U+yyxAiejjlagzSTQgJHuVvka2VNPQlKfoGehYRc79WKq9E4gDw==", + "requires": { + "@types/node": "^14", + "debug": "^4.1.1" + }, + "dependencies": { + "@types/node": { + "version": "14.18.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.37.tgz", + "integrity": "sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==" + } + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -25582,6 +25867,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -26612,6 +26902,21 @@ "safe-buffer": "^5.0.1" } }, + "kitx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/kitx/-/kitx-2.1.0.tgz", + "integrity": "sha512-C/5v9MtIX7aHGOjwn5BmrrbNkJSf7i0R5mRzmh13GSAdRqQ7bYQo/Su2pTYNylFicqKNTVX3HML9k1u8k51+pQ==", + "requires": { + "@types/node": "^12.0.2" + }, + "dependencies": { + "@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + } + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -28167,8 +28472,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "schema-utils": { "version": "3.1.1", @@ -28422,6 +28726,11 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==" + }, "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -29632,6 +29941,20 @@ "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "requires": {} }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xmldoc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-1.2.0.tgz", diff --git a/server/package.json b/server/package.json index c0900ead94..e23a9e368f 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,9 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@alicloud/dysmsapi20170525": "^2.0.23", + "@alicloud/openapi-client": "^0.4.5", + "@alicloud/tea-util": "^1.4.5", "@aws-sdk/client-s3": "^3.245.0", "@aws-sdk/client-sts": "^3.226.0", "@kubernetes/client-node": "^0.17.1", diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 988131688f..1e997c5570 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -27,11 +27,21 @@ model User { personalAccessTokens PersonalAccessToken[] } +model UserPassword { + id String @id @default(auto()) @map("_id") @db.ObjectId + uid String @db.ObjectId + password String + state String @default("Active") // Active, Inactive + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model UserProfile { id String @id @default(auto()) @map("_id") @db.ObjectId uid String @unique @db.ObjectId openid String? from String? + openData Json? avatar String? name String? createdAt DateTime @default(now()) @@ -51,7 +61,7 @@ model PersonalAccessToken { user User @relation(fields: [uid], references: [id]) } -// region schemas +// region schemas type RegionClusterConf { driver String // kubernetes @@ -464,3 +474,40 @@ model WebsiteHosting { bucket StorageBucket @relation(fields: [bucketName], references: [name]) } + +enum AuthProviderState { + Enabled + Disabled +} + +model AuthProvider { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String @unique + type String + bind Json + register Boolean + default Boolean + state AuthProviderState + config Json +} + +// Sms schemas +enum SmsVerifyCodeType { + Signin + Signup + ResetPassword + Bind + Unbind + ChangePhone +} + +model SmsVerifyCode { + id String @id @default(auto()) @map("_id") @db.ObjectId + phone String + code String + ip String + type SmsVerifyCodeType + state Int @default(0) // 0: created, 1: used + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index 4e75202d10..9e2880d072 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -1,3 +1,4 @@ +import { SmsService } from 'src/auth/phone/sms.service' import { Module } from '@nestjs/common' import { JwtModule } from '@nestjs/jwt' import { PassportModule } from '@nestjs/passport' @@ -9,6 +10,12 @@ import { JwtStrategy } from './jwt.strategy' import { AuthController } from './auth.controller' import { HttpModule } from '@nestjs/axios' import { PatService } from 'src/user/pat.service' +import { UserPasswordController } from './user-passwd/user-password.controller' +import { UserPasswordService } from './user-passwd/user-password.service' +import { PhoneController } from './phone/phone.controller' +import { PhoneService } from './phone/phone.service' +import { AuthenticationController } from './authentication.controller' +import { AuthenticationService } from './authentication.service' @Module({ imports: [ @@ -20,8 +27,22 @@ import { PatService } from 'src/user/pat.service' UserModule, HttpModule, ], - providers: [AuthService, JwtStrategy, CasdoorService, PatService], + providers: [ + AuthService, + JwtStrategy, + CasdoorService, + PatService, + UserPasswordService, + PhoneService, + SmsService, + AuthenticationService, + ], exports: [AuthService], - controllers: [AuthController], + controllers: [ + AuthController, + UserPasswordController, + PhoneController, + AuthenticationController, + ], }) export class AuthModule {} diff --git a/server/src/auth/authentication.controller.ts b/server/src/auth/authentication.controller.ts new file mode 100644 index 0000000000..7dd1219781 --- /dev/null +++ b/server/src/auth/authentication.controller.ts @@ -0,0 +1,31 @@ +import { AuthenticationService } from './authentication.service' +import { Controller, Get, Post } from '@nestjs/common' +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ResponseUtil } from 'src/utils/response' + +@ApiTags('Authentication - New') +@Controller('auth') +export class AuthenticationController { + constructor(private readonly authenticationService: AuthenticationService) {} + + /** + * Auth providers + */ + @ApiOperation({ summary: 'Auth providers' }) + @ApiResponse({ type: ResponseUtil }) + @Get('providers') + async getProviders() { + const providers = await this.authenticationService.getProviders() + return ResponseUtil.ok(providers) + } + + /** + * Auth providers + */ + @ApiOperation({ summary: 'Bind username' }) + @ApiResponse({ type: ResponseUtil }) + @Post('bind/username') + async bindUsername(username: string) { + // TODO + } +} diff --git a/server/src/auth/authentication.service.ts b/server/src/auth/authentication.service.ts new file mode 100644 index 0000000000..51b581c540 --- /dev/null +++ b/server/src/auth/authentication.service.ts @@ -0,0 +1,65 @@ +import { JwtService } from '@nestjs/jwt' +import { PrismaService } from 'src/prisma/prisma.service' +import { Injectable, Logger } from '@nestjs/common' +import { User } from '@prisma/client' +import { + PASSWORD_AUTH_PROVIDER_NAME, + PHONE_AUTH_PROVIDER_NAME, +} from 'src/constants' + +@Injectable() +export class AuthenticationService { + logger: Logger = new Logger(AuthenticationService.name) + constructor( + private readonly prismaService: PrismaService, + private readonly jwtService: JwtService, + ) {} + + /** + * Get all auth provides + * @returns + */ + async getProviders() { + return await this.prismaService.authProvider.findMany({ + where: { state: 'Enabled' }, + select: { + id: false, + name: true, + type: true, + bind: true, + register: true, + default: true, + state: true, + config: false, + }, + }) + } + + async getPhoneProvider() { + return await this.getProvider(PHONE_AUTH_PROVIDER_NAME) + } + + async getPasswdProvider() { + return await this.getProvider(PASSWORD_AUTH_PROVIDER_NAME) + } + + // Get auth provider by name + async getProvider(name: string) { + return await this.prismaService.authProvider.findUnique({ + where: { name }, + }) + } + + /** + * Get access token by user + * @param user + * @returns + */ + getAccessTokenByUser(user: User): string { + const payload = { + sub: user.id, + } + const token = this.jwtService.sign(payload) + return token + } +} diff --git a/server/src/auth/dto/passwd-reset.dto.ts b/server/src/auth/dto/passwd-reset.dto.ts new file mode 100644 index 0000000000..1ded520418 --- /dev/null +++ b/server/src/auth/dto/passwd-reset.dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger' +import { SmsVerifyCodeType } from '@prisma/client' +import { IsEnum, IsNotEmpty, IsString, Length } from 'class-validator' + +export class PasswdResetDto { + @ApiProperty({ + description: 'new password, 8-32 characters', + example: 'laf-user-password', + }) + @IsString() + @IsNotEmpty() + @Length(8, 32) + password: string + + @ApiProperty({ + description: 'phone', + example: '13805718888', + }) + @IsString() + @Length(11, 11) + phone: string + + @ApiProperty({ + description: 'verify code', + example: '032456', + }) + @IsString() + @Length(6, 6) + code: string + + @ApiProperty({ + description: 'type', + example: 'ResetPassword', + }) + @IsEnum(SmsVerifyCodeType) + type: SmsVerifyCodeType +} diff --git a/server/src/auth/dto/passwd-signin.dto.ts b/server/src/auth/dto/passwd-signin.dto.ts new file mode 100644 index 0000000000..ba49ac6893 --- /dev/null +++ b/server/src/auth/dto/passwd-signin.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString, Length } from 'class-validator' + +export class PasswdSigninDto { + @ApiProperty({ + description: 'username', + example: 'laf-user', + }) + @IsString() + @IsNotEmpty() + @Length(8, 16) + username: string + + @ApiProperty({ + description: 'password, 8-16 characters', + example: 'laf-user-password', + }) + @IsString() + @IsNotEmpty() + @Length(8, 32) + password: string +} diff --git a/server/src/auth/dto/passwd-signup.dto.ts b/server/src/auth/dto/passwd-signup.dto.ts new file mode 100644 index 0000000000..f247017a45 --- /dev/null +++ b/server/src/auth/dto/passwd-signup.dto.ts @@ -0,0 +1,47 @@ +import { Optional } from '@nestjs/common' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { SmsVerifyCodeType } from '@prisma/client' +import { IsEnum, IsNotEmpty, IsString, Length } from 'class-validator' + +export class PasswdSignupDto { + @ApiProperty({ + description: 'username', + example: 'laf-user', + }) + @IsString() + @IsNotEmpty() + @Length(8, 16) + username: string + + @ApiProperty({ + description: 'password, 8-16 characters', + example: 'laf-user-password', + }) + @IsString() + @IsNotEmpty() + @Length(8, 32) + password: string + + @ApiPropertyOptional({ + description: 'phone', + example: '13805718888', + }) + @IsString() + @Length(11, 11) + phone: string + + @ApiPropertyOptional({ + description: 'verify code', + example: '032456', + }) + @IsString() + @Length(6, 6) + code: string + + @ApiPropertyOptional({ + description: 'type', + example: 'Signup', + }) + @IsEnum(SmsVerifyCodeType) + type: SmsVerifyCodeType +} diff --git a/server/src/auth/dto/phone-signin.dto.ts b/server/src/auth/dto/phone-signin.dto.ts new file mode 100644 index 0000000000..1b62a7de00 --- /dev/null +++ b/server/src/auth/dto/phone-signin.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { IsNotEmpty, IsString, Length, IsOptional } from 'class-validator' + +export class PhoneSigninDto { + @ApiProperty({ + description: 'phone', + example: '13805718888', + }) + @IsString() + @IsNotEmpty() + @Length(11, 11) + phone: string + + @ApiProperty({}) + @IsNotEmpty() + @Length(6, 6) + code: string + + @ApiProperty({ + description: 'username', + example: 'laf-user', + }) + @IsOptional() + @IsString() + @Length(8, 16) + username: string + + @ApiProperty({ + description: 'password, 8-32 characters', + example: 'laf-user-password', + }) + @IsOptional() + @IsString() + @Length(8, 32) + password: string +} diff --git a/server/src/auth/dto/send-phone-code.dto.ts b/server/src/auth/dto/send-phone-code.dto.ts new file mode 100644 index 0000000000..02886087a4 --- /dev/null +++ b/server/src/auth/dto/send-phone-code.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger' +import { SmsVerifyCodeType } from '@prisma/client' +import { IsEnum, IsNotEmpty, IsString, Length } from 'class-validator' + +export class SendPhoneCodeDto { + @ApiProperty({ + description: 'phone', + example: '13805718888', + }) + @IsString() + @IsNotEmpty() + @Length(11, 11) + phone: string + + @ApiProperty({ + description: 'verify code type', + example: 'Signin | Signup | ResetPassword | Bind | Unbind | ChangePhone', + }) + @IsNotEmpty() + @IsEnum(SmsVerifyCodeType) + type: SmsVerifyCodeType +} diff --git a/server/src/auth/phone/phone.controller.ts b/server/src/auth/phone/phone.controller.ts new file mode 100644 index 0000000000..5edf51d7ce --- /dev/null +++ b/server/src/auth/phone/phone.controller.ts @@ -0,0 +1,88 @@ +import { SmsService } from 'src/auth/phone/sms.service' +import { SmsVerifyCodeType } from '@prisma/client' +import { IRequest } from 'src/utils/interface' +import { Body, Controller, Logger, Post, Req } from '@nestjs/common' +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ResponseUtil } from 'src/utils/response' +import { SendPhoneCodeDto } from '../dto/send-phone-code.dto' +import { PhoneService } from './phone.service' +import { PhoneSigninDto } from '../dto/phone-signin.dto' +import { AuthenticationService } from '../authentication.service' +import { UserService } from 'src/user/user.service' +import { AuthBindingType, AuthProviderBinding } from '../types' + +@ApiTags('Authentication - New') +@Controller('auth') +export class PhoneController { + private readonly logger = new Logger(PhoneController.name) + + constructor( + private readonly phoneService: PhoneService, + private readonly smsService: SmsService, + private readonly authService: AuthenticationService, + private readonly userService: UserService, + ) {} + + /** + * send phone code + */ + @ApiOperation({ summary: 'Send phone verify code' }) + @ApiResponse({ type: ResponseUtil }) + @Post('phone/sms/code') + async sendCode(@Req() req: IRequest, @Body() dto: SendPhoneCodeDto) { + const { phone, type } = dto + const ip = req.headers['x-real-ip'] as string + + const err = await this.phoneService.sendCode(phone, type, ip) + if (err) { + return ResponseUtil.error(err) + } + return ResponseUtil.ok('success') + } + + /** + * Signin by phone and verify code + */ + @ApiOperation({ summary: 'Signin by phone and verify code' }) + @ApiResponse({ type: ResponseUtil }) + @Post('phone/signin') + async signin(@Body() dto: PhoneSigninDto) { + const { phone, code } = dto + // check if code valid + const err = await this.smsService.validCode( + phone, + code, + SmsVerifyCodeType.Signin, + ) + if (err) return ResponseUtil.error(err) + + // check if user exists + const user = await this.userService.findByPhone(phone) + if (user) { + const token = this.phoneService.signin(user) + return ResponseUtil.ok(token) + } + + // user not exist + const provider = await this.authService.getPhoneProvider() + if (provider.register === false) { + return ResponseUtil.error('register is not allowed') + } + + // check if username and password is needed + let signupWithUsername = false + const bind = provider.bind as any as AuthProviderBinding + if (bind.username === AuthBindingType.Required) { + const { username, password } = dto + signupWithUsername = true + if (!username || !password) { + return ResponseUtil.error('username and password is required') + } + } + + // user not exist, signup and signin + const newUser = await this.phoneService.signup(dto, signupWithUsername) + const data = this.phoneService.signin(newUser) + return ResponseUtil.ok(data) + } +} diff --git a/server/src/auth/phone/phone.service.ts b/server/src/auth/phone/phone.service.ts new file mode 100644 index 0000000000..a7bad632d7 --- /dev/null +++ b/server/src/auth/phone/phone.service.ts @@ -0,0 +1,128 @@ +import { Injectable, Logger } from '@nestjs/common' +import { SmsVerifyCodeType, User } from '@prisma/client' +import { PrismaService } from 'src/prisma/prisma.service' +import { SmsService } from 'src/auth/phone/sms.service' +import { UserService } from 'src/user/user.service' +import { AuthenticationService } from '../authentication.service' +import { PhoneSigninDto } from '../dto/phone-signin.dto' +import { hashPassword } from 'src/utils/crypto' +import { SmsVerifyCodeState } from '../types' + +@Injectable() +export class PhoneService { + private readonly logger = new Logger(PhoneService.name) + constructor( + private readonly prisma: PrismaService, + private readonly smsService: SmsService, + private readonly userService: UserService, + private readonly authService: AuthenticationService, + ) {} + + /** + * Send phone verify code + * @param phone phone number + * @param type sms type Signin | Signup | ResetPassword + * @param ip client ip + * @returns + */ + async sendCode(phone: string, type: SmsVerifyCodeType, ip: string) { + // check if phone number satisfy the send condition + let err = await this.smsService.checkSendable(phone, ip) + if (err) { + return err + } + + // Send sms code + const code = Math.floor(Math.random() * 900000 + 100000).toString() + err = await this.smsService.sendPhoneCode(phone, code) + if (err) { + return err + } + + // save to database, start transaction + await this.prisma.$transaction(async (tx) => { + // disable previous same type code + await tx.smsVerifyCode.updateMany({ + where: { + phone, + type, + state: SmsVerifyCodeState.Active, + }, + data: { + state: SmsVerifyCodeState.Used, + }, + }) + + // Save sms code to database + await tx.smsVerifyCode.create({ + data: { + phone, + code, + type, + ip, + }, + }) + }) + + return null + } + + /** + * Signup a user by phone + * @param dto + * @returns + */ + async signup(dto: PhoneSigninDto, withUsername = false) { + const { phone, username, password } = dto + + // start transaction + const user = await this.prisma.$transaction(async (tx) => { + // create user + const user = await tx.user.create({ + data: { + phone, + username: username || phone, + profile: { create: { name: username || phone } }, + }, + }) + if (!withUsername) { + return user + } + // create password if need + await tx.userPassword.create({ + data: { + uid: user.id, + password: hashPassword(password), + state: 'Active', + }, + }) + return user + }) + return user + } + + /** + * signin a user, return token and if bind password + * @param user user + * @returns token and if bind password + */ + signin(user: User) { + const token = this.authService.getAccessTokenByUser(user) + return token + } + + // check if current user has bind password + async ifBindPassword(user: User) { + const count = await this.prisma.userPassword.count({ + where: { + uid: user.id, + state: 'Active', + }, + }) + + if (count === 0) { + return false + } + return true + } +} diff --git a/server/src/auth/phone/sms.service.ts b/server/src/auth/phone/sms.service.ts new file mode 100644 index 0000000000..402e172c9a --- /dev/null +++ b/server/src/auth/phone/sms.service.ts @@ -0,0 +1,163 @@ +import { AuthenticationService } from '../authentication.service' +import { Injectable, Logger } from '@nestjs/common' +import { Prisma, SmsVerifyCodeType } from '@prisma/client' +import Dysmsapi, * as dysmsapi from '@alicloud/dysmsapi20170525' +import * as OpenApi from '@alicloud/openapi-client' +import * as Util from '@alicloud/tea-util' +import { + ALISMS_KEY, + LIMIT_CODE_PER_IP_PER_DAY, + ONE_DAY_IN_MILLISECONDS, + ONE_MINUTE_IN_MILLISECONDS, + CODE_VALIDITY, +} from 'src/constants' +import { PrismaService } from 'src/prisma/prisma.service' +import { SmsVerifyCodeState } from '../types' + +@Injectable() +export class SmsService { + private logger = new Logger(SmsService.name) + constructor( + private readonly prisma: PrismaService, + private readonly authService: AuthenticationService, + ) {} + + /** + * send sms login code to given phone number + * @param dto phone number + * @param ip client ip + */ + async sendPhoneCode(phone: string, code: string) { + try { + this.logger.debug(`send sms code: ${code} to ${phone}`) + + const res = await this.sendAlismsCode(phone, code.toString()) + if (res.body.code !== 'OK') { + return `ALISMS_ERROR: ${res.body.message}` + } + return null + } catch (error) { + this.logger.error(error, error.response?.body) + return error.message + } + } + + // check if phone number satisfy the send condition + async checkSendable(phone: string, ip: string) { + // Check if valid phone number + if (!/^1[3456789]\d{9}$/.test(phone)) { + return 'INVALID_PHONE' + } + + // Check if phone number has been send sms code in 1 minute + const count = await this.prisma.smsVerifyCode.count({ + where: { + phone: phone, + createdAt: { + gt: new Date(Date.now() - ONE_MINUTE_IN_MILLISECONDS), + }, + }, + }) + if (count > 0) { + return 'REQUEST_OVERLIMIT: phone number has been send sms code in 1 minute' + } + + // Check if ip has been send sms code beyond 30 times in 24 hours + const countIps = await this.prisma.smsVerifyCode.count({ + where: { + ip: ip, + createdAt: { + gt: new Date(Date.now() - ONE_DAY_IN_MILLISECONDS), + }, + }, + }) + if (countIps > LIMIT_CODE_PER_IP_PER_DAY) { + return `REQUEST_OVERLIMIT: ip has been send sms code beyond ${LIMIT_CODE_PER_IP_PER_DAY} times in 24 hours` + } + + return null + } + + // save sended code to database + async saveSmsCode(data: Prisma.SmsVerifyCodeCreateInput) { + await this.prisma.smsVerifyCode.create({ + data, + }) + } + + // Valid given phone and code with code type + async validCode(phone: string, code: string, type: SmsVerifyCodeType) { + const total = await this.prisma.smsVerifyCode.count({ + where: { + phone, + code, + type, + state: SmsVerifyCodeState.Active, + createdAt: { gte: new Date(Date.now() - CODE_VALIDITY) }, + }, + }) + + if (total === 0) return 'invalid code' + // Disable verify code after valid + await this.disableCode(phone, code, type) + return null + } + + // Disable verify code + async disableCode(phone: string, code: string, type: SmsVerifyCodeType) { + await this.prisma.smsVerifyCode.updateMany({ + where: { + phone, + code, + type, + state: SmsVerifyCodeState.Active, + }, + data: { + state: SmsVerifyCodeState.Used, + }, + }) + } + + // Disable same type verify code + async disableSameTypeCode(phone: string, type: SmsVerifyCodeType) { + await this.prisma.smsVerifyCode.updateMany({ + where: { + phone, + type, + state: SmsVerifyCodeState.Active, + }, + data: { + state: SmsVerifyCodeState.Used, + }, + }) + } + + // send sms code to phone using alisms + private async sendAlismsCode(phone: string, code: string) { + const { accessKeyId, accessKeySecret, templateCode, signName, endpoint } = + await this.loadAlismsConfig() + + const sendSmsRequest = new dysmsapi.SendSmsRequest({ + phoneNumbers: phone, + signName, + templateCode, + templateParam: `{"code":${code}}`, + }) + + const config = new OpenApi.Config({ + accessKeyId, + accessKeySecret, + endpoint, + }) + + const client = new Dysmsapi(config) + const runtime = new Util.RuntimeOptions({}) + return await client.sendSmsWithOptions(sendSmsRequest, runtime) + } + + // load alisms config from database + private async loadAlismsConfig() { + const phoneProvider = await this.authService.getPhoneProvider() + return phoneProvider.config[ALISMS_KEY] + } +} diff --git a/server/src/auth/types.ts b/server/src/auth/types.ts new file mode 100644 index 0000000000..fc7772f9df --- /dev/null +++ b/server/src/auth/types.ts @@ -0,0 +1,31 @@ +export enum AuthBindingType { + Required = 'required', + Optional = 'optional', + None = 'none', +} + +export enum SmsVerifyCodeState { + Active = 0, + Used = 1, +} + +export enum UserPasswordState { + Active = 'Active', + Inactive = 'Inactive', +} + +export interface AuthProviderBinding { + username: AuthBindingType + phone: AuthBindingType + email: AuthBindingType + github: AuthBindingType + wechat: AuthBindingType +} + +export interface AlismsConfig { + accessKeyId: string + accessKeySecret: string + endpoint: string + signName: string + templateCode: string +} diff --git a/server/src/auth/user-passwd/user-password.controller.ts b/server/src/auth/user-passwd/user-password.controller.ts new file mode 100644 index 0000000000..1cfa923fda --- /dev/null +++ b/server/src/auth/user-passwd/user-password.controller.ts @@ -0,0 +1,121 @@ +import { AuthenticationService } from '../authentication.service' +import { UserPasswordService } from './user-password.service' +import { Body, Controller, Logger, Post, Req } from '@nestjs/common' +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ResponseUtil } from 'src/utils/response' +import { UserService } from '../../user/user.service' +import { PasswdSignupDto } from '../dto/passwd-signup.dto' +import { PasswdSigninDto } from '../dto/passwd-signin.dto' +import { AuthBindingType, AuthProviderBinding } from '../types' +import { SmsService } from '../phone/sms.service' +import { PasswdResetDto } from '../dto/passwd-reset.dto' +import { IRequest } from 'src/utils/interface' +import { PASSWORD_AUTH_PROVIDER_NAME } from 'src/constants' + +@ApiTags('Authentication - New') +@Controller('auth') +export class UserPasswordController { + private readonly logger = new Logger(UserPasswordService.name) + constructor( + private readonly userService: UserService, + private readonly passwdService: UserPasswordService, + private readonly authService: AuthenticationService, + private readonly smsService: SmsService, + ) {} + + /** + * Signup by username and password + */ + @ApiOperation({ summary: 'Signup by user-password' }) + @ApiResponse({ type: ResponseUtil }) + @Post('passwd/signup') + async signup(@Body() dto: PasswdSignupDto) { + const { username, password, phone } = dto + // check if user exists + const doc = await this.userService.user({ username }) + if (doc) { + return ResponseUtil.error('user already exists') + } + + // check if register is allowed + const provider = await this.authService.getPasswdProvider() + if (provider.register === false) { + return ResponseUtil.error('register is not allowed') + } + + // valid phone code if needed + const bind = provider.bind as any as AuthProviderBinding + if (bind.phone === AuthBindingType.Required) { + const { phone, code, type } = dto + // valid phone has been binded + const user = await this.userService.findByPhone(phone) + if (user) { + return ResponseUtil.error('phone has been binded') + } + const err = await this.smsService.validCode(phone, code, type) + if (err) { + return ResponseUtil.error(err) + } + } + + // signup user + const user = await this.passwdService.signup(username, password, phone) + + // signin for created user + const token = this.passwdService.signin(user) + if (!token) { + return ResponseUtil.error('failed to get access token') + } + return ResponseUtil.ok({ token, user }) + } + + /** + * Signin by username and password + */ + @ApiOperation({ summary: 'Signin by user-password' }) + @ApiResponse({ type: ResponseUtil }) + @Post('passwd/signin') + async signin(@Body() dto: PasswdSigninDto) { + // check if user exists + const user = await this.userService.find(dto.username) + if (!user) { + return ResponseUtil.error('user not found') + } + + // check if password is correct + const err = await this.passwdService.validPasswd(user.id, dto.password) + if (err) { + return ResponseUtil.error(err) + } + + // signin for user + const token = await this.passwdService.signin(user) + if (!token) { + return ResponseUtil.error('failed to get access token') + } + return ResponseUtil.ok(token) + } + + /** + * Reset password + */ + @ApiOperation({ summary: 'Reset password' }) + @ApiResponse({ type: ResponseUtil }) + @Post('passwd/reset') + async reset(@Body() dto: PasswdResetDto, @Req() req: IRequest) { + // valid phone code + const { phone, code, type } = dto + let err = await this.smsService.validCode(phone, code, type) + if (err) { + return ResponseUtil.error(err) + } + + // reset password + err = await this.passwdService.resetPasswd(req.user.id, dto.password) + if (err) { + return ResponseUtil.error(err) + } + + return ResponseUtil.ok('success') + } +} diff --git a/server/src/auth/user-passwd/user-password.service.ts b/server/src/auth/user-passwd/user-password.service.ts new file mode 100644 index 0000000000..fee0889215 --- /dev/null +++ b/server/src/auth/user-passwd/user-password.service.ts @@ -0,0 +1,92 @@ +import { Injectable, Logger } from '@nestjs/common' +import { PrismaService } from 'src/prisma/prisma.service' +import { User } from '@prisma/client' +import { hashPassword } from 'src/utils/crypto' +import { AuthenticationService } from '../authentication.service' +import { UserPasswordState } from '../types' + +@Injectable() +export class UserPasswordService { + private readonly logger = new Logger(UserPasswordService.name) + constructor( + private readonly prisma: PrismaService, + private readonly authService: AuthenticationService, + ) {} + + // Singup by username and password + async signup(username: string, password: string, phone: string) { + // start transaction + const user = await this.prisma.$transaction(async (tx) => { + // create user + const user = await tx.user.create({ + data: { + username, + phone, + profile: { create: { name: username } }, + }, + }) + + // create password + await tx.userPassword.create({ + data: { + uid: user.id, + password: hashPassword(password), + state: UserPasswordState.Active, + }, + }) + + return user + }) + + return user + } + + // Signin for user, means get access token + signin(user: User) { + return this.authService.getAccessTokenByUser(user) + } + + // valid if password is correct + async validPasswd(uid: string, passwd: string) { + const userPasswd = await this.prisma.userPassword.findFirst({ + where: { uid, state: UserPasswordState.Active }, + }) + if (!userPasswd) { + return 'password not exists' + } + + if (userPasswd.password !== hashPassword(passwd)) { + return 'password incorrect' + } + + return null + } + + // reset password + async resetPasswd(uid: string, passwd: string) { + // start transaction + const update = await this.prisma.$transaction(async (tx) => { + // create new password + const np = await tx.userPassword.create({ + data: { + uid, + password: hashPassword(passwd), + state: UserPasswordState.Active, + }, + }) + + // disable old password + await tx.userPassword.updateMany({ + where: { uid }, + data: { state: UserPasswordState.Inactive }, + }) + + return np + }) + if (!update) { + return 'reset password failed' + } + + return null + } +} diff --git a/server/src/constants.ts b/server/src/constants.ts index 52df22a90f..6f7687ca41 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -176,8 +176,20 @@ export const MINIO_COMMON_USER_POLICY = 'laf_owner_by_prefix' export const SEVEN_DAYS_IN_SECONDS = 60 * 60 * 24 * 7 // 7 days in seconds export const FOREVER_IN_SECONDS = 60 * 60 * 24 * 365 * 1000 // 1000 years in seconds export const TASK_LOCK_INIT_TIME = new Date(0) // 1970-01-01 00:00:00 +export const ONE_DAY_IN_MILLISECONDS = 60 * 60 * 24 * 1000 // 1 day in milliseconds +export const ONE_MINUTE_IN_MILLISECONDS = 60 * 1000 // 1 minute in milliseconds // Resource units export const CPU_UNIT = 1000 export const MB = 1024 * 1024 export const GB = 1024 * MB + +// auth constants +export const PHONE_AUTH_PROVIDER_NAME = 'phone' +export const PASSWORD_AUTH_PROVIDER_NAME = 'user-password' + +// Sms constants +export const ALISMS_KEY = 'alisms' +export const LIMIT_CODE_FREQUENCY = 60 * 1000 // 60 seconds (in milliseconds) +export const LIMIT_CODE_PER_IP_PER_DAY = 30 // 30 times +export const CODE_VALIDITY = 10 * 60 * 1000 // 10 minutes (in milliseconds) diff --git a/server/src/initializer/initializer.service.ts b/server/src/initializer/initializer.service.ts index 187e1deb0e..17eed64131 100644 --- a/server/src/initializer/initializer.service.ts +++ b/server/src/initializer/initializer.service.ts @@ -147,4 +147,57 @@ export class InitializerService { } } } + + async createDefaultAuthProvider() { + // check if exists + const existed = await this.prisma.authProvider.count() + if (existed) { + this.logger.debug('default auth provider already exists') + return + } + + // create default auth provider - user-password + const resPassword = await this.prisma.authProvider.create({ + data: { + name: 'user-password', + type: 'user-password', + bind: { + password: 'optional', + phone: 'required', + email: 'optional', + }, + register: true, + default: true, + state: 'Enabled', + config: { usernameField: 'username', passwordField: 'password' }, + }, + }) + + // create auth provider - phone code + const resPhone = await this.prisma.authProvider.create({ + data: { + name: 'phone', + type: 'phone', + bind: { + password: 'required', + phone: 'optional', + email: 'optional', + }, + register: true, + default: false, + state: 'Enabled', + config: { + alisms: {}, + }, + }, + }) + + this.logger.verbose( + 'Created default auth providers: ' + + resPassword.name + + ' ' + + resPhone.name, + ) + return resPhone + } } diff --git a/server/src/main.ts b/server/src/main.ts index 847b715d6a..f815eab168 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -50,6 +50,7 @@ async function bootstrap() { await initService.createDefaultRegion() await initService.createDefaultBundle() await initService.createDefaultRuntime() + await initService.createDefaultAuthProvider() await initService.initMinioAlias() } catch (error) { console.error(error) diff --git a/server/src/user/dto/user.response.ts b/server/src/user/dto/user.response.ts index 49e2cf4da7..8c5e465269 100644 --- a/server/src/user/dto/user.response.ts +++ b/server/src/user/dto/user.response.ts @@ -18,6 +18,9 @@ export class UserProfileDto implements UserProfile { from: string + @ApiProperty() + openData: any + @ApiProperty() createdAt: Date diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index cb945e98e4..7bd7a3715b 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -80,4 +80,23 @@ export class UserService { where, }) } + + // find user by username | phone | email + async find(username: string) { + // match either username or phone or email + return await this.prisma.user.findFirst({ + where: { + OR: [{ username }, { phone: username }, { email: username }], + }, + }) + } + + // find user by phone + async findByPhone(phone: string) { + return await this.prisma.user.findFirst({ + where: { + phone, + }, + }) + } } diff --git a/server/src/utils/crypto.ts b/server/src/utils/crypto.ts new file mode 100644 index 0000000000..5ed262ec09 --- /dev/null +++ b/server/src/utils/crypto.ts @@ -0,0 +1,8 @@ +import * as crypto from 'crypto' + +// use sha256 to hash the password +export function hashPassword(password: string): string { + const hash = crypto.createHash('sha256') + hash.update(password) + return hash.digest('hex') +} From 06fe492c64925914c5e0b0f812d2a6fa6c0cab53 Mon Sep 17 00:00:00 2001 From: sulnong Date: Tue, 14 Mar 2023 11:09:24 +0800 Subject: [PATCH 2/4] feat(authentication): impl bind username and phone --- server/src/auth/authentication.controller.ts | 84 ++++++++++++++++++-- server/src/auth/dto/bind-phone.dto.ts | 21 +++++ server/src/auth/dto/bind-username.dto.ts | 30 +++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 server/src/auth/dto/bind-phone.dto.ts create mode 100644 server/src/auth/dto/bind-username.dto.ts diff --git a/server/src/auth/authentication.controller.ts b/server/src/auth/authentication.controller.ts index 7dd1219781..50d8ee6dd7 100644 --- a/server/src/auth/authentication.controller.ts +++ b/server/src/auth/authentication.controller.ts @@ -1,12 +1,23 @@ import { AuthenticationService } from './authentication.service' -import { Controller, Get, Post } from '@nestjs/common' +import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common' import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' import { ResponseUtil } from 'src/utils/response' +import { JwtAuthGuard } from './jwt.auth.guard' +import { BindUsernameDto } from './dto/bind-username.dto' +import { IRequest } from 'src/utils/interface' +import { BindPhoneDto } from './dto/bind-phone.dto' +import { SmsService } from './phone/sms.service' +import { SmsVerifyCodeType } from '@prisma/client' +import { UserService } from 'src/user/user.service' @ApiTags('Authentication - New') @Controller('auth') export class AuthenticationController { - constructor(private readonly authenticationService: AuthenticationService) {} + constructor( + private readonly authenticationService: AuthenticationService, + private readonly smsService: SmsService, + private readonly userService: UserService, + ) {} /** * Auth providers @@ -20,12 +31,75 @@ export class AuthenticationController { } /** - * Auth providers + * Bind phone + */ + @ApiOperation({ summary: 'Bind username' }) + @ApiResponse({ type: ResponseUtil }) + @UseGuards(JwtAuthGuard) + @Post('bind/phone') + async bindPhone(@Body() dto: BindPhoneDto, @Req() req: IRequest) { + const { phone, code } = dto + // check code valid + const err = await this.smsService.validCode( + phone, + code, + SmsVerifyCodeType.Bind, + ) + if (err) { + return ResponseUtil.error(err) + } + + // check phone if have already been bound + const user = await this.userService.find(phone) + if (user) { + return ResponseUtil.error('phone already been bound') + } + + // bind phone + await this.userService.updateUser({ + where: { + id: req.user.id, + }, + data: { + phone, + }, + }) + } + + /** + * Bind username, not support bind existed username */ @ApiOperation({ summary: 'Bind username' }) @ApiResponse({ type: ResponseUtil }) + @UseGuards(JwtAuthGuard) @Post('bind/username') - async bindUsername(username: string) { - // TODO + async bindUsername(@Body() dto: BindUsernameDto, @Req() req: IRequest) { + const { username, phone, code } = dto + + // check code valid + const err = await this.smsService.validCode( + phone, + code, + SmsVerifyCodeType.Bind, + ) + if (err) { + return ResponseUtil.error(err) + } + + // check username if have already been bound + const user = await this.userService.find(username) + if (user) { + return ResponseUtil.error('username already been bound') + } + + // bind username + await this.userService.updateUser({ + where: { + id: req.user.id, + }, + data: { + username, + }, + }) } } diff --git a/server/src/auth/dto/bind-phone.dto.ts b/server/src/auth/dto/bind-phone.dto.ts new file mode 100644 index 0000000000..5a36ec2a4f --- /dev/null +++ b/server/src/auth/dto/bind-phone.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString, Length } from 'class-validator' + +export class BindPhoneDto { + @ApiProperty({ + description: 'phone number', + example: '13805718888', + }) + @IsString() + @IsNotEmpty() + @Length(11, 11) + phone: string + + @ApiProperty({ + description: 'sms verify code', + example: '032476', + }) + @IsNotEmpty() + @Length(6, 6) + code: string +} diff --git a/server/src/auth/dto/bind-username.dto.ts b/server/src/auth/dto/bind-username.dto.ts new file mode 100644 index 0000000000..046e8cfaec --- /dev/null +++ b/server/src/auth/dto/bind-username.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString, Length } from 'class-validator' + +export class BindUsernameDto { + @ApiProperty({ + description: 'username', + example: 'laf-user', + }) + @IsString() + @IsNotEmpty() + @Length(8, 16) + username: string + + @ApiProperty({ + description: 'phone', + example: '13805718888', + }) + @IsString() + @IsNotEmpty() + @Length(11, 11) + phone: string + + @ApiProperty({ + description: 'sms verify code', + example: '032476', + }) + @IsNotEmpty() + @Length(6, 6) + code: string +} From b21d0a5ff488249174148c8790a0fad5cdcc3a90 Mon Sep 17 00:00:00 2001 From: lunaragon Date: Sun, 19 Mar 2023 21:58:21 +0800 Subject: [PATCH 3/4] feat(authentication): adjust to code review feedbacks --- server/prisma/schema.prisma | 1 - server/src/auth/authentication.service.ts | 1 - server/src/auth/dto/bind-phone.dto.ts | 4 +- server/src/auth/dto/bind-username.dto.ts | 6 +-- server/src/auth/dto/passwd-reset.dto.ts | 8 ++-- server/src/auth/dto/passwd-signin.dto.ts | 6 +-- server/src/auth/dto/passwd-signup.dto.ts | 13 +++--- server/src/auth/dto/phone-signin.dto.ts | 18 +++++--- server/src/auth/dto/send-phone-code.dto.ts | 4 +- server/src/auth/phone/phone.service.ts | 41 +++++++++---------- server/src/initializer/initializer.service.ts | 2 - 11 files changed, 51 insertions(+), 53 deletions(-) diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 1e997c5570..e57ae54d09 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -483,7 +483,6 @@ enum AuthProviderState { model AuthProvider { id String @id @default(auto()) @map("_id") @db.ObjectId name String @unique - type String bind Json register Boolean default Boolean diff --git a/server/src/auth/authentication.service.ts b/server/src/auth/authentication.service.ts index 51b581c540..f8299ac3f7 100644 --- a/server/src/auth/authentication.service.ts +++ b/server/src/auth/authentication.service.ts @@ -25,7 +25,6 @@ export class AuthenticationService { select: { id: false, name: true, - type: true, bind: true, register: true, default: true, diff --git a/server/src/auth/dto/bind-phone.dto.ts b/server/src/auth/dto/bind-phone.dto.ts index 5a36ec2a4f..3d558cd600 100644 --- a/server/src/auth/dto/bind-phone.dto.ts +++ b/server/src/auth/dto/bind-phone.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger' -import { IsNotEmpty, IsString, Length } from 'class-validator' +import { IsNotEmpty, IsString, Length, Matches } from 'class-validator' export class BindPhoneDto { @ApiProperty({ @@ -8,7 +8,7 @@ export class BindPhoneDto { }) @IsString() @IsNotEmpty() - @Length(11, 11) + @Matches(/^1[3-9]\d{9}$/) phone: string @ApiProperty({ diff --git a/server/src/auth/dto/bind-username.dto.ts b/server/src/auth/dto/bind-username.dto.ts index 046e8cfaec..ef760263f0 100644 --- a/server/src/auth/dto/bind-username.dto.ts +++ b/server/src/auth/dto/bind-username.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger' -import { IsNotEmpty, IsString, Length } from 'class-validator' +import { IsNotEmpty, IsString, Length, Matches } from 'class-validator' export class BindUsernameDto { @ApiProperty({ @@ -8,7 +8,7 @@ export class BindUsernameDto { }) @IsString() @IsNotEmpty() - @Length(8, 16) + @Length(3, 64) username: string @ApiProperty({ @@ -17,7 +17,7 @@ export class BindUsernameDto { }) @IsString() @IsNotEmpty() - @Length(11, 11) + @Matches(/^1[3-9]\d{9}$/) phone: string @ApiProperty({ diff --git a/server/src/auth/dto/passwd-reset.dto.ts b/server/src/auth/dto/passwd-reset.dto.ts index 1ded520418..9ea7407548 100644 --- a/server/src/auth/dto/passwd-reset.dto.ts +++ b/server/src/auth/dto/passwd-reset.dto.ts @@ -1,15 +1,15 @@ import { ApiProperty } from '@nestjs/swagger' import { SmsVerifyCodeType } from '@prisma/client' -import { IsEnum, IsNotEmpty, IsString, Length } from 'class-validator' +import { IsEnum, IsNotEmpty, IsString, Length, Matches } from 'class-validator' export class PasswdResetDto { @ApiProperty({ - description: 'new password, 8-32 characters', + description: 'new password, 8-64 characters', example: 'laf-user-password', }) @IsString() @IsNotEmpty() - @Length(8, 32) + @Length(8, 64) password: string @ApiProperty({ @@ -17,7 +17,7 @@ export class PasswdResetDto { example: '13805718888', }) @IsString() - @Length(11, 11) + @Matches(/^1[3-9]\d{9}$/) phone: string @ApiProperty({ diff --git a/server/src/auth/dto/passwd-signin.dto.ts b/server/src/auth/dto/passwd-signin.dto.ts index ba49ac6893..df2e31b085 100644 --- a/server/src/auth/dto/passwd-signin.dto.ts +++ b/server/src/auth/dto/passwd-signin.dto.ts @@ -8,15 +8,15 @@ export class PasswdSigninDto { }) @IsString() @IsNotEmpty() - @Length(8, 16) + @Length(3, 64) username: string @ApiProperty({ - description: 'password, 8-16 characters', + description: 'password, 8-64 characters', example: 'laf-user-password', }) @IsString() @IsNotEmpty() - @Length(8, 32) + @Length(8, 64) password: string } diff --git a/server/src/auth/dto/passwd-signup.dto.ts b/server/src/auth/dto/passwd-signup.dto.ts index f247017a45..fb22db0b57 100644 --- a/server/src/auth/dto/passwd-signup.dto.ts +++ b/server/src/auth/dto/passwd-signup.dto.ts @@ -1,25 +1,24 @@ -import { Optional } from '@nestjs/common' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { SmsVerifyCodeType } from '@prisma/client' -import { IsEnum, IsNotEmpty, IsString, Length } from 'class-validator' +import { IsEnum, IsNotEmpty, IsString, Length, Matches } from 'class-validator' export class PasswdSignupDto { @ApiProperty({ - description: 'username', + description: 'username, 3-64 characters', example: 'laf-user', }) @IsString() @IsNotEmpty() - @Length(8, 16) + @Length(3, 64) username: string @ApiProperty({ - description: 'password, 8-16 characters', + description: 'password, 8-64 characters', example: 'laf-user-password', }) @IsString() @IsNotEmpty() - @Length(8, 32) + @Length(8, 64) password: string @ApiPropertyOptional({ @@ -27,7 +26,7 @@ export class PasswdSignupDto { example: '13805718888', }) @IsString() - @Length(11, 11) + @Matches(/^1[3-9]\d{9}$/) phone: string @ApiPropertyOptional({ diff --git a/server/src/auth/dto/phone-signin.dto.ts b/server/src/auth/dto/phone-signin.dto.ts index 1b62a7de00..ba136ce7ba 100644 --- a/server/src/auth/dto/phone-signin.dto.ts +++ b/server/src/auth/dto/phone-signin.dto.ts @@ -1,5 +1,11 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' -import { IsNotEmpty, IsString, Length, IsOptional } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' +import { + IsNotEmpty, + IsString, + Length, + IsOptional, + Matches, +} from 'class-validator' export class PhoneSigninDto { @ApiProperty({ @@ -8,7 +14,7 @@ export class PhoneSigninDto { }) @IsString() @IsNotEmpty() - @Length(11, 11) + @Matches(/^1[3-9]\d{9}$/) phone: string @ApiProperty({}) @@ -22,15 +28,15 @@ export class PhoneSigninDto { }) @IsOptional() @IsString() - @Length(8, 16) + @Length(3, 64) username: string @ApiProperty({ - description: 'password, 8-32 characters', + description: 'password, 8-64 characters', example: 'laf-user-password', }) @IsOptional() @IsString() - @Length(8, 32) + @Length(8, 64) password: string } diff --git a/server/src/auth/dto/send-phone-code.dto.ts b/server/src/auth/dto/send-phone-code.dto.ts index 02886087a4..1d6dda8e06 100644 --- a/server/src/auth/dto/send-phone-code.dto.ts +++ b/server/src/auth/dto/send-phone-code.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger' import { SmsVerifyCodeType } from '@prisma/client' -import { IsEnum, IsNotEmpty, IsString, Length } from 'class-validator' +import { IsEnum, IsNotEmpty, IsString, Matches } from 'class-validator' export class SendPhoneCodeDto { @ApiProperty({ @@ -9,7 +9,7 @@ export class SendPhoneCodeDto { }) @IsString() @IsNotEmpty() - @Length(11, 11) + @Matches(/^1[3-9]\d{9}$/) phone: string @ApiProperty({ diff --git a/server/src/auth/phone/phone.service.ts b/server/src/auth/phone/phone.service.ts index a7bad632d7..a5093a14b0 100644 --- a/server/src/auth/phone/phone.service.ts +++ b/server/src/auth/phone/phone.service.ts @@ -39,29 +39,26 @@ export class PhoneService { return err } - // save to database, start transaction - await this.prisma.$transaction(async (tx) => { - // disable previous same type code - await tx.smsVerifyCode.updateMany({ - where: { - phone, - type, - state: SmsVerifyCodeState.Active, - }, - data: { - state: SmsVerifyCodeState.Used, - }, - }) + // disable previous sms code + await this.prisma.smsVerifyCode.updateMany({ + where: { + phone, + type, + state: SmsVerifyCodeState.Active, + }, + data: { + state: SmsVerifyCodeState.Used, + }, + }) - // Save sms code to database - await tx.smsVerifyCode.create({ - data: { - phone, - code, - type, - ip, - }, - }) + // Save new sms code to database + await this.prisma.smsVerifyCode.create({ + data: { + phone, + code, + type, + ip, + }, }) return null diff --git a/server/src/initializer/initializer.service.ts b/server/src/initializer/initializer.service.ts index 17eed64131..d7e1b71c18 100644 --- a/server/src/initializer/initializer.service.ts +++ b/server/src/initializer/initializer.service.ts @@ -160,7 +160,6 @@ export class InitializerService { const resPassword = await this.prisma.authProvider.create({ data: { name: 'user-password', - type: 'user-password', bind: { password: 'optional', phone: 'required', @@ -177,7 +176,6 @@ export class InitializerService { const resPhone = await this.prisma.authProvider.create({ data: { name: 'phone', - type: 'phone', bind: { password: 'required', phone: 'optional', From 013f1616dc0f90993cae4ae1ad20672c992d9e43 Mon Sep 17 00:00:00 2001 From: lunaragon Date: Sun, 19 Mar 2023 22:15:16 +0800 Subject: [PATCH 4/4] feat(authentication): modify code according to cr-gpt --- server/src/auth/authentication.service.ts | 4 ++-- server/src/auth/phone/phone.controller.ts | 2 +- server/src/auth/phone/sms.service.ts | 8 ++++---- server/src/constants.ts | 4 ++-- server/src/initializer/initializer.service.ts | 5 +++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/server/src/auth/authentication.service.ts b/server/src/auth/authentication.service.ts index f8299ac3f7..67d058a661 100644 --- a/server/src/auth/authentication.service.ts +++ b/server/src/auth/authentication.service.ts @@ -1,7 +1,7 @@ import { JwtService } from '@nestjs/jwt' import { PrismaService } from 'src/prisma/prisma.service' import { Injectable, Logger } from '@nestjs/common' -import { User } from '@prisma/client' +import { AuthProviderState, User } from '@prisma/client' import { PASSWORD_AUTH_PROVIDER_NAME, PHONE_AUTH_PROVIDER_NAME, @@ -21,7 +21,7 @@ export class AuthenticationService { */ async getProviders() { return await this.prismaService.authProvider.findMany({ - where: { state: 'Enabled' }, + where: { state: AuthProviderState.Enabled }, select: { id: false, name: true, diff --git a/server/src/auth/phone/phone.controller.ts b/server/src/auth/phone/phone.controller.ts index 5edf51d7ce..b13b2d75c8 100644 --- a/server/src/auth/phone/phone.controller.ts +++ b/server/src/auth/phone/phone.controller.ts @@ -66,7 +66,7 @@ export class PhoneController { // user not exist const provider = await this.authService.getPhoneProvider() if (provider.register === false) { - return ResponseUtil.error('register is not allowed') + return ResponseUtil.error('user not exists') } // check if username and password is needed diff --git a/server/src/auth/phone/sms.service.ts b/server/src/auth/phone/sms.service.ts index 402e172c9a..5cf4ccda3e 100644 --- a/server/src/auth/phone/sms.service.ts +++ b/server/src/auth/phone/sms.service.ts @@ -7,8 +7,8 @@ import * as Util from '@alicloud/tea-util' import { ALISMS_KEY, LIMIT_CODE_PER_IP_PER_DAY, - ONE_DAY_IN_MILLISECONDS, - ONE_MINUTE_IN_MILLISECONDS, + MILLISECONDS_PER_DAY, + MILLISECONDS_PER_MINUTE, CODE_VALIDITY, } from 'src/constants' import { PrismaService } from 'src/prisma/prisma.service' @@ -54,7 +54,7 @@ export class SmsService { where: { phone: phone, createdAt: { - gt: new Date(Date.now() - ONE_MINUTE_IN_MILLISECONDS), + gt: new Date(Date.now() - MILLISECONDS_PER_MINUTE), }, }, }) @@ -67,7 +67,7 @@ export class SmsService { where: { ip: ip, createdAt: { - gt: new Date(Date.now() - ONE_DAY_IN_MILLISECONDS), + gt: new Date(Date.now() - MILLISECONDS_PER_DAY), }, }, }) diff --git a/server/src/constants.ts b/server/src/constants.ts index 2d3d0db23d..82b1b00fa0 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -182,8 +182,8 @@ export const SEVEN_DAYS_IN_SECONDS = 60 * 60 * 24 * 7 // 7 days in seconds export const ONE_MONTH_IN_SECONDS = 60 * 60 * 24 * 31 // 31 days in seconds export const FOREVER_IN_SECONDS = 60 * 60 * 24 * 365 * 1000 // 1000 years in seconds export const TASK_LOCK_INIT_TIME = new Date(0) // 1970-01-01 00:00:00 -export const ONE_DAY_IN_MILLISECONDS = 60 * 60 * 24 * 1000 // 1 day in milliseconds -export const ONE_MINUTE_IN_MILLISECONDS = 60 * 1000 // 1 minute in milliseconds +export const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000 // 1 day in milliseconds +export const MILLISECONDS_PER_MINUTE = 60 * 1000 // 1 minute in milliseconds // Resource units export const CPU_UNIT = 1000 diff --git a/server/src/initializer/initializer.service.ts b/server/src/initializer/initializer.service.ts index 9925404ae0..23ee2ab1b1 100644 --- a/server/src/initializer/initializer.service.ts +++ b/server/src/initializer/initializer.service.ts @@ -1,4 +1,5 @@ import { Injectable, Logger } from '@nestjs/common' +import { AuthProviderState } from '@prisma/client' import { RegionService } from 'src/region/region.service' import { MinioService } from 'src/storage/minio/minio.service' import { CPU_UNIT, ServerConfig } from '../constants' @@ -195,7 +196,7 @@ export class InitializerService { }, register: true, default: true, - state: 'Enabled', + state: AuthProviderState.Enabled, config: { usernameField: 'username', passwordField: 'password' }, }, }) @@ -211,7 +212,7 @@ export class InitializerService { }, register: true, default: false, - state: 'Enabled', + state: AuthProviderState.Disabled, config: { alisms: {}, },