diff --git a/api/.env.defaults b/api/.env.defaults
index ad9987672442b84904a71d8526b7e050267b3924..0320f1402f9ae2e2f1cfa4380a4445073470dece 100644
--- a/api/.env.defaults
+++ b/api/.env.defaults
@@ -1,3 +1,13 @@
+# ENGINE
ENGINE_TYPE=exareme
ENGINE_BASE_URL=http://127.0.0.1:8080/services/
+
+# SERVER
GATEWAY_PORT=8081
+
+# AUTHENTICATION
+AUTH_SKIP=false
+AUTH_JWT_SECRET=SecretForDevPurposeOnly
+AUTH_JWT_TOKEN_EXPIRES_IN=2d
+AUTH_COOKIE_SAME_SITE=strict
+AUTH_COOKIE_SECURE=true
\ No newline at end of file
diff --git a/api/package-lock.json b/api/package-lock.json
index 3d4c786544e197a15f2560fff6c55d4c3107e612..8eef2433f6b4fe6ff3de3b3ffa3bfe2939bb1d0a 100644
--- a/api/package-lock.json
+++ b/api/package-lock.json
@@ -15,13 +15,19 @@
"@nestjs/config": "^1.0.1",
"@nestjs/core": "^8.0.0",
"@nestjs/graphql": "^10.0.6",
+ "@nestjs/jwt": "^8.0.0",
+ "@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "^8.0.2",
"apollo-server-express": "^3.6.3",
"axios": "^0.21.1",
+ "cookie-parser": "^1.4.6",
"graphql": "^15.5.3",
"graphql-type-json": "^0.3.2",
"jsonata": "^1.8.5",
+ "passport": "^0.5.2",
+ "passport-jwt": "^4.0.0",
+ "passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
@@ -29,6 +35,7 @@
},
"devDependencies": {
"@eclass/semantic-release-docker": "^3.0.0",
+ "@jest-mock/express": "^1.4.5",
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.2.2",
@@ -36,9 +43,12 @@
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/gitlab": "^7.0.4",
+ "@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13",
"@types/jest": "^27.0.1",
"@types/node": "^16.0.0",
+ "@types/passport-jwt": "^3.0.6",
+ "@types/passport-local": "^1.0.34",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
@@ -47,6 +57,7 @@
"eslint-plugin-prettier": "^3.4.0",
"husky": "^7.0.2",
"jest": "^27.0.6",
+ "jest-mock": "^27.5.1",
"prettier": "^2.3.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
@@ -1064,6 +1075,12 @@
"node": ">=8"
}
},
+ "node_modules/@jest-mock/express": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@jest-mock/express/-/express-1.4.5.tgz",
+ "integrity": "sha512-bERM1jnutyH7VMahdaOHAKy7lgX47zJ7+RTz2eMz0wlCttd9CkhsKFEyoWmJBSz/ow0nVj3lCuRqLem4QDYFkQ==",
+ "dev": true
+ },
"node_modules/@jest/console": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
@@ -1819,6 +1836,18 @@
}
}
},
+ "node_modules/@nestjs/jwt": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz",
+ "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==",
+ "dependencies": {
+ "@types/jsonwebtoken": "8.5.4",
+ "jsonwebtoken": "8.5.1"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/@nestjs/mapped-types": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz",
@@ -1838,6 +1867,15 @@
}
}
},
+ "node_modules/@nestjs/passport": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.2.1.tgz",
+ "integrity": "sha512-HXEKMLX1x865+lsJB4srwKHBciDNAhWY1Ha+xbxYRbk7J5leGDoHJAmeqe+Wb3NDn5nkboggLV87t0q2mbYc8w==",
+ "peerDependencies": {
+ "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "passport": "^0.4.0 || ^0.5.0"
+ }
+ },
"node_modules/@nestjs/platform-express": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.4.0.tgz",
@@ -2561,6 +2599,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/cookie-parser": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz",
+ "integrity": "sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/cookiejar": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -2680,6 +2727,14 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "node_modules/@types/jsonwebtoken": {
+ "version": "8.5.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz",
+ "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/keyv": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz",
@@ -2722,6 +2777,47 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "node_modules/@types/passport": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz",
+ "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/passport-jwt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz",
+ "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/jsonwebtoken": "*",
+ "@types/passport-strategy": "*"
+ }
+ },
+ "node_modules/@types/passport-local": {
+ "version": "1.0.34",
+ "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.34.tgz",
+ "integrity": "sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/passport": "*",
+ "@types/passport-strategy": "*"
+ }
+ },
+ "node_modules/@types/passport-strategy": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
+ "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/passport": "*"
+ }
+ },
"node_modules/@types/prettier": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz",
@@ -3897,6 +3993,11 @@
"ieee754": "^1.1.13"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4542,6 +4643,26 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookie-parser": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+ "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+ "dependencies": {
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-parser/node_modules/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -5094,6 +5215,14 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -8312,6 +8441,54 @@
"node": "*"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
+ "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=4",
+ "npm": ">=1.4.28"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/keyv": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.1.1.tgz",
@@ -8449,23 +8626,41 @@
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=",
"dev": true
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+ },
"node_modules/lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
"dev": true
},
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+ },
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
- "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
- "dev": true
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
- "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
- "dev": true
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
@@ -8484,6 +8679,11 @@
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
},
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
"node_modules/lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -11875,6 +12075,50 @@
"node": ">= 0.8"
}
},
+ "node_modules/passport": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.5.2.tgz",
+ "integrity": "sha512-w9n/Ot5I7orGD4y+7V3EFJCQEznE5RxHamUxcqLT2QoJY0f2JdN8GyHonYFvN0Vz+L6lUJfVhrk2aZz2LbuREw==",
+ "dependencies": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.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-local": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
+ "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
+ "dependencies": {
+ "passport-strategy": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -11921,6 +12165,11 @@
"node": ">=8"
}
},
+ "node_modules/pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
+ },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -15925,6 +16174,12 @@
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true
},
+ "@jest-mock/express": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@jest-mock/express/-/express-1.4.5.tgz",
+ "integrity": "sha512-bERM1jnutyH7VMahdaOHAKy7lgX47zJ7+RTz2eMz0wlCttd9CkhsKFEyoWmJBSz/ow0nVj3lCuRqLem4QDYFkQ==",
+ "dev": true
+ },
"@jest/console": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
@@ -16463,12 +16718,27 @@
"ws": "8.5.0"
}
},
+ "@nestjs/jwt": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz",
+ "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==",
+ "requires": {
+ "@types/jsonwebtoken": "8.5.4",
+ "jsonwebtoken": "8.5.1"
+ }
+ },
"@nestjs/mapped-types": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz",
"integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==",
"requires": {}
},
+ "@nestjs/passport": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.2.1.tgz",
+ "integrity": "sha512-HXEKMLX1x865+lsJB4srwKHBciDNAhWY1Ha+xbxYRbk7J5leGDoHJAmeqe+Wb3NDn5nkboggLV87t0q2mbYc8w==",
+ "requires": {}
+ },
"@nestjs/platform-express": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.4.0.tgz",
@@ -17066,6 +17336,15 @@
"@types/node": "*"
}
},
+ "@types/cookie-parser": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz",
+ "integrity": "sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
"@types/cookiejar": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -17185,6 +17464,14 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/jsonwebtoken": {
+ "version": "8.5.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz",
+ "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/keyv": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz",
@@ -17227,6 +17514,47 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "@types/passport": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz",
+ "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
+ "@types/passport-jwt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz",
+ "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*",
+ "@types/jsonwebtoken": "*",
+ "@types/passport-strategy": "*"
+ }
+ },
+ "@types/passport-local": {
+ "version": "1.0.34",
+ "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.34.tgz",
+ "integrity": "sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*",
+ "@types/passport": "*",
+ "@types/passport-strategy": "*"
+ }
+ },
+ "@types/passport-strategy": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
+ "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*",
+ "@types/passport": "*"
+ }
+ },
"@types/prettier": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz",
@@ -18141,6 +18469,11 @@
"ieee754": "^1.1.13"
}
},
+ "buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -18652,6 +18985,22 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
},
+ "cookie-parser": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+ "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+ "requires": {
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6"
+ },
+ "dependencies": {
+ "cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+ }
+ }
+ },
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -19095,6 +19444,14 @@
}
}
},
+ "ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -21513,6 +21870,49 @@
"through": ">=2.2.7 <3"
}
},
+ "jsonwebtoken": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
+ "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
+ "requires": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^5.6.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ }
+ }
+ },
+ "jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "requires": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "requires": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"keyv": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.1.1.tgz",
@@ -21625,23 +22025,41 @@
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=",
"dev": true
},
+ "lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
+ "lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+ },
+ "lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+ },
"lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
"dev": true
},
+ "lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+ },
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
- "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
- "dev": true
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
- "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
- "dev": true
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.memoize": {
"version": "4.1.2",
@@ -21660,6 +22078,11 @@
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
},
+ "lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -24084,6 +24507,37 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
+ "passport": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.5.2.tgz",
+ "integrity": "sha512-w9n/Ot5I7orGD4y+7V3EFJCQEznE5RxHamUxcqLT2QoJY0f2JdN8GyHonYFvN0Vz+L6lUJfVhrk2aZz2LbuREw==",
+ "requires": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.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-local": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
+ "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
+ "requires": {
+ "passport-strategy": "1.x.x"
+ }
+ },
+ "passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
+ },
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -24118,6 +24572,11 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
+ "pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
+ },
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
diff --git a/api/package.json b/api/package.json
index 510c73eab121cf232606d6e7c976e44af667b4c5..ac9d8e2879f8235ecbf9d47acfb6cb7e6eea8d12 100644
--- a/api/package.json
+++ b/api/package.json
@@ -29,13 +29,19 @@
"@nestjs/config": "^1.0.1",
"@nestjs/core": "^8.0.0",
"@nestjs/graphql": "^10.0.6",
+ "@nestjs/jwt": "^8.0.0",
+ "@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "^8.0.2",
"apollo-server-express": "^3.6.3",
"axios": "^0.21.1",
+ "cookie-parser": "^1.4.6",
"graphql": "^15.5.3",
"graphql-type-json": "^0.3.2",
"jsonata": "^1.8.5",
+ "passport": "^0.5.2",
+ "passport-jwt": "^4.0.0",
+ "passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
@@ -43,6 +49,7 @@
},
"devDependencies": {
"@eclass/semantic-release-docker": "^3.0.0",
+ "@jest-mock/express": "^1.4.5",
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.2.2",
@@ -50,9 +57,12 @@
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/gitlab": "^7.0.4",
+ "@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13",
"@types/jest": "^27.0.1",
"@types/node": "^16.0.0",
+ "@types/passport-jwt": "^3.0.6",
+ "@types/passport-local": "^1.0.34",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
@@ -61,6 +71,7 @@
"eslint-plugin-prettier": "^3.4.0",
"husky": "^7.0.2",
"jest": "^27.0.6",
+ "jest-mock": "^27.5.1",
"prettier": "^2.3.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
diff --git a/api/src/auth/auth-constants.ts b/api/src/auth/auth-constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c904d4554a98dc825e4d2e7d6183fbf7590cebcc
--- /dev/null
+++ b/api/src/auth/auth-constants.ts
@@ -0,0 +1,11 @@
+export const authConstants = {
+ JWTSecret: 'AUTH_JWT_SECRET',
+ skipAuth: 'AUTH_SKIP',
+ expiresIn: 'AUTH_JWT_TOKEN_EXPIRES_IN',
+ cookie: {
+ name: 'jwt-gateway',
+ sameSite: 'AUTH_COOKIE_SAME_SITE',
+ secure: 'AUTH_COOKIE_SECURE',
+ httpOnly: 'AUTH_COOKIE_HTTPONLY',
+ },
+};
diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3a5381e281ff0715c1f08a862b4090f3f133c3f0
--- /dev/null
+++ b/api/src/auth/auth.module.ts
@@ -0,0 +1,37 @@
+import { Module } from '@nestjs/common';
+import { ConfigModule, ConfigService } from '@nestjs/config';
+import { JwtModule } from '@nestjs/jwt';
+import { PassportModule } from '@nestjs/passport';
+import { authConstants } from './auth-constants';
+import { AuthResolver } from './auth.resolver';
+import { AuthService } from './auth.service';
+import { JwtBearerStrategy } from './strategies/jwt-bearer.strategy';
+import { JwtCookiesStrategy } from './strategies/jwt-cookies.strategy';
+import { LocalStrategy } from './strategies/local.strategy';
+
+@Module({
+ imports: [
+ PassportModule.register({
+ session: false,
+ }),
+ JwtModule.registerAsync({
+ imports: [ConfigModule],
+ useFactory: async (configService: ConfigService) => ({
+ secret: configService.get<string>(authConstants.JWTSecret),
+ signOptions: {
+ expiresIn: configService.get<string>(authConstants.expiresIn),
+ },
+ }),
+ inject: [ConfigService],
+ }),
+ ],
+ providers: [
+ AuthService,
+ LocalStrategy,
+ JwtBearerStrategy,
+ JwtCookiesStrategy,
+ AuthResolver,
+ ],
+ exports: [AuthService],
+})
+export class AuthModule {}
diff --git a/api/src/auth/auth.resolver.spec.ts b/api/src/auth/auth.resolver.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc74383f2ad204a9b0e6a8c4503456e361081193
--- /dev/null
+++ b/api/src/auth/auth.resolver.spec.ts
@@ -0,0 +1,79 @@
+import { getMockRes } from '@jest-mock/express';
+import { Test, TestingModule } from '@nestjs/testing';
+import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
+import LocalService from '../engine/connectors/local/main.connector';
+import { ENGINE_SERVICE } from '../engine/engine.constants';
+import { authConstants } from './auth-constants';
+import { AuthResolver } from './auth.resolver';
+import { AuthService } from './auth.service';
+import { User } from './models/user.model';
+
+const moduleMocker = new ModuleMocker(global);
+
+describe('AuthResolver', () => {
+ let resolver: AuthResolver;
+ const { res } = getMockRes();
+ const mockCookie = jest.fn();
+ const mockClearCookie = jest.fn();
+
+ res.cookie = mockCookie;
+ res.clearCookie = mockClearCookie;
+
+ const user: User = {
+ id: 'testing',
+ username: 'testing',
+ };
+
+ const authData = {
+ accessToken: 'DummyToken',
+ };
+
+ const credentials = {
+ username: 'guest',
+ password: 'guest123',
+ };
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ {
+ provide: ENGINE_SERVICE,
+ useClass: LocalService,
+ },
+ AuthResolver,
+ ],
+ })
+ .useMocker((token) => {
+ if (token === AuthService) {
+ return {
+ login: jest.fn().mockResolvedValue(authData),
+ };
+ }
+ if (typeof token === 'function') {
+ const mockMetadata = moduleMocker.getMetadata(
+ token,
+ ) as MockFunctionMetadata<any, any>;
+ const Mock = moduleMocker.generateFromMetadata(mockMetadata);
+ return new Mock();
+ }
+ })
+ .compile();
+
+ resolver = module.get<AuthResolver>(AuthResolver);
+ });
+
+ it('login', async () => {
+ const data = await resolver.login(res, user, credentials);
+
+ expect(mockCookie.mock.calls[0][0]).toBe(authConstants.cookie.name);
+ expect(mockCookie.mock.calls[0][1]).toBe(authData.accessToken);
+ expect(data.accessToken).toBe(authData.accessToken);
+ expect(data.user).toBe(user);
+ });
+
+ it('logout', () => {
+ resolver.logout(res, user);
+
+ expect(mockClearCookie.mock.calls[0][0]).toBe(authConstants.cookie.name);
+ });
+});
diff --git a/api/src/auth/auth.resolver.ts b/api/src/auth/auth.resolver.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6a5f5698a787dea72e3836b3ebc197aef7fea8cf
--- /dev/null
+++ b/api/src/auth/auth.resolver.ts
@@ -0,0 +1,80 @@
+import {
+ Inject,
+ InternalServerErrorException,
+ Logger,
+ UseGuards,
+} from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { Response } from 'express';
+import { parseToBoolean } from '../common/interfaces/utilities.interface';
+import { ENGINE_SERVICE } from '../engine/engine.constants';
+import { IEngineService } from '../engine/engine.interfaces';
+import { authConstants } from './auth-constants';
+import { AuthService } from './auth.service';
+import { GQLResponse } from './decorators/gql-request.decoractor';
+import { CurrentUser } from './decorators/user.decorator';
+import { JwtAuthGuard } from './guards/jwt-auth.guard';
+import { LocalAuthGuard } from './guards/local-auth.guard';
+import { AuthenticationInput } from './inputs/authentication.input';
+import { User } from './models/user.model';
+import { AuthenticationOutput } from './outputs/authentication.output';
+
+//Custom defined type because Pick<CookieOptions, 'sameSite'> does not work
+type SameSiteType = boolean | 'lax' | 'strict' | 'none' | undefined;
+
+@Resolver()
+export class AuthResolver {
+ private readonly logger = new Logger(AuthResolver.name);
+
+ constructor(
+ @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService,
+ private readonly authService: AuthService,
+ private readonly configService: ConfigService,
+ ) {}
+
+ @Mutation(() => AuthenticationOutput)
+ @UseGuards(LocalAuthGuard)
+ async login(
+ @GQLResponse() res: Response,
+ @CurrentUser() user: User,
+ @Args('variables') inputs: AuthenticationInput,
+ ): Promise<AuthenticationOutput> {
+ this.logger.verbose(`${inputs.username} logged in`);
+
+ const data = await this.authService.login(user);
+ if (!data)
+ throw new InternalServerErrorException(
+ `Error during the authentication process`,
+ );
+
+ res.cookie(authConstants.cookie.name, data.accessToken, {
+ httpOnly: parseToBoolean(
+ this.configService.get(authConstants.cookie.httpOnly, 'true'),
+ ),
+ sameSite: this.configService.get<SameSiteType>(
+ authConstants.cookie.sameSite,
+ 'strict',
+ ),
+ secure: parseToBoolean(
+ this.configService.get(authConstants.cookie.secure, 'true'),
+ ),
+ });
+
+ return {
+ user,
+ accessToken: data.accessToken,
+ };
+ }
+
+ @Mutation(() => Boolean)
+ @UseGuards(JwtAuthGuard)
+ logout(@GQLResponse() res: Response, @CurrentUser() user: User): boolean {
+ this.logger.verbose(`${user.username} logged out`);
+
+ res.clearCookie(authConstants.cookie.name);
+ this.engineService.logout?.();
+
+ return true;
+ }
+}
diff --git a/api/src/auth/auth.service.spec.ts b/api/src/auth/auth.service.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9122005451083e42a0cd056b161c4d4897f82252
--- /dev/null
+++ b/api/src/auth/auth.service.spec.ts
@@ -0,0 +1,69 @@
+import { JwtService } from '@nestjs/jwt';
+import { Test, TestingModule } from '@nestjs/testing';
+import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
+import LocalService from '../engine/connectors/local/main.connector';
+import {
+ ENGINE_MODULE_OPTIONS,
+ ENGINE_SERVICE,
+} from '../engine/engine.constants';
+import { AuthService } from './auth.service';
+import { User } from './models/user.model';
+
+const moduleMocker = new ModuleMocker(global);
+
+describe('AuthService', () => {
+ let service: AuthService;
+ const user: User = {
+ id: 'dummy',
+ username: 'dummy64',
+ };
+ const jwtToken = 'JWTDummyToken';
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ {
+ provide: ENGINE_SERVICE,
+ useClass: LocalService,
+ },
+ {
+ provide: ENGINE_MODULE_OPTIONS,
+ useValue: {
+ type: 'local',
+ baseurl: 'test',
+ },
+ },
+ AuthService,
+ ],
+ })
+ .useMocker((token) => {
+ if (token === JwtService) {
+ return {
+ sign: jest.fn().mockReturnValue(jwtToken),
+ };
+ }
+ if (typeof token === 'function') {
+ const mockMetadata = moduleMocker.getMetadata(
+ token,
+ ) as MockFunctionMetadata<any, any>;
+ const Mock = moduleMocker.generateFromMetadata(mockMetadata);
+ return new Mock();
+ }
+ })
+ .compile();
+
+ service = module.get<AuthService>(AuthService);
+ });
+
+ it('login', () => {
+ const data = service.login(user);
+
+ expect(data.accessToken).toBe(jwtToken);
+ });
+
+ it('validateUser', async () => {
+ const data = await service.validateUser('guest', 'password123');
+
+ expect(!!data).toBeTruthy();
+ });
+});
diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e992fa760c238d0d475115e87cf52e5e07a0d5f3
--- /dev/null
+++ b/api/src/auth/auth.service.ts
@@ -0,0 +1,26 @@
+import { Inject, Injectable, NotImplementedException } from '@nestjs/common';
+import { JwtService } from '@nestjs/jwt';
+import { ENGINE_SERVICE } from '../engine/engine.constants';
+import { IEngineService } from '../engine/engine.interfaces';
+import { User } from './models/user.model';
+import { AuthenticationOutput } from './outputs/authentication.output';
+
+@Injectable()
+export class AuthService {
+ constructor(
+ @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService,
+ private jwtService: JwtService,
+ ) {}
+
+ async validateUser(username: string, password: string): Promise<User> {
+ if (!this.engineService.login) throw new NotImplementedException();
+ return await this.engineService.login?.(username, password);
+ }
+
+ async login(user: User): Promise<Pick<AuthenticationOutput, 'accessToken'>> {
+ const payload = { username: user.username, sub: user };
+ return Promise.resolve({
+ accessToken: this.jwtService.sign(payload),
+ });
+ }
+}
diff --git a/api/src/auth/decorators/gql-request.decoractor.ts b/api/src/auth/decorators/gql-request.decoractor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4de3409deea7848aa33e7a66d11a842b029ac5a9
--- /dev/null
+++ b/api/src/auth/decorators/gql-request.decoractor.ts
@@ -0,0 +1,14 @@
+import { createParamDecorator, ExecutionContext } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { Response } from 'express';
+
+/**
+ * Access to graphQL request through the context
+ * @returns request from graphql
+ */
+export const GQLResponse = createParamDecorator(
+ (data: unknown, context: ExecutionContext): Response => {
+ const ctx = GqlExecutionContext.create(context);
+ return ctx.getContext().res;
+ },
+);
diff --git a/api/src/auth/decorators/user.decorator.ts b/api/src/auth/decorators/user.decorator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cbb566332c2d214cb3d33ec1b9ff7c458f9992e3
--- /dev/null
+++ b/api/src/auth/decorators/user.decorator.ts
@@ -0,0 +1,14 @@
+import { createParamDecorator, ExecutionContext } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { User } from '../models/user.model';
+
+/**
+ * Retrieve the current user within the graphQL request
+ * @returns instance of User or undefined
+ */
+export const CurrentUser = createParamDecorator(
+ (data: unknown, context: ExecutionContext): User | undefined => {
+ const ctx = GqlExecutionContext.create(context);
+ return ctx.getContext().req.user;
+ },
+);
diff --git a/api/src/auth/guards/jwt-auth.guard.ts b/api/src/auth/guards/jwt-auth.guard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4afaca585a6117aca640b98ef5c641c7cbde9837
--- /dev/null
+++ b/api/src/auth/guards/jwt-auth.guard.ts
@@ -0,0 +1,35 @@
+import { ExecutionContext, Injectable } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { AuthGuard } from '@nestjs/passport';
+import { Observable } from 'rxjs';
+import { parseToBoolean } from '../../common/interfaces/utilities.interface';
+import { authConstants } from '../auth-constants';
+
+@Injectable()
+export class JwtAuthGuard extends AuthGuard(['jwt-cookies', 'jwt-bearer']) {
+ constructor(private readonly configService: ConfigService) {
+ super();
+ }
+
+ getRequest(context: ExecutionContext) {
+ const ctx = GqlExecutionContext.create(context);
+ const gqlReq = ctx.getContext().req;
+
+ return gqlReq ?? ctx.switchToHttp().getRequest();
+ }
+
+ canActivate(
+ context: ExecutionContext,
+ ): boolean | Promise<boolean> | Observable<boolean> {
+ const skipAuth = parseToBoolean(
+ this.configService.get(authConstants.skipAuth, 'false'),
+ );
+
+ if (skipAuth) {
+ return true;
+ }
+
+ return super.canActivate(context);
+ }
+}
diff --git a/api/src/auth/guards/local-auth.guard.ts b/api/src/auth/guards/local-auth.guard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77782dcac3a3bc9950b5b2a0f8224c940f16c003
--- /dev/null
+++ b/api/src/auth/guards/local-auth.guard.ts
@@ -0,0 +1,18 @@
+import { ExecutionContext, Injectable } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { AuthGuard } from '@nestjs/passport';
+
+@Injectable()
+export class LocalAuthGuard extends AuthGuard('local') {
+ //override to handle graphql specific use case
+ getRequest(context: ExecutionContext) {
+ const ctx = GqlExecutionContext.create(context);
+ const gqlReq = ctx.getContext().req;
+ if (gqlReq) {
+ const { variables } = ctx.getArgs();
+ gqlReq.body = variables;
+ return gqlReq;
+ }
+ return context.switchToHttp().getRequest();
+ }
+}
diff --git a/api/src/auth/inputs/authentication.input.ts b/api/src/auth/inputs/authentication.input.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba31ea5e97bfde9fa9bd96bcf98d9921610ab479
--- /dev/null
+++ b/api/src/auth/inputs/authentication.input.ts
@@ -0,0 +1,10 @@
+import { Field, InputType } from '@nestjs/graphql';
+
+@InputType()
+export class AuthenticationInput {
+ @Field()
+ username: string;
+
+ @Field()
+ password: string;
+}
diff --git a/api/src/auth/models/user.model.ts b/api/src/auth/models/user.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2352d59008cc9bf68feefd7b954108cf161730ed
--- /dev/null
+++ b/api/src/auth/models/user.model.ts
@@ -0,0 +1,21 @@
+import { Field, ObjectType } from '@nestjs/graphql';
+
+@ObjectType()
+export class User {
+ @Field()
+ id: string;
+
+ @Field()
+ username: string;
+
+ @Field({ nullable: true })
+ fullname?: string;
+
+ @Field({ nullable: true })
+ email?: string;
+
+ @Field({ nullable: true })
+ agreeNDA?: boolean;
+
+ extraFields?: Record<string, any>;
+}
diff --git a/api/src/auth/outputs/authentication.output.ts b/api/src/auth/outputs/authentication.output.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7f4590c2ddbfb3e1a7a4cb69e8127b3cfc07c3c0
--- /dev/null
+++ b/api/src/auth/outputs/authentication.output.ts
@@ -0,0 +1,11 @@
+import { Field, ObjectType } from '@nestjs/graphql';
+import { User } from '../models/user.model';
+
+@ObjectType()
+export class AuthenticationOutput {
+ @Field(() => User)
+ user: User;
+
+ @Field()
+ accessToken: string;
+}
diff --git a/api/src/auth/strategies/jwt-bearer.strategy.ts b/api/src/auth/strategies/jwt-bearer.strategy.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9439c87cf54accd4684455c1ce66df564d3af2ac
--- /dev/null
+++ b/api/src/auth/strategies/jwt-bearer.strategy.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { PassportStrategy } from '@nestjs/passport';
+import { ExtractJwt, Strategy } from 'passport-jwt';
+import { authConstants } from '../auth-constants';
+
+@Injectable()
+export class JwtBearerStrategy extends PassportStrategy(
+ Strategy,
+ 'jwt-bearer',
+) {
+ constructor(private readonly configService: ConfigService) {
+ super({
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+ ignoreExpiration: false,
+ secretOrKey: configService.get<string>(authConstants.JWTSecret),
+ });
+ }
+
+ async validate(payload: any) {
+ return payload.sub;
+ }
+}
diff --git a/api/src/auth/strategies/jwt-cookies.strategy.ts b/api/src/auth/strategies/jwt-cookies.strategy.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29ca031c34660149bb3a90e30ebed97432f7e9ca
--- /dev/null
+++ b/api/src/auth/strategies/jwt-cookies.strategy.ts
@@ -0,0 +1,32 @@
+import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { PassportStrategy } from '@nestjs/passport';
+import { Request } from 'express';
+import { Strategy } from 'passport-jwt';
+import { authConstants } from '../auth-constants';
+
+@Injectable()
+export class JwtCookiesStrategy extends PassportStrategy(
+ Strategy,
+ 'jwt-cookies',
+) {
+ constructor(private readonly configService: ConfigService) {
+ super({
+ jwtFromRequest: JwtCookiesStrategy.extractFromCookie,
+ ignoreExpiration: false,
+ secretOrKey: configService.get<string>(authConstants.JWTSecret),
+ });
+ }
+
+ static extractFromCookie = function (req: Request) {
+ let token = null;
+ if (req && req.cookies) {
+ token = req.cookies[authConstants.cookie.name];
+ }
+ return token;
+ };
+
+ async validate(payload: any) {
+ return payload.sub;
+ }
+}
diff --git a/api/src/auth/strategies/local.strategy.ts b/api/src/auth/strategies/local.strategy.ts
new file mode 100644
index 0000000000000000000000000000000000000000..166e9efb0d75dcb31a56ab1c4e09ceb9c8dc846a
--- /dev/null
+++ b/api/src/auth/strategies/local.strategy.ts
@@ -0,0 +1,29 @@
+import { Strategy } from 'passport-local';
+import { PassportStrategy } from '@nestjs/passport';
+import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { AuthService } from '../auth.service';
+import { User } from '../models/user.model';
+import { ContextIdFactory, ModuleRef } from '@nestjs/core';
+
+@Injectable()
+export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
+ constructor(private moduleRef: ModuleRef) {
+ super({
+ passReqToCallback: true,
+ });
+ }
+
+ async validate(
+ request: Request,
+ username: string,
+ password: string,
+ ): Promise<User> {
+ const contextId = ContextIdFactory.getByRequest(request);
+ const authService = await this.moduleRef.resolve(AuthService, contextId);
+ const user = await authService.validateUser(username, password);
+ if (!user) {
+ throw new UnauthorizedException();
+ }
+ return user;
+ }
+}
diff --git a/api/src/common/interfaces/utilities.interface.ts b/api/src/common/interfaces/utilities.interface.ts
index db02ef99e3f4c67f65f320f1d0bfe1a2a6b832f5..762534ac111d76f505e55c787d771ff6eb164d13 100644
--- a/api/src/common/interfaces/utilities.interface.ts
+++ b/api/src/common/interfaces/utilities.interface.ts
@@ -16,3 +16,16 @@ export enum MIME_TYPES {
HTML = 'text/html',
TEXT = 'text/plain',
}
+
+/**
+ * Utility method to convert string value to boolean
+ * @param value string value to be converted
+ * @returns true if string value equals to 'true', false otherwise
+ */
+export const parseToBoolean = (value: string): boolean => {
+ try {
+ return value.toLowerCase() == 'true';
+ } catch {
+ return false;
+ }
+};
diff --git a/api/src/common/utilities.ts b/api/src/common/utilities.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c109df5e5b8c5506844a4e43a7b1d7612440a135
--- /dev/null
+++ b/api/src/common/utilities.ts
@@ -0,0 +1,21 @@
+import {
+ HttpException,
+ InternalServerErrorException,
+ NotFoundException,
+ RequestTimeoutException,
+ UnauthorizedException,
+} from '@nestjs/common';
+import axios from 'axios';
+
+export const errorAxiosHandler = (e: any) => {
+ if (!axios.isAxiosError(e)) throw new InternalServerErrorException(e);
+
+ if (e.response.status === 401) throw new UnauthorizedException();
+ if (e.response.status === 404) throw new NotFoundException();
+ if (e.response.status === 408) throw new RequestTimeoutException();
+ if (e.response.status === 500) throw new InternalServerErrorException();
+
+ if (e.response) throw new HttpException(e.response.data, e.response.status);
+
+ throw new InternalServerErrorException('Unknown error');
+};
diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts
index 81f7bf6bb6ecba86cba15b2350d460e57dadf933..2e4f3be2ebce9f21319426a07cda073005fcd4ec 100644
--- a/api/src/engine/connectors/datashield/main.connector.ts
+++ b/api/src/engine/connectors/datashield/main.connector.ts
@@ -1,9 +1,11 @@
import { HttpService } from '@nestjs/axios';
-import { Inject, Logger } from '@nestjs/common';
+import { Inject, Logger, NotImplementedException } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
-import { firstValueFrom, Observable } from 'rxjs';
+import { catchError, firstValueFrom, Observable } from 'rxjs';
+import { User } from 'src/auth/models/user.model';
import { MIME_TYPES } from 'src/common/interfaces/utilities.interface';
+import { errorAxiosHandler } from 'src/common/utilities';
import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants';
import {
IConfiguration,
@@ -43,22 +45,50 @@ export default class DataShieldService implements IEngineService {
return {};
}
- logout(): void {
- throw new Error('Method not implemented.');
+ async login(username: string, password: string): Promise<User> {
+ const loginPath = this.options.baseurl + 'login';
+
+ const user: User = {
+ id: username,
+ username,
+ extraFields: {
+ sid: '',
+ },
+ };
+
+ const loginData = await firstValueFrom(
+ this.httpService
+ .get(loginPath, {
+ auth: { username, password },
+ })
+ .pipe(catchError((e) => errorAxiosHandler(e))),
+ );
+
+ const cookies = (loginData.headers['set-cookie'] as string[]) ?? [];
+ if (loginData.headers && loginData.headers['set-cookie']) {
+ cookies.forEach((cookie) => {
+ const [key, value] = cookie.split(/={1}/);
+ if (key === 'sid') {
+ user.extraFields.sid = value;
+ }
+ });
+ }
+
+ return user;
}
getAlgorithms(): Algorithm[] | Promise<Algorithm[]> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
- async getHistogram(variable: string): Promise<RawResult> {
+ async getHistogram(variable: string, cookie?: string): Promise<RawResult> {
const path =
this.options.baseurl + `histogram?var=${variable}&type=combine`;
const response = await firstValueFrom(
this.httpService.get(path, {
headers: {
- cookie: this.req['req'].headers['cookie'],
+ cookie,
},
}),
);
@@ -87,13 +117,16 @@ export default class DataShieldService implements IEngineService {
};
}
- async getDescriptiveStats(variable: string): Promise<TableResult> {
+ async getDescriptiveStats(
+ variable: string,
+ cookie?: string,
+ ): Promise<TableResult> {
const path = this.options.baseurl + `quantiles?var=${variable}&type=split`;
const response = await firstValueFrom(
this.httpService.get(path, {
headers: {
- cookie: this.req['req'].headers['cookie'],
+ cookie,
},
}),
);
@@ -111,6 +144,10 @@ export default class DataShieldService implements IEngineService {
data: ExperimentCreateInput,
isTransient: boolean,
): Promise<Experiment> {
+ const user = this.req.user as User;
+ const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join(
+ ';',
+ );
const expResult: Experiment = {
id: `${data.algorithm.id}-${Date.now()}`,
variables: data.variables,
@@ -126,7 +163,7 @@ export default class DataShieldService implements IEngineService {
case 'MULTIPLE_HISTOGRAMS': {
expResult.results = await Promise.all<RawResult>(
data.variables.map(
- async (variable) => await this.getHistogram(variable),
+ async (variable) => await this.getHistogram(variable, cookie),
),
);
break;
@@ -134,7 +171,8 @@ export default class DataShieldService implements IEngineService {
case 'DESCRIPTIVE_STATS': {
expResult.results = await Promise.all<TableResult>(
[...data.variables, ...data.coVariables].map(
- async (variable) => await this.getDescriptiveStats(variable),
+ async (variable) =>
+ await this.getDescriptiveStats(variable, cookie),
),
);
break;
@@ -157,40 +195,23 @@ export default class DataShieldService implements IEngineService {
}
getExperiment(id: string): Experiment | Promise<Experiment> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
removeExperiment(id: string): PartialExperiment | Promise<PartialExperiment> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
editExperient(
id: string,
expriment: ExperimentEditInput,
): Experiment | Promise<Experiment> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
async getDomains(): Promise<Domain[]> {
- const loginPath = this.options.baseurl + 'login';
-
- const loginData = await firstValueFrom(
- this.httpService.get(loginPath, {
- auth: { username: 'guest', password: 'guest123' },
- }),
- );
-
- const cookies = (loginData.headers['set-cookie'] as string[]) ?? [];
- if (loginData.headers && loginData.headers['set-cookie']) {
- cookies.forEach((cookie) => {
- const [key, value] = cookie.split(/={1}/);
- this.req.res.cookie(key, value, {
- httpOnly: true,
- //sameSite: 'none',
- });
- });
- }
-
+ const user = this.req.user as User;
+ const cookies = [`sid=${user.extraFields['sid']}`, `user=${user.id}`];
const path = this.options.baseurl + 'getvars';
const response = await firstValueFrom(
@@ -216,27 +237,27 @@ export default class DataShieldService implements IEngineService {
}
editActiveUser(): Observable<string> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
getExperimentREST(): Observable<string> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
deleteExperiment(): Observable<string> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
editExperimentREST(): Observable<string> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
startExperimentTransient(): Observable<string> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
startExperiment(): Observable<string> {
- throw new Error('Method not implemented.');
+ throw new NotImplementedException();
}
getExperiments(): string {
diff --git a/api/src/engine/connectors/local/main.connector.ts b/api/src/engine/connectors/local/main.connector.ts
index 003b3c8cc2927a84dda52f8efe97b7d44edb8ab8..af5cd6b66443af0f60c8d0db97e596f3b2810fa5 100644
--- a/api/src/engine/connectors/local/main.connector.ts
+++ b/api/src/engine/connectors/local/main.connector.ts
@@ -9,10 +9,14 @@ import {
import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model';
import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input';
import { Algorithm } from 'src/engine/models/experiment/algorithm.model';
+import { User } from 'src/auth/models/user.model';
export default class LocalService implements IEngineService {
- logout(): void {
- throw new Error('Method not implemented.');
+ login(): User | Promise<User> {
+ return {
+ id: '1',
+ username: 'LocalServiceUser',
+ };
}
getAlgorithms(): Algorithm[] | Promise<Algorithm[]> {
diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts
index 6338a6b073b79e1a11c64ce895944b6268c50548..ffd77ead399601950d98d82433a5967deec7f71a 100644
--- a/api/src/engine/engine.controller.ts
+++ b/api/src/engine/engine.controller.ts
@@ -1,18 +1,5 @@
-import {
- Controller,
- Get,
- Inject,
- NotFoundException,
- Param,
- Post,
- Req,
- Res,
- UseInterceptors,
-} from '@nestjs/common';
-import { Request, Response } from 'express';
-import { join } from 'path/posix';
+import { Controller, Get, Inject, Post, UseInterceptors } from '@nestjs/common';
import { Observable } from 'rxjs';
-import { AssetsService } from './assets.service';
import { ENGINE_SERVICE } from './engine.constants';
import { IEngineService } from './engine.interfaces';
import { ErrorsInterceptor } from './interceptors/errors.interceptor';
@@ -22,35 +9,8 @@ import { ErrorsInterceptor } from './interceptors/errors.interceptor';
export class EngineController {
constructor(
@Inject(ENGINE_SERVICE) private readonly engineService: IEngineService,
- private readonly assetsService: AssetsService,
) {}
- @Get('assets/:name')
- getFile(
- @Req() request: Request,
- @Res() response: Response,
- @Param('name') filename: string,
- ) {
- if (filename.endsWith('.md')) {
- const baseurl =
- request.protocol +
- '://' +
- join(request.get('host'), process.env.BASE_URL_CONTEXT ?? '', 'assets'); // not full url, should consider "/services"
- const text = this.assetsService.getMarkdown(filename, baseurl);
- response.setHeader('Content-Type', 'text/markdown');
- return response.send(text);
- }
-
- const filepath = this.assetsService.getAssetFile(filename);
-
- // Test if the file exist, if not send 404
- if (filepath) {
- return response.sendFile(filepath);
- } else {
- throw new NotFoundException();
- }
- }
-
@Get('/algorithms')
getAlgorithms(): Observable<string> | string {
return this.engineService.getAlgorithmsREST();
diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts
index 69da9f65a819f323731ac0a75f59a39811c58be3..281ae1f3f5e51ba1c42ec28b91ede0f6637c668a 100644
--- a/api/src/engine/engine.interfaces.ts
+++ b/api/src/engine/engine.interfaces.ts
@@ -1,4 +1,5 @@
import { Observable } from 'rxjs';
+import { User } from 'src/auth/models/user.model';
import { Configuration } from './models/configuration.model';
import { Domain } from './models/domain.model';
import { Algorithm } from './models/experiment/algorithm.model';
@@ -69,7 +70,18 @@ export interface IEngineService {
editActiveUser(): Observable<string> | string;
- logout(): void;
+ logout?(): void;
+
+ /**
+ * Method that login a user with username and password
+ * @param username
+ * @param password
+ * @returns User object or empty if user not found
+ */
+ login?(
+ username: string,
+ password: string,
+ ): Promise<User | undefined> | User | undefined;
getPassthrough?(suffix: string): Observable<string> | string;
}
diff --git a/api/src/engine/engine.module.ts b/api/src/engine/engine.module.ts
index 8831eb2aeb9f62e9469de310502d7d2740e6926d..3f5cf47b16136f4485649ad6ef81b96ed2bc32e5 100644
--- a/api/src/engine/engine.module.ts
+++ b/api/src/engine/engine.module.ts
@@ -1,11 +1,8 @@
-import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { HttpModule, HttpService } from '@nestjs/axios';
import { DynamicModule, Global, Logger, Module } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
-import { GraphQLModule } from '@nestjs/graphql';
import { Request } from 'express';
-import { join } from 'path';
-import { AssetsService } from './assets.service';
+import { IncomingMessage } from 'http';
import { ENGINE_MODULE_OPTIONS, ENGINE_SERVICE } from './engine.constants';
import { EngineController } from './engine.controller';
import { IEngineOptions, IEngineService } from './engine.interfaces';
@@ -16,64 +13,52 @@ import { EngineResolver } from './engine.resolver';
export class EngineModule {
private static readonly logger = new Logger(EngineModule.name);
- static async forRootAsync(options: IEngineOptions): Promise<DynamicModule> {
+ static forRoot(options?: Partial<IEngineOptions>): DynamicModule {
const optionsProvider = {
provide: ENGINE_MODULE_OPTIONS,
- useValue: options,
+ useValue: {
+ type: process.env.ENGINE_TYPE,
+ baseurl: process.env.ENGINE_BASE_URL,
+ ...(options ?? {}),
+ },
};
const engineProvider = {
provide: ENGINE_SERVICE,
useFactory: async (httpService: HttpService, req: Request) => {
- return await this.createEngineConnection(options, httpService, req);
+ return await this.createEngineConnection(
+ optionsProvider.useValue,
+ httpService,
+ req,
+ );
},
inject: [HttpService, REQUEST],
};
return {
module: EngineModule,
- imports: [
- HttpModule,
- GraphQLModule.forRoot<ApolloDriverConfig>({
- driver: ApolloDriver,
- autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
- context: ({ req, res }) => ({ req, res }),
- cors: {
- credentials: true,
- origin: [
- /http:\/\/localhost($|:\d*)/,
- /http:\/\/127.0.0.1($|:\d*)/,
- ],
- },
- }),
- ],
- providers: [
- optionsProvider,
- engineProvider,
- EngineResolver,
- AssetsService,
- ],
+ imports: [HttpModule],
+ providers: [optionsProvider, engineProvider, EngineResolver],
controllers: [EngineController],
exports: [optionsProvider, engineProvider],
};
}
private static async createEngineConnection(
- options: IEngineOptions,
+ opt: IEngineOptions,
httpService: HttpService,
req: Request,
): Promise<IEngineService> {
try {
- const service = await import(
- `./connectors/${options.type}/main.connector`
- );
- const engine = new service.default(options, httpService, req);
+ const service = await import(`./connectors/${opt.type}/main.connector`);
+ const gqlRequest = req && req['req']; // graphql headers exception
+ const request =
+ gqlRequest && gqlRequest instanceof IncomingMessage ? gqlRequest : req;
+ const engine = new service.default(opt, httpService, request);
return engine;
} catch (e) {
- this.logger.error(
- `There is a problem with the connector '${options.type}'`,
- );
+ this.logger.error(`There is a problem with the connector '${opt.type}'`);
this.logger.verbose(e);
}
}
diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts
index a67967c8d1db1c13b9cfcf2aa64ae12a19661d96..ac8845c7e04e77ddb05f56cebf1b1e02ad316fdd 100644
--- a/api/src/engine/engine.resolver.ts
+++ b/api/src/engine/engine.resolver.ts
@@ -1,8 +1,9 @@
-import { Inject, UseInterceptors } from '@nestjs/common';
+import { Inject, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
+import { Md5 } from 'ts-md5';
import { ENGINE_MODULE_OPTIONS, ENGINE_SERVICE } from './engine.constants';
import { IEngineOptions, IEngineService } from './engine.interfaces';
-import { ErrorsInterceptor } from './interceptors/errors.interceptor';
import { Configuration } from './models/configuration.model';
import { Domain } from './models/domain.model';
import { Algorithm } from './models/experiment/algorithm.model';
@@ -13,9 +14,8 @@ import {
import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input';
import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input';
import { ListExperiments } from './models/experiment/list-experiments.model';
-import { Md5 } from 'ts-md5';
-@UseInterceptors(ErrorsInterceptor)
+@UseGuards(JwtAuthGuard)
@Resolver()
export class EngineResolver {
constructor(
diff --git a/api/src/engine/interceptors/errors.interceptor.ts b/api/src/engine/interceptors/errors.interceptor.ts
index 528e62ae1b525d3f8618070a20c924cc0f140e7d..47e61fc3aab815fa820b1180b655a65529723dc0 100644
--- a/api/src/engine/interceptors/errors.interceptor.ts
+++ b/api/src/engine/interceptors/errors.interceptor.ts
@@ -1,4 +1,3 @@
-import { HttpService } from '@nestjs/axios';
import {
CallHandler,
HttpException,
@@ -17,7 +16,6 @@ export class ErrorsInterceptor implements NestInterceptor {
private readonly logger: Logger;
constructor(
- private httpService: HttpService,
@Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions,
) {
// Logger name is the engine name
diff --git a/api/src/files/files.controller.spec.ts b/api/src/files/files.controller.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4ff16a6ce0d67ecfb2e16e7b3ded0df966e7878e
--- /dev/null
+++ b/api/src/files/files.controller.spec.ts
@@ -0,0 +1,42 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants';
+import { FilesController } from './files.controller';
+import { FilesService } from './files.service';
+
+describe('FilesController', () => {
+ let service: FilesService;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ controllers: [FilesController],
+ providers: [
+ {
+ provide: ENGINE_MODULE_OPTIONS,
+ useValue: {
+ type: 'test',
+ baseurl: 'test',
+ },
+ },
+ FilesService,
+ ],
+ }).compile();
+
+ service = module.get<FilesService>(FilesService);
+ });
+
+ it('getAssetFile', () => {
+ const filePathEmpty = service.getAssetFile('FILE_THAT_DOES_NOT_EXIST.txt');
+ const filePath = service.getAssetFile('tos.md');
+ const fileWithLFI = service.getAssetFile('../../../.env');
+
+ expect(filePathEmpty).toBeUndefined();
+ expect(fileWithLFI).toBeUndefined();
+ expect(filePath).toEqual(expect.anything());
+ });
+
+ it('markdown', () => {
+ const fileContent = service.getMarkdown('login.md', 'http://localtest');
+ expect(!!fileContent).toBeTruthy();
+ expect(fileContent.includes('http://localtest')).toBeTruthy();
+ });
+});
diff --git a/api/src/files/files.controller.ts b/api/src/files/files.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91f3f8549eaf05822ed51d456b29dc03e49a5a99
--- /dev/null
+++ b/api/src/files/files.controller.ts
@@ -0,0 +1,42 @@
+import {
+ Controller,
+ Get,
+ NotFoundException,
+ Param,
+ Req,
+ Res,
+} from '@nestjs/common';
+import { Request, Response } from 'express';
+import { join } from 'path/posix';
+import { FilesService } from './files.service';
+
+@Controller()
+export class FilesController {
+ constructor(private readonly filesService: FilesService) {}
+
+ @Get('assets/:name')
+ getFile(
+ @Req() request: Request,
+ @Res() response: Response,
+ @Param('name') filename: string,
+ ) {
+ if (filename.endsWith('.md')) {
+ const baseurl =
+ request.protocol +
+ '://' +
+ join(request.get('host'), process.env.BASE_URL_CONTEXT ?? '', 'assets'); // not full url, should consider "/services"
+ const text = this.filesService.getMarkdown(filename, baseurl);
+ response.setHeader('Content-Type', 'text/markdown');
+ return response.send(text);
+ }
+
+ const filepath = this.filesService.getAssetFile(filename);
+
+ // Test if the file exist, if not send 404
+ if (filepath) {
+ return response.sendFile(filepath);
+ } else {
+ throw new NotFoundException();
+ }
+ }
+}
diff --git a/api/src/files/files.module.ts b/api/src/files/files.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bcfb777b965a099ff7337d7b716797a5b8fe368c
--- /dev/null
+++ b/api/src/files/files.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+import { FilesController } from './files.controller';
+import { FilesService } from './files.service';
+
+@Module({
+ controllers: [FilesController],
+ providers: [FilesService],
+})
+export class FilesModule {}
diff --git a/api/src/engine/assets.service.ts b/api/src/files/files.service.ts
similarity index 90%
rename from api/src/engine/assets.service.ts
rename to api/src/files/files.service.ts
index 4abde93c41659838fd369e8cdd4bbf195970b220..685ab86da64b8f1ecf1ead4f11a4e8fe5701ff5d 100644
--- a/api/src/engine/assets.service.ts
+++ b/api/src/files/files.service.ts
@@ -1,11 +1,11 @@
import { Inject, Injectable } from '@nestjs/common';
import * as fs from 'fs';
import { join } from 'path/posix';
-import { ENGINE_MODULE_OPTIONS } from './engine.constants';
-import { IEngineOptions } from './engine.interfaces';
+import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants';
+import { IEngineOptions } from '../engine/engine.interfaces';
@Injectable()
-export class AssetsService {
+export class FilesService {
constructor(
@Inject(ENGINE_MODULE_OPTIONS)
private readonly engineOptions: IEngineOptions,
diff --git a/api/src/main.ts b/api/src/main.ts
index 97c880c6c04e645b925fe1262997de18282174fa..e33633323795e658f72a120ce67e44dec64a1e1b 100644
--- a/api/src/main.ts
+++ b/api/src/main.ts
@@ -1,7 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
-import { join } from 'path/posix';
import { AppModule } from './main/app.module';
+import * as cookieParser from 'cookie-parser';
const CORS_URL = process.env.CORS_URL ?? process.env.ENGINE_BASE_URL;
@@ -17,6 +17,8 @@ async function bootstrap() {
},
});
+ app.use(cookieParser());
+
await app.listen(process.env.GATEWAY_PORT);
}
bootstrap();
diff --git a/api/src/main/app.module.ts b/api/src/main/app.module.ts
index 7638ce2f6f28ce28f57f57974833ba8207669bba..dc204f2c50b100bccff9430051c040538c50da4e 100644
--- a/api/src/main/app.module.ts
+++ b/api/src/main/app.module.ts
@@ -1,6 +1,11 @@
+import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
+import { GraphQLModule } from '@nestjs/graphql';
+import { join } from 'path';
+import { AuthModule } from 'src/auth/auth.module';
import { EngineModule } from 'src/engine/engine.module';
+import { FilesModule } from 'src/files/files.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@@ -10,10 +15,21 @@ import { AppService } from './app.service';
isGlobal: true,
envFilePath: ['.env', '.env.defaults'],
}),
- EngineModule.forRootAsync({
+ GraphQLModule.forRoot<ApolloDriverConfig>({
+ driver: ApolloDriver,
+ autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
+ context: ({ req, res }) => ({ req, res }),
+ cors: {
+ credentials: true,
+ origin: [/http:\/\/localhost($|:\d*)/, /http:\/\/127.0.0.1($|:\d*)/],
+ },
+ }),
+ EngineModule.forRoot({
type: process.env.ENGINE_TYPE,
baseurl: process.env.ENGINE_BASE_URL,
}),
+ AuthModule,
+ FilesModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/api/src/schema.gql b/api/src/schema.gql
index 9fb8aadc4ad75433fbaa7215ecde5afe71110e58..0804f393996f71a257f74595bc84f138262bc2f4 100644
--- a/api/src/schema.gql
+++ b/api/src/schema.gql
@@ -2,6 +2,19 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
+type User {
+ id: String!
+ username: String!
+ fullname: String
+ email: String
+ agreeNDA: Boolean
+}
+
+type AuthenticationOutput {
+ user: User!
+ accessToken: String!
+}
+
type Configuration {
connectorId: String!
galaxy: Boolean
@@ -224,6 +237,8 @@ type Mutation {
createExperiment(data: ExperimentCreateInput!, isTransient: Boolean = false): Experiment!
editExperiment(id: String!, data: ExperimentEditInput!): Experiment!
removeExperiment(id: String!): PartialExperiment!
+ login(variables: AuthenticationInput!): AuthenticationOutput!
+ logout: Boolean!
}
input ExperimentCreateInput {
@@ -265,3 +280,8 @@ input ExperimentEditInput {
shared: Boolean
viewed: Boolean
}
+
+input AuthenticationInput {
+ username: String!
+ password: String!
+}