feat(server/frontend): wIP: auth flows are being revamped. HIC SVNT DRACONES

This commit is contained in:
Dimitrie Stefanescu
2020-12-26 20:39:35 +02:00
parent ad9b917a64
commit a5542cf8cd
26 changed files with 1207 additions and 1253 deletions
+89 -29
View File
@@ -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",
+5 -5
View File
@@ -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>
-19
View File
@@ -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>
+170
View File
@@ -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>
-235
View File
@@ -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>
&nbsp;{{ 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>
-279
View File
@@ -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>
+87 -69
View File
@@ -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
}
+17 -6
View File
@@ -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 @@
&nbsp; 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>
+25
View File
@@ -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
-26
View File
@@ -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' )
+41 -17
View File
@@ -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' )
} )( )
}
+30 -24
View File
@@ -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'
}
}
}
+13 -18
View File
@@ -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
View File
@@ -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(() => {
+45
View File
@@ -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>
+125
View File
@@ -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>
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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>
+85 -144
View File
@@ -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>
&nbsp;
<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')}`
)
}
}
}
+106 -91
View File
@@ -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
+199 -192
View File
@@ -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
View File
@@ -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']
}