From 66704c8eae995827ec05b5f13b992769ba3d2f2e Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Mon, 18 Jan 2021 21:01:09 -0600 Subject: [PATCH 01/13] create initial project dashboard page with graphql client and initial queries --- project-dashboard/graphql.min.js | 1 + project-dashboard/index.html | 12 ++++ project-dashboard/scripts.js | 102 +++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 project-dashboard/graphql.min.js create mode 100644 project-dashboard/index.html create mode 100644 project-dashboard/scripts.js diff --git a/project-dashboard/graphql.min.js b/project-dashboard/graphql.min.js new file mode 100644 index 0000000..eb55796 --- /dev/null +++ b/project-dashboard/graphql.min.js @@ -0,0 +1 @@ +(function(){function __extend(){var extended={},deep=false,i=0,length=arguments.length;if(Object.prototype.toString.call(arguments[0])=="[object Boolean]"){deep=arguments[0];i++}var merge=function(obj){for(var prop in obj){if(Object.prototype.hasOwnProperty.call(obj,prop)){if(deep&&Object.prototype.toString.call(obj[prop])=="[object Object]"){extended[prop]=__extend(true,extended[prop],obj[prop])}else{extended[prop]=obj[prop]}}}};for(;i0&&fragmentRegexp.test(fragment)){that.collectFragments(fragment,fragments).forEach(function(fragment){collectedFragments.unshift(fragment)})}}});return __unique(collectedFragments)};GraphQLClient.prototype.processQuery=function(query,fragments){if(typeof query=="object"&&query.hasOwnProperty("kind")&&query.hasOwnProperty("definitions")){throw new Error("Do not use graphql AST to send requests. Please generate query as string first using `graphql.print(query)`")}var fragmentRegexp=GraphQLClient.FRAGMENT_PATTERN;var collectedFragments=this.collectFragments(query,fragments);query=query.replace(fragmentRegexp,function(_,$m){return"... "+$m.split(".").join(FRAGMENT_SEPERATOR)});return[query].concat(collectedFragments.filter(function(fragment){return!query.match(fragment)})).join("\n")};GraphQLClient.prototype.autoDeclare=function(query,variables){var that=this;var typeMap={string:"String",number:function(value){return value%1===0?"Int":"Float"},boolean:"Boolean"};return query.replace(GraphQLClient.AUTODECLARE_PATTERN,function(){var types=[];for(var key in variables){var value=variables[key];var keyAndType=key.split(/^(.*?)\!/);if(keyAndType.length>1){keyAndType=keyAndType.slice(1);keyAndType[1]=keyAndType[1].replace(/(.*?)\!$/,"$1")}var mapping=typeMap[typeof value];var mappedType=typeof mapping==="function"?mapping(value):mapping;if(!key.match("!")&&keyAndType[0].match(/_?id/i)){mappedType="ID"}var type=keyAndType[1]||mappedType;if(type){types.push("$"+keyAndType[0]+": "+type+"!")}}types=types.join(", ");return types?"("+types+")":""})};GraphQLClient.prototype.cleanAutoDeclareAnnotations=function(variables){if(!variables)variables={};var newVariables={};for(var key in variables){var value=variables[key];var keyAndType=key.split("!");newVariables[keyAndType[0]]=value}return newVariables};GraphQLClient.prototype.buildFragments=function(fragments){var that=this;fragments=this.flatten(fragments||{});var fragmentObject={};for(var name in fragments){var fragment=fragments[name];if(typeof fragment=="object"){fragmentObject[name]=that.buildFragments(fragment)}else{fragmentObject[name]="\nfragment "+name+" "+fragment}}return fragmentObject};GraphQLClient.prototype.buildQuery=function(query,variables){return this.autoDeclare(this.processQuery(query,this._fragments),variables)};GraphQLClient.prototype.parseType=function(query){var match=query.trim().match(/^(query|mutation|subscription)/);if(!match)return"query";return match[1]};GraphQLClient.prototype.createSenderFunction=function(debug){var that=this;return function(query,originalQuery,type){if(__isTagCall(query)){return that.run(that.ql.apply(that,arguments))}var caller=function(variables,requestOptions){if(!requestOptions)requestOptions={};if(!variables)variables={};var fragmentedQuery=that.buildQuery(query,variables);var headers=__extend(that.options.headers||{},requestOptions.headers||{});return new Promise(function(resolve,reject){__request(debug,that.options.method||"post",that.getUrl(),headers,{query:fragmentedQuery,variables:that.cleanAutoDeclareAnnotations(variables)},!!that.options.asJSON,that.options.onRequestError,function(response,status){if(status==200){if(response.errors){reject(response.errors)}else if(response.data){resolve(response.data)}else{resolve(response)}}else{reject(response)}})})};caller.merge=function(mergeName,variables){if(!type){type=that.parseType(query);query=query.trim().replace(/^(query|mutation|subscription)\s*/,"").trim().replace(GraphQLClient.AUTODECLARE_PATTERN,"").trim().replace(/^\{|\}$/g,"")}if(!originalQuery){originalQuery=query}that._transaction[mergeName]=that._transaction[mergeName]||{query:[],mutation:[]};return new Promise(function(resolve){that._transaction[mergeName][type].push({type:type,query:originalQuery,variables:variables,resolver:resolve})})};if(arguments.length>3){return caller.apply(null,Array.prototype.slice.call(arguments,3))}return caller}};GraphQLClient.prototype.commit=function(mergeName){if(!this._transaction[mergeName]){throw new Error("You cannot commit the merge "+mergeName+" without creating it first.")}var that=this;var resolveMap={};var mergedVariables={};var mergedQueries={};Object.keys(this._transaction[mergeName]).forEach(function(method){if(that._transaction[mergeName][method].length===0)return;var subQuery=that._transaction[mergeName][method].map(function(merge){var reqId="merge"+Math.random().toString().split(".")[1].substr(0,6);resolveMap[reqId]=merge.resolver;var query=merge.query.replace(/\$([^\.\,\s\)]*)/g,function(_,m){if(!merge.variables){throw new Error("Unused variable on merge "+mergeName+": $"+m[0])}var matchingKey=Object.keys(merge.variables).filter(function(key){return key===m||key.match(new RegExp("^"+m+"!"))})[0];var variable=reqId+"__"+matchingKey;mergedVariables[method]=mergedVariables[method]||{};mergedVariables[method][variable]=merge.variables[matchingKey];return"$"+variable.split("!")[0]});var alias=query.trim().match(/^[^\(]+\:/);if(!alias){alias=query.replace(/^\{|\}$/gm,"").trim().match(/^[^\(\{]+/)[0]+":"}else{query=query.replace(/^[^\(]+\:/,"")}return reqId+"_"+alias+query}).join("\n");mergedQueries[method]=mergedQueries[method]||[];mergedQueries[method].push(method+" (@autodeclare) {\n"+subQuery+"\n }")});return Promise.all(Object.keys(mergedQueries).map(function(method){var query=mergedQueries[method].join("\n");var variables=mergedVariables[method];return that._sender(query,query,null,variables)})).then(function(responses){var newResponses={};responses.forEach(function(response){Object.keys(response).forEach(function(mergeKey){var parsedKey=mergeKey.match(/^(merge\d+)\_(.*)/);if(!parsedKey){throw new Error("Multiple root keys detected on response. Merging doesn't support it yet.")}var reqId=parsedKey[1];var fieldName=parsedKey[2];var newResponse={};newResponse[fieldName]=response[mergeKey];newResponses[fieldName]=(newResponses[fieldName]||[]).concat([response[mergeKey]]);resolveMap[reqId](newResponse)})});return newResponses}).catch(function(responses){return{error:true,errors:responses}}).finally(function(responses){that._transaction[mergeName]={query:[],mutation:[]};return responses})};GraphQLClient.prototype.createHelpers=function(sender){var that=this;function helper(query){if(__isTagCall(query)){that.__prefix=this.prefix;that.__suffix=this.suffix;var result=that.run(that.ql.apply(that,arguments));that.__prefix="";that.__suffix="";return result}var caller=sender(this.prefix+" "+query+" "+this.suffix,query.trim(),this.type);if(arguments.length>1&&arguments[1]!=null){return caller.apply(null,Array.prototype.slice.call(arguments,1))}return caller}var helpers=[{method:"mutate",type:"mutation"},{method:"query",type:"query"},{method:"subscribe",type:"subscription"}];helpers.forEach(function(m){that[m.method]=function(query,variables,options){if(that.options.alwaysAutodeclare===true||options&&options.declare===true){return helper.call({type:m.type,prefix:m.type+" (@autodeclare) {",suffix:"}"},query,variables)}return helper.call({type:m.type,prefix:m.type,suffix:""},query,variables)};that[m.method].run=function(query,options){return that[m.method](query,options)({})}});this.run=function(query){return sender(query,originalQuery,m.type,{})}};GraphQLClient.prototype.fragments=function(){return this._fragments};GraphQLClient.prototype.getOptions=function(){return this.options||{}};GraphQLClient.prototype.headers=function(newHeaders){return this.options.headers=__extend(this.options.headers,newHeaders)};GraphQLClient.prototype.fragment=function(fragment){if(typeof fragment=="string"){var _fragment=this._fragments[fragment.replace(/\./g,FRAGMENT_SEPERATOR)];if(!_fragment){throw"Fragment "+fragment+" not found!"}return _fragment.trim()}else{this.options.fragments=__extend(true,this.options.fragments,fragment);this._fragments=this.buildFragments(this.options.fragments);return this._fragments}};GraphQLClient.prototype.ql=function(strings){fragments=Array.prototype.slice.call(arguments,1);fragments=fragments.map(function(fragment){if(typeof fragment=="string"){return fragment.match(/fragment\s+([^\s]*)\s/)[1]}});var query=typeof strings=="string"?strings:strings.reduce(function(acc,seg,i){return acc+fragments[i-1]+seg});query=this.buildQuery(query);query=((this.__prefix||"")+" "+query+" "+(this.__suffix||"")).trim();return query};(function(root,factory){if(typeof define==="function"&&define.amd){define(function(){return root.graphql=factory(GraphQLClient)})}else if(typeof module==="object"&&module.exports){module.exports=factory(root.GraphQLClient)}else{root.graphql=factory(root.GraphQLClient)}})(this||self,function(){return GraphQLClient})})(); \ No newline at end of file diff --git a/project-dashboard/index.html b/project-dashboard/index.html new file mode 100644 index 0000000..65cfbd1 --- /dev/null +++ b/project-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + Project Dashboard + + + +

Project Dashboard

+ + + diff --git a/project-dashboard/scripts.js b/project-dashboard/scripts.js new file mode 100644 index 0000000..45b0e2a --- /dev/null +++ b/project-dashboard/scripts.js @@ -0,0 +1,102 @@ +(async function () { + const graph = graphql('https://api.github.com/graphql', { + method: 'POST', + asJSON: true, + headers: { + 'Authorization': 'bearer 7329b9bb56f49b69df5e3e1584be9d8bae847fa7' + }, + }); + + const repo_name = 'test-project-management-data'; + const repo_owner = 'BenHenning'; + + let repository_query = graph(`query($repo_name: String!, $repo_owner: String!, $labels: [String!], $first: Int, $after: String) { + repository(name: $repo_name, owner: $repo_owner) { + ptis: issues(labels: $labels, first: $first, after: $after) { + totalCount + nodes { + bodyText + bodyUrl + number + milestone { + title + number + } + title + url + projectCards(first: 10) { + nodes { + project { + name + number + } + column { + name + } + } + } + } + pageInfo { + hasNextPage + } + } + } + }`); + + let all_issues_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { + repository(name: $repo_name, owner: $repo_owner) { + all_issues: issues(first: $first, after: $after) { + totalCount + nodes { + number + title + milestone { + number + } + } + pageInfo { + hasNextPage + } + } + } + }`); + + let milestones_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { + repository(name: $repo_name, owner: $repo_owner) { + milestones(first: $first, after: $after) { + totalCount + pageInfo { + hasNextPage + } + nodes { + dueOn + number + title + url + progressPercentage + } + } + } + }`); + + let repositories = await repository_query({ + repo_name, + repo_owner, + labels: 'Type: PTI', + first: 100, + }); + + let all_issues = await all_issues_query({ + repo_name, + repo_owner, + first: 100, + }); + + let milestones = await milestones_query({ + repo_name, + repo_owner, + first: 100, + }); + + console.log(repositories, all_issues, milestones); +})(); From 72f5ba391c39e4c8beebbaaaec94e4d6bdc879f8 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Mon, 18 Jan 2021 21:24:17 -0600 Subject: [PATCH 02/13] remove graphql library and use from cdn instead --- project-dashboard/graphql.min.js | 1 - project-dashboard/index.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 project-dashboard/graphql.min.js diff --git a/project-dashboard/graphql.min.js b/project-dashboard/graphql.min.js deleted file mode 100644 index eb55796..0000000 --- a/project-dashboard/graphql.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){function __extend(){var extended={},deep=false,i=0,length=arguments.length;if(Object.prototype.toString.call(arguments[0])=="[object Boolean]"){deep=arguments[0];i++}var merge=function(obj){for(var prop in obj){if(Object.prototype.hasOwnProperty.call(obj,prop)){if(deep&&Object.prototype.toString.call(obj[prop])=="[object Object]"){extended[prop]=__extend(true,extended[prop],obj[prop])}else{extended[prop]=obj[prop]}}}};for(;i0&&fragmentRegexp.test(fragment)){that.collectFragments(fragment,fragments).forEach(function(fragment){collectedFragments.unshift(fragment)})}}});return __unique(collectedFragments)};GraphQLClient.prototype.processQuery=function(query,fragments){if(typeof query=="object"&&query.hasOwnProperty("kind")&&query.hasOwnProperty("definitions")){throw new Error("Do not use graphql AST to send requests. Please generate query as string first using `graphql.print(query)`")}var fragmentRegexp=GraphQLClient.FRAGMENT_PATTERN;var collectedFragments=this.collectFragments(query,fragments);query=query.replace(fragmentRegexp,function(_,$m){return"... "+$m.split(".").join(FRAGMENT_SEPERATOR)});return[query].concat(collectedFragments.filter(function(fragment){return!query.match(fragment)})).join("\n")};GraphQLClient.prototype.autoDeclare=function(query,variables){var that=this;var typeMap={string:"String",number:function(value){return value%1===0?"Int":"Float"},boolean:"Boolean"};return query.replace(GraphQLClient.AUTODECLARE_PATTERN,function(){var types=[];for(var key in variables){var value=variables[key];var keyAndType=key.split(/^(.*?)\!/);if(keyAndType.length>1){keyAndType=keyAndType.slice(1);keyAndType[1]=keyAndType[1].replace(/(.*?)\!$/,"$1")}var mapping=typeMap[typeof value];var mappedType=typeof mapping==="function"?mapping(value):mapping;if(!key.match("!")&&keyAndType[0].match(/_?id/i)){mappedType="ID"}var type=keyAndType[1]||mappedType;if(type){types.push("$"+keyAndType[0]+": "+type+"!")}}types=types.join(", ");return types?"("+types+")":""})};GraphQLClient.prototype.cleanAutoDeclareAnnotations=function(variables){if(!variables)variables={};var newVariables={};for(var key in variables){var value=variables[key];var keyAndType=key.split("!");newVariables[keyAndType[0]]=value}return newVariables};GraphQLClient.prototype.buildFragments=function(fragments){var that=this;fragments=this.flatten(fragments||{});var fragmentObject={};for(var name in fragments){var fragment=fragments[name];if(typeof fragment=="object"){fragmentObject[name]=that.buildFragments(fragment)}else{fragmentObject[name]="\nfragment "+name+" "+fragment}}return fragmentObject};GraphQLClient.prototype.buildQuery=function(query,variables){return this.autoDeclare(this.processQuery(query,this._fragments),variables)};GraphQLClient.prototype.parseType=function(query){var match=query.trim().match(/^(query|mutation|subscription)/);if(!match)return"query";return match[1]};GraphQLClient.prototype.createSenderFunction=function(debug){var that=this;return function(query,originalQuery,type){if(__isTagCall(query)){return that.run(that.ql.apply(that,arguments))}var caller=function(variables,requestOptions){if(!requestOptions)requestOptions={};if(!variables)variables={};var fragmentedQuery=that.buildQuery(query,variables);var headers=__extend(that.options.headers||{},requestOptions.headers||{});return new Promise(function(resolve,reject){__request(debug,that.options.method||"post",that.getUrl(),headers,{query:fragmentedQuery,variables:that.cleanAutoDeclareAnnotations(variables)},!!that.options.asJSON,that.options.onRequestError,function(response,status){if(status==200){if(response.errors){reject(response.errors)}else if(response.data){resolve(response.data)}else{resolve(response)}}else{reject(response)}})})};caller.merge=function(mergeName,variables){if(!type){type=that.parseType(query);query=query.trim().replace(/^(query|mutation|subscription)\s*/,"").trim().replace(GraphQLClient.AUTODECLARE_PATTERN,"").trim().replace(/^\{|\}$/g,"")}if(!originalQuery){originalQuery=query}that._transaction[mergeName]=that._transaction[mergeName]||{query:[],mutation:[]};return new Promise(function(resolve){that._transaction[mergeName][type].push({type:type,query:originalQuery,variables:variables,resolver:resolve})})};if(arguments.length>3){return caller.apply(null,Array.prototype.slice.call(arguments,3))}return caller}};GraphQLClient.prototype.commit=function(mergeName){if(!this._transaction[mergeName]){throw new Error("You cannot commit the merge "+mergeName+" without creating it first.")}var that=this;var resolveMap={};var mergedVariables={};var mergedQueries={};Object.keys(this._transaction[mergeName]).forEach(function(method){if(that._transaction[mergeName][method].length===0)return;var subQuery=that._transaction[mergeName][method].map(function(merge){var reqId="merge"+Math.random().toString().split(".")[1].substr(0,6);resolveMap[reqId]=merge.resolver;var query=merge.query.replace(/\$([^\.\,\s\)]*)/g,function(_,m){if(!merge.variables){throw new Error("Unused variable on merge "+mergeName+": $"+m[0])}var matchingKey=Object.keys(merge.variables).filter(function(key){return key===m||key.match(new RegExp("^"+m+"!"))})[0];var variable=reqId+"__"+matchingKey;mergedVariables[method]=mergedVariables[method]||{};mergedVariables[method][variable]=merge.variables[matchingKey];return"$"+variable.split("!")[0]});var alias=query.trim().match(/^[^\(]+\:/);if(!alias){alias=query.replace(/^\{|\}$/gm,"").trim().match(/^[^\(\{]+/)[0]+":"}else{query=query.replace(/^[^\(]+\:/,"")}return reqId+"_"+alias+query}).join("\n");mergedQueries[method]=mergedQueries[method]||[];mergedQueries[method].push(method+" (@autodeclare) {\n"+subQuery+"\n }")});return Promise.all(Object.keys(mergedQueries).map(function(method){var query=mergedQueries[method].join("\n");var variables=mergedVariables[method];return that._sender(query,query,null,variables)})).then(function(responses){var newResponses={};responses.forEach(function(response){Object.keys(response).forEach(function(mergeKey){var parsedKey=mergeKey.match(/^(merge\d+)\_(.*)/);if(!parsedKey){throw new Error("Multiple root keys detected on response. Merging doesn't support it yet.")}var reqId=parsedKey[1];var fieldName=parsedKey[2];var newResponse={};newResponse[fieldName]=response[mergeKey];newResponses[fieldName]=(newResponses[fieldName]||[]).concat([response[mergeKey]]);resolveMap[reqId](newResponse)})});return newResponses}).catch(function(responses){return{error:true,errors:responses}}).finally(function(responses){that._transaction[mergeName]={query:[],mutation:[]};return responses})};GraphQLClient.prototype.createHelpers=function(sender){var that=this;function helper(query){if(__isTagCall(query)){that.__prefix=this.prefix;that.__suffix=this.suffix;var result=that.run(that.ql.apply(that,arguments));that.__prefix="";that.__suffix="";return result}var caller=sender(this.prefix+" "+query+" "+this.suffix,query.trim(),this.type);if(arguments.length>1&&arguments[1]!=null){return caller.apply(null,Array.prototype.slice.call(arguments,1))}return caller}var helpers=[{method:"mutate",type:"mutation"},{method:"query",type:"query"},{method:"subscribe",type:"subscription"}];helpers.forEach(function(m){that[m.method]=function(query,variables,options){if(that.options.alwaysAutodeclare===true||options&&options.declare===true){return helper.call({type:m.type,prefix:m.type+" (@autodeclare) {",suffix:"}"},query,variables)}return helper.call({type:m.type,prefix:m.type,suffix:""},query,variables)};that[m.method].run=function(query,options){return that[m.method](query,options)({})}});this.run=function(query){return sender(query,originalQuery,m.type,{})}};GraphQLClient.prototype.fragments=function(){return this._fragments};GraphQLClient.prototype.getOptions=function(){return this.options||{}};GraphQLClient.prototype.headers=function(newHeaders){return this.options.headers=__extend(this.options.headers,newHeaders)};GraphQLClient.prototype.fragment=function(fragment){if(typeof fragment=="string"){var _fragment=this._fragments[fragment.replace(/\./g,FRAGMENT_SEPERATOR)];if(!_fragment){throw"Fragment "+fragment+" not found!"}return _fragment.trim()}else{this.options.fragments=__extend(true,this.options.fragments,fragment);this._fragments=this.buildFragments(this.options.fragments);return this._fragments}};GraphQLClient.prototype.ql=function(strings){fragments=Array.prototype.slice.call(arguments,1);fragments=fragments.map(function(fragment){if(typeof fragment=="string"){return fragment.match(/fragment\s+([^\s]*)\s/)[1]}});var query=typeof strings=="string"?strings:strings.reduce(function(acc,seg,i){return acc+fragments[i-1]+seg});query=this.buildQuery(query);query=((this.__prefix||"")+" "+query+" "+(this.__suffix||"")).trim();return query};(function(root,factory){if(typeof define==="function"&&define.amd){define(function(){return root.graphql=factory(GraphQLClient)})}else if(typeof module==="object"&&module.exports){module.exports=factory(root.GraphQLClient)}else{root.graphql=factory(root.GraphQLClient)}})(this||self,function(){return GraphQLClient})})(); \ No newline at end of file diff --git a/project-dashboard/index.html b/project-dashboard/index.html index 65cfbd1..ec0476a 100644 --- a/project-dashboard/index.html +++ b/project-dashboard/index.html @@ -3,7 +3,7 @@ Project Dashboard - +

Project Dashboard

From 02745d859ed0fb89a39e589dc72829bf28584a59 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Mon, 18 Jan 2021 21:28:58 -0600 Subject: [PATCH 03/13] move PAT to variable and do not commit it --- project-dashboard/scripts.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project-dashboard/scripts.js b/project-dashboard/scripts.js index 45b0e2a..38e3b28 100644 --- a/project-dashboard/scripts.js +++ b/project-dashboard/scripts.js @@ -1,9 +1,10 @@ (async function () { + const PAT = ''; const graph = graphql('https://api.github.com/graphql', { method: 'POST', asJSON: true, headers: { - 'Authorization': 'bearer 7329b9bb56f49b69df5e3e1584be9d8bae847fa7' + 'Authorization': `bearer ${PAT}` }, }); From 9e4d4539538bb684f7d7355eadda2c80070b2d76 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Wed, 20 Jan 2021 22:01:09 -0600 Subject: [PATCH 04/13] create an https server for development to test web crypto functions --- .gitignore | 1 + README.md | 14 ++++++++++++++ start.py | 14 ++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 .gitignore create mode 100755 start.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..945071b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +development.pem diff --git a/README.md b/README.md index 6ccd2d9..ec5cc2a 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,17 @@ or ``` Then navigate to `localhost:8181` in a browser. + +### Project Dashboard + +The Oppia Project Dashboard uses the web crypto API, which requires a TLS connection. +To start an HTTPS simple server with a generated self-signed certificate, execute: + +```shell +$ python3 start.py +``` + +Then navigate to `https://localhost:8181/project-dashboard` in a browser. Note that +you will need to explicitly permit the browser to accept this self-signed certificate. +Chrome won't let you do that, but there's a workaround. Just click anywhere on the +error page and type `thisisunsafe` and the page will load. diff --git a/start.py b/start.py new file mode 100755 index 0000000..be5f3f5 --- /dev/null +++ b/start.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from http.server import HTTPServer, SimpleHTTPRequestHandler, HTTPStatus +import ssl +from os import path, system + +# Generate private key and self-signed cert if it doesn't exist +if not path.isfile('development.pem'): + system('/bin/bash -c "openssl req -new -x509 -keyout development.pem -out development.pem -days 365 -nodes -subj /CN=localhost/ -reqexts SAN -extensions SAN -config <(cat /etc/ssl/openssl.cnf <(printf \'[SAN]\nsubjectAltName=DNS:localhost\'))"') + +# Start server +httpd = HTTPServer(('0.0.0.0', 8181), SimpleHTTPRequestHandler) +httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True, certfile='development.pem') +httpd.serve_forever() From 441a638ae1ac0348242e01c8a0d57a2a7428ea81 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Wed, 20 Jan 2021 22:02:04 -0600 Subject: [PATCH 05/13] add indexeddb abstraction with a crypto keystore and an app datastore --- project-dashboard/scripts/db.js | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 project-dashboard/scripts/db.js diff --git a/project-dashboard/scripts/db.js b/project-dashboard/scripts/db.js new file mode 100644 index 0000000..d7edd99 --- /dev/null +++ b/project-dashboard/scripts/db.js @@ -0,0 +1,91 @@ +/** + * An abstraction layer for IndexedDB + */ + +const name = 'OppiaProjectDashboard'; +const version = 1; // long long; incrementing fires onupgradeneeded event +let db = null; + +/** + * Open IndexedDB + * @returns {boolean} + */ +const open = async () => { + let handle = new Promise((resolve, reject) => { + let req = indexedDB.open(name, version); + req.onsuccess = event => resolve(event.target.result); + req.onupgradeneeded = (event) => { + let db = event.target.result; + + // Create keystore + if (!db.objectStoreNames.contains('keystore')) { + let keystore = db.createObjectStore('keystore', { autoIncrement: true, keyPath: 'name' }); + keystore.createIndex('name', 'name', { unique: true }); + } + + // Create datastore + if (!db.objectStoreNames.contains('datastore')) { + let datastore = db.createObjectStore('datastore', { autoIncrement: true }); + // TODO: Create indexes + } + + resolve(db); + }; + req.onerror = reject; + req.onblocked = reject; + }); + + db = await handle; + return true; +}; + +/** + * Close IndexedDB + * @returns {boolean} + */ +const close = async () => { + if (db) { + await db.close(); + db = null; + } + return true +}; + +/** + * Write a key into the keystore + * @param {string} name - Name of the key to store (must be unique) + * @param {CryptoKey} key + * @returns {boolean} + */ +const setKey = async (name, key) => { + if (!db) await open(); + + let transaction = db.transaction(['keystore'], 'readwrite'); + let objectStore = transaction.objectStore('keystore'); + await objectStore.add({ name, key }); + return true; +}; + +/** + * Get a key from the keystore + * @param {string} name + * @returns {CryptoKey} + */ +const getKey = async (name) => { + if (!db) await open(); + + let key = new Promise((resolve, reject) => { + let transaction = db.transaction(['keystore'], 'readonly'); + let objectStore = transaction.objectStore('keystore'); + let op = objectStore.get(name); + op.onsuccess = (event) => resolve(event.target.result); + op.onerror = reject; + }); + + await key; + return key; +}; + +// TODO: Write getters and setters for datastore + +export { open, close, setKey, getKey }; From c12cfb72dc8bce3574a59da54c398b23bd67e26c Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Wed, 20 Jan 2021 22:02:27 -0600 Subject: [PATCH 06/13] move graphql functions into separate module --- project-dashboard/scripts/graphql.js | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 project-dashboard/scripts/graphql.js diff --git a/project-dashboard/scripts/graphql.js b/project-dashboard/scripts/graphql.js new file mode 100644 index 0000000..fc7483e --- /dev/null +++ b/project-dashboard/scripts/graphql.js @@ -0,0 +1,110 @@ +/** + * Query GitHub API via GraphQL + * @param {Promise} pat_promise - GitHub Personal Access Token + */ +const queryData = async (pat_promise) => { + const pat = await pat_promise; + + const graph = graphql('https://api.github.com/graphql', { + method: 'POST', + asJSON: true, + headers: { + 'Authorization': `bearer ${pat}` + }, + }); + + const repo_name = 'test-project-management-data'; + const repo_owner = 'BenHenning'; + + let repository_query = graph(`query($repo_name: String!, $repo_owner: String!, $labels: [String!], $first: Int, $after: String) { + repository(name: $repo_name, owner: $repo_owner) { + ptis: issues(labels: $labels, first: $first, after: $after) { + totalCount + nodes { + bodyText + bodyUrl + number + milestone { + title + number + } + title + url + projectCards(first: 10) { + nodes { + project { + name + number + } + column { + name + } + } + } + } + pageInfo { + hasNextPage + } + } + } + }`); + + let all_issues_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { + repository(name: $repo_name, owner: $repo_owner) { + all_issues: issues(first: $first, after: $after) { + totalCount + nodes { + number + title + milestone { + number + } + } + pageInfo { + hasNextPage + } + } + } + }`); + + let milestones_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { + repository(name: $repo_name, owner: $repo_owner) { + milestones(first: $first, after: $after) { + totalCount + pageInfo { + hasNextPage + } + nodes { + dueOn + number + title + url + progressPercentage + } + } + } + }`); + + let repositories = await repository_query({ + repo_name, + repo_owner, + labels: 'Type: PTI', + first: 100, + }); + + let all_issues = await all_issues_query({ + repo_name, + repo_owner, + first: 100, + }); + + let milestones = await milestones_query({ + repo_name, + repo_owner, + first: 100, + }); + + console.log(repositories, all_issues, milestones); +}; + +export default queryData; From 5cc18d2261cb5e9bf838e0e851811d8e8b378ba1 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Wed, 20 Jan 2021 22:03:10 -0600 Subject: [PATCH 07/13] add a module for securely handling and storing github personal access tokens --- project-dashboard/scripts/pat.js | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 project-dashboard/scripts/pat.js diff --git a/project-dashboard/scripts/pat.js b/project-dashboard/scripts/pat.js new file mode 100644 index 0000000..3741412 --- /dev/null +++ b/project-dashboard/scripts/pat.js @@ -0,0 +1,63 @@ +import { getKey, setKey } from './db.js'; + +/** + * Get Personal Access Token from Local Storage + * @returns {string} + */ +const getPAT = async () => { + let ciphertext = localStorage.getItem('PAT'); + if (!ciphertext) throw new Error('No PAT currently stored in local storage'); + + let iv = localStorage.getItem('iv'); + if (!iv) throw new Error('No IV currently stored in local storage'); + + // Ciphertext and IV were stored as strings in local storage + // Split by comma to convert them back into Uint8Arrays + ciphertext = Uint8Array.from(ciphertext.split(',')); + iv = Uint8Array.from(iv.split(',')); + + // Get the symmetric key and decrypt the ciphertext + let { key } = await getKey('pat_key'); + let pat = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext); + + // Convert plaintext data into a string + return (new TextDecoder()).decode(pat); +}; + +/** + * Set Personal Access Token to Local Storage + * @param {string} pat - The Personal Access Token entered by user + */ +const setPAT = async (pat) => { + // Get encryption key + let key = await getKey('pat_key'); + + if (!key) { + // Create symmetric key for encrypting PAT for local storage + key = await crypto.subtle.generateKey({ + name: 'AES-GCM', + length: 256, + }, + false, // do not allow export + ['encrypt', 'decrypt']); + + // Save key + await setKey('pat_key', key); + } else ({ key } = key); + + // Encode PAT into Uint8Array + const encoder = new TextEncoder(); + let plaintext = encoder.encode(pat); + + // Generate initialization vector + let iv = crypto.getRandomValues(new Uint8Array(12)); + + // Encrypt plaintext + let ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext); + + // Save ciphertext and IV to local storage as strings + localStorage.setItem('PAT', new Uint8Array(ciphertext).toString()); // ciphertext is of type buffer + localStorage.setItem('iv', iv.toString()); +}; + +export { getPAT, setPAT }; From c28864180dd8b18aecc357eea383f3a05c589dd3 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Wed, 20 Jan 2021 22:03:49 -0600 Subject: [PATCH 08/13] moved startup functions into their own file --- project-dashboard/scripts.js | 103 --------------------------- project-dashboard/scripts/startup.js | 44 ++++++++++++ 2 files changed, 44 insertions(+), 103 deletions(-) delete mode 100644 project-dashboard/scripts.js create mode 100644 project-dashboard/scripts/startup.js diff --git a/project-dashboard/scripts.js b/project-dashboard/scripts.js deleted file mode 100644 index 38e3b28..0000000 --- a/project-dashboard/scripts.js +++ /dev/null @@ -1,103 +0,0 @@ -(async function () { - const PAT = ''; - const graph = graphql('https://api.github.com/graphql', { - method: 'POST', - asJSON: true, - headers: { - 'Authorization': `bearer ${PAT}` - }, - }); - - const repo_name = 'test-project-management-data'; - const repo_owner = 'BenHenning'; - - let repository_query = graph(`query($repo_name: String!, $repo_owner: String!, $labels: [String!], $first: Int, $after: String) { - repository(name: $repo_name, owner: $repo_owner) { - ptis: issues(labels: $labels, first: $first, after: $after) { - totalCount - nodes { - bodyText - bodyUrl - number - milestone { - title - number - } - title - url - projectCards(first: 10) { - nodes { - project { - name - number - } - column { - name - } - } - } - } - pageInfo { - hasNextPage - } - } - } - }`); - - let all_issues_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { - repository(name: $repo_name, owner: $repo_owner) { - all_issues: issues(first: $first, after: $after) { - totalCount - nodes { - number - title - milestone { - number - } - } - pageInfo { - hasNextPage - } - } - } - }`); - - let milestones_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { - repository(name: $repo_name, owner: $repo_owner) { - milestones(first: $first, after: $after) { - totalCount - pageInfo { - hasNextPage - } - nodes { - dueOn - number - title - url - progressPercentage - } - } - } - }`); - - let repositories = await repository_query({ - repo_name, - repo_owner, - labels: 'Type: PTI', - first: 100, - }); - - let all_issues = await all_issues_query({ - repo_name, - repo_owner, - first: 100, - }); - - let milestones = await milestones_query({ - repo_name, - repo_owner, - first: 100, - }); - - console.log(repositories, all_issues, milestones); -})(); diff --git a/project-dashboard/scripts/startup.js b/project-dashboard/scripts/startup.js new file mode 100644 index 0000000..dc857d1 --- /dev/null +++ b/project-dashboard/scripts/startup.js @@ -0,0 +1,44 @@ +import { open } from './db.js'; +import { getPAT, setPAT } from './pat.js'; +import queryData from './graphql.js'; + +// Open Database +open(); + +// If the Personal Access Token is not in local storage, prompt user for one +if (!localStorage.getItem('PAT')) { + // Show the authenticate form and bind validation functions + const prompt = document.querySelector('#authenticate'); + const main = document.querySelector('main'); + prompt.classList.add('show'); + main.classList.add('blur'); + + const pat_input = document.querySelector('#authenticate input'); + + pat_input.addEventListener('input', () => { + pat_input.setCustomValidity(''); // reset message + pat_input.checkValidity(); + }); + + pat_input.addEventListener('invalid', () => { + pat_input.setCustomValidity('A GitHub Personal Access token is a 40 character hexadecimal string'); + }); + + // Handle form submission + document.querySelector('#authenticate form').addEventListener('submit', async (event) => { + event.preventDefault(); + event.stopPropagation(); + + await setPAT(pat_input.value); + + // Send graphql query + queryData(getPAT()); + + // Hide prompt + prompt.classList.remove('show'); + main.classList.remove('blur'); + }); +} else { + // Otherwise send graphql query + queryData(getPAT()); +} From 6ba03d6a698b47d127f212b771b117a451b0ed64 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Wed, 20 Jan 2021 22:04:26 -0600 Subject: [PATCH 09/13] add basic styles. add prompt for retrieving user personal access token in order to connect to github api --- project-dashboard/index.html | 23 +++++++++++++++++++++-- project-dashboard/styles.css | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 project-dashboard/styles.css diff --git a/project-dashboard/index.html b/project-dashboard/index.html index ec0476a..a9ddf67 100644 --- a/project-dashboard/index.html +++ b/project-dashboard/index.html @@ -4,9 +4,28 @@ Project Dashboard + + -

Project Dashboard

- +
+

Authenticate with Github

+

In order to use the Oppia Project Dashboard, you need to provide a personal access token.

+
+ + +
+
    +
  1. Navigate to https://github.com/settings/tokens and login if necessary.
  2. +
  3. Click "Generate new token"
  4. +
  5. You can use "Oppia Project Dashboard" as the note.
  6. +
  7. Select only the "public_repo" checkbox. Then click Generate Token.
  8. +
  9. Paste the token in the field above and click Login.
  10. +
+
+
+

Project Dashboard

+
+ diff --git a/project-dashboard/styles.css b/project-dashboard/styles.css new file mode 100644 index 0000000..356f982 --- /dev/null +++ b/project-dashboard/styles.css @@ -0,0 +1,36 @@ +/* Authentication Prompt */ +#authenticate { + display: flex; + position: absolute; + width: 100%; + height: 100%; + z-index: -1; + flex-direction: column; + align-items: center; + justify-content: center; + transition: opacity 1s ease-in-out; + opacity: 0; + top: 0; + pointer-events: none; +} +#authenticate.show { + opacity: 1; + pointer-events: all; + z-index: 100; +} +#authenticate form { + width: 100%; + display: flex; + justify-content: center; + margin: 20px; +} +#authenticate form input { + padding: 10px; + width: 50%; + margin-right: 10px; +} +#authenticate form ol { margin-top: 30px; } + +/* Main element */ +main { transition: filter 1s ease-in-out; } +main.blur { filter: blur(5px); } From 027e0f09fb598fadb83d8d7ae6a4fed901b516b5 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Fri, 22 Jan 2021 14:26:34 -0600 Subject: [PATCH 10/13] add windows considerations for https --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index ec5cc2a..9a70339 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,18 @@ Then navigate to `https://localhost:8181/project-dashboard` in a browser. Note you will need to explicitly permit the browser to accept this self-signed certificate. Chrome won't let you do that, but there's a workaround. Just click anywhere on the error page and type `thisisunsafe` and the page will load. + +#### Windows + +If you are using Windows, you have a few options: + +1. You can install a VM or spin up a Docker container with Linux, and run the script + from there, using a shared volume and forwarding tcp/8181 to the windows host. +2. You can install the Linux subsystem for Windows under Add or remove programs > + Windows Features. Then, in the Microsoft Store you can download a linux distribution + such as Ubuntu. +3. You can install OpenSSL for Windows and manually run this command to generate the + `development.pem` file: + ```shell + $ /bin/bash -c "openssl req -new -x509 -keyout development.pem -out development.pem -days 365 -nodes -subj /CN=localhost/ -reqexts SAN -extensions SAN -config <(cat /etc/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:localhost'))" + ``` From 56eb3574213afd6bc02a3a988d3ffca13a2c9435 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Fri, 22 Jan 2021 14:32:15 -0600 Subject: [PATCH 11/13] Update README.md change project dashboard heading Co-authored-by: Ben Henning --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a70339..b6054a4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ or Then navigate to `localhost:8181` in a browser. -### Project Dashboard +### Oppia Android Project Dashboard The Oppia Project Dashboard uses the web crypto API, which requires a TLS connection. To start an HTTPS simple server with a generated self-signed certificate, execute: From ac1a7e8cf690f49338b4eaa40fb081737ba06a47 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Fri, 22 Jan 2021 15:04:42 -0600 Subject: [PATCH 12/13] minor style fix --- project-dashboard/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project-dashboard/styles.css b/project-dashboard/styles.css index 356f982..8b9b973 100644 --- a/project-dashboard/styles.css +++ b/project-dashboard/styles.css @@ -19,7 +19,7 @@ z-index: 100; } #authenticate form { - width: 100%; + width: 90%; display: flex; justify-content: center; margin: 20px; From 394b44bf2e295767a31429a967e045e8ae21df61 Mon Sep 17 00:00:00 2001 From: Micah Henning Date: Fri, 22 Jan 2021 15:05:05 -0600 Subject: [PATCH 13/13] documentation and code style conformity changes --- project-dashboard/scripts/db.js | 20 +++++++---- project-dashboard/scripts/graphql.js | 51 ++++++++++++++++------------ project-dashboard/scripts/pat.js | 34 ++++++++++++------- project-dashboard/scripts/startup.js | 20 +++++------ 4 files changed, 73 insertions(+), 52 deletions(-) diff --git a/project-dashboard/scripts/db.js b/project-dashboard/scripts/db.js index d7edd99..9f69b85 100644 --- a/project-dashboard/scripts/db.js +++ b/project-dashboard/scripts/db.js @@ -1,5 +1,7 @@ /** - * An abstraction layer for IndexedDB + * An abstraction layer for IndexedDB. + * + * @module */ const name = 'OppiaProjectDashboard'; @@ -7,7 +9,8 @@ const version = 1; // long long; incrementing fires onupgradeneeded event let db = null; /** - * Open IndexedDB + * Open IndexedDB. + * * @returns {boolean} */ const open = async () => { @@ -40,7 +43,8 @@ const open = async () => { }; /** - * Close IndexedDB + * Close IndexedDB. + * * @returns {boolean} */ const close = async () => { @@ -52,8 +56,9 @@ const close = async () => { }; /** - * Write a key into the keystore - * @param {string} name - Name of the key to store (must be unique) + * Write a key into the keystore. + * + * @param {string} name - Name of the key to store (must be unique). * @param {CryptoKey} key * @returns {boolean} */ @@ -67,7 +72,8 @@ const setKey = async (name, key) => { }; /** - * Get a key from the keystore + * Get a key from the keystore. + * * @param {string} name * @returns {CryptoKey} */ @@ -86,6 +92,6 @@ const getKey = async (name) => { return key; }; -// TODO: Write getters and setters for datastore +// TODO(55): Write getters and setters for datastore export { open, close, setKey, getKey }; diff --git a/project-dashboard/scripts/graphql.js b/project-dashboard/scripts/graphql.js index fc7483e..d06f36d 100644 --- a/project-dashboard/scripts/graphql.js +++ b/project-dashboard/scripts/graphql.js @@ -1,9 +1,16 @@ /** - * Query GitHub API via GraphQL - * @param {Promise} pat_promise - GitHub Personal Access Token + * Methods for Querying GitHub's GraphQL API. + * + * @module */ -const queryData = async (pat_promise) => { - const pat = await pat_promise; + +/** + * Query GitHub API via GraphQL. + * + * @param {Promise} patPromise - GitHub Personal Access Token. + */ +const queryData = async (patPromise) => { + const pat = await patPromise; const graph = graphql('https://api.github.com/graphql', { method: 'POST', @@ -13,11 +20,11 @@ const queryData = async (pat_promise) => { }, }); - const repo_name = 'test-project-management-data'; - const repo_owner = 'BenHenning'; + const repoName = 'test-project-management-data'; + const repoOwner = 'BenHenning'; - let repository_query = graph(`query($repo_name: String!, $repo_owner: String!, $labels: [String!], $first: Int, $after: String) { - repository(name: $repo_name, owner: $repo_owner) { + let repositoryQuery = graph(`query($repoName: String!, $repoOwner: String!, $labels: [String!], $first: Int, $after: String) { + repository(name: $repoName, owner: $repoOwner) { ptis: issues(labels: $labels, first: $first, after: $after) { totalCount nodes { @@ -49,8 +56,8 @@ const queryData = async (pat_promise) => { } }`); - let all_issues_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { - repository(name: $repo_name, owner: $repo_owner) { + let allIssuesQuery = graph(`query($repoName: String!, $repoOwner: String!, $first: Int, $after: String) { + repository(name: $repoName, owner: $repoOwner) { all_issues: issues(first: $first, after: $after) { totalCount nodes { @@ -67,8 +74,8 @@ const queryData = async (pat_promise) => { } }`); - let milestones_query = graph(`query($repo_name: String!, $repo_owner: String!, $first: Int, $after: String) { - repository(name: $repo_name, owner: $repo_owner) { + let milestonesQuery = graph(`query($repoName: String!, $repoOwner: String!, $first: Int, $after: String) { + repository(name: $repoName, owner: $repoOwner) { milestones(first: $first, after: $after) { totalCount pageInfo { @@ -85,26 +92,26 @@ const queryData = async (pat_promise) => { } }`); - let repositories = await repository_query({ - repo_name, - repo_owner, + let repositories = await repositoryQuery({ + repoName, + repoOwner, labels: 'Type: PTI', first: 100, }); - let all_issues = await all_issues_query({ - repo_name, - repo_owner, + let allIssues = await allIssuesQuery({ + repoName, + repoOwner, first: 100, }); - let milestones = await milestones_query({ - repo_name, - repo_owner, + let milestones = await milestonesQuery({ + repoName, + repoOwner, first: 100, }); - console.log(repositories, all_issues, milestones); + console.log(repositories, allIssues, milestones); }; export default queryData; diff --git a/project-dashboard/scripts/pat.js b/project-dashboard/scripts/pat.js index 3741412..c7a92bc 100644 --- a/project-dashboard/scripts/pat.js +++ b/project-dashboard/scripts/pat.js @@ -1,34 +1,42 @@ +/** + * Methods for securely processing and storing GitHub + * Personal Access Tokens (PATs). + * + * @method + */ import { getKey, setKey } from './db.js'; /** - * Get Personal Access Token from Local Storage + * Get Personal Access Token from Local Storage. + * * @returns {string} */ -const getPAT = async () => { +const getPat = async () => { let ciphertext = localStorage.getItem('PAT'); if (!ciphertext) throw new Error('No PAT currently stored in local storage'); - let iv = localStorage.getItem('iv'); - if (!iv) throw new Error('No IV currently stored in local storage'); + let initVector = localStorage.getItem('initVector'); + if (!initVector) throw new Error('No IV currently stored in local storage'); // Ciphertext and IV were stored as strings in local storage // Split by comma to convert them back into Uint8Arrays ciphertext = Uint8Array.from(ciphertext.split(',')); - iv = Uint8Array.from(iv.split(',')); + initVector = Uint8Array.from(initVector.split(',')); // Get the symmetric key and decrypt the ciphertext let { key } = await getKey('pat_key'); - let pat = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext); + let pat = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: initVector }, key, ciphertext); // Convert plaintext data into a string return (new TextDecoder()).decode(pat); }; /** - * Set Personal Access Token to Local Storage - * @param {string} pat - The Personal Access Token entered by user + * Set Personal Access Token to Local Storage. + * + * @param {string} pat - The Personal Access Token entered by user. */ -const setPAT = async (pat) => { +const setPat = async (pat) => { // Get encryption key let key = await getKey('pat_key'); @@ -50,14 +58,14 @@ const setPAT = async (pat) => { let plaintext = encoder.encode(pat); // Generate initialization vector - let iv = crypto.getRandomValues(new Uint8Array(12)); + let initVector = crypto.getRandomValues(new Uint8Array(12)); // Encrypt plaintext - let ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext); + let ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: initVector }, key, plaintext); // Save ciphertext and IV to local storage as strings localStorage.setItem('PAT', new Uint8Array(ciphertext).toString()); // ciphertext is of type buffer - localStorage.setItem('iv', iv.toString()); + localStorage.setItem('initVector', initVector.toString()); }; -export { getPAT, setPAT }; +export { getPat, setPat }; diff --git a/project-dashboard/scripts/startup.js b/project-dashboard/scripts/startup.js index dc857d1..a158efd 100644 --- a/project-dashboard/scripts/startup.js +++ b/project-dashboard/scripts/startup.js @@ -1,5 +1,5 @@ import { open } from './db.js'; -import { getPAT, setPAT } from './pat.js'; +import { getPat, setPat } from './pat.js'; import queryData from './graphql.js'; // Open Database @@ -13,15 +13,15 @@ if (!localStorage.getItem('PAT')) { prompt.classList.add('show'); main.classList.add('blur'); - const pat_input = document.querySelector('#authenticate input'); + const patInput = document.querySelector('#authenticate input'); - pat_input.addEventListener('input', () => { - pat_input.setCustomValidity(''); // reset message - pat_input.checkValidity(); + patInput.addEventListener('input', () => { + patInput.setCustomValidity(''); // reset message + patInput.checkValidity(); }); - pat_input.addEventListener('invalid', () => { - pat_input.setCustomValidity('A GitHub Personal Access token is a 40 character hexadecimal string'); + patInput.addEventListener('invalid', () => { + patInput.setCustomValidity('A GitHub Personal Access token is a 40 character hexadecimal string'); }); // Handle form submission @@ -29,10 +29,10 @@ if (!localStorage.getItem('PAT')) { event.preventDefault(); event.stopPropagation(); - await setPAT(pat_input.value); + await setPat(patInput.value); // Send graphql query - queryData(getPAT()); + queryData(getPat()); // Hide prompt prompt.classList.remove('show'); @@ -40,5 +40,5 @@ if (!localStorage.getItem('PAT')) { }); } else { // Otherwise send graphql query - queryData(getPAT()); + queryData(getPat()); }