Merge branch 'master' into feature/post-request-tests

This commit is contained in:
Nicholas Palenchar
2020-01-11 11:18:46 -05:00
12 changed files with 334 additions and 123 deletions

View File

@@ -1,89 +1,144 @@
const redirectUri = `${ window.location.origin }/`;
const redirectUri = `${window.location.origin}/`;
//////////////////////////////////////////////////////////////////////
// GENERAL HELPER FUNCTIONS
// Make a POST request and parse the response as JSON
const sendPostRequest = async(url, params) => {
let body = Object.keys(params).map(key => key + '=' + params[key]).join('&');
/**
* Makes a POST request and parse the response as JSON
*
* @param {String} url - The resource
* @param {Object} params - Configuration options
* @returns {Object}
*/
const sendPostRequest = async (url, params) => {
const body = Object.keys(params)
.map(key => `${key}=${params[key]}`)
.join("&");
const options = {
method: 'post',
method: "post",
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
body
}
};
try {
const response = await fetch(url, options);
const data = await response.json();
return data;
} catch (err) {
console.error('Request failed', err);
console.error("Request failed", err);
throw err;
}
}
// Parse a query string into an object
const parseQueryString = string => {
if(string == "") { return {}; }
let segments = string.split("&").map(s => s.split("=") );
let queryString = {};
segments.forEach(s => queryString[s[0]] = s[1]);
};
/**
* Parse a query string into an object
*
* @param {String} searchQuery - The search query params
* @returns {Object}
*/
const parseQueryString = searchQuery => {
if (searchQuery === "") {
return {};
}
const segments = searchQuery.split("&").map(s => s.split("="));
const queryString = segments.reduce(
(obj, el) => ({ ...obj, [el[0]]: el[1] }),
{}
);
return queryString;
}
// Get OAuth configuration from OpenID Discovery endpoint
};
/**
* Get OAuth configuration from OpenID Discovery endpoint
*
* @returns {Object}
*/
const getTokenConfiguration = async endpoint => {
const options = {
method: 'GET',
method: "GET",
headers: {
'Content-type': 'application/json'
"Content-type": "application/json"
}
}
};
try {
const response = await fetch(endpoint, options);
const config = await response.json();
return config;
} catch (err) {
console.error('Request failed', err);
console.error("Request failed", err);
throw err;
}
}
};
//////////////////////////////////////////////////////////////////////
// PKCE HELPER FUNCTIONS
// Generate a secure random string using the browser crypto functions
/**
* Generates a secure random string using the browser crypto functions
*
* @returns {Object}
*/
const generateRandomString = () => {
var array = new Uint32Array(28);
const array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
return Array.from(array, dec => `0${dec.toString(16)}`.substr(-2)).join("");
};
/**
* Calculate the SHA256 hash of the input text
*
* @returns {Promise<ArrayBuffer>}
*/
const sha256 = plain => {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
const base64urlencode = str => {
// Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts.
return window.crypto.subtle.digest("SHA-256", data);
};
/**
* Encodes the input string into Base64 format
*
* @param {String} str - The string to be converted
* @returns {Promise<ArrayBuffer>}
*/
const base64urlencode = (
str // Converts the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
) =>
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
const pkceChallengeFromVerifier = async(v) => {
let hashed = await sha256(v);
return base64urlencode(hashed);
}
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
/**
* Return the base64-urlencoded sha256 hash for the PKCE challenge
*
* @param {String} v - The randomly generated string
* @returns {String}
*/
const pkceChallengeFromVerifier = async v => {
const hashed = await sha256(v);
return base64urlencode(hashed);
};
//////////////////////////////////////////////////////////////////////
// OAUTH REQUEST
// Initiate PKCE Auth Code flow when requested
const tokenRequest = async({
/**
* Initiates PKCE Auth Code flow when requested
*
* @param {Object} - The necessary params
* @returns {Void}
*/
const tokenRequest = async ({
oidcDiscoveryUrl,
grantType,
authUrl,
@@ -91,85 +146,93 @@ const tokenRequest = async({
clientId,
scope
}) => {
// Check oauth configuration
if (oidcDiscoveryUrl !== '') {
const { authorization_endpoint, token_endpoint } = await getTokenConfiguration(oidcDiscoveryUrl);
if (oidcDiscoveryUrl !== "") {
const {
authorization_endpoint,
token_endpoint
} = await getTokenConfiguration(oidcDiscoveryUrl);
authUrl = authorization_endpoint;
accessTokenUrl = token_endpoint;
}
// Store oauth information
localStorage.setItem('token_endpoint', accessTokenUrl);
localStorage.setItem('client_id', clientId);
localStorage.setItem("token_endpoint", accessTokenUrl);
localStorage.setItem("client_id", clientId);
// Create and store a random state value
const state = generateRandomString();
localStorage.setItem('pkce_state', state);
localStorage.setItem("pkce_state", state);
// Create and store a new PKCE code_verifier (the plaintext random secret)
const code_verifier = generateRandomString();
localStorage.setItem('pkce_code_verifier', code_verifier);
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
const code_challenge = await pkceChallengeFromVerifier(code_verifier);
// Build the authorization URL
const buildUrl = () => {
return authUrl
+ `?response_type=${grantType}`
+ '&client_id='+encodeURIComponent(clientId)
+ '&state='+encodeURIComponent(state)
+ '&scope='+encodeURIComponent(scope)
+ '&redirect_uri='+encodeURIComponent(redirectUri)
+ '&code_challenge='+encodeURIComponent(code_challenge)
+ '&code_challenge_method=S256'
;
}
const buildUrl = () =>
`${authUrl + `?response_type=${grantType}`}&client_id=${encodeURIComponent(
clientId
)}&state=${encodeURIComponent(state)}&scope=${encodeURIComponent(
scope
)}&redirect_uri=${encodeURIComponent(
redirectUri
)}&code_challenge=${encodeURIComponent(
code_challenge
)}&code_challenge_method=S256`;
// Redirect to the authorization server
window.location = buildUrl();
}
};
//////////////////////////////////////////////////////////////////////
// OAUTH REDIRECT HANDLING
// Handle the redirect back from the authorization server and
// get an access token from the token endpoint
const oauthRedirect = async() => {
let tokenResponse = '';
/**
* Handle the redirect back from the authorization server and
* get an access token from the token endpoint
*
* @returns {Object}
*/
const oauthRedirect = async () => {
let tokenResponse = "";
let q = parseQueryString(window.location.search.substring(1));
// Check if the server returned an error string
if(q.error) {
alert('Error returned from authorization server: '+q.error);
if (q.error) {
alert(`Error returned from authorization server: ${q.error}`);
}
// If the server returned an authorization code, attempt to exchange it for an access token
if(q.code) {
if (q.code) {
// Verify state matches what we set at the beginning
if(localStorage.getItem('pkce_state') != q.state) {
alert('Invalid state');
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
try {
// Exchange the authorization code for an access token
tokenResponse = await sendPostRequest(localStorage.getItem('token_endpoint'), {
grant_type: 'authorization_code',
code: q.code,
client_id: localStorage.getItem('client_id'),
redirect_uri: redirectUri,
code_verifier: localStorage.getItem('pkce_code_verifier')
});
tokenResponse = await sendPostRequest(
localStorage.getItem("token_endpoint"),
{
grant_type: "authorization_code",
code: q.code,
client_id: localStorage.getItem("client_id"),
redirect_uri: redirectUri,
code_verifier: localStorage.getItem("pkce_code_verifier")
}
);
} catch (err) {
console.log(error.error+'\n\n'+error.error_description);
console.log(`${error.error}\n\n${error.error_description}`);
}
}
// Clean these up since we don't need them anymore
localStorage.removeItem('pkce_state');
localStorage.removeItem('pkce_code_verifier');
localStorage.removeItem('token_endpoint');
localStorage.removeItem('client_id');
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
localStorage.removeItem("token_endpoint");
localStorage.removeItem("client_id");
return tokenResponse;
}
return tokenResponse;
}
};
export { tokenRequest, oauthRedirect }
export { tokenRequest, oauthRedirect };

View File

@@ -58,6 +58,7 @@
padding: 8px 16px;
font-size: 16px;
font-family: "Roboto Mono", monospace;
font-weight: 400;
&:last-child {
border-radius: 0 0 8px 8px;

View File

@@ -0,0 +1,38 @@
<template>
<span>
<span class="argumentName">
{{ argName }}
</span>
:
<typelink :type="argType" :jumpTypeCallback="jumpCallback" />
</span>
</template>
<style></style>
<script>
import typelink from "./typelink";
export default {
components: {
typelink: typelink
},
props: {
gqlArg: Object
},
computed: {
argName() {
return this.gqlArg.name;
},
argType() {
return this.gqlArg.type;
}
},
methods: {
jumpCallback(typeName) {}
}
};
</script>

View File

@@ -1,6 +1,23 @@
<template>
<div class="field-box">
<div class="field-title">{{ fieldString }}</div>
<div class="field-title">
{{ fieldName }}
<span v-if="fieldArgs.length > 0">
(
<span v-for="(field, index) in fieldArgs" :key="index">
{{ field.name }}:
<typelink
:gqlType="field.type"
:jumpTypeCallback="jumpTypeCallback"
/>
<span v-if="index !== fieldArgs.length - 1">
,
</span>
</span>
) </span
>:
<typelink :gqlType="gqlField.type" :jumpTypeCallback="jumpTypeCallback" />
</div>
<div class="field-desc" v-if="gqlField.description">
{{ gqlField.description }}
</div>
@@ -36,9 +53,16 @@
</style>
<script>
import typelink from "./typelink";
export default {
components: {
typelink: typelink
},
props: {
gqlField: Object
gqlField: Object,
jumpTypeCallback: Function
},
computed: {
@@ -52,10 +76,17 @@ export default {
);
}, "");
const argsString = args.length > 0 ? `(${args})` : "";
return `${
this.gqlField.name
}${argsString}: ${this.gqlField.type.toString()}`;
},
fieldName() {
return this.gqlField.name;
},
fieldArgs() {
return this.gqlField.args || [];
}
}
};

View File

@@ -8,7 +8,7 @@
<div v-if="gqlType.getFields">
<h5>FIELDS</h5>
<div v-for="field in gqlType.getFields()" :key="field.name">
<gql-field :gqlField="field" />
<gql-field :gqlField="field" :jumpTypeCallback="jumpTypeCallback" />
</div>
</div>
</div>
@@ -35,8 +35,10 @@ export default {
components: {
"gql-field": () => import("./field")
},
props: {
gqlType: {}
gqlType: {},
jumpTypeCallback: Function
}
};
</script>

View File

@@ -0,0 +1,34 @@
<template>
<span class="typelink" @click="jumpToType">{{ typeString }}</span>
</template>
<style>
.typelink {
color: var(--ac-color);
font-family: "Roboto Mono", monospace;
font-weight: 400;
cursor: pointer;
}
</style>
<script>
export default {
props: {
gqlType: null,
// (typeName: string) => void
jumpTypeCallback: Function
},
computed: {
typeString() {
return this.gqlType.toString();
}
},
methods: {
jumpToType() {
this.jumpTypeCallback(this.gqlType);
}
}
};
</script>

View File

@@ -298,6 +298,7 @@ ol li {
top: 10px;
right: 10px;
font-family: "Roboto Mono", monospace;
font-weight: 400;
background-color: transparent;
padding: 2px 6px;
border-radius: 8px;

View File

@@ -549,8 +549,8 @@ export default {
this.$store.state.postwoman.settings.THEME_CLASS || "";
// Load theme color data from settings, or use default color.
let color = this.$store.state.postwoman.settings.THEME_COLOR || "#50fa7b";
let vibrant = this.$store.state.postwoman.settings.THEME_COLOR_VIBRANT;
if (vibrant == null) vibrant = true;
let vibrant =
this.$store.state.postwoman.settings.THEME_COLOR_VIBRANT || true;
document.documentElement.style.setProperty("--ac-color", color);
document.documentElement.style.setProperty(
"--act-color",

43
package-lock.json generated
View File

@@ -1437,11 +1437,11 @@
}
},
"@nuxtjs/google-analytics": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@nuxtjs/google-analytics/-/google-analytics-2.2.2.tgz",
"integrity": "sha512-uzjdj9GEvPa1jB2soPAeTH75u8qbEAMV36i9PLfkv23emfUIPF1PcgsRFJOBT1x19Zeg9ywC4U8q76li912vEQ==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@nuxtjs/google-analytics/-/google-analytics-2.2.3.tgz",
"integrity": "sha512-dPwgsRNECtZqHdmnbJRFy3T4DDVakrpeN7vM1DwAIV1FXYlIBMKvdi8nt1v8TPU4IZdaoXrQodfeNMCooPo/7g==",
"requires": {
"vue-analytics": "^5.18.0"
"vue-analytics": "^5.22.1"
}
},
"@nuxtjs/google-tag-manager": {
@@ -3721,9 +3721,9 @@
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
},
"cypress": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-3.8.1.tgz",
"integrity": "sha512-eLk5OpL/ZMDfQx9t7ZaDUAGVcvSOPTi7CG1tiUnu9BGk7caBiDhuFi3Tz/D5vWqH/Dl6Uh4X+Au4W+zh0xzbXw==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-3.8.2.tgz",
"integrity": "sha512-aTs0u3+dfEuLe0Ct0FVO5jD1ULqxbuqWUZwzBm0rxdLgLxIAOI/A9f/WkgY5Cfy1TEXe8pKC6Wal0ZpnkdGRSw==",
"dev": true,
"requires": {
"@cypress/listr-verbose-renderer": "0.4.1",
@@ -3737,6 +3737,7 @@
"commander": "2.15.1",
"common-tags": "1.8.0",
"debug": "3.2.6",
"eventemitter2": "4.1.2",
"execa": "0.10.0",
"executable": "4.1.1",
"extract-zip": "1.6.7",
@@ -4302,6 +4303,12 @@
"through": "~2.3.1"
}
},
"eventemitter2": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz",
"integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU=",
"dev": true
},
"eventemitter3": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
@@ -9352,22 +9359,22 @@
}
},
"sass-loader": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz",
"integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.1.tgz",
"integrity": "sha512-ANR2JHuoxzCI+OPDA0hJBv1Y16A2021hucu0S3DOGgpukKzq9W+4vX9jhIqs4qibT5E7RIRsHMMrN0kdF5nUig==",
"dev": true,
"requires": {
"clone-deep": "^4.0.1",
"loader-utils": "^1.2.3",
"neo-async": "^2.6.1",
"schema-utils": "^2.1.0",
"semver": "^6.3.0"
"schema-utils": "^2.6.1",
"semver": "^7.1.1"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.1.tgz",
"integrity": "sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A==",
"dev": true
}
}
@@ -10972,9 +10979,9 @@
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
},
"vue-analytics": {
"version": "5.19.1",
"resolved": "https://registry.npmjs.org/vue-analytics/-/vue-analytics-5.19.1.tgz",
"integrity": "sha512-vNMgKJn85sHw+7Q/iat+yVcUnHLd3sgXiFJvm7+NuyN3Wa+6TQY8G0yK/pUCJFW0bA+3QNAe6tPtUvq8mapG7A=="
"version": "5.22.1",
"resolved": "https://registry.npmjs.org/vue-analytics/-/vue-analytics-5.22.1.tgz",
"integrity": "sha512-HPKQMN7gfcUqS5SxoO0VxqLRRSPkG1H1FqglsHccz6BatBatNtm/Vyy8brApktZxNCfnAkrSVDpxg3/FNDeOgQ=="
},
"vue-client-only": {
"version": "2.0.0",

View File

@@ -19,7 +19,7 @@
},
"dependencies": {
"@nuxtjs/axios": "^5.9.2",
"@nuxtjs/google-analytics": "^2.2.2",
"@nuxtjs/google-analytics": "^2.2.3",
"@nuxtjs/google-tag-manager": "^2.3.1",
"@nuxtjs/pwa": "^3.0.0-beta.19",
"@nuxtjs/robots": "^2.4.2",
@@ -36,9 +36,9 @@
"yargs-parser": "^16.1.0"
},
"devDependencies": {
"cypress": "^3.8.1",
"cypress": "^3.8.2",
"node-sass": "^4.13.0",
"sass-loader": "^8.0.0",
"sass-loader": "^8.0.1",
"start-server-and-test": "^1.10.6"
}
}

