Add website api login mutation

This commit is contained in:
Dominik Korsa 2021-01-18 12:31:18 +01:00
parent c106d7607e
commit 9007437bd3
No known key found for this signature in database
GPG key ID: 546F986F71A6FE6E
19 changed files with 385 additions and 27 deletions

View file

@ -29,6 +29,10 @@
"no-console": "off", "no-console": "off",
"max-len": ["off"], "max-len": ["off"],
"import/first": ["off"], "import/first": ["off"],
"max-classes-per-file": ["off"] "max-classes-per-file": ["off"],
"@typescript-eslint/consistent-type-imports": ["error", {
"prefer": "type-imports",
"disallowTypeAnnotations": true
}]
} }
} }

1
backend/.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules node_modules
.env .env
dist dist
secrets

View file

@ -578,6 +578,20 @@
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@wulkanowy/sdk": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@wulkanowy/sdk/-/sdk-0.1.1.tgz",
"integrity": "sha512-ktBYmd4nNeAs5STH9rf9EaAxPxSCN2PWtuP+rqW980RcWIv6IJddYvW1j8u60VFoThxMPWMpRHr7MeRqjKaC4A==",
"requires": {
"axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1",
"cheerio": "github:dominik-korsa/cheerio#export-types",
"date-fns": "^2.16.1",
"date-fns-tz": "^1.0.12",
"querystring": "^0.2.0",
"tough-cookie": "^4.0.0"
}
},
"abstract-logging": { "abstract-logging": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@ -979,6 +993,30 @@
} }
} }
}, },
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"axios-cookiejar-support": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz",
"integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==",
"requires": {
"is-redirect": "^1.0.0",
"pify": "^5.0.0"
},
"dependencies": {
"pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="
}
}
},
"backo2": { "backo2": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@ -1033,6 +1071,11 @@
} }
} }
}, },
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -1154,6 +1197,37 @@
} }
} }
}, },
"cheerio": {
"version": "github:dominik-korsa/cheerio#0204881418326ab4ddd4b68938c7de32580c9ae3",
"from": "github:dominik-korsa/cheerio#export-types",
"requires": {
"cheerio-select": "^1.1.0",
"dom-serializer": "~1.2.0",
"domhandler": "^4.0.0",
"htmlparser2": "^6.0.0",
"parse5": "^6.0.1",
"parse5-htmlparser2-tree-adapter": "^6.0.1"
},
"dependencies": {
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
}
}
},
"cheerio-select": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.1.0.tgz",
"integrity": "sha512-hOcy3Ps4lg6tMWM/q2NL5ItM+BTt1C8TNbtb9ZGscqDbGhKX0TzLRy9QJ2/KVlmmEbMjCNBublWdO4TPz51SdA==",
"requires": {
"css-select": "^3.1.2",
"css-what": "^4.0.0",
"domelementtype": "^2.1.0",
"domhandler": "^4.0.0",
"domutils": "^2.4.4"
}
},
"chokidar": { "chokidar": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.0.tgz",
@ -1272,6 +1346,23 @@
"which": "^2.0.1" "which": "^2.0.1"
} }
}, },
"css-select": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz",
"integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==",
"requires": {
"boolbase": "^1.0.0",
"css-what": "^4.0.0",
"domhandler": "^4.0.0",
"domutils": "^2.4.3",
"nth-check": "^2.0.0"
}
},
"css-what": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz",
"integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A=="
},
"cssfilter": { "cssfilter": {
"version": "0.0.10", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
@ -1286,6 +1377,16 @@
"array-find-index": "^1.0.1" "array-find-index": "^1.0.1"
} }
}, },
"date-fns": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz",
"integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ=="
},
"date-fns-tz": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.0.12.tgz",
"integrity": "sha512-Ca+9pjGkU90XDHnclfSjz9o7g/ZqyYyYI0aCYmbf65P75oy8gktuaRslO3UPXl3ADgAnF9/KCykQkpU3/xvtWQ=="
},
"dateformat": { "dateformat": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
@ -1381,6 +1482,39 @@
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
}, },
"dom-serializer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz",
"integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w=="
},
"domhandler": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz",
"integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==",
"requires": {
"domelementtype": "^2.1.0"
}
},
"domutils": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz",
"integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==",
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0"
}
},
"dotenv": { "dotenv": {
"version": "8.2.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
@ -1417,6 +1551,11 @@
"ansi-colors": "^4.1.1" "ansi-colors": "^4.1.1"
} }
}, },
"entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
},
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -2041,6 +2180,11 @@
"integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==",
"dev": true "dev": true
}, },
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
},
"for-each": { "for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -2274,6 +2418,17 @@
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"dev": true "dev": true
}, },
"htmlparser2": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz",
"integrity": "sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.4.4",
"entities": "^2.0.0"
}
},
"http-errors": { "http-errors": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
@ -2419,6 +2574,11 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true "dev": true
}, },
"is-redirect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
},
"is-regex": { "is-regex": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
@ -2844,6 +3004,14 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true "dev": true
}, },
"nth-check": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
"requires": {
"boolbase": "^1.0.0"
}
},
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -3132,6 +3300,11 @@
"ipaddr.js": "1.9.1" "ipaddr.js": "1.9.1"
} }
}, },
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
"pump": { "pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -3146,6 +3319,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}, },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"queue-microtask": { "queue-microtask": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz",
@ -3724,6 +3902,16 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
}, },
"tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"requires": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.1.2"
}
},
"tree-kill": { "tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -3930,6 +4118,11 @@
"random-bytes": "~1.0.0" "random-bytes": "~1.0.0"
} }
}, },
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
},
"uri-js": { "uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View file

@ -17,6 +17,7 @@
"@types/express": "^4.17.10", "@types/express": "^4.17.10",
"@types/lodash": "^4.14.167", "@types/lodash": "^4.14.167",
"@types/node": "^14.14.21", "@types/node": "^14.14.21",
"@wulkanowy/sdk": "^0.1.1",
"apollo-server-fastify": "^2.19.2", "apollo-server-fastify": "^2.19.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"fastify": "^3.10.1", "fastify": "^3.10.1",

View file

@ -1,4 +1,5 @@
import { Connection, createConnection, Repository } from 'typeorm'; import type { Connection, Repository } from 'typeorm';
import { createConnection } from 'typeorm';
import Application from './entities/application'; import Application from './entities/application';
class Database { class Database {

View file

@ -1,7 +1,8 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { ObjectID } from 'typeorm';
import { import {
BaseEntity, BaseEntity,
Column, Entity, ObjectID, ObjectIdColumn, Column, Entity, ObjectIdColumn,
} from 'typeorm'; } from 'typeorm';
@Entity() @Entity()

View file

@ -3,7 +3,7 @@ import { nanoid } from 'nanoid';
import { scopes } from '../../constants'; import { scopes } from '../../constants';
import database from '../../database/database'; import database from '../../database/database';
import { ParamError, ScopeError } from '../../errors'; import { ParamError, ScopeError } from '../../errors';
import { MyFastifyInstance } from '../../types'; import type { MyFastifyInstance } from '../../types';
import { import {
getSessionData, isObject, parseScopeParam, validateOptionalParam, validateParam, getSessionData, isObject, parseScopeParam, validateOptionalParam, validateParam,
} from '../../utils'; } from '../../utils';

View file

@ -1,4 +1,4 @@
import { import type {
MyFastifyInstance, MyFastifyInstance,
} from '../../types'; } from '../../types';
import registerAuthorize from './authorize'; import registerAuthorize from './authorize';

View file

@ -0,0 +1,15 @@
export class UnknownPromptError extends Error {
public name = 'UnknownPromptError';
public constructor() {
super('Unknown prompt');
}
}
export class InvalidCredentialsError extends Error {
public name = 'InvalidCredentialsError';
public constructor() {
super('User with provided credentials not found');
}
}

View file

@ -1,14 +1,18 @@
import { ApolloServer } from 'apollo-server-fastify'; import { ApolloServer } from 'apollo-server-fastify';
import { buildSchema } from 'type-graphql'; import { buildSchema } from 'type-graphql';
import { ApolloContext, MyFastifyInstance } from '../../types'; import type { ApolloContext, MyFastifyInstance } from '../../types';
import { getSessionData } from '../../utils'; import { getSessionData } from '../../utils';
import LoginResolver from './resolvers/login-resolver';
import PromptInfoResolver from './resolvers/prompt-info-resolver'; import PromptInfoResolver from './resolvers/prompt-info-resolver';
import { WebsiteAPIContext } from './types'; import type { WebsiteAPIContext } from './types';
export default async function registerWebsiteApi(server: MyFastifyInstance): Promise<void> { export default async function registerWebsiteApi(server: MyFastifyInstance): Promise<void> {
const schema = await buildSchema({ const schema = await buildSchema({
authMode: 'error', authMode: 'error',
resolvers: [PromptInfoResolver], resolvers: [
PromptInfoResolver,
LoginResolver,
],
}); });
const apolloServer = new ApolloServer({ const apolloServer = new ApolloServer({
schema, schema,
@ -19,7 +23,7 @@ export default async function registerWebsiteApi(server: MyFastifyInstance): Pro
}); });
await server.register(apolloServer.createHandler({ await server.register(apolloServer.createHandler({
cors: { cors: {
origin: 'https://google.com', origin: false,
}, },
})); }));
console.log(apolloServer.graphqlPath); console.log(apolloServer.graphqlPath);

View file

@ -0,0 +1,10 @@
import { Field, Int, ObjectType } from 'type-graphql';
@ObjectType()
export default class LoginResultStudent {
@Field(() => Int)
public studentId!: number;
@Field(() => String)
public name!: string;
}

View file

@ -0,0 +1,11 @@
import { Field, ObjectType } from 'type-graphql';
import LoginResultStudent from './login-result-student';
@ObjectType()
export default class LoginResult {
@Field(() => String)
public encryptedPrivateKey!: string;
@Field(() => [LoginResultStudent])
public students!: LoginResultStudent[];
}

View file

@ -0,0 +1,58 @@
/* eslint-disable class-methods-use-this */
import { Client } from '@wulkanowy/sdk';
import type { DiaryInfo } from '@wulkanowy/sdk/dist/diary/interfaces/diary/diary-info';
import _ from 'lodash';
import {
Arg, Ctx, Mutation, Resolver,
} from 'type-graphql';
import {
encryptSymmetrical, encryptWithPublicKey, generatePrivatePublicPair, requireEnvHex,
} from '../../../utils';
import { UnknownPromptError } from '../errors';
import LoginResult from '../models/login-result';
import type LoginResultStudent from '../models/login-result-student';
import type { WebsiteAPIContext } from '../types';
@Resolver()
export default class LoginResolver {
@Mutation(() => LoginResult)
public async login(
@Arg('promptId') promptId: string,
@Arg('username') username: string,
@Arg('password') password: string,
@Arg('host') host: string,
@Ctx() { sessionData }: WebsiteAPIContext,
): Promise<LoginResult> {
const prompt = sessionData.prompts.get(promptId);
if (!prompt) throw new UnknownPromptError();
const client = new Client(host, () => ({
username,
password,
}));
await client.login();
const diaryList = await client.getDiaryList();
const { privateKey, publicKey } = await generatePrivatePublicPair();
const encryptedPrivateKey = encryptSymmetrical(
privateKey,
requireEnvHex('CREDENTIALS_PRIVATE_KEY_ENCRYPT_KEY'),
);
const encryptedPassword = encryptWithPublicKey(password, publicKey);
console.log(diaryList.map((e) => e.serialized.info));
const students = _.toPairs(_.groupBy(diaryList.map((e) => e.serialized.info), 'studentId'))
.map(([, diaryInfoList]: [string, DiaryInfo[]]) => diaryInfoList[0])
.map<LoginResultStudent>((diaryInfo) => ({
name: `${diaryInfo.studentFirstName} ${diaryInfo.studentSurname}`,
studentId: diaryInfo.studentId,
}));
prompt.loginInfo = {
encryptedPassword,
host,
username,
availableStudentIds: students.map(({ studentId }) => studentId),
};
return {
encryptedPrivateKey,
students,
};
}
}

View file

@ -1,23 +1,25 @@
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import type { ResolverInterface } from 'type-graphql';
import { import {
Arg, Ctx, FieldResolver, Query, Resolver, ResolverInterface, Root, Arg, Ctx, FieldResolver, Query, Resolver, Root,
} from 'type-graphql'; } from 'type-graphql';
import database from '../../../database/database'; import database from '../../../database/database';
import { UnknownPromptError } from '../errors';
import PromptInfo from '../models/prompt-info'; import PromptInfo from '../models/prompt-info';
import PromptInfoApplication from '../models/prompt-info-application'; import type PromptInfoApplication from '../models/prompt-info-application';
import { WebsiteAPIContext } from '../types'; import type { WebsiteAPIContext } from '../types';
@Resolver(PromptInfo) @Resolver(PromptInfo)
export default class PromptInfoResolver implements ResolverInterface<PromptInfo> { export default class PromptInfoResolver implements ResolverInterface<PromptInfo> {
@Query(() => PromptInfo) @Query(() => PromptInfo)
public promptInfo( public promptInfo(
@Arg('id') id: string, @Arg('promptId') promptId: string,
@Ctx() { sessionData }: WebsiteAPIContext, @Ctx() { sessionData }: WebsiteAPIContext,
): Partial<PromptInfo> { ): Partial<PromptInfo> {
const prompt = sessionData.prompts.get(id); const prompt = sessionData.prompts.get(promptId);
if (!prompt) throw new Error('Prompt data not found'); if (!prompt) throw new UnknownPromptError();
return { return {
id, id: promptId,
clientId: prompt.clientId, clientId: prompt.clientId,
scopes: prompt.scopes, scopes: prompt.scopes,
}; };

View file

@ -1,4 +1,5 @@
import { ApolloContext, SessionData } from '../../types'; import type SessionData from '../../session-data';
import type { ApolloContext } from '../../types';
export interface WebsiteAPIContext extends ApolloContext { export interface WebsiteAPIContext extends ApolloContext {
sessionData: SessionData; sessionData: SessionData;

View file

@ -0,0 +1,5 @@
import type { Prompt } from './types';
export default class SessionData {
public prompts = new Map<string, Prompt>();
}

View file

@ -1,6 +1,7 @@
import { import type {
FastifyInstance, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, FastifyInstance, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault,
} from 'fastify'; } from 'fastify';
import type SessionData from './session-data';
export interface Prompt { export interface Prompt {
clientId: string; clientId: string;
@ -11,10 +12,12 @@ export interface Prompt {
value: string; value: string;
method: 'plain' | 'S256'; method: 'plain' | 'S256';
}; };
} loginInfo?: {
host: string;
export interface SessionData { username: string;
prompts: Map<string, Prompt> encryptedPassword: string;
availableStudentIds: number[];
};
} }
export interface Session { export interface Session {

View file

@ -0,0 +1,43 @@
import * as crypto from 'crypto';
import * as util from 'util';
export function generatePrivatePublicPair(): Promise<{
privateKey: string;
publicKey: string;
}> {
return util.promisify(crypto.generateKeyPair)('rsa', {
modulusLength: 1024,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
}
export function encryptSymmetrical(value: string, key: Buffer): string {
const ivBuffer = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, ivBuffer);
const encrypted = Buffer.concat([cipher.update(value), cipher.final()]);
return `${encrypted.toString('base64')}@${ivBuffer.toString('base64')}`;
}
export function decryptSymmetrical(encrypted: string, key: Buffer): string {
const [iv, encryptedData] = encrypted.split('@');
const ivBuffer = Buffer.from(iv, 'base64');
const encryptedBuffer = Buffer.from(encryptedData, 'base64');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, ivBuffer);
const decrypted = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
return decrypted.toString();
}
export function encryptWithPublicKey(value: string, publicKey: string): string {
return crypto.publicEncrypt(publicKey, Buffer.from(value)).toString('base64');
}
export function decryptWithPrivateKey(encrypted: string, privateKey: string): string {
return crypto.privateDecrypt(privateKey, Buffer.from(encrypted, 'base64')).toString();
}

View file

@ -1,6 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import { ParamError } from './errors'; import { ParamError } from '../errors';
import { Prompt, Session, SessionData } from './types'; import SessionData from '../session-data';
import type { Session } from '../types';
export * from './crypto';
export function requireEnv(name: string): string { export function requireEnv(name: string): string {
const value = process.env[name]; const value = process.env[name];
@ -8,6 +11,10 @@ export function requireEnv(name: string): string {
return value; return value;
} }
export function requireEnvHex(name: string): Buffer {
return Buffer.from(requireEnv(name), 'hex');
}
export function parseIntStrict(value: string, radix = 10): number { export function parseIntStrict(value: string, radix = 10): number {
const number = parseInt(value, radix); const number = parseInt(value, radix);
if (_.isNaN(number)) throw new Error(`Cannot parse ${value} to int`); if (_.isNaN(number)) throw new Error(`Cannot parse ${value} to int`);
@ -47,9 +54,7 @@ export function parseScopeParam(key: string, value: unknown): string[] {
export function getSessionData(session: Session): SessionData { export function getSessionData(session: Session): SessionData {
if (session.data === undefined) { if (session.data === undefined) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
session.data = { session.data = new SessionData();
prompts: new Map<string, Prompt>(),
};
} }
return session.data; return session.data;
} }