feat(server/frontend): wIP: auth flows are being revamped. HIC SVNT DRACONES
This commit is contained in:
Generated
+89
-29
@@ -1125,9 +1125,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz",
|
||||
"integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"parent-module": "^1.0.0",
|
||||
@@ -6221,9 +6221,9 @@
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.4.tgz",
|
||||
"integrity": "sha512-jE21SelIgWrGKoXGfGPA524Zt1IJFBnktwfFMHDlEYRx5FZOdc+4eEH9mkA6PuhExrq3HVpJnY8hMYUzAMl0OA=="
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.6.tgz",
|
||||
"integrity": "sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.7.0",
|
||||
@@ -6504,9 +6504,9 @@
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"eslint": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.15.0.tgz",
|
||||
"integrity": "sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz",
|
||||
"integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
@@ -6543,7 +6543,7 @@
|
||||
"semver": "^7.2.1",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"strip-json-comments": "^3.1.0",
|
||||
"table": "^5.2.3",
|
||||
"table": "^6.0.4",
|
||||
"text-table": "^0.2.0",
|
||||
"v8-compile-cache": "^2.0.3"
|
||||
},
|
||||
@@ -6557,6 +6557,12 @@
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
@@ -6648,15 +6654,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz",
|
||||
"integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -6702,6 +6714,28 @@
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"slice-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
||||
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -6717,6 +6751,32 @@
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.0.4.tgz",
|
||||
"integrity": "sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.4",
|
||||
"lodash": "^4.17.20",
|
||||
"slice-ansi": "^4.0.0",
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||
@@ -6871,24 +6931,24 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz",
|
||||
"integrity": "sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz",
|
||||
"integrity": "sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.2.0.tgz",
|
||||
"integrity": "sha512-4mt0yIv6rBDNtvis/g22a0ozJ12GfcdEzX77u0ICYjKlxOVtGrKGEvo0cbOObHaKDg9a9kJcoaNodqE4TPfS2A==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.3.0.tgz",
|
||||
"integrity": "sha512-4rc9xrZgwT4aLz3XE6lrHu+FZtDLWennYvtzVvvS81kW9c65U4DUzQQWAFjDCgCFvN6HYWxi7ueEtxZVSB+f0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-utils": "^2.1.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"semver": "^7.3.2",
|
||||
"vue-eslint-parser": "^7.2.0"
|
||||
"vue-eslint-parser": "^7.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
@@ -9674,9 +9734,9 @@
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.6.tgz",
|
||||
"integrity": "sha512-7vVuSEZ8g/HH3hK/BH/+7u/NJj7x9VY4EHzujLDcqAQLiOUeFJYAsfSAyoWtR17lKrx7b08qyIno4lffwrzTaA=="
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.7.tgz",
|
||||
"integrity": "sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
@@ -14487,9 +14547,9 @@
|
||||
}
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.2.0.tgz",
|
||||
"integrity": "sha512-uVcQqe8sUNzdHGcRHMd2Z/hl6qEaWrAmglTKP92Fnq9TYU9un8xsyFgEdFJaXh/1rd7h8Aic1GaiQow5nVneow==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.3.0.tgz",
|
||||
"integrity": "sha512-n5PJKZbyspD0+8LnaZgpEvNCrjQx1DyDHw8JdWwoxhhC+yRip4TAvSDpXGf9SWX6b0umeB5aR61gwUo6NVvFxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
@@ -14644,9 +14704,9 @@
|
||||
}
|
||||
},
|
||||
"vuetify": {
|
||||
"version": "2.3.21",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.3.21.tgz",
|
||||
"integrity": "sha512-c9FOjkpVPDoIim88wbfqSIuCsH3jtgQQBC1iMW+ZFxf/Bj+d73HySL2LhEnZwAQT7XTAUGfad4aLPfcNZzK5YQ=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.0.tgz",
|
||||
"integrity": "sha512-FBFAtg1ZnNwDBhMzENCzgh0hBV+HMjXejrxeRQqTfKPojKQSQFswtdHatUPmlkArDulZC73GRs2F/IwdF48o5g=="
|
||||
},
|
||||
"vuetify-image-input": {
|
||||
"version": "19.1.0",
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
background-color: #333333;
|
||||
color: #0A66FF;
|
||||
}
|
||||
@media screen and (prefers-color-scheme: light) {
|
||||
body {
|
||||
background-color: white;
|
||||
color: black;
|
||||
color: #0A66FF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -28,7 +28,7 @@
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div style='
|
||||
width: 500px;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
line-height: 300px;
|
||||
font-family: sans-serif !important;
|
||||
@@ -43,7 +43,7 @@
|
||||
font-size: 18px;
|
||||
'
|
||||
>
|
||||
🔓 Logging you in...
|
||||
Loading.
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.marked-preview h1 {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.marked-preview h2 {
|
||||
padding-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.marked-preview h3 {
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.marked-preview hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-active::before {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.v-card__text,
|
||||
.v-card__title {
|
||||
word-break: normal !important;
|
||||
}
|
||||
|
||||
/*.v-application code {
|
||||
background-color: #969696;
|
||||
color: #171717;
|
||||
padding: 0 0.4rem;
|
||||
}*/
|
||||
|
||||
/* TOOLTIPs */
|
||||
|
||||
.tooltip {
|
||||
display: block !important;
|
||||
z-index: 10000;
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-inner {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 5px 10px 4px;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='top'] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='top'] .tooltip-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='bottom'] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='bottom'] .tooltip-arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
top: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='right'] {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='right'] .tooltip-arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
left: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='left'] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='left'] .tooltip-arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-inner {
|
||||
background: #f9f9f9;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 30px rgba(black, 0.1);
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-arrow {
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, visibility 0.15s;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,235 +0,0 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-container v-if="!error" fluid fill-height>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col xs="10" sm="6" md="5" lg="4" xl="3" class="">
|
||||
<v-card class="elevation-20" rounded="lg">
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<p v-if="app.firstparty" class="title font-weight-light">
|
||||
Signing in to
|
||||
<b>{{ app.name }}</b>
|
||||
<v-tooltip v-if="app.firstparty" bottom>
|
||||
<template #activator="{ on }">
|
||||
<v-icon color="primary" style="margin-top: -6px" v-on="on">
|
||||
mdi-shield-check
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>Verified application.</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
<p
|
||||
v-if="!app.firstparty && !isFinalizing"
|
||||
class="title font-weight-light"
|
||||
>
|
||||
You need to sign in first
|
||||
to authorize
|
||||
<span class="primary--text">
|
||||
<b>{{ app.name }}</b>
|
||||
</span>
|
||||
<span v-if="app.author">
|
||||
by
|
||||
<b>{{ app.author }}.</b>
|
||||
</span>
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<router-view></router-view>
|
||||
<v-container v-if="!isFinalizing">
|
||||
<v-row>
|
||||
<template v-for="s in strategies">
|
||||
<v-col :key="s.name" cols="12" class="text-center py-0 my-0">
|
||||
<v-btn
|
||||
block
|
||||
large
|
||||
tile
|
||||
:color="s.color"
|
||||
dark
|
||||
class="my-2"
|
||||
:href="`${s.url}?appId=${appId}&challenge=${challenge}${
|
||||
suuid ? '&suuid=' + suuid : ''
|
||||
}`"
|
||||
>
|
||||
{{ s.name }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text class="blue-grey lighten-5">
|
||||
<div class="text-center">
|
||||
<b>{{ serverInfo.name }}</b>
|
||||
<br />
|
||||
deployed by
|
||||
<br />
|
||||
<b>{{ serverInfo.company }}</b>
|
||||
<br />
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<b>Terms of Service:</b>
|
||||
{{ serverInfo.termsOfService }}
|
||||
<br />
|
||||
<b>Support:</b>
|
||||
{{ serverInfo.adminContact }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-container v-else fluid fill-height>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col xs="10" sm="6" md="5" lg="4" xl="3" class="">
|
||||
<v-card class="elevation-20" color="red">
|
||||
<v-card-text class="white--text title">
|
||||
<v-icon color="white">mdi-bug</v-icon>
|
||||
{{ errorMessage }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import debounce from 'lodash.debounce'
|
||||
import crs from 'crypto-random-string'
|
||||
export default {
|
||||
name: 'AppAuth',
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
adminContact
|
||||
termsOfService
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
}
|
||||
authStrategies {
|
||||
id
|
||||
name
|
||||
color
|
||||
icon
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
app: {
|
||||
query() {
|
||||
return gql` query { app( id: "${this.appId}") { id name redirectUrl scopes {name description} } } `
|
||||
},
|
||||
skip() {
|
||||
return this.appId === null
|
||||
},
|
||||
data({ data }, key) {
|
||||
if (data.errors) {
|
||||
this.error = true
|
||||
this.errorMessage =
|
||||
'Invalid app authorization request: app not registered on this server.'
|
||||
console.log('Error: No such application!!!!!')
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
this.error = true
|
||||
this.errorMessage = `Invalid app authorization request: could not find app with id "${this.appId}" on this server.`
|
||||
console.log('Error: No such application')
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
data: () => ({
|
||||
serverInfo: {
|
||||
name: 'Loading',
|
||||
authStrategies: []
|
||||
},
|
||||
appId: null,
|
||||
challenge: null,
|
||||
suuid: null,
|
||||
app: {
|
||||
name: null,
|
||||
author: null,
|
||||
firstparty: null,
|
||||
scopes: []
|
||||
},
|
||||
loggedIn: null,
|
||||
profile: {
|
||||
user: null
|
||||
},
|
||||
user: {
|
||||
profile: null
|
||||
},
|
||||
error: false,
|
||||
errorMessage: null
|
||||
}),
|
||||
computed: {
|
||||
isFinalizing() {
|
||||
return this.$route.path.indexOf('/finalize') !== -1
|
||||
},
|
||||
hasLocalStrategy() {
|
||||
return this.serverInfo.authStrategies.findIndex((s) => s.id === 'local') !== -1
|
||||
},
|
||||
strategies() {
|
||||
return this.serverInfo.authStrategies.filter((s) => s.id !== 'local')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get('appId')
|
||||
let challenge = urlParams.get('challenge')
|
||||
let suuid = urlParams.get('suuid')
|
||||
this.suuid = suuid
|
||||
|
||||
if (!appId) this.appId = 'spklwebapp'
|
||||
else this.appId = appId
|
||||
if (!challenge && this.appId === 'spklwebapp') {
|
||||
if (localStorage.getItem('appChallenge')) {
|
||||
// Do nothing!
|
||||
} else {
|
||||
this.challenge = crs({
|
||||
length: 10
|
||||
})
|
||||
localStorage.setItem('appChallenge', this.challenge)
|
||||
}
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
} else {
|
||||
if (window.location.href.indexOf('/finalize') === -1) {
|
||||
this.error = true
|
||||
this.errorMessage = 'Invalid app authorization request: missing challenge.'
|
||||
}
|
||||
}
|
||||
},
|
||||
async beforeCreate() {
|
||||
// checks login
|
||||
let token = localStorage.getItem('AuthToken')
|
||||
if (token) {
|
||||
let testResponse = await fetch('/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `{ user { id } }`
|
||||
})
|
||||
})
|
||||
let data = (await testResponse.json()).data
|
||||
if (data.user) return true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToStrategy() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,279 +0,0 @@
|
||||
<template>
|
||||
<v-app id="speckle">
|
||||
<v-app-bar app color="background2">
|
||||
<v-container class="py-0 fill-height hidden-sm-and-down">
|
||||
<v-btn text to="/" active-class="no-active">
|
||||
<v-img contain max-height="30" max-width="30" src="./assets/logo.svg" />
|
||||
<div class="mt-1">
|
||||
<span class="primary--text"><b></b></span>
|
||||
</div>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-for="link in navLinks"
|
||||
:key="link.name"
|
||||
text
|
||||
class="text-uppercase"
|
||||
:to="link.link"
|
||||
>
|
||||
{{ link.name }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-responsive max-width="300">
|
||||
<search-bar />
|
||||
</v-responsive>
|
||||
<user-menu-top :user="user" />
|
||||
</v-container>
|
||||
<v-container class="hidden-md-and-up">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-menu
|
||||
:value="showMobileMenu"
|
||||
transition="slide-y-transition"
|
||||
bottom
|
||||
offset-y
|
||||
:close-on-content-click="false"
|
||||
min-width="100%"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on" @click="showMobileMenu = true">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="background2">
|
||||
<v-row>
|
||||
<v-col v-for="link in navLinks" :key="link.name" cols="12">
|
||||
<v-btn text block :to="link.link">
|
||||
{{ link.name }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" class="px-10 pb-7">
|
||||
<v-divider class="mb-5"></v-divider>
|
||||
<search-bar />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
<v-btn text to="/" active-class="no-active" icon>
|
||||
<v-img contain max-height="40" max-width="40" src="./assets/logo.svg" />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col class="text-right" style="margin-top: 5px">
|
||||
<user-menu-top :user="user" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
<v-main :style="background">
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import userQuery from './graphql/user.gql'
|
||||
import gql from 'graphql-tag'
|
||||
import UserMenuTop from './components/UserMenuTop'
|
||||
import SearchBar from './components/SearchBar'
|
||||
|
||||
export default {
|
||||
components: { UserMenuTop, SearchBar },
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
showMobileMenu: false,
|
||||
streams: { items: [] },
|
||||
selectedSearchResult: null,
|
||||
navLinks: [
|
||||
{ link: '/streams', name: 'streams' },
|
||||
{ link: '/profile', name: 'profile' },
|
||||
{ link: '/help', name: 'help' }
|
||||
]
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
prefetch: true,
|
||||
query: userQuery
|
||||
},
|
||||
streams: {
|
||||
query: gql`
|
||||
query Streams($query: String) {
|
||||
streams(query: $query) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
query: this.search
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.search || this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
background() {
|
||||
let theme = this.$vuetify.theme.dark ? 'dark' : 'light'
|
||||
return `background-color: ${this.$vuetify.theme.themes[theme].background};`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(_) {
|
||||
this.showMobileMenu = false
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.marked-preview h1 {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.marked-preview h2 {
|
||||
padding-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.marked-preview h3 {
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.marked-preview hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-active::before {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/*.v-application code {
|
||||
background-color: #969696;
|
||||
color: #171717;
|
||||
padding: 0 0.4rem;
|
||||
}*/
|
||||
|
||||
/* TOOLTIPs */
|
||||
|
||||
.tooltip {
|
||||
display: block !important;
|
||||
z-index: 10000;
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-inner {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 5px 10px 4px;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='top'] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='top'] .tooltip-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='bottom'] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='bottom'] .tooltip-arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
top: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='right'] {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='right'] .tooltip-arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
left: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='left'] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^='left'] .tooltip-arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-inner {
|
||||
background: #f9f9f9;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 30px rgba(black, 0.1);
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-arrow {
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, visibility 0.15s;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +1,41 @@
|
||||
import crs from "crypto-random-string"
|
||||
import crs from 'crypto-random-string'
|
||||
|
||||
const appId = "spklwebapp"
|
||||
const appSecret = "spklwebapp"
|
||||
const appId = 'spklwebapp'
|
||||
const appSecret = 'spklwebapp'
|
||||
|
||||
export async function signIn() {
|
||||
// Stage 0: if we have an access code, exchange it for a token
|
||||
const accessCode = new URLSearchParams(window.location.search).get(
|
||||
"access_code"
|
||||
)
|
||||
export async function checkAccessCodeAndGetTokens() {
|
||||
const accessCode = new URLSearchParams(window.location.search).get('access_code')
|
||||
if (accessCode) {
|
||||
let response = await getTokenFromAccessCode(accessCode)
|
||||
if (response.hasOwnProperty("token")) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (response.hasOwnProperty('token')) {
|
||||
localStorage.clear()
|
||||
localStorage.setItem("AuthToken", response.token)
|
||||
localStorage.setItem("RefreshToken", response.refreshToken)
|
||||
await prefetchUserAndSetSuuid()
|
||||
window.history.replaceState({}, document.title, "/")
|
||||
localStorage.setItem('AuthToken', response.token)
|
||||
localStorage.setItem('RefreshToken', response.refreshToken)
|
||||
window.history.replaceState({}, document.title, '/')
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
throw new Error(`No access code present in the url: ${window.location.href}`)
|
||||
}
|
||||
|
||||
// Stage 1: check if there is an existing valid token by pinging the graphql api
|
||||
let token = localStorage.getItem("AuthToken")
|
||||
if (token) {
|
||||
let data = await prefetchUserAndSetSuuid()
|
||||
// if res.data.user is non null, means the ping was ok & token is valid
|
||||
if (data.user) return true
|
||||
}
|
||||
|
||||
// Stage 2: check if we have a valid refresh token by using it!
|
||||
let refreshToken = localStorage.getItem("RefreshToken")
|
||||
|
||||
if (refreshToken) {
|
||||
let refreshResponse = await fetch("/auth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refreshToken: refreshToken,
|
||||
appId: appId,
|
||||
appSecret: appSecret
|
||||
})
|
||||
})
|
||||
|
||||
let data = await refreshResponse.json()
|
||||
|
||||
if (data.hasOwnProperty("token")) {
|
||||
localStorage.setItem("AuthToken", data.token)
|
||||
localStorage.setItem("RefreshToken", data.refreshToken)
|
||||
await prefetchUserAndSetSuuid()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// tried all avenues, means we need to init a full authorization flow.
|
||||
// this will essentially refresh the browser window, so no need to return.
|
||||
redirectToAuth()
|
||||
return false
|
||||
}
|
||||
|
||||
async function prefetchUserAndSetSuuid() {
|
||||
let token = localStorage.getItem("AuthToken")
|
||||
export async function prefetchUserAndSetSuuid() {
|
||||
let token = localStorage.getItem('AuthToken')
|
||||
if (token) {
|
||||
let testResponse = await fetch("/graphql", {
|
||||
method: "POST",
|
||||
let testResponse = await fetch('/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: "Bearer " + token,
|
||||
"Content-Type": "application/json"
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ query: `{ user { id suuid } }` })
|
||||
})
|
||||
|
||||
let data = (await testResponse.json()).data
|
||||
if (data.user) {
|
||||
localStorage.setItem("suuid", data.user.suuid)
|
||||
localStorage.setItem("uuid", data.user.id)
|
||||
localStorage.setItem('suuid', data.user.suuid)
|
||||
localStorage.setItem('uuid', data.user.id)
|
||||
}
|
||||
|
||||
return data
|
||||
@@ -83,17 +43,16 @@ async function prefetchUserAndSetSuuid() {
|
||||
}
|
||||
|
||||
export async function getTokenFromAccessCode(accessCode) {
|
||||
console.log("found local challenge: " + localStorage.getItem("appChallenge"))
|
||||
let response = await fetch("/auth/token", {
|
||||
method: "POST",
|
||||
let response = await fetch('/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessCode: accessCode,
|
||||
appId: appId,
|
||||
appSecret: appSecret,
|
||||
challenge: localStorage.getItem("appChallenge")
|
||||
challenge: localStorage.getItem('appChallenge')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -104,9 +63,68 @@ export async function getTokenFromAccessCode(accessCode) {
|
||||
export function redirectToAuth() {
|
||||
// Reaching this stage means we're initialising a full new auth flow,
|
||||
// TIP: also means we need to refresh the app challenge as well.
|
||||
localStorage.setItem("appChallenge", crs({ length: 10 }))
|
||||
localStorage.setItem('appChallenge', crs({ length: 10 }))
|
||||
// Finally, redirect to the auth lock.
|
||||
window.location = `/auth?app_id=spklwebapp&challenge=${localStorage.getItem(
|
||||
"appChallenge"
|
||||
)}`
|
||||
window.location = `/auth?appId=${appId}&challenge=${localStorage.getItem('appChallenge')}`
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export async function signIn() {
|
||||
// Stage 0: if we have an access code, exchange it for a token
|
||||
const accessCode = new URLSearchParams(window.location.search).get('access_code')
|
||||
if (accessCode) {
|
||||
let response = await getTokenFromAccessCode(accessCode)
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (response.hasOwnProperty('token')) {
|
||||
localStorage.clear()
|
||||
localStorage.setItem('AuthToken', response.token)
|
||||
localStorage.setItem('RefreshToken', response.refreshToken)
|
||||
await prefetchUserAndSetSuuid()
|
||||
window.history.replaceState({}, document.title, '/')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 1: check if there is an existing valid token by pinging the graphql api
|
||||
let token = localStorage.getItem('AuthToken')
|
||||
if (token) {
|
||||
let data = await prefetchUserAndSetSuuid()
|
||||
// if res.data.user is non null, means the ping was ok & token is valid
|
||||
if (data.user) return true
|
||||
}
|
||||
|
||||
// Stage 2: check if we have a valid refresh token by using it!
|
||||
let refreshToken = localStorage.getItem('RefreshToken')
|
||||
|
||||
if (refreshToken) {
|
||||
let refreshResponse = await fetch('/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refreshToken: refreshToken,
|
||||
appId: appId,
|
||||
appSecret: appSecret
|
||||
})
|
||||
})
|
||||
|
||||
let data = await refreshResponse.json()
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (data.hasOwnProperty('token')) {
|
||||
localStorage.setItem('AuthToken', data.token)
|
||||
localStorage.setItem('RefreshToken', data.refreshToken)
|
||||
await prefetchUserAndSetSuuid()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// tried all avenues, means we need to init a full authorization flow.
|
||||
// this will essentially refresh the browser window, so no need to return.
|
||||
redirectToAuth()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<v-card-title class="mr-8">
|
||||
<v-card-title class="mr-8 display-1">
|
||||
<router-link v-show="!isHomeRoute" :to="'/streams/' + stream.id" class="text-decoration-none">
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
@@ -44,14 +44,18 @@
|
||||
link sharing off
|
||||
</span>
|
||||
</p>
|
||||
<v-divider class="pb-2"></v-divider>
|
||||
<v-btn
|
||||
v-if="userRole === 'owner' && isHomeRoute"
|
||||
block
|
||||
small
|
||||
plain
|
||||
color="primary"
|
||||
text
|
||||
class="px-0"
|
||||
@click="editStreamDialog = true"
|
||||
>
|
||||
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
|
||||
Edit
|
||||
<v-icon small class="ml-3">mdi-cog-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-dialog v-model="editStreamDialog" max-width="500">
|
||||
<edit-stream-dialog
|
||||
@@ -83,9 +87,17 @@
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-btn v-if="userRole === 'owner'" block small @click="dialogShare = true">
|
||||
<v-divider class="pb-2 mt-2"></v-divider>
|
||||
<v-btn
|
||||
v-if="userRole === 'owner'"
|
||||
small
|
||||
plain
|
||||
color="primary"
|
||||
text
|
||||
@click="dialogShare = true"
|
||||
>
|
||||
<v-icon small class="mr-2">mdi-account-multiple</v-icon>
|
||||
Manage
|
||||
<v-icon small class="ml-3">mdi-account-multiple</v-icon>
|
||||
</v-btn>
|
||||
<v-dialog v-model="dialogShare" max-width="500">
|
||||
<stream-share-dialog
|
||||
@@ -153,4 +165,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-card class="elevation-0 transparent pa-5 pb-0">
|
||||
<v-card-text class="text-h3 text-sm-h4 text-md-h3 primary--text">
|
||||
<span class="primary--text"><b>Speckle</b></span>
|
||||
<span class="font-weight-light">, the data platform for the built environment.</span>
|
||||
</v-card-text>
|
||||
<div class="">
|
||||
<v-card-text class="text-h6 font-weight-regular">
|
||||
Speckle helps leading AEC companies freely exchange data between software silos and automate
|
||||
design and delivery processes:
|
||||
<span class="primary--text text--disabled">
|
||||
join 100s of designers, architects, engineers and developers building the digital future
|
||||
of AEC.
|
||||
</span>
|
||||
</v-card-text>
|
||||
</div>
|
||||
<v-card-text v-if="serverInfo">{{ serverInfo.name }} @ {{ serverInfo.company }}</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Blurb',
|
||||
props: ['serverInfo']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div v-if="strategies && strategies.length !== 0">
|
||||
<v-card-title class="justify-center py-2 body-1 text--secondary">
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
Sign in with
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
</v-card-title>
|
||||
<v-card-text class="pb-5">
|
||||
<template v-for="s in strategies">
|
||||
<v-col :key="s.name" cols="12" class="text-center py-1 my-0">
|
||||
<v-btn
|
||||
dark
|
||||
block
|
||||
:color="s.color"
|
||||
:href="`${s.url}?appId=${appId}&challenge=${challenge}${
|
||||
suuid ? '&suuid=' + suuid : ''
|
||||
}`"
|
||||
>
|
||||
<v-icon small class="mr-5">{{ s.icon }}</v-icon>
|
||||
{{ s.name }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Strategies',
|
||||
props: ['strategies', 'appId', 'challenge', 'suuid']
|
||||
}
|
||||
</script>
|
||||
@@ -4,12 +4,7 @@
|
||||
<v-card-title class="subtitle-1">Edit Server Info</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation
|
||||
@submit.prevent="agree"
|
||||
>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
@@ -25,11 +20,7 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-text-field
|
||||
v-model="server.company"
|
||||
filled
|
||||
label="Company"
|
||||
></v-text-field>
|
||||
<v-text-field v-model="server.company" filled label="Company"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
@@ -66,9 +57,7 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!valid" color="primary" text @click.native="agree">
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn :disabled="!valid" color="primary" text @click.native="agree">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -95,7 +84,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"server.name"(val) {
|
||||
'server.name'(val) {
|
||||
this.nameRules = []
|
||||
}
|
||||
},
|
||||
@@ -119,9 +108,9 @@ export default {
|
||||
},
|
||||
agree() {
|
||||
this.nameRules = [
|
||||
(v) => !!v || "Servers need a name too!",
|
||||
(v) => (v && v.length <= 100) || "Name must be less than 25 characters",
|
||||
(v) => (v && v.length >= 3) || "Name must be at least 3 characters"
|
||||
(v) => !!v || 'Servers need a name too!',
|
||||
(v) => (v && v.length <= 100) || 'Name must be less than 25 characters',
|
||||
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||
]
|
||||
|
||||
let self = this
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import App from './AppAuth.vue'
|
||||
import router from './router/auth-router'
|
||||
import vuetify from './plugins/vuetify';
|
||||
import { createProvider } from './vue-apollo'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
let urlParams = new URLSearchParams( window.location.search )
|
||||
let appId = urlParams.get( 'appId' ) || 'spklwebapp'
|
||||
let token = urlParams.get( 'token' )
|
||||
let refreshToken = urlParams.get( 'refreshToken' )
|
||||
|
||||
if ( token )
|
||||
localStorage.setItem( 'AuthToken', token )
|
||||
|
||||
if ( refreshToken )
|
||||
localStorage.setItem( 'RefreshToken', token )
|
||||
|
||||
|
||||
new Vue( {
|
||||
router,
|
||||
vuetify,
|
||||
apolloProvider: createProvider( ),
|
||||
render: h => h( App )
|
||||
} ).$mount( '#app' )
|
||||
@@ -1,11 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import App from './AppFrontend.vue'
|
||||
import App from './App.vue'
|
||||
|
||||
import { createProvider } from './vue-apollo'
|
||||
import { signIn } from './auth-helpers'
|
||||
import { signIn, checkAccessCodeAndGetTokens, prefetchUserAndSetSuuid } from './auth-helpers'
|
||||
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
@@ -24,26 +23,51 @@ Vue.use( VTooltip, { defaultDelay: 300 } )
|
||||
|
||||
import VueMatomo from 'vue-matomo'
|
||||
|
||||
/* Semicolon of Doom */
|
||||
;
|
||||
/* Semicolon of Doom */
|
||||
Vue.use( VueMatomo, {
|
||||
host: 'https://speckle.matomo.cloud',
|
||||
siteId: 4,
|
||||
router: router,
|
||||
userId: localStorage.getItem( 'suuid' )
|
||||
} )
|
||||
|
||||
( async ( ) => {
|
||||
let result = await signIn( )
|
||||
if ( !result ) return
|
||||
let AuthToken = localStorage.getItem('AuthToken')
|
||||
let RefreshToken = localStorage.getItem('RefreshToken')
|
||||
|
||||
Vue.use( VueMatomo, {
|
||||
host: 'https://speckle.matomo.cloud',
|
||||
siteId: 4,
|
||||
router: router,
|
||||
userId: localStorage.getItem( 'suuid' )
|
||||
} )
|
||||
if(AuthToken) {
|
||||
console.log( 'haz auth token')
|
||||
prefetchUserAndSetSuuid()
|
||||
.then(res => {
|
||||
initVue()
|
||||
})
|
||||
.catch( err => {
|
||||
if(RefreshToken) {
|
||||
// TODO: try to rotate token & prefetch user, etc.
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
checkAccessCodeAndGetTokens()
|
||||
.then(res => {
|
||||
return prefetchUserAndSetSuuid()
|
||||
})
|
||||
.then(res => {
|
||||
initVue()
|
||||
})
|
||||
.catch( err => {
|
||||
initVue()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function initVue() {
|
||||
new Vue( {
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
apolloProvider: createProvider( ),
|
||||
render: h => h( App )
|
||||
} ).$mount( '#app' )
|
||||
} )( )
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import "@mdi/font/css/materialdesignicons.css"
|
||||
import Vue from "vue"
|
||||
import Vuetify from "vuetify/lib"
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib'
|
||||
|
||||
Vue.use(Vuetify)
|
||||
|
||||
let darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
let hasDarkMode = localStorage.getItem('darkModeEnabled')
|
||||
if (!hasDarkMode && darkMediaQuery) {
|
||||
localStorage.setItem('darkModeEnabled', 'dark')
|
||||
}
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: "mdi"
|
||||
iconfont: 'mdi'
|
||||
},
|
||||
theme: {
|
||||
dark: localStorage.getItem("darkModeEnabled") === "dark",
|
||||
dark: localStorage.getItem('darkModeEnabled') === 'dark',
|
||||
themes: {
|
||||
light: {
|
||||
primary: "#047EFB", //blue
|
||||
secondary: "#7BBCFF", //light blue
|
||||
accent: "#FCF25E", //yellow
|
||||
error: "#FF5555", //red
|
||||
warning: "#FF9100", //orange
|
||||
info: "#313BCF", //dark blue
|
||||
success: "#4caf50",
|
||||
background: "#eeeeee",
|
||||
background2: "#ffffff",
|
||||
text: "#FFFFFF"
|
||||
primary: '#047EFB', //blue
|
||||
secondary: '#7BBCFF', //light blue
|
||||
accent: '#FCF25E', //yellow
|
||||
error: '#FF5555', //red
|
||||
warning: '#FF9100', //orange
|
||||
info: '#313BCF', //dark blue
|
||||
success: '#4caf50',
|
||||
background: '#eeeeee',
|
||||
background2: '#ffffff',
|
||||
text: '#FFFFFF'
|
||||
},
|
||||
dark: {
|
||||
primary: "#047EFB", //blue
|
||||
secondary: "#7BBCFF", //light blue
|
||||
accent: "#FCF25E", //yellow
|
||||
error: "#FF5555", //red
|
||||
warning: "#FF9100", //orange
|
||||
info: "#313BCF", //dark blue
|
||||
success: "#4caf50",
|
||||
background: "#3a3b3c",
|
||||
background2: "#303132"
|
||||
primary: '#047EFB', //blue
|
||||
secondary: '#7BBCFF', //light blue
|
||||
accent: '#FCF25E', //yellow
|
||||
error: '#FF5555', //red
|
||||
warning: '#FF9100', //orange
|
||||
info: '#313BCF', //dark blue
|
||||
success: '#4caf50',
|
||||
background: '#3a3b3c',
|
||||
background2: '#303132'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import Vue from "vue"
|
||||
import VueRouter from "vue-router"
|
||||
import Home from "../views/Home.vue"
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/auth",
|
||||
name: "Login",
|
||||
component: () => import("../views/auth/Login.vue")
|
||||
path: '/auth',
|
||||
name: 'Login',
|
||||
component: () => import('../views/auth/Login.vue')
|
||||
},
|
||||
{
|
||||
path: "/auth/login",
|
||||
name: "Login",
|
||||
component: () => import("../views/auth/Login.vue")
|
||||
path: '/auth/register',
|
||||
name: 'Register',
|
||||
component: () => import('../views/auth/Registration.vue')
|
||||
},
|
||||
{
|
||||
path: "/auth/register",
|
||||
name: "Register",
|
||||
component: () => import("../views/auth/Registration.vue")
|
||||
},
|
||||
{
|
||||
path: "/auth/finalize",
|
||||
name: "AuthorizeApp",
|
||||
component: () => import("../views/auth/AuthorizeApp.vue")
|
||||
path: '/auth/finalize',
|
||||
name: 'AuthorizeApp',
|
||||
component: () => import('../views/auth/AuthorizeApp.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
+118
-61
@@ -5,85 +5,125 @@ Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
meta: {
|
||||
title: 'Home | Speckle'
|
||||
},
|
||||
component: () => import('../views/Home.vue')
|
||||
},
|
||||
{
|
||||
path: '/streams',
|
||||
name: 'streams',
|
||||
meta: {
|
||||
title: 'Streams | Speckle'
|
||||
},
|
||||
component: () => import('../views/Streams.vue')
|
||||
},
|
||||
{
|
||||
path: '/streams/:streamId',
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
},
|
||||
component: () => import('../views/Stream.vue'),
|
||||
path: '/authn',
|
||||
name: 'Auth',
|
||||
component: () => import('../views/Auth.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'stream',
|
||||
path: 'login',
|
||||
name: 'Login',
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
title: 'Login | Speckle'
|
||||
},
|
||||
component: () => import('../views/StreamMain.vue')
|
||||
component: () => import('../views/auth/Login.vue')
|
||||
},
|
||||
{
|
||||
path: 'branches/:branchName',
|
||||
name: 'branch',
|
||||
path: 'register',
|
||||
name: 'Register',
|
||||
meta: {
|
||||
title: 'Branch | Speckle'
|
||||
title: 'Register | Speckle'
|
||||
},
|
||||
component: () => import('../views/Branch.vue')
|
||||
component: () => import('../views/auth/Registration.vue')
|
||||
},
|
||||
{
|
||||
path: 'commits/:commitId',
|
||||
name: 'commit',
|
||||
path: 'verify/:appId/:challenge',
|
||||
name: 'Authorize App',
|
||||
meta: {
|
||||
title: 'Commit | Speckle'
|
||||
title: 'Authorizing App | Speckle'
|
||||
},
|
||||
component: () => import('../views/Commit.vue')
|
||||
},
|
||||
{
|
||||
path: 'objects/:objectId',
|
||||
name: 'objects',
|
||||
meta: {
|
||||
title: 'Object | Speckle'
|
||||
},
|
||||
component: () => import('../views/Object.vue')
|
||||
component: () => import('../views/auth/AuthorizeApp.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
path: '/',
|
||||
meta: {
|
||||
title: 'Your Profile | Speckle'
|
||||
title: 'Home | Speckle'
|
||||
},
|
||||
component: () => import('../views/Profile.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile/:userId',
|
||||
name: 'user profile',
|
||||
meta: {
|
||||
title: 'User Profile | Speckle'
|
||||
},
|
||||
component: () => import('../views/ProfileUser.vue')
|
||||
},
|
||||
{
|
||||
path: '/help',
|
||||
name: 'help',
|
||||
meta: {
|
||||
title: 'Help | Speckle'
|
||||
},
|
||||
component: () => import('../views/Help.vue')
|
||||
component: () => import('../views/Frontend.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
meta: {
|
||||
title: 'Streams | Speckle'
|
||||
},
|
||||
component: () => import('../views/Home.vue')
|
||||
},
|
||||
{
|
||||
path: 'streams',
|
||||
name: 'streams',
|
||||
meta: {
|
||||
title: 'Streams | Speckle'
|
||||
},
|
||||
component: () => import('../views/Streams.vue')
|
||||
},
|
||||
{
|
||||
path: 'streams/:streamId',
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
},
|
||||
component: () => import('../views/Stream.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'stream',
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
},
|
||||
component: () => import('../views/StreamMain.vue')
|
||||
},
|
||||
{
|
||||
path: 'branches/:branchName',
|
||||
name: 'branch',
|
||||
meta: {
|
||||
title: 'Branch | Speckle'
|
||||
},
|
||||
component: () => import('../views/Branch.vue')
|
||||
},
|
||||
{
|
||||
path: 'commits/:commitId',
|
||||
name: 'commit',
|
||||
meta: {
|
||||
title: 'Commit | Speckle'
|
||||
},
|
||||
component: () => import('../views/Commit.vue')
|
||||
},
|
||||
{
|
||||
path: 'objects/:objectId',
|
||||
name: 'objects',
|
||||
meta: {
|
||||
title: 'Object | Speckle'
|
||||
},
|
||||
component: () => import('../views/Object.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'profile',
|
||||
meta: {
|
||||
title: 'Your Profile | Speckle'
|
||||
},
|
||||
component: () => import('../views/Profile.vue')
|
||||
},
|
||||
{
|
||||
path: 'profile/:userId',
|
||||
name: 'user profile',
|
||||
meta: {
|
||||
title: 'User Profile | Speckle'
|
||||
},
|
||||
component: () => import('../views/ProfileUser.vue')
|
||||
},
|
||||
{
|
||||
path: 'help',
|
||||
name: 'help',
|
||||
meta: {
|
||||
title: 'Help | Speckle'
|
||||
},
|
||||
component: () => import('../views/Help.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
@@ -117,6 +157,23 @@ const router = new VueRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (!(to.name === 'Login' || to.name === 'Register') && !localStorage.getItem('uuid')) {
|
||||
localStorage.setItem('shouldRedirectTo', to.name)
|
||||
return next({ name: 'Login' })
|
||||
} else if ((to.name === 'Login' || to.name === 'Register') && !!localStorage.getItem('uuid')) {
|
||||
return next({ name: 'home' })
|
||||
} else return next()
|
||||
|
||||
// else if (localStorage.getItem('uuid')) {
|
||||
// let shouldRedirectTo = localStorage.getItem('shouldRedirectTo')
|
||||
// localStorage.removeItem('shouldRedirectTo')
|
||||
// if (shouldRedirectTo) {
|
||||
// return next() // TODO: redirect to prev url
|
||||
// } else return next()
|
||||
// }
|
||||
})
|
||||
|
||||
//TODO: include stream name in page title eg `My Cool Stream | Speckle`
|
||||
router.afterEach((to, from) => {
|
||||
Vue.nextTick(() => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<v-app id="speckle-auth">
|
||||
<v-container fill-height fluid>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col v-if="showBlurb" cols="12" md="6" lg="6" xl="4">
|
||||
<blurb :server-info="serverInfo" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="4" xl="3">
|
||||
<router-view></router-view>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import Blurb from '../components/auth/Blurb'
|
||||
|
||||
export default {
|
||||
components: { Blurb },
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
showBlurb() {
|
||||
// return true
|
||||
return this.$route.name === 'Login' || this.$route.name === 'Register'
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<v-app id="speckle">
|
||||
<v-app-bar app color="background2">
|
||||
<v-container class="py-0 fill-height hidden-sm-and-down">
|
||||
<v-btn text to="/" active-class="no-active">
|
||||
<v-img contain max-height="30" max-width="30" src="@/assets/logo.svg" />
|
||||
<div class="mt-1">
|
||||
<span class="primary--text"><b></b></span>
|
||||
</div>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-for="link in navLinks"
|
||||
:key="link.name"
|
||||
text
|
||||
class="text-uppercase"
|
||||
:to="link.link"
|
||||
>
|
||||
{{ link.name }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-responsive max-width="300">
|
||||
<search-bar />
|
||||
</v-responsive>
|
||||
<user-menu-top :user="user" />
|
||||
</v-container>
|
||||
<v-container class="hidden-md-and-up">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-menu
|
||||
:value="showMobileMenu"
|
||||
transition="slide-y-transition"
|
||||
bottom
|
||||
offset-y
|
||||
:close-on-content-click="false"
|
||||
min-width="100%"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on" @click="showMobileMenu = true">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="background2">
|
||||
<v-row>
|
||||
<v-col v-for="link in navLinks" :key="link.name" cols="12">
|
||||
<v-btn text block :to="link.link">
|
||||
{{ link.name }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" class="px-10 pb-7">
|
||||
<v-divider class="mb-5"></v-divider>
|
||||
<search-bar />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
<v-btn text to="/" active-class="no-active" icon>
|
||||
<v-img contain max-height="40" max-width="40" src="@/assets/logo.svg" />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col class="text-right" style="margin-top: 5px">
|
||||
<user-menu-top :user="user" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
<v-main :style="background">
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import userQuery from '../graphql/user.gql'
|
||||
import UserMenuTop from '../components/UserMenuTop'
|
||||
import SearchBar from '../components/SearchBar'
|
||||
|
||||
export default {
|
||||
components: { UserMenuTop, SearchBar },
|
||||
data() {
|
||||
return {
|
||||
loggedIn: null,
|
||||
search: '',
|
||||
showMobileMenu: false,
|
||||
streams: { items: [] },
|
||||
selectedSearchResult: null,
|
||||
navLinks: [
|
||||
{ link: '/streams', name: 'streams' },
|
||||
{ link: '/profile', name: 'profile' },
|
||||
{ link: '/help', name: 'help' }
|
||||
]
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
user: {
|
||||
query: userQuery
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
background() {
|
||||
let theme = this.$vuetify.theme.dark ? 'dark' : 'light'
|
||||
return `background-color: ${this.$vuetify.theme.themes[theme].background};`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.showMobileMenu = false
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
@@ -29,12 +29,12 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col v-if="$apollo.loading" sm="12" md="8" lg="9" xl="7">
|
||||
<v-col v-if="$apollo.loading" sm="12" md="8" lg="9" xl="7" class="pt-10">
|
||||
<v-skeleton-loader type="list-item-two-line, list-item-three-line"></v-skeleton-loader>
|
||||
</v-col>
|
||||
<v-col v-if="recentActivity && !$apollo.loading" sm="12" md="8" lg="9" xl="7">
|
||||
<v-col v-if="recentActivity && !$apollo.loading" cols="12" sm="12" md="8" lg="9" xl="7" class="pt-10">
|
||||
<v-row>
|
||||
<v-col class="pt-0">
|
||||
<v-col xxxclass="pt-0">
|
||||
<v-card class="pa-5" elevation="0" rounded="lg" color="background2">
|
||||
<v-subheader class="text-uppercase">Recent activity:</v-subheader>
|
||||
<v-chip-group
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<v-col cols="12" sm="12" md="4" lg="3" xl="2">
|
||||
<user-info-card :user="user"></user-info-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="8" lg="9" xl="7">
|
||||
<v-col cols="12" sm="12" md="8" lg="9" xl="7" class="pt-10">
|
||||
<v-card v-if="user" class="mb-3">
|
||||
<v-card-text class="body-1">
|
||||
<span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<v-col cols="12" sm="12" md="4" lg="3" xl="2">
|
||||
<user-info-card :user="user"></user-info-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="8" lg="9" xl="10">
|
||||
<v-col cols="12" sm="12" md="8" lg="9" xl="10" class="pt-10">
|
||||
<v-card class="mb-3 elevation-0" color="background2">
|
||||
<v-card-title>
|
||||
{{ user.name }} has {{ user.streams.totalCount }} public streams and
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<v-col cols="12" sm="12" md="4" lg="3" xl="2">
|
||||
<sidebar-stream :user-role="userRole"></sidebar-stream>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="8" lg="9" xl="7">
|
||||
<v-col cols="12" sm="12" md="8" lg="9" xl="7" class="pt-10">
|
||||
<router-view :user-role="userRole"></router-view>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -1,164 +1,105 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row v-if="state === 0" style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<div>
|
||||
<p class="title font-weight-light text-center">
|
||||
Authorize
|
||||
<span class="primary--text">
|
||||
<b>{{ app.name }}</b>
|
||||
</span>
|
||||
by
|
||||
<b>{{ app.author }}</b>
|
||||
?
|
||||
</p>
|
||||
<p class="caption text-center">
|
||||
Clicking allow will redirect you to
|
||||
<i>{{ app.redirectUrl }}</i>
|
||||
</p>
|
||||
</div>
|
||||
<v-expansion-panels
|
||||
v-show="!app.firstparty"
|
||||
v-model="panel"
|
||||
multiple
|
||||
hover
|
||||
tile
|
||||
flat
|
||||
small
|
||||
>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header class="elevation-0">
|
||||
<b>Requested permissions:</b>
|
||||
<template #actions>
|
||||
<v-icon color="primary">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<ul class="my-3">
|
||||
<template v-for="scope in app.scopes">
|
||||
<li :key="scope.name">
|
||||
<b>{{ scope.name }}</b>
|
||||
: {{ scope.description }}
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn block tile color="primary" @click="allow">Allow</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn block tile color="secondary" @click="deny">Deny</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="state === 1">
|
||||
<v-col cols="12">
|
||||
<p class="title font-weight-light text-center">
|
||||
Permissions denied.
|
||||
<br />
|
||||
You can safely close this page.
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="state === 2">
|
||||
<v-col cols="12">
|
||||
<p class="title font-weight-light text-center">
|
||||
<b>Permissions granted.</b>
|
||||
<br />
|
||||
You can now safely close this page.
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar v-model="registrationError" multi-line>
|
||||
{{ errorMessage }}
|
||||
<v-btn color="red" text @click="registrationError = false">Close</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
<v-card v-if="!$apollo.loading" rounded="lg" class="py-4 elevation-10">
|
||||
<v-card-text class="text-h5 font-weight-regular text-center pt-10">
|
||||
<v-icon v-if="app.trustByDefault" class="mr-2 primary--text">mdi-shield-check</v-icon>
|
||||
<b class="primary--text">{{ app.name }}</b>
|
||||
is requesting access to your Speckle account.
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-expansion-panels v-if="!app.trustByDefault" v-model="panel" flat hover class="py-3">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header class="">
|
||||
<b>App Info & Requested permissions ({{ app.scopes.length }})</b>
|
||||
<template #actions>
|
||||
<v-icon color="primary">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content class="">
|
||||
<p>
|
||||
<b>Author:</b>
|
||||
{{ app.author.name }}
|
||||
<user-avatar
|
||||
:id="app.author.id"
|
||||
:name="app.author.name"
|
||||
:avatar="app.author.avatar"
|
||||
:size="20"
|
||||
class="ml-1"
|
||||
></user-avatar>
|
||||
</p>
|
||||
<p>
|
||||
<b>Description:</b>
|
||||
{{ app.description ? app.description : 'No description provided.' }}
|
||||
</p>
|
||||
<v-divider class="mb-4" />
|
||||
<p><b>Permissions:</b></p>
|
||||
<template v-for="scope in app.scopes">
|
||||
<p :key="scope.name" class="caption">
|
||||
<b>{{ scope.name }}</b>
|
||||
|
||||
<span class="text--disabled">{{ scope.description }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center px-10">
|
||||
<v-btn color="error" style="width: 50%" :href="`${app.redirectUrl}?denied=true`">Deny</v-btn>
|
||||
<v-btn color="primary" style="width: 50%" @click="allow">Allow</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import { onLogin } from "../../vue-apollo"
|
||||
import debounce from "lodash.debounce"
|
||||
import gql from 'graphql-tag'
|
||||
import UserAvatar from '../../components/UserAvatar'
|
||||
|
||||
export default {
|
||||
name: "AuthorizeApp",
|
||||
name: 'AuthorizeApp',
|
||||
components: { UserAvatar },
|
||||
apollo: {
|
||||
app: {
|
||||
query() {
|
||||
return gql` query { app( id: "${this.appId}") { id name redirectUrl scopes {name description} } } `
|
||||
},
|
||||
skip() {
|
||||
return this.appId === null
|
||||
},
|
||||
result({ data, loading, networkStatus }) {
|
||||
if (data.app.firstparty) {
|
||||
let redirectUrl =
|
||||
data.app.redirectUrl === "self" ? "/" : data.app.redirectUrl
|
||||
try {
|
||||
window.location = `${redirectUrl}?access_code=${this.accessCode}`
|
||||
} catch (err) {
|
||||
// Fetch?
|
||||
query: gql`
|
||||
query getApp($id: String!) {
|
||||
app(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
trustByDefault
|
||||
redirectUrl
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
}
|
||||
author {
|
||||
name
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.appId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
state: 0,
|
||||
currentUrl: window.location.origin,
|
||||
panel: [0],
|
||||
registrationError: false,
|
||||
errorMessage: "",
|
||||
appId: null,
|
||||
app: {
|
||||
name: null,
|
||||
author: null,
|
||||
firstparty: null,
|
||||
scopes: []
|
||||
},
|
||||
token: null,
|
||||
accessCode: null
|
||||
}),
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
this.appId = urlParams.get("appId") || "spklwebapp"
|
||||
this.accessCode = urlParams.get("access_code")
|
||||
if (!this.accessCode) {
|
||||
this.$router.push({
|
||||
name: "Login",
|
||||
query: {
|
||||
appId: urlParams.get("appId")
|
||||
}
|
||||
})
|
||||
return
|
||||
data() {
|
||||
return {
|
||||
panel: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deny() {
|
||||
this.state = 1
|
||||
window.history.replaceState({}, document.title, "/auth/finalize")
|
||||
fetch(`${this.app.redirectUrl}?success=false`, {
|
||||
method: "GET"
|
||||
})
|
||||
.then()
|
||||
.catch()
|
||||
window.location.replace(`${this.app.redirectUrl}?denied=true`)
|
||||
},
|
||||
async allow() {
|
||||
this.state = 2
|
||||
if (this.app.redirectUrl === "self")
|
||||
window.location = `${location.origin}/?access_code=${this.accessCode}`
|
||||
else {
|
||||
try {
|
||||
window.location = `${this.app.redirectUrl}?access_code=${this.accessCode}`
|
||||
} catch (err) {
|
||||
fetch(`${this.app.redirectUrl}?access_code=${this.accessCode}`, {
|
||||
method: "GET"
|
||||
})
|
||||
.then()
|
||||
.catch()
|
||||
}
|
||||
}
|
||||
window.location.replace(
|
||||
`${window.location.origin}/auth/accesscode?appId=${this.app.id}&challenge=${
|
||||
this.$route.params.challenge
|
||||
}&token=${localStorage.getItem('AuthToken')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,73 @@
|
||||
<template>
|
||||
<v-container v-if="hasLocalStrategy" fluid>
|
||||
<v-form ref="form">
|
||||
<v-row style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
label="your email"
|
||||
:rules="validation.emailRules"
|
||||
solo
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
block
|
||||
large
|
||||
color="primary"
|
||||
style="top: -22px"
|
||||
@click="loginUser"
|
||||
>
|
||||
Log in
|
||||
</v-btn>
|
||||
<p class="text-center">
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
block
|
||||
color="primary"
|
||||
:to="{
|
||||
name: 'Register',
|
||||
query: {
|
||||
appId: $route.query.appId,
|
||||
challenge: $route.query.challenge,
|
||||
suuid: $route.query.suuid
|
||||
}
|
||||
}"
|
||||
>
|
||||
Create Account
|
||||
</v-btn>
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-snackbar v-model="registrationError" multi-line>
|
||||
{{ errorMessage }}
|
||||
<v-btn color="red" text @click="registrationError = false">Close</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
<v-card class="elevation-20" rounded="lg">
|
||||
<v-card-title class="justify-center pt-10 pb-2">Interoperability in seconds</v-card-title>
|
||||
<strategies :strategies="strategies" :app-id="appId" :challenge="challenge" :suuid="suuid" />
|
||||
<div v-if="hasLocalStrategy">
|
||||
<v-card-title class="justify-center pb-5 pt-0 body-1 text--secondary">
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
Login with email & password
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
</v-card-title>
|
||||
<v-alert v-model="registrationError" type="error" :icon="null" text multi-line dismissible>
|
||||
<v-row align="center">
|
||||
<v-col class="grow">
|
||||
{{ errorMessage }}
|
||||
</v-col>
|
||||
<v-col v-if="errorMessage.toLowerCase().includes('email taken')" class="shrink">
|
||||
<v-btn color="primary" plain :to="loginRoute">Login</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-alert>
|
||||
<v-card-text>
|
||||
<v-form ref="form" class="px-3">
|
||||
<v-row style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
label="your email"
|
||||
:rules="validation.emailRules"
|
||||
filled
|
||||
single-line
|
||||
prepend-icon="mdi-email"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
filled
|
||||
single-line
|
||||
style="margin-top: -12px"
|
||||
prepend-icon="mdi-form-textbox-password"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn block large color="primary" @click="loginUser">Log in</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-title class="justify-center caption">
|
||||
<div class="mx-4 align-self-center">Don't have an account?</div>
|
||||
<div class="mx-4 align-self-center">
|
||||
<v-btn color="primary" plain text :to="registerRoute">Register</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import { onLogin } from "../../vue-apollo"
|
||||
import debounce from "lodash.debounce"
|
||||
import crs from "crypto-random-string"
|
||||
import gql from 'graphql-tag'
|
||||
import crs from 'crypto-random-string'
|
||||
import Blurb from '../../components/auth/Blurb'
|
||||
import Strategies from '../../components/auth/Strategies'
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
name: 'Login',
|
||||
components: { Blurb, Strategies },
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
@@ -92,38 +97,50 @@ export default {
|
||||
serverInfo: { authStrategies: [] },
|
||||
form: { email: null, password: null },
|
||||
validation: {
|
||||
passwordRules: [(v) => !!v || "Required"],
|
||||
passwordRules: [(v) => !!v || 'Required'],
|
||||
emailRules: [
|
||||
(v) => !!v || "E-mail is required",
|
||||
(v) => /.+@.+\..+/.test(v) || "E-mail must be valid"
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
]
|
||||
},
|
||||
registrationError: false,
|
||||
errorMessage: "",
|
||||
appId: null,
|
||||
errorMessage: '',
|
||||
serverApp: null,
|
||||
suuid: null
|
||||
appId: null,
|
||||
suuid: null,
|
||||
challenge: null
|
||||
}),
|
||||
computed: {
|
||||
strategies() {
|
||||
return this.serverInfo.authStrategies.filter((s) => s.id !== 'local')
|
||||
},
|
||||
hasLocalStrategy() {
|
||||
return (
|
||||
this.serverInfo.authStrategies.findIndex((s) => s.id === "local") !== -1
|
||||
)
|
||||
return this.serverInfo.authStrategies.findIndex((s) => s.id === 'local') !== -1
|
||||
},
|
||||
registerRoute() {
|
||||
return {
|
||||
name: 'Register',
|
||||
query: {
|
||||
appId: this.$route.query.appId,
|
||||
challenge: this.$route.query.challenge,
|
||||
suuid: this.$route.query.suuid
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get("appId")
|
||||
let challenge = urlParams.get("challenge")
|
||||
let suuid = urlParams.get("suuid")
|
||||
let appId = urlParams.get('appId')
|
||||
let challenge = urlParams.get('challenge')
|
||||
let suuid = urlParams.get('suuid')
|
||||
this.suuid = suuid
|
||||
|
||||
if (!appId) this.appId = "spklwebapp"
|
||||
if (!appId) this.appId = 'spklwebapp'
|
||||
else this.appId = appId
|
||||
|
||||
if (!challenge && this.appId === "spklwebapp") {
|
||||
if (!challenge && this.appId === 'spklwebapp') {
|
||||
this.challenge = crs({ length: 10 })
|
||||
localStorage.setItem("appChallenge", this.challenge)
|
||||
localStorage.setItem('appChallenge', this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
@@ -132,7 +149,7 @@ export default {
|
||||
async loginUser() {
|
||||
try {
|
||||
let valid = this.$refs.form.validate()
|
||||
if (!valid) throw new Error("Form validation failed")
|
||||
if (!valid) throw new Error('Form validation failed')
|
||||
|
||||
let user = {
|
||||
email: this.form.email,
|
||||
@@ -141,25 +158,23 @@ export default {
|
||||
|
||||
if (this.suuid) user.suuid = this.suuid
|
||||
|
||||
let res = await fetch(
|
||||
`/auth/local/login?appId=${this.appId}&challenge=${this.challenge}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
redirect: "follow", // obvs not working
|
||||
body: JSON.stringify(user)
|
||||
}
|
||||
)
|
||||
let res = await fetch(`/auth/local/login?appId=${this.appId}&challenge=${this.challenge}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
redirect: 'follow', // obvs not working
|
||||
body: JSON.stringify(user)
|
||||
})
|
||||
|
||||
let data = await res.json()
|
||||
console.log(data)
|
||||
|
||||
if (data.err) throw new Error(data.message)
|
||||
|
||||
if (res.redirected) {
|
||||
window.location = res.url
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Login failed")
|
||||
}
|
||||
} catch (err) {
|
||||
this.errorMessage = err.message
|
||||
this.registrationError = true
|
||||
|
||||
@@ -1,145 +1,137 @@
|
||||
<template>
|
||||
<v-container v-if="hasLocalStrategy" fluid>
|
||||
<v-form ref="form">
|
||||
<v-row style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
label="your email"
|
||||
:rules="validation.emailRules"
|
||||
solo
|
||||
></v-text-field>
|
||||
<v-card class="elevation-20" rounded="lg">
|
||||
<v-card-title class="justify-center pt-10 pb-2">Interoperability in seconds</v-card-title>
|
||||
<strategies :strategies="strategies" :app-id="appId" :challenge="challenge" :suuid="suuid" />
|
||||
<v-card-title class="justify-center pb-5 pt-0 body-1 text--secondary">
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
Create an account
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
</v-card-title>
|
||||
<v-alert v-model="registrationError" type="error" :icon="null" text multi-line dismissible>
|
||||
<v-row align="center">
|
||||
<v-col class="grow">
|
||||
{{ errorMessage }}
|
||||
</v-col>
|
||||
<v-col xs="6" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.firstName"
|
||||
label="first name"
|
||||
:rules="validation.nameRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col xs="6" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.lastName"
|
||||
label="last name"
|
||||
:rules="validation.nameRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12">
|
||||
<v-text-field
|
||||
v-model="form.company"
|
||||
label="company/team"
|
||||
:rules="validation.companyRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
@keydown="debouncedPwdTest"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.passwordConf"
|
||||
label="confirm password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" class="py-2 px-2" style="margin-top: -18px">
|
||||
<v-row no-gutters align="center">
|
||||
<!-- <v-col cols='3' class='caption flex-shrink-1 flex-grow-0'>Strength:</v-col> -->
|
||||
<v-col
|
||||
cols="12"
|
||||
class="flex-grow-1 flex-shrink-0"
|
||||
style="min-width: 100px; max-width: 100%"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-show="true"
|
||||
v-model="passwordStrength"
|
||||
height="5"
|
||||
class="mt-1 mb-0"
|
||||
:color="`${
|
||||
passwordStrength >= 75
|
||||
? 'green'
|
||||
: passwordStrength >= 50
|
||||
? 'orange'
|
||||
: 'red'
|
||||
}`"
|
||||
></v-progress-linear>
|
||||
</v-col>
|
||||
<v-col cols="12" class="caption text-center mt-3">
|
||||
{{
|
||||
this.pwdSuggestions
|
||||
? this.pwdSuggestions
|
||||
: this.form.password
|
||||
? "Looks good."
|
||||
: "Password strength"
|
||||
}}
|
||||
<span v-if="this.form.password !== this.form.passwordConf">
|
||||
<b>Passwords do not match.</b>
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
block
|
||||
large
|
||||
color="primary"
|
||||
style="margin-top: -0px"
|
||||
@click="registerUser"
|
||||
>
|
||||
Sign Up
|
||||
</v-btn>
|
||||
<p class="text-center">
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
block
|
||||
color="primary"
|
||||
:to="{
|
||||
name: 'Login',
|
||||
query: {
|
||||
appId: $route.query.appId,
|
||||
challenge: $route.query.challenge,
|
||||
suuid: $route.query.suuid
|
||||
}
|
||||
}"
|
||||
class="mt-5"
|
||||
>
|
||||
Login
|
||||
</v-btn>
|
||||
</p>
|
||||
<v-col v-if="errorMessage.toLowerCase().includes('email taken')" class="shrink">
|
||||
<v-btn color="primary" plain :to="loginRoute">Login</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar v-model="registrationError" multi-line>
|
||||
{{ errorMessage }}
|
||||
<v-btn color="red" text @click="registrationError = false">Close</v-btn>
|
||||
</v-snackbar>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</v-alert>
|
||||
<v-card-text>
|
||||
<v-form ref="form" class="px-3">
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
label="your email"
|
||||
:rules="validation.emailRules"
|
||||
filled
|
||||
single-line
|
||||
prepend-icon="mdi-email"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12">
|
||||
<v-text-field
|
||||
v-model="form.firstName"
|
||||
label="name"
|
||||
:rules="validation.nameRules"
|
||||
filled
|
||||
single-line
|
||||
style="margin-top: -12px"
|
||||
prepend-icon="mdi-account"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12">
|
||||
<v-text-field
|
||||
v-model="form.company"
|
||||
label="company/team"
|
||||
:rules="validation.companyRules"
|
||||
filled
|
||||
single-line
|
||||
style="margin-top: -12px"
|
||||
prepend-icon="mdi-office-building"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
filled
|
||||
single-line
|
||||
style="margin-top: -12px"
|
||||
prepend-icon="mdi-form-textbox-password"
|
||||
@keydown="debouncedPwdTest"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.passwordConf"
|
||||
label="confirm password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
filled
|
||||
single-line
|
||||
style="margin-top: -12px"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" class="py-2 pl-9" style="margin-top: -18px">
|
||||
<v-row v-show="passwordStrength !== 1 && this.form.password" no-gutters align="center">
|
||||
<v-col
|
||||
cols="12"
|
||||
class="flex-grow-1 flex-shrink-0"
|
||||
style="min-width: 100px; max-width: 100%"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-model="passwordStrength"
|
||||
height="5"
|
||||
class="mt-1 mb-0"
|
||||
:color="`${
|
||||
passwordStrength >= 75 ? 'green' : passwordStrength >= 50 ? 'orange' : 'red'
|
||||
}`"
|
||||
></v-progress-linear>
|
||||
</v-col>
|
||||
<v-col cols="12" class="caption text-center mt-3">
|
||||
{{
|
||||
this.pwdSuggestions
|
||||
? this.pwdSuggestions
|
||||
: this.form.password
|
||||
? 'Looks good.'
|
||||
: null
|
||||
}}
|
||||
<span v-if="this.form.password !== this.form.passwordConf">
|
||||
<b>Passwords do not match.</b>
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn block large color="primary" @click="registerUser">Create Account</v-btn>
|
||||
<p class="text-center"></p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-title class="justify-center caption">
|
||||
<div class="mx-4 align-self-center">Already have an account?</div>
|
||||
<div class="mx-4 align-self-center">
|
||||
<v-btn color="primary" plain text :to="loginRoute">Login</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import { onLogin } from "../../vue-apollo"
|
||||
import debounce from "lodash.debounce"
|
||||
import crs from "crypto-random-string"
|
||||
import gql from 'graphql-tag'
|
||||
import debounce from 'lodash.debounce'
|
||||
import crs from 'crypto-random-string'
|
||||
|
||||
import Blurb from '../../components/auth/Blurb'
|
||||
import Strategies from '../../components/auth/Strategies'
|
||||
|
||||
export default {
|
||||
name: "Registration",
|
||||
name: 'Registration',
|
||||
components: { Blurb, Strategies },
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
@@ -165,11 +157,72 @@ export default {
|
||||
`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
serverInfo: { authStrategies: [] },
|
||||
form: {
|
||||
email: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
company: null,
|
||||
password: null,
|
||||
passwordConf: null
|
||||
},
|
||||
registrationError: false,
|
||||
errorMessage: '',
|
||||
validation: {
|
||||
// companyRules: [(v) => !!v || 'Required'],
|
||||
passwordRules: [(v) => !!v || 'Required'],
|
||||
nameRules: [
|
||||
(v) => !!v || 'Required',
|
||||
(v) => (v && v.length <= 60) || 'Name must be less than 10 characters'
|
||||
],
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
]
|
||||
},
|
||||
passwordStrength: 1,
|
||||
pwdSuggestions: null,
|
||||
appId: null,
|
||||
challenge: null,
|
||||
suuid: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loginRoute() {
|
||||
return {
|
||||
name: 'Login',
|
||||
query: {
|
||||
appId: this.$route.query.appId,
|
||||
challenge: this.$route.query.challenge,
|
||||
suuid: this.$route.query.suuid
|
||||
}
|
||||
}
|
||||
},
|
||||
strategies() {
|
||||
return this.serverInfo.authStrategies.filter((s) => s.id !== 'local')
|
||||
},
|
||||
hasLocalStrategy() {
|
||||
return (
|
||||
this.serverInfo.authStrategies.findIndex((s) => s.id === "local") !== -1
|
||||
)
|
||||
return this.serverInfo.authStrategies.findIndex((s) => s.id === 'local') !== -1
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get('appId')
|
||||
let challenge = urlParams.get('challenge')
|
||||
let suuid = urlParams.get('suuid')
|
||||
|
||||
this.suuid = suuid
|
||||
console.log(this.suuid)
|
||||
if (!appId) this.appId = 'spklwebapp'
|
||||
else this.appId = appId
|
||||
|
||||
if (!challenge && this.appId === 'spklwebapp') {
|
||||
this.challenge = crs({ length: 10 })
|
||||
localStorage.setItem('appChallenge', this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -183,10 +236,9 @@ export default {
|
||||
async registerUser() {
|
||||
try {
|
||||
let valid = this.$refs.form.validate()
|
||||
if (!valid) throw new Error("Form validation failed")
|
||||
if (this.form.password !== this.form.passwordConf)
|
||||
throw new Error("Passwords do not match")
|
||||
if (this.passwordStrength < 3) throw new Error("Password too weak")
|
||||
if (!valid) return
|
||||
if (this.form.password !== this.form.passwordConf) throw new Error('Passwords do not match')
|
||||
if (this.passwordStrength < 3) throw new Error('Password too weak')
|
||||
|
||||
let user = {
|
||||
email: this.form.email,
|
||||
@@ -200,15 +252,18 @@ export default {
|
||||
let res = await fetch(
|
||||
`/auth/local/register?appId=${this.appId}&challenge=${this.challenge}`,
|
||||
{
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
redirect: "follow", // obvs not working
|
||||
redirect: 'follow', // obvs not working
|
||||
body: JSON.stringify(user)
|
||||
}
|
||||
)
|
||||
|
||||
let data = await res.json()
|
||||
if (data.err) throw new Error(data.err)
|
||||
|
||||
if (res.redirected) {
|
||||
window.location = res.url
|
||||
}
|
||||
@@ -217,54 +272,6 @@ export default {
|
||||
this.registrationError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
serverInfo: { authStrategies: [] },
|
||||
form: {
|
||||
email: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
company: null,
|
||||
password: null,
|
||||
passwordConf: null
|
||||
},
|
||||
registrationError: false,
|
||||
errorMessage: "",
|
||||
validation: {
|
||||
companyRules: [(v) => !!v || "Required"],
|
||||
passwordRules: [(v) => !!v || "Required"],
|
||||
nameRules: [
|
||||
(v) => !!v || "Required",
|
||||
(v) => (v && v.length <= 10) || "Name must be less than 10 characters"
|
||||
],
|
||||
emailRules: [
|
||||
(v) => !!v || "E-mail is required",
|
||||
(v) => /.+@.+\..+/.test(v) || "E-mail must be valid"
|
||||
]
|
||||
},
|
||||
passwordStrength: 1,
|
||||
pwdSuggestions: null,
|
||||
appId: null,
|
||||
challenge: null,
|
||||
suuid: null
|
||||
}),
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get("appId")
|
||||
let challenge = urlParams.get("challenge")
|
||||
let suuid = urlParams.get("suuid")
|
||||
|
||||
this.suuid = suuid
|
||||
console.log(this.suuid)
|
||||
if (!appId) this.appId = "spklwebapp"
|
||||
else this.appId = appId
|
||||
|
||||
if (!challenge && this.appId === "spklwebapp") {
|
||||
this.challenge = crs({ length: 10 })
|
||||
localStorage.setItem("appChallenge", this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+7
-14
@@ -1,26 +1,19 @@
|
||||
module.exports = {
|
||||
pages: {
|
||||
app: {
|
||||
entry: "src/main-frontend.js",
|
||||
title: "Speckle",
|
||||
template: "public/app.html",
|
||||
filename: "app.html"
|
||||
},
|
||||
auth: {
|
||||
entry: "src/main-auth.js",
|
||||
title: "Speckle Authentication",
|
||||
template: "public/auth.html",
|
||||
filename: "auth.html"
|
||||
entry: 'src/main-frontend.js',
|
||||
title: 'Speckle',
|
||||
template: 'public/app.html',
|
||||
filename: 'app.html'
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /^\/$/, to: "/app.html" },
|
||||
{ from: /\/auth/, to: "/auth.html" },
|
||||
{ from: /./, to: "/app.html" }
|
||||
{ from: /^\/$/, to: '/app.html' },
|
||||
{ from: /./, to: '/app.html' }
|
||||
]
|
||||
}
|
||||
},
|
||||
transpileDependencies: ["vuetify"]
|
||||
transpileDependencies: ['vuetify']
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user