diff --git a/.vscode/settings.json b/.vscode/settings.json index c09d578c60..e57d9f1b10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,7 @@ "compat", "containerd", "creationforms", + "datasource", "dockerode", "EJSON", "entrypoint", diff --git a/server/.env b/server/.env index b57578a7df..d7cebdc663 100644 --- a/server/.env +++ b/server/.env @@ -1,7 +1,45 @@ + +# database +DATABASE_URL=postgresql://adm1n:passw0rd@localhost:5432/sys_db?schema=public + +# jwt settings +JWT_SECRET=abc123 +JWT_EXPIRES_IN=7d + +# casdoor settings CASDOOR_ENDPOINT=http://localhost:30070 CASDOOR_ORG_NAME=built-in CASDOOR_APP_NAME=app-built-in CASDOOR_CLIENT_ID=a71f65e93723c436027e CASDOOR_CLIENT_SECRET=0d7e157be08055867b81456df3c222ea7c68a097 CASDOOR_REDIRECT_URI=http://localhost:3000/v1/code2token -CASDOOR_PUBLIC_CERT=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUUzVENDQXNXZ0F3SUJBZ0lEQWVKQU1BMEdDU3FHU0liM0RRRUJDd1VBTUNneERqQU1CZ05WQkFvVEJXRmsKYldsdU1SWXdGQVlEVlFRREV3MWpaWEowTFdKMWFXeDBMV2x1TUI0WERUSXlNVEV4TURFeU16UXdNRm9YRFRReQpNVEV4TURFeU16UXdNRm93S0RFT01Bd0dBMVVFQ2hNRllXUnRhVzR4RmpBVUJnTlZCQU1URFdObGNuUXRZblZwCmJIUXRhVzR3Z2dJaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQ0R3QXdnZ0lLQW9JQ0FRRElQdXdWbzVlcHBnM0EKQW4vUGd3TGMyZmp6aHB1SkpPUGF1cjlyclJoL0Q1b1Bxa1l1MCtaK0pSOEhkQWUwOUppSnEzbFNGTmJ0MTdQYQpxRFgwNFV4UTI0cmNZa0oyQkhzemNSM2FEUjl1RGJ3dThvRXdZUWxyYThYdEMwdCtkWVZubGZLc1FMcmhtVFVmCk5LNlhWaURnVXBJK1BHUTV0Q2tJaG9PdktvVG5aRUhkTmR6RXZvejVjSHo1NWVyOGRJS21SaEduQk10aGUwZ0QKVFAySytHODlLODdET3Y1azFZMVJNbVViSzN1T1hxUFhHNnJIU0tsQWdvMEJlTjBSNlAwd3pBMUVqNG03WWx4ZAp5SjdWSDYzK1VlS3dEYU1NZThXUFZKRnJ3MCtWRllVS1F2S3NCdkxwN1J3ejBuaDRCVlpJcVJhRVB0WXkrRytDCjJ0aFlNL2N4UldISFRUcWFNVEs4dG9VdEZia0xPcEFVNDhWWU1uY2l3MUJBY0NEUGhjWmFxQi9hYWthUFJqQXAKTFlOTk5Ec1g5ZXM1YVdmUlE2R1JFRmw5VlcvZ2d0MkhVbzFvNFcvQitQbFZJTDd6cGRucFprUXN0SU9YSklqNAp5RmtqYUR5UkNJd2RlZmtMS1pBWjZLOXppWFhWVC8vNllrd1hUZG8yTlZ0aS9aa2FzanFxNFJPdkFJakZIYTdWCmh2MHh5T21MU1VqQ0o0Qk4zS0xSVnVrenpqMlNKNG15OVMvS0UyUG1BREp0Rmdqd05ReVVZaE9BQWFpTHNWYW0KWFdBaWRWd05KdlJHZS84RCtnN1BhSU4zTlhpYS9IQkJ1S2ZKaitrNW1aeGU1RVRPN084UE5rZlIwVjI5d1YrMwo0dndDRU9CRzZrT2JLTVl2Z2d0Njd5ek1RUVFwendJREFRQUJveEF3RGpBTUJnTlZIUk1CQWY4RUFqQUFNQTBHCkNTcUdTSWIzRFFFQkN3VUFBNElDQVFDU2lNQWVVOFgveVFNalJTYlBqcHRpREZyeTd1RWk4RFNUVWdnLzhLWksKbklyTFRIUTdKVW5EcnowUmJyUVZMcXRGMkljMTBEYjZDQStlR29idGxvcTBVdHcxTUQySHRVZWswNnhTeFRHbwpoSGxkK2RJeGtCWlVBcjFYU3htN0t6NE9TSUZ1QkNHV0hObVg3U2FxTWhTRDZ5L2tNK1lCZVQvQ1l2Z1B1cmR4CmJIbElORUM1V0daN2Rsbmk5bmZ2QmgwREtQcnZOSE9uZnh0RnJUbEJFMDhuT3NxZDhZdmNUTzdMWmJ5eDAzNDgKeWpIK2hWOTRLanVrUVJPV0owY2NwckkxWEFCQjVNWnJtTUVLZEg4V2xoWnVSWlVIaEV0WWJyc0RmNzNQWHcyTQp4VkYvNDRSVUdYOVBOU1pEQzJ6QzNRaHNPM1l2TFZOQkp2KzNZeHF4cjdJRnYvUno1MHc1Mm0vOVRoR0NRNGRwCnZGQ3FoOTlGdGUvNE93RkdTVEVldlcvM3Qvb0RZOEwrdTZSMUZ0TmVDbWxBQXlNUHNKa0pMT0xHTVphWXZvUTkKVDArTFpMSU56UERPMUZMa3IrZUJaQUZ1Q01lVGg1NTVHd2Y0MmRlM1lQU0hFdmtyMTVpbGFGK2V4aGlINEMxeApCRGFpVytzMlhDUHdCMFZDaE1lNzZwR0J2SUtXN2lxaXJSUGJwSGM1T3Z3eUl5Q3ZsMEtFQXdJYWxOREdzNThDCmxHdDVqdU1IOXhreVFkRERBRTlKOEx0bmh6WDBTSnMxcXRrSVJvR2dHK21ORzRFVk15ajNCa0w0Z1RnZlFQQSsKemxwamhLbXF2cWVBMitLbm03MUpXZFN1SUdvdC80YnUxWW4vbjFocU5pTGtKSFF4dW0vVytHZ0tnM29SMUd2Rgo4Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K \ No newline at end of file +CASDOOR_PUBLIC_CERT="-----BEGIN CERTIFICATE----- +MIIE3TCCAsWgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMCgxDjAMBgNVBAoTBWFk +bWluMRYwFAYDVQQDEw1jZXJ0LWJ1aWx0LWluMB4XDTIyMTExNjEzMDc0OFoXDTQy +MTExNjEzMDc0OFowKDEOMAwGA1UEChMFYWRtaW4xFjAUBgNVBAMTDWNlcnQtYnVp +bHQtaW4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDeqvhS5dxRpHx+ +CwW7NfIZpI6A0CwY4garfmfy2Tug4NBI7E7VK5dIYCvRwGrXGFxfuJegXghuXJL7 +t0MXU5A+Na919DV/PPXJe4ev7YGsh73yIVXS8KpB4bV6rqbSVWDrxwSa4dHBPadX +u/tPQ+HGiPN5JU/+rp7BVM/WaMaChc9sfYMv/LIj0Y3kA9dgezBaAYP+IV8mKqs5 ++MGKpFvxCY3+EGWPVuSSzjJWhrOKqBE8LksV6Djd55WRYgtxLNokUj7/1jFm331M +dLS8Iio++TzjAQWzM2JJVTfSPZeMoaSF+7m77oiGwPDwVM1RbJNqwMbP25Whjk2L +LLZEZfMgL0ej6e0FqNbvd5ehod+L3GM7fR8Q2XxRJhIAY8K6FgxrE8FzLn4mw8pn +n0fkbtb5Al946CgpqKzy94x6q2u9DG1U7YIy3aD/AO0Xbf+TwD7A1uWyr9IAfVQr +P7wJJUJu9cak+ryMw+Y2ezfxda7icMhIQvqx07xkoEY6fKwJ++89HalxUw8nlPqX +sZwI9nWC9w2yaW5sPOy+5bKTtbMnBQmocVb76w0pfp4hEVRHAqNm9XXqwuFXC4A2 +GnjhfLCPsTTEbfTVGSl4EpN/ECYrIA/oHXActRTwntSQoyAicJa6SLK2t6/B9HrO +XWRDTK0L5unEEMhVTfNZzJgmTqX+EwIDAQABoxAwDjAMBgNVHRMBAf8EAjAAMA0G +CSqGSIb3DQEBCwUAA4ICAQBsPjtWCLr1rqMzbkJegubKNt60lQB3mVeFLcATK9/n +1j0J9Hy51rFE2H9cGM0etCoxCtcdYkZHlTo1qVNN8ZmYlzbmpfhCDa+rb812Bk/1 +TdaYcSMZWSMyesYUNHe05n7vxMnz5R4GTadIfFJzGYm+FkxSPAbADkW1oMU0rTbD +zkmTa2ITgzaVFpKontuG27nnebP1JQ2TBBsYO0UBSRhOBPMfgj4udqw1Npp/XfFx +7GlLoGezYYH7t7/ICTAG4R6bwGNJCJMwPDxpz/QmIG73xX2BSg9l8QaFlYhGrmNu +CahXcagxxrsP/BXZuMuIkWSRA0DoSFFgqsijYK7IiUvLa1K923vr0v+a7pGug5Qk +n69BrBPr/rzS/OVHqGe2KHDmwHz0EP/JRtT/Yz31d8+QkK0DG9wYKLyh/+VohAl8 +aXuFuVJTbYPHjlqGdOCE+xQwP//BOv3HffLQm9Aloi1cTuX6BwB0L3d68QwjTiIy +ZMWDQQcDf3F+r2BOeAOtspQGlu7yYB5iHT2GJ81GU+M1OUA0ng5VV5Zix+P/gpgO +6COWblvsjy20tZWEIQxhVHI3PKCMbRg2GcDT1/C4ax40nQVZ+gH4JW1KeE4qqWYb +6COWblvsjy20tZWEIQxhVHI3PKCMbRg2GcDT1/C4ax40nQVZ+gH4JW1KeE4qqWYb +3A== +-----END CERTIFICATE----- +" diff --git a/server/package-lock.json b/server/package-lock.json index 6dcab18008..7a00c441f2 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -14,13 +14,18 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^9.0.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^6.1.3", "@nestjs/terminus": "^9.1.2", "@nestjs/throttler": "^3.1.0", + "@prisma/client": "^4.6.1", "casdoor-nodejs-sdk": "^1.3.0", + "dotenv": "^16.0.3", "nanoid": "^3.3.4", + "passport-jwt": "^4.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -41,6 +46,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "28.1.3", "prettier": "^2.3.2", + "prisma": "^4.6.1", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "28.0.8", @@ -3089,6 +3095,14 @@ "rxjs": "^6.0.0 || ^7.2.0" } }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/@nestjs/config/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -3135,6 +3149,18 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", + "integrity": "sha512-ZsXGY/wMYKzEhymw2+dxiwrHTRKIKrGszx6r2EjQqNLypdXMQu0QrujwZJ8k3+XQV4snmuJwwNakQoA2ILfq8w==", + "dependencies": { + "@types/jsonwebtoken": "8.5.8", + "jsonwebtoken": "8.5.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.0.tgz", @@ -3154,6 +3180,15 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", + "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.0.tgz", @@ -3415,6 +3450,38 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@prisma/client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.6.1.tgz", + "integrity": "sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.6.1.tgz", + "integrity": "sha512-3u2/XxvxB+Q7cMXHnKU0CpBiUK1QWqpgiBv28YDo1zOIJE3FCF8DI2vrp6vuwjGt5h0JGXDSvmSf4D4maVjJdw==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32.tgz", + "integrity": "sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -3651,6 +3718,14 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -3660,8 +3735,7 @@ "node_modules/@types/node": { "version": "16.18.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", - "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==", - "dev": true + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -5911,9 +5985,9 @@ } }, "node_modules/dotenv": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", - "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", "engines": { "node": ">=12" } @@ -9995,6 +10069,41 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "dependencies": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -10045,6 +10154,12 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -10273,6 +10388,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.6.1.tgz", + "integrity": "sha512-BR4itMCuzrDV4tn3e2TF+nh1zIX/RVU0isKtKoN28ADeoJ9nYaMhiuRRkFd2TZN8+l/XfYzoRKyHzUFXLQhmBQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.6.1" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -15059,6 +15191,11 @@ "uuid": "8.3.2" }, "dependencies": { + "dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -15080,12 +15217,27 @@ "uuid": "9.0.0" } }, + "@nestjs/jwt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", + "integrity": "sha512-ZsXGY/wMYKzEhymw2+dxiwrHTRKIKrGszx6r2EjQqNLypdXMQu0QrujwZJ8k3+XQV4snmuJwwNakQoA2ILfq8w==", + "requires": { + "@types/jsonwebtoken": "8.5.8", + "jsonwebtoken": "8.5.1" + } + }, "@nestjs/mapped-types": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.0.tgz", "integrity": "sha512-NTFwPZkQWsArQH8QSyFWGZvJ08gR+R4TofglqZoihn/vU+ktHEJjMqsIsADwb7XD97DhiD+TVv5ac+jG33BHrg==", "requires": {} }, + "@nestjs/passport": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", + "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==", + "requires": {} + }, "@nestjs/platform-express": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.0.tgz", @@ -15261,6 +15413,25 @@ } } }, + "@prisma/client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.6.1.tgz", + "integrity": "sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==", + "requires": { + "@prisma/engines-version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32" + } + }, + "@prisma/engines": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.6.1.tgz", + "integrity": "sha512-3u2/XxvxB+Q7cMXHnKU0CpBiUK1QWqpgiBv28YDo1zOIJE3FCF8DI2vrp6vuwjGt5h0JGXDSvmSf4D4maVjJdw==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32.tgz", + "integrity": "sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==" + }, "@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -15493,6 +15664,14 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "requires": { + "@types/node": "*" + } + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -15502,8 +15681,7 @@ "@types/node": { "version": "16.18.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", - "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==", - "dev": true + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==" }, "@types/parse-json": { "version": "4.0.0", @@ -17217,9 +17395,9 @@ "dev": true }, "dotenv": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", - "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" }, "dotenv-expand": { "version": "8.0.3", @@ -20295,6 +20473,31 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "peer": true, + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "requires": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -20333,6 +20536,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -20499,6 +20708,15 @@ } } }, + "prisma": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.6.1.tgz", + "integrity": "sha512-BR4itMCuzrDV4tn3e2TF+nh1zIX/RVU0isKtKoN28ADeoJ9nYaMhiuRRkFd2TZN8+l/XfYzoRKyHzUFXLQhmBQ==", + "devOptional": true, + "requires": { + "@prisma/engines": "4.6.1" + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/server/package.json b/server/package.json index 51db84bdaf..876ba91ee1 100644 --- a/server/package.json +++ b/server/package.json @@ -26,13 +26,18 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^9.0.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^6.1.3", "@nestjs/terminus": "^9.1.2", "@nestjs/throttler": "^3.1.0", + "@prisma/client": "^4.6.1", "casdoor-nodejs-sdk": "^1.3.0", + "dotenv": "^16.0.3", "nanoid": "^3.3.4", + "passport-jwt": "^4.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -53,6 +58,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "28.1.3", "prettier": "^2.3.2", + "prisma": "^4.6.1", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "28.0.8", diff --git a/server/prisma/migrations/20221121054527_init/migration.sql b/server/prisma/migrations/20221121054527_init/migration.sql new file mode 100644 index 0000000000..7edb21c563 --- /dev/null +++ b/server/prisma/migrations/20221121054527_init/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "name" TEXT +); + +-- CreateTable +CREATE TABLE "UserProfile" ( + "uid" TEXT NOT NULL, + "openid" TEXT NOT NULL, + "from" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserProfile_uid_key" ON "UserProfile"("uid"); + +-- AddForeignKey +ALTER TABLE "UserProfile" ADD CONSTRAINT "UserProfile_uid_fkey" FOREIGN KEY ("uid") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20221121100108_add_fields/migration.sql b/server/prisma/migrations/20221121100108_add_fields/migration.sql new file mode 100644 index 0000000000..e3efe4b062 --- /dev/null +++ b/server/prisma/migrations/20221121100108_add_fields/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to drop the column `name` on the `User` table. All the data in the column will be lost. + - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "name", +ADD COLUMN "username" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "UserProfile" ADD COLUMN "avatar" TEXT, +ADD COLUMN "name" TEXT, +ALTER COLUMN "openid" DROP NOT NULL, +ALTER COLUMN "from" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/server/prisma/migrations/migration_lock.toml b/server/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000000..fbffa92c2b --- /dev/null +++ b/server/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 4da70ce4ed..747b5874ea 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -10,20 +10,19 @@ datasource db { url = env("DATABASE_URL") } - model User { - id Int @default(autoincrement()) @id - email String @unique - phone String @unique - name String? - posts Post[] + id String @unique + username String @unique + email String @unique + phone String @unique + UserProfile UserProfile? } -model Post { - id Int @default(autoincrement()) @id - title String - content String? - published Boolean? @default(false) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? -} \ No newline at end of file +model UserProfile { + uid String @unique + openid String? + from String? + avatar String? + name String? + user User @relation(fields: [uid], references: [id]) +} diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts index ea6b350406..4e09ccf8f9 100644 --- a/server/src/app.controller.ts +++ b/server/src/app.controller.ts @@ -1,22 +1,18 @@ -import { Controller, Get, Query, Res } from '@nestjs/common' +import { Controller, Get, Query, Req, Res, UseGuards } from '@nestjs/common' import { Response } from 'express' -import { AppService } from './app.service' - +import { AuthService } from './auth/auth.service' +import { JwtAuthGuard } from './auth/jwt-auth.guard' +import { ResponseUtil } from './utils/response' @Controller() export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello() - } + constructor(private readonly authService: AuthService) {} /** * Redirect to login page */ @Get('login') async getSigninUrl(@Res() res: Response) { - const url = this.appService.getSignInUrl() + const url = this.authService.getSignInUrl() return res.redirect(url) } @@ -27,7 +23,7 @@ export class AppController { */ @Get('register') async getSignupUrl(@Res() res: Response) { - const url = this.appService.getSignUpUrl() + const url = this.authService.getSignUpUrl() return res.redirect(url) } @@ -38,17 +34,17 @@ export class AppController { */ @Get('code2token') async code2token(@Query('code') code: string) { - const token = await this.appService.code2token(code) + const token = await this.authService.code2token(code) if (!token) { - return { - error: 'invalid code', - } + return ResponseUtil.error('invalid code') } - const user = this.appService.getAuthSDK().parseJwtToken(token) - return { - access_token: token, - user: user, - } + return ResponseUtil.ok(token) + } + + @UseGuards(JwtAuthGuard) + @Get('profile') + async getProfile(@Req() request) { + return request.user } } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 386cfe33dc..86d8a19621 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,30 +1,27 @@ import { Module } from '@nestjs/common' import { AppController } from './app.controller' import { AppService } from './app.service' -import { AppsModule } from './apps/apps.module' import { CollectionsModule } from './collections/collections.module' import { WebsitesModule } from './websites/websites.module' import { BucketsModule } from './buckets/buckets.module' import { PoliciesModule } from './policies/policies.module' import { FunctionsModule } from './functions/functions.module' -import { ConfigModule } from '@nestjs/config' import { HttpModule } from '@nestjs/axios' import { CoreModule } from './core/core.module' +import { ApplicationsModule } from './applications/applications.module' +import { AuthModule } from './auth/auth.module' @Module({ imports: [ - ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], - isGlobal: true, - }), - AppsModule, FunctionsModule, PoliciesModule, BucketsModule, WebsitesModule, CollectionsModule, HttpModule, + AuthModule, CoreModule, + ApplicationsModule, ], controllers: [AppController], providers: [AppService], diff --git a/server/src/app.service.ts b/server/src/app.service.ts index ff4812ee4d..5965cb5a0f 100644 --- a/server/src/app.service.ts +++ b/server/src/app.service.ts @@ -1,80 +1,4 @@ import { Injectable } from '@nestjs/common' -import { SDK, Config } from 'casdoor-nodejs-sdk' -import * as querystring from 'node:querystring' -@Injectable() -export class AppService { - getHello(): string { - return 'Hello laf!' - } - - /** - * Get auth config of casdoor - * @returns - */ - getAuthConfig() { - const authCfg: Config = { - endpoint: process.env.CASDOOR_ENDPOINT, - clientId: process.env.CASDOOR_CLIENT_ID, - clientSecret: process.env.CASDOOR_CLIENT_SECRET, - certificate: Buffer.from( - process.env.CASDOOR_PUBLIC_CERT, - 'base64', - ).toString('ascii'), - orgName: process.env.CASDOOR_ORG_NAME, - } - return authCfg - } - - /** - * Create casdoor SDK instance - * @returns - */ - getAuthSDK() { - const sdk = new SDK(this.getAuthConfig()) - return sdk - } - /** - * Get user token by code - * @param code - * @returns - */ - async code2token(code: string): Promise { - try { - const token = await this.getAuthSDK().getAuthToken(code) - return token - } catch (error) { - return null - } - } - - /** - * Generate login url - * @returns - */ - getSignInUrl(): string { - const authCfg = this.getAuthConfig() - const query = { - client_id: authCfg.clientId, - redirect_uri: process.env.CASDOOR_REDIRECT_URI, - response_type: 'code', - scope: 'openid,profile,phone,email', - state: 'casdoor', - } - const encoded_query = querystring.encode(query) - const base_api = `${authCfg.endpoint}/login/oauth/authorize` - const url = `${base_api}?${encoded_query}` - return url - } - - /** - * Generate register url - * @returns - */ - getSignUpUrl(): string { - const authCfg = this.getAuthConfig() - const app_name = process.env.CASDOOR_APP_NAME - const url = `${authCfg.endpoint}/signup/${app_name}` - return url - } -} +@Injectable() +export class AppService {} diff --git a/server/src/applications/applications.controller.spec.ts b/server/src/applications/applications.controller.spec.ts new file mode 100644 index 0000000000..11c5a23bdb --- /dev/null +++ b/server/src/applications/applications.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationsController } from './applications.controller'; +import { ApplicationsService } from './applications.service'; + +describe('ApplicationsController', () => { + let controller: ApplicationsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ApplicationsController], + providers: [ApplicationsService], + }).compile(); + + controller = module.get(ApplicationsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/server/src/applications/applications.controller.ts b/server/src/applications/applications.controller.ts new file mode 100644 index 0000000000..e5573b1345 --- /dev/null +++ b/server/src/applications/applications.controller.ts @@ -0,0 +1,67 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, +} from '@nestjs/common' +import { ResponseUtil } from 'src/utils/response' +import { ApplicationsService } from './applications.service' +import { CreateApplicationDto } from './dto/create-application.dto' +import { UpdateApplicationDto } from './dto/update-application.dto' + +@Controller('applications') +export class ApplicationsController { + constructor(private readonly appService: ApplicationsService) {} + + /** + * Create application + * @returns + */ + @Post() + async create(@Body() dto: CreateApplicationDto) { + const error = dto.validate() + if (error) { + return ResponseUtil.error(error) + } + + // create namespace + const appid = this.appService.generateAppid(6) + const namespace = await this.appService.createAppNamespace(appid) + if (!namespace) { + return ResponseUtil.error('create app namespace error') + } + + // create app + const app = await this.appService.create(appid, dto) + if (!app) { + return ResponseUtil.error('create app error') + } + return ResponseUtil.ok(app) + } + + @Get() + findAll() { + return this.appService.findAll() + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.appService.findOne(+id) + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() updateApplicationDto: UpdateApplicationDto, + ) { + return this.appService.update(+id, updateApplicationDto) + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.appService.remove(+id) + } +} diff --git a/server/src/applications/applications.module.ts b/server/src/applications/applications.module.ts new file mode 100644 index 0000000000..c70f8be099 --- /dev/null +++ b/server/src/applications/applications.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { ApplicationsService } from './applications.service' +import { ApplicationsController } from './applications.controller' + +@Module({ + controllers: [ApplicationsController], + providers: [ApplicationsService], +}) +export class ApplicationsModule {} diff --git a/server/src/apps/apps.service.spec.ts b/server/src/applications/applications.service.spec.ts similarity index 76% rename from server/src/apps/apps.service.spec.ts rename to server/src/applications/applications.service.spec.ts index fe5cb9381f..594ebf668e 100644 --- a/server/src/apps/apps.service.spec.ts +++ b/server/src/applications/applications.service.spec.ts @@ -1,19 +1,19 @@ import { Test, TestingModule } from '@nestjs/testing' import { CoreModule } from '../core/core.module' -import { AppsService } from './apps.service' -import { CreateAppDto } from './dto/create-app.dto' -import { ApplicationState } from './entities/app.entity' +import { ApplicationsService } from './applications.service' +import { CreateApplicationDto } from './dto/create-application.dto' +import { ApplicationState } from './entities/application.entity' describe('AppsService', () => { - let service: AppsService + let service: ApplicationsService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [CoreModule], - providers: [AppsService], + providers: [ApplicationsService], }).compile() - service = module.get(AppsService) + service = module.get(ApplicationsService) }) it('should be defined', () => { @@ -23,15 +23,15 @@ describe('AppsService', () => { describe('AppService create app', () => { const timeout = 60 * 1000 - let service: AppsService + let service: ApplicationsService let appid: string beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [CoreModule], - providers: [AppsService], + providers: [ApplicationsService], }).compile() - service = module.get(AppsService) + service = module.get(ApplicationsService) }, timeout) jest.setTimeout(timeout) @@ -46,7 +46,7 @@ describe('AppService create app', () => { } it('should create app', async () => { - const dto = new CreateAppDto() + const dto = new CreateApplicationDto() dto.name = 'test-for-create-app' dto.state = ApplicationState.ApplicationStateRunning dto.region = 'default' diff --git a/server/src/apps/apps.service.ts b/server/src/applications/applications.service.ts similarity index 80% rename from server/src/apps/apps.service.ts rename to server/src/applications/applications.service.ts index 7c04fe8a9a..3704576ffe 100644 --- a/server/src/apps/apps.service.ts +++ b/server/src/applications/applications.service.ts @@ -1,14 +1,14 @@ import { Injectable, Logger } from '@nestjs/common' import { KubernetesService } from '../core/kubernetes.service' -import { CreateAppDto } from './dto/create-app.dto' -import { UpdateAppDto } from './dto/update-app.dto' -import { Application, ApplicationSpec } from './entities/app.entity' +import { Application, ApplicationSpec } from './entities/application.entity' import * as k8s from '@kubernetes/client-node' import * as nanoid from 'nanoid' +import { CreateApplicationDto } from './dto/create-application.dto' +import { UpdateApplicationDto } from './dto/update-application.dto' @Injectable() -export class AppsService { - private readonly logger = new Logger(AppsService.name) +export class ApplicationsService { + private readonly logger = new Logger(ApplicationsService.name) constructor(public k8sClient: KubernetesService) {} // create app namespace @@ -37,7 +37,7 @@ export class AppsService { return nano() } - async create(appid: string, dto: CreateAppDto) { + async create(appid: string, dto: CreateApplicationDto) { // create app resources const app = new Application() app.metadata.name = dto.name @@ -67,7 +67,7 @@ export class AppsService { return `This action returns a #${id} app` } - update(id: number, updateAppDto: UpdateAppDto) { + update(id: number, dto: UpdateApplicationDto) { return `This action updates a #${id} app` } diff --git a/server/src/apps/dto/create-app.dto.ts b/server/src/applications/dto/create-application.dto.ts similarity index 80% rename from server/src/apps/dto/create-app.dto.ts rename to server/src/applications/dto/create-application.dto.ts index 1486dbce33..b0faf8d425 100644 --- a/server/src/apps/dto/create-app.dto.ts +++ b/server/src/applications/dto/create-application.dto.ts @@ -1,6 +1,6 @@ -import { ApplicationState } from '../entities/app.entity' +import { ApplicationState } from '../entities/application.entity' -export class CreateAppDto { +export class CreateApplicationDto { name: string state: ApplicationState region: string diff --git a/server/src/applications/dto/update-application.dto.ts b/server/src/applications/dto/update-application.dto.ts new file mode 100644 index 0000000000..7bd12c0342 --- /dev/null +++ b/server/src/applications/dto/update-application.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateApplicationDto } from './create-application.dto'; + +export class UpdateApplicationDto extends PartialType(CreateApplicationDto) {} diff --git a/server/src/apps/entities/app.entity.ts b/server/src/applications/entities/application.entity.ts similarity index 100% rename from server/src/apps/entities/app.entity.ts rename to server/src/applications/entities/application.entity.ts diff --git a/server/src/apps/apps.controller.spec.ts b/server/src/apps/apps.controller.spec.ts deleted file mode 100644 index ced6102d4c..0000000000 --- a/server/src/apps/apps.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { CoreModule } from '../core/core.module' -import { AppsController } from './apps.controller' -import { AppsService } from './apps.service' - -describe('AppsController', () => { - let controller: AppsController - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [CoreModule], - controllers: [AppsController], - providers: [AppsService], - }).compile() - - controller = module.get(AppsController) - }) - - it('should be defined', () => { - expect(controller).toBeDefined() - }) -}) diff --git a/server/src/apps/apps.controller.ts b/server/src/apps/apps.controller.ts deleted file mode 100644 index 69006b34fc..0000000000 --- a/server/src/apps/apps.controller.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Logger, -} from '@nestjs/common' -import { AppsService } from './apps.service' -import { CreateAppDto } from './dto/create-app.dto' -import { UpdateAppDto } from './dto/update-app.dto' -import { ResponseUtil } from '../utils/response' - -@Controller('apps') -export class AppsController { - private readonly logger = new Logger(AppsController.name) - constructor(private readonly appsService: AppsService) {} - - /** - * Create application - * @returns - */ - @Post() - async create(@Body() dto: CreateAppDto) { - const error = dto.validate() - if (error) { - return ResponseUtil.error(error) - } - - // create namespace - const appid = this.appsService.generateAppid(6) - const namespace = await this.appsService.createAppNamespace(appid) - if (!namespace) { - return ResponseUtil.error('create app namespace error') - } - - // create app - const app = await this.appsService.create(appid, dto) - if (!app) { - return ResponseUtil.error('create app error') - } - return ResponseUtil.ok(app) - } - - @Get() - findAll() { - return this.appsService.findAll() - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.appsService.findOne(+id) - } - - @Patch(':id') - update(@Param('id') id: string, @Body() updateAppDto: UpdateAppDto) { - return this.appsService.update(+id, updateAppDto) - } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.appsService.remove(+id) - } -} diff --git a/server/src/apps/apps.module.ts b/server/src/apps/apps.module.ts deleted file mode 100644 index b4a57852ef..0000000000 --- a/server/src/apps/apps.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common' -import { AppsService } from './apps.service' -import { AppsController } from './apps.controller' - -@Module({ - controllers: [AppsController], - providers: [AppsService], -}) -export class AppsModule {} diff --git a/server/src/apps/dto/update-app.dto.ts b/server/src/apps/dto/update-app.dto.ts deleted file mode 100644 index 6606d43ca8..0000000000 --- a/server/src/apps/dto/update-app.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types' -import { CreateAppDto } from './create-app.dto' - -export class UpdateAppDto extends PartialType(CreateAppDto) {} diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index 0985f4e94b..bf1e504b52 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -1,18 +1,22 @@ import { Module } from '@nestjs/common' import { JwtModule } from '@nestjs/jwt' import { PassportModule } from '@nestjs/passport' +import { Constants } from 'src/constants' +import { UsersModule } from '../users/users.module' import { AuthService } from './auth.service' +import { CasdoorService } from './casdoor.service' import { JwtStrategy } from './jwt.strategy' @Module({ imports: [ PassportModule, JwtModule.register({ - // publicKey: process.env.CASDOOR_PUBLIC_CERT, - // signOptions: { expiresIn: '60s' }, + secret: Constants.JWT_SECRET, + signOptions: { expiresIn: Constants.JWT_EXPIRES_IN }, }), + UsersModule, ], - providers: [AuthService, JwtStrategy], + providers: [AuthService, JwtStrategy, CasdoorService], exports: [AuthService], }) export class AuthModule {} diff --git a/server/src/auth/auth.service.spec.ts b/server/src/auth/auth.service.spec.ts index 800ab6626a..52d97a6d2d 100644 --- a/server/src/auth/auth.service.spec.ts +++ b/server/src/auth/auth.service.spec.ts @@ -1,18 +1,18 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthService } from './auth.service'; +import { Test, TestingModule } from '@nestjs/testing' +import { AuthService } from './auth.service' describe('AuthService', () => { - let service: AuthService; + let service: AuthService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [AuthService], - }).compile(); + }).compile() - service = module.get(AuthService); - }); + service = module.get(AuthService) + }) it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); + expect(service).toBeDefined() + }) +}) diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 125ece563c..f2c971efc1 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -1,69 +1,83 @@ import { Injectable, Logger } from '@nestjs/common' import { JwtService } from '@nestjs/jwt' -import { SDK, Config } from 'casdoor-nodejs-sdk' -import * as querystring from 'node:querystring' +import { CasdoorService } from './casdoor.service' +import { User } from '@prisma/client' +import { UsersService } from 'src/users/users.service' @Injectable() export class AuthService { - private logger = new Logger() - constructor(private jwtService: JwtService) {} + logger: Logger = new Logger(AuthService.name) + constructor( + private readonly jwtService: JwtService, + private readonly casdoorService: CasdoorService, + private readonly userService: UsersService, + ) {} /** - * Get auth config of casdoor - * @returns - */ - getCasdoorConfig() { - const authCfg: Config = { - endpoint: process.env.CASDOOR_ENDPOINT, - clientId: process.env.CASDOOR_CLIENT_ID, - clientSecret: process.env.CASDOOR_CLIENT_SECRET, - certificate: process.env.CASDOOR_PUBLIC_CERT, - orgName: process.env.CASDOOR_ORG_NAME, - } - return authCfg - } - - /** - * Create casdoor SDK instance - * @returns - */ - getCasdoorSDK() { - const sdk = new SDK(this.getCasdoorConfig()) - return sdk - } - - /** - * Get user token by code + * Get user token by casdoor code: + * - code is casdoor code + * - user token is laf server token, NOT casdoor token * @param code * @returns */ async code2token(code: string): Promise { try { - const token = await this.getCasdoorSDK().getAuthToken(code) - return token + const casdoorUser = await this.casdoorService.code2user(code) + if (!casdoorUser) return null + + // Get or create laf user + const profile = await this.userService.getProfileByOpenid(casdoorUser.id) + let user = profile?.user + if (!user) { + user = await this.userService.create({ + id: this.userService.generateUserId(), + username: casdoorUser.name, + email: casdoorUser.email, + phone: casdoorUser.phone, + UserProfile: { + create: { + openid: casdoorUser.id, + name: casdoorUser.displayName, + avatar: casdoorUser.avatar, + }, + }, + }) + } else { + // update it + user = await this.userService.updateUser({ + where: { id: user.id }, + data: { + username: casdoorUser.name, + email: casdoorUser.email, + phone: casdoorUser.phone, + UserProfile: { + update: { + name: casdoorUser.displayName, + avatar: casdoorUser.avatar, + }, + }, + }, + }) + } + + return this.getAccessTokenByUser(user) } catch (error) { + this.logger.error(error) return null } } /** - * Get user info by token - * @param token + * Get access token by user + * @param user * @returns */ - async getUserInfo(token: string) { - try { - const user = this.jwtService.verify(token, { - publicKey: this.getCasdoorConfig().certificate, - }) - Object.keys(user).forEach((key) => { - if (user[key] === '' || user[key] === null) delete user[key] - }) - return user - } catch (err) { - this.logger.error(err) - return null + getAccessTokenByUser(user: User): string { + const payload = { + sub: user.id, } + const token = this.jwtService.sign(payload) + return token } /** @@ -71,18 +85,7 @@ export class AuthService { * @returns */ getSignInUrl(): string { - const authCfg = this.getCasdoorConfig() - const query = { - client_id: authCfg.clientId, - redirect_uri: process.env.CASDOOR_REDIRECT_URI, - response_type: 'code', - scope: 'openid,profile,phone,email', - state: 'casdoor', - } - const encoded_query = querystring.encode(query) - const base_api = `${authCfg.endpoint}/login/oauth/authorize` - const url = `${base_api}?${encoded_query}` - return url + return this.casdoorService.getSignInUrl() } /** @@ -90,9 +93,6 @@ export class AuthService { * @returns */ getSignUpUrl(): string { - const authCfg = this.getCasdoorConfig() - const app_name = process.env.CASDOOR_APP_NAME - const url = `${authCfg.endpoint}/signup/${app_name}` - return url + return this.casdoorService.getSignUpUrl() } } diff --git a/server/src/auth/casdoor.service.ts b/server/src/auth/casdoor.service.ts new file mode 100644 index 0000000000..16e9940b99 --- /dev/null +++ b/server/src/auth/casdoor.service.ts @@ -0,0 +1,104 @@ +import { Injectable, Logger } from '@nestjs/common' +import { JwtService } from '@nestjs/jwt' +import { SDK, Config } from 'casdoor-nodejs-sdk' +import * as querystring from 'node:querystring' +import { Constants } from '../constants' + +@Injectable() +export class CasdoorService { + private logger = new Logger() + + /** + * Get auth config of casdoor + * @returns + */ + getCasdoorConfig() { + const authCfg: Config = { + endpoint: Constants.CASDOOR_ENDPOINT, + clientId: Constants.CASDOOR_CLIENT_ID, + clientSecret: Constants.CASDOOR_CLIENT_SECRET, + certificate: Constants.CASDOOR_PUBLIC_CERT, + orgName: Constants.CASDOOR_ORG_NAME, + } + return authCfg + } + + /** + * Create casdoor SDK instance + * @returns + */ + getCasdoorSDK() { + const sdk = new SDK(this.getCasdoorConfig()) + return sdk + } + + /** + * Get user from code directly + * @param code + * @returns + */ + async code2user(code: string) { + const token = await this.code2token(code) + const user = this.getUserInfo(token) + return user + } + + /** + * Get user token by code + * @param code + * @returns + */ + async code2token(code: string): Promise { + try { + const token = await this.getCasdoorSDK().getAuthToken(code) + return token + } catch (error) { + return null + } + } + + /** + * Get user info by token + * @param token + * @returns + */ + getUserInfo(token: string) { + try { + const user = this.getCasdoorSDK().parseJwtToken(token) + return user + } catch (err) { + this.logger.error(err) + return null + } + } + + /** + * Generate login url + * @returns + */ + getSignInUrl(): string { + const authCfg = this.getCasdoorConfig() + const query = { + client_id: authCfg.clientId, + redirect_uri: process.env.CASDOOR_REDIRECT_URI, + response_type: 'code', + scope: 'openid,profile,phone,email', + state: 'casdoor', + } + const encoded_query = querystring.encode(query) + const base_api = `${authCfg.endpoint}/login/oauth/authorize` + const url = `${base_api}?${encoded_query}` + return url + } + + /** + * Generate register url + * @returns + */ + getSignUpUrl(): string { + const authCfg = this.getCasdoorConfig() + const app_name = Constants.CASDOOR_APP_NAME + const url = `${authCfg.endpoint}/signup/${app_name}` + return url + } +} diff --git a/server/src/auth/jwt.strategy.ts b/server/src/auth/jwt.strategy.ts index d241a651b1..c340b0f14f 100644 --- a/server/src/auth/jwt.strategy.ts +++ b/server/src/auth/jwt.strategy.ts @@ -1,14 +1,16 @@ import { Injectable } from '@nestjs/common' import { PassportStrategy } from '@nestjs/passport' import { ExtractJwt, Strategy } from 'passport-jwt' +import { Constants } from '../constants' +import { UsersService } from '../users/users.service' @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { + constructor(private readonly userService: UsersService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: process.env.CASDOOR_PUBLIC_CERT, + secretOrKey: Constants.JWT_SECRET, }) } @@ -18,15 +20,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) { * @returns */ async validate(payload: any) { - return { - id: payload.sub, - owner: payload.owner, - username: payload.name, - displayName: payload.displayName, - email: payload.email, - avatar: payload.avatar, - emailVerified: payload.emailVerified, - phone: payload.phone, - } + const id = payload.sub + const user = await this.userService.user({ id }, true) + return user } } diff --git a/server/src/constants.ts b/server/src/constants.ts new file mode 100644 index 0000000000..bd18dff2ae --- /dev/null +++ b/server/src/constants.ts @@ -0,0 +1,44 @@ +import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import +dotenv.config({ path: '.env.local' }) +dotenv.config() + +export class Constants { + static get JWT_SECRET() { + if (!process.env.JWT_SECRET) { + throw new Error('JWT_SECRET is not defined') + } + return process.env.JWT_SECRET + } + + static get JWT_EXPIRES_IN() { + return process.env.JWT_EXPIRES_IN || '7d' + } + + static get CASDOOR_ENDPOINT() { + return process.env.CASDOOR_ENDPOINT + } + + static get CASDOOR_APP_NAME() { + return process.env.CASDOOR_APP_NAME + } + + static get CASDOOR_CLIENT_ID() { + return process.env.CASDOOR_CLIENT_ID + } + + static get CASDOOR_CLIENT_SECRET() { + return process.env.CASDOOR_CLIENT_SECRET + } + + static get CASDOOR_REDIRECT_URI() { + return process.env.CASDOOR_REDIRECT_URI + } + + static get CASDOOR_PUBLIC_CERT() { + return process.env.CASDOOR_PUBLIC_CERT + } + + static get CASDOOR_ORG_NAME() { + return process.env.CASDOOR_ORG_NAME + } +} diff --git a/server/src/users/users.service.ts b/server/src/users/users.service.ts index 9fb490e6e9..24c3a209e1 100644 --- a/server/src/users/users.service.ts +++ b/server/src/users/users.service.ts @@ -1,17 +1,52 @@ import { Injectable } from '@nestjs/common' import { Prisma, User } from '@prisma/client' import { PrismaService } from '../prisma.service' +import * as nanoid from 'nanoid' @Injectable() export class UsersService { constructor(private prisma: PrismaService) {} - async user(input: Prisma.UserWhereUniqueInput) { + generateUserId() { + const nano = nanoid.customAlphabet( + '1234567890abcdefghijklmnopqrstuvwxyz', + 12, + ) + return nano() + } + + async create(data: Prisma.UserCreateInput): Promise { + if (!data.id) { + data.id = this.generateUserId() + } + + return this.prisma.user.create({ + data, + }) + } + + async user(input: Prisma.UserWhereUniqueInput, withProfile = false) { return this.prisma.user.findUnique({ where: input, + include: { + UserProfile: withProfile, + }, }) } + async profile(input: Prisma.UserProfileWhereInput, withUser = true) { + return this.prisma.userProfile.findFirst({ + where: input, + include: { + user: withUser, + }, + }) + } + + async getProfileByOpenid(openid: string) { + return this.profile({ openid }, true) + } + async users(params: { skip?: number take?: number