View File

@@ -288,7 +288,10 @@
</label>
<div v-if="queryFields.length > 0" class="tab">
<div v-for="field in queryFields" :key="field.name">
<gql-field :gqlField="field" />
<gql-field
:gqlField="field"
:jumpTypeCallback="handleJumpToType"
/>
</div>
</div>
@@ -304,7 +307,10 @@
</label>
<div v-if="mutationFields.length > 0" class="tab">
<div v-for="field in mutationFields" :key="field.name">
<gql-field :gqlField="field" />
<gql-field
:gqlField="field"
:jumpTypeCallback="handleJumpToType"
/>
</div>
</div>
@@ -320,7 +326,10 @@
</label>
<div v-if="subscriptionFields.length > 0" class="tab">
<div v-for="field in subscriptionFields" :key="field.name">
<gql-field :gqlField="field" />
<gql-field
:gqlField="field"
:jumpTypeCallback="handleJumpToType"
/>
</div>
</div>
@@ -335,8 +344,15 @@
{{ $t("types") }}
</label>
<div v-if="gqlTypes.length > 0" class="tab">
<div v-for="type in gqlTypes" :key="type.name">
<gql-type :gqlType="type" />
<div
v-for="type in gqlTypes"
:key="type.name"
:id="`type_${type.name}`"
>
<gql-type
:gqlType="type"
:jumpTypeCallback="handleJumpToType"
/>
</div>
</div>
</section>
@@ -563,6 +579,24 @@ export default {
}
},
methods: {
handleJumpToType(type) {
const typesTab = document.getElementById("gqltypes-tab");
typesTab.checked = true;
const rootTypeName = this.resolveRootType(type).name;
const target = document.getElementById(`type_${rootTypeName}`);
if (target) {
target.scrollIntoView({
behavior: "smooth"
});
}
},
resolveRootType(type) {
let t = type;
while (t.ofType != null) t = t.ofType;
return t;
},
copySchema() {
this.$refs.copySchemaCode.innerHTML = this.doneButton;
const aux = document.createElement("textarea");

View File

@@ -75,7 +75,7 @@ export const state = () => ({
export const mutations = {
applySetting({ settings }, setting) {
if (
setting == null ||
setting === null ||
!(setting instanceof Array) ||
setting.length !== 2
) {