forked from platypush/platypush
[#419] API tokens - frontend implementation.
This commit is contained in:
parent
a8343cb45b
commit
c13623c3f7
9 changed files with 647 additions and 348 deletions
|
@ -4,7 +4,7 @@
|
|||
<Application v-if="selectedPanel === 'application'" />
|
||||
<Users :session-token="sessionToken" :current-user="currentUser"
|
||||
v-if="selectedPanel === 'users' && currentUser" />
|
||||
<Token :session-token="sessionToken" :current-user="currentUser"
|
||||
<Tokens :current-user="currentUser"
|
||||
v-else-if="selectedPanel === 'tokens' && currentUser" />
|
||||
</main>
|
||||
</div>
|
||||
|
@ -12,13 +12,13 @@
|
|||
|
||||
<script>
|
||||
import Application from "@/components/panels/Settings/Application";
|
||||
import Token from "@/components/panels/Settings/Token";
|
||||
import Tokens from "@/components/panels/Settings/Tokens/Index";
|
||||
import Users from "@/components/panels/Settings/Users";
|
||||
import Utils from "@/Utils";
|
||||
|
||||
export default {
|
||||
name: "Settings",
|
||||
components: {Application, Users, Token},
|
||||
components: {Application, Users, Tokens},
|
||||
mixins: [Utils],
|
||||
emits: ['change-page'],
|
||||
|
||||
|
@ -39,16 +39,9 @@ export default {
|
|||
async refresh() {
|
||||
this.sessionToken = this.getCookies()['session_token']
|
||||
this.currentUser = await this.request('user.get_user_by_session', {session_token: this.sessionToken})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
selectedPanel(value) {
|
||||
this.setUrlArgs({page: value})
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
updatePage() {
|
||||
const args = this.getUrlArgs()
|
||||
let page = null
|
||||
if (args.page?.length) {
|
||||
|
@ -58,13 +51,27 @@ export default {
|
|||
}
|
||||
|
||||
this.$emit('change-page', page)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
selectedPanel(value) {
|
||||
this.setUrlArgs({page: value})
|
||||
},
|
||||
|
||||
$route() {
|
||||
this.updatePage()
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.updatePage()
|
||||
await this.refresh()
|
||||
this.setUrlArgs({page: this.selectedPanel})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
$header-height: 3em;
|
||||
|
||||
.settings-container {
|
||||
|
@ -90,19 +97,21 @@ $header-height: 3em;
|
|||
}
|
||||
}
|
||||
|
||||
@include until($tablet) {
|
||||
main {
|
||||
height: calc(100% - #{$header-height});
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@include from($tablet) {
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
|
|
|
@ -1,322 +0,0 @@
|
|||
<template>
|
||||
<div class="token-container">
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<Modal ref="tokenModal">
|
||||
<div class="token-container">
|
||||
<label>
|
||||
This is your generated token. Treat it carefully and do not share it with untrusted parties.<br/>
|
||||
Also, make sure to save it - it WILL NOT be displayed again.
|
||||
</label>
|
||||
|
||||
<textarea class="token" v-text="token" @focus="onTokenSelect" />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal ref="sessionTokenModal">
|
||||
<div class="token-container">
|
||||
<label>
|
||||
This is your current session token.
|
||||
It will be invalidated once you log out of the current session.
|
||||
</label>
|
||||
|
||||
<textarea class="token" v-text="sessionToken" @focus="onTokenSelect" />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div class="body">
|
||||
<div class="description">
|
||||
<p>
|
||||
Platypush provides two types of tokens:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>JWT tokens</b> are bearer-only, and they contain encrypted
|
||||
authentication information.<br/>
|
||||
They can be used as permanent or time-based tokens to
|
||||
authenticate with the Platypush API.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>Session tokens</b> are randomly generated tokens stored on the
|
||||
application database. A session token generated in this session
|
||||
will expire when you log out of it.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>Generate a JWT authentication token that can be used for API calls to the <code>/execute</code> endpoint.</p><br/>
|
||||
<p>You can include the token in your requests in any of the following ways:</p>
|
||||
|
||||
<ul>
|
||||
<li>Specify it on the <code>Authorization: Bearer</code> header;</li>
|
||||
<li>Specify it on the <code>X-Token</code> header;</li>
|
||||
<li>
|
||||
Specify it as a URL parameter: <code>http://site:8008/execute?token=...</code>
|
||||
for a JWT token and <code>...?session_token=...</code> for a
|
||||
session token;
|
||||
</li>
|
||||
<li>Specify it on the body of your JSON request:
|
||||
<code>{"type":"request", "action", "...", "token":"..."}</code> for
|
||||
a JWT token, or <code>"session_token"</code> for a session token.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Confirm your credentials in order to generate a new JWT token.</p>
|
||||
<p>
|
||||
<i>Show session token</i> will instead show the token cookie associated
|
||||
to the current session.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<form @submit.prevent="generateToken" ref="generateTokenForm">
|
||||
<label>
|
||||
<span>Username</span>
|
||||
<span>
|
||||
<input type="text" name="username" :value="currentUser.username" disabled>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Confirm password</span>
|
||||
<span>
|
||||
<input type="password" name="password">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Token validity in days</span>
|
||||
<span>
|
||||
<input type="text" name="validityDays">
|
||||
</span>
|
||||
<span class="note">
|
||||
Decimal values are also supported - e.g. <i>0.5</i> means half a
|
||||
day (12 hours). An empty or zero value means that the token has
|
||||
no expiry date.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="submit" class="btn btn-primary" value="Generate JWT token">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="button" class="btn btn-default" value="Show session token"
|
||||
@click.stop="$refs.sessionTokenModal.show()">
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Loading from "@/components/Loading";
|
||||
import Utils from "@/Utils";
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
export default {
|
||||
name: "Token",
|
||||
components: {Modal, Loading},
|
||||
mixins: [Utils],
|
||||
|
||||
props: {
|
||||
currentUser: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
sessionToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
token: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async generateToken(event) {
|
||||
const username = this.currentUser.username
|
||||
const password = event.target.password.value
|
||||
let validityDays = event.target.validityDays?.length ? parseInt(event.target.validityDays.value) : 0
|
||||
if (!validityDays)
|
||||
validityDays = null
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
this.token = (await axios.post('/auth', {
|
||||
username: username,
|
||||
password: password,
|
||||
expiry_days: validityDays,
|
||||
})).data.token
|
||||
|
||||
if (this.token?.length)
|
||||
this.$refs.tokenModal.show()
|
||||
} catch (e) {
|
||||
console.error(e.toString())
|
||||
this.notify({
|
||||
text: e.toString(),
|
||||
error: true,
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
onTokenSelect(event) {
|
||||
event.target.select()
|
||||
document.execCommand('copy')
|
||||
|
||||
this.notify({
|
||||
text: 'Token copied to clipboard',
|
||||
image: {
|
||||
iconClass: 'fa fa-check',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.token-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: .15em;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
background: $background-color;
|
||||
display: flex;
|
||||
|
||||
.description {
|
||||
text-align: left;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 1em .5em;
|
||||
|
||||
li {
|
||||
list-style: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 250pt;
|
||||
|
||||
.note {
|
||||
display: block;
|
||||
font-size: .75em;
|
||||
margin: -.75em 0 2em 0;
|
||||
}
|
||||
|
||||
span {
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type=password] {
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.modal {
|
||||
.content {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.token-container {
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10em;
|
||||
margin-top: 1em;
|
||||
border-radius: 1em;
|
||||
border: none;
|
||||
background: $active-glow-bg-1;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: calc(#{$desktop} - 1px)) {
|
||||
.token-container {
|
||||
.body {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
justify-content: center;
|
||||
box-shadow: $border-shadow-top;
|
||||
margin-top: -1em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
.token-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.description {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 50%;
|
||||
justify-content: right;
|
||||
padding: 1em;
|
||||
|
||||
label {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
max-width: 650pt;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
margin-top: 1.5em;
|
||||
border-radius: 1em;
|
||||
border: $default-border-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div class="token-container">
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<Modal ref="tokenModal">
|
||||
<div class="token-container">
|
||||
<label>
|
||||
This is your generated token. Treat it carefully and do not share it with untrusted parties.<br/>
|
||||
Also, make sure to save it - it WILL NOT be displayed again.
|
||||
</label>
|
||||
|
||||
<textarea class="token" v-text="token" @focus="copyToClipboard($event.target.value)" />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal title="Generate an API token"
|
||||
ref="tokenParamsModal"
|
||||
@open="$nextTick(() => $refs.password.focus())"
|
||||
@close="$refs.generateTokenForm.reset()">
|
||||
<div class="form-container">
|
||||
<p>Confirm your credentials in order to generate a new API token.</p>
|
||||
|
||||
<form @submit.prevent="generateToken" ref="generateTokenForm">
|
||||
<label>
|
||||
<span>Confirm password</span>
|
||||
<span>
|
||||
<input type="password" name="password" ref="password" placeholder="Password">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>
|
||||
A friendly name used to identify this token - such as <code>My
|
||||
App</code> or <code>My Site</code>.
|
||||
</span>
|
||||
<span>
|
||||
<input type="text" name="name" placeholder="Token name">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Token validity in days</span>
|
||||
<span>
|
||||
<input type="text" name="validityDays" placeholder="Validity in days">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<span class="note">
|
||||
Decimal values are also supported - e.g. <i>0.5</i> means half a
|
||||
day (12 hours). An empty or zero value means that the token has
|
||||
no expiry date.
|
||||
</span>
|
||||
|
||||
<label>
|
||||
<input type="submit" class="btn btn-primary" value="Generate API Token">
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div class="body">
|
||||
<label class="generate-btn-container">
|
||||
<button class="btn btn-primary" @click="$refs.tokenParamsModal.show()">
|
||||
Generate API Token
|
||||
</button>
|
||||
</label>
|
||||
|
||||
<p>
|
||||
<b>API tokens</b> are randomly generated tokens that are stored
|
||||
encrypted on the server, and can be used to authenticate with the
|
||||
Platypush API.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When compared to the
|
||||
<a href="/#settings?page=tokens&type=jwt">JWT tokens</a>, API tokens
|
||||
have the following advantages:
|
||||
|
||||
<ul>
|
||||
<li>They can be revoked at any time by the user who generated
|
||||
them, while JWT tokens can only be revoked by changing the
|
||||
user's password.</li>
|
||||
<li>Their payload is random and not generated from the user's
|
||||
password, so even if an attacker gains access to the server's
|
||||
encryption keys, they cannot impersonate the user.</li>
|
||||
<li>They can be generated with a friendly name that can be used
|
||||
to identify the token.</li>
|
||||
</ul>
|
||||
|
||||
<Description />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Description from "./Description";
|
||||
import Loading from "@/components/Loading";
|
||||
import Utils from "@/Utils";
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
export default {
|
||||
name: "Token",
|
||||
mixins: [Utils],
|
||||
components: {
|
||||
Description,
|
||||
Loading,
|
||||
Modal,
|
||||
},
|
||||
|
||||
props: {
|
||||
currentUser: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
token: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async generateToken(event) {
|
||||
const username = this.currentUser.username
|
||||
const password = event.target.password.value
|
||||
const name = event.target.name.value
|
||||
let validityDays = event.target.validityDays?.length ? parseInt(event.target.validityDays.value) : 0
|
||||
if (!validityDays)
|
||||
validityDays = null
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
this.token = (await axios.post('/auth?type=token', {
|
||||
username: username,
|
||||
password: password,
|
||||
name: name,
|
||||
expiry_days: validityDays,
|
||||
})).data.token
|
||||
|
||||
if (this.token?.length)
|
||||
this.$refs.tokenModal.show()
|
||||
} catch (e) {
|
||||
console.error(e.toString())
|
||||
this.notify({
|
||||
text: e.toString(),
|
||||
error: true,
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "style.scss";
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<p>
|
||||
You can use your token to authenticate calls to the <code>/execute</code> endpoint or the Websocket routes.<br/><br/>
|
||||
|
||||
You can include the token in your requests in any of the following ways:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Specify it on the <code>Authorization: Bearer <token></code>
|
||||
header (replace <code><token></code> with your token).
|
||||
</li>
|
||||
<li>
|
||||
Specify it on the <code>X-Token <token></code> header (replace
|
||||
<code><token></code> with your token).
|
||||
</li>
|
||||
<li>
|
||||
Specify it as a URL parameter: <code>http://site:8008/execute?token=...</code>.
|
||||
</li>
|
||||
<li>
|
||||
Specify it on the body of your JSON request:
|
||||
<code>{"type":"request", "action", "...", "token":"..."}</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</template>
|
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div class="tokens-container">
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<div class="main" v-else>
|
||||
<div class="header">
|
||||
<div class="tabs-container">
|
||||
<Tabs>
|
||||
<Tab :selected="tokenType === 'api'"
|
||||
@input="tokenType = 'api'">
|
||||
API Tokens
|
||||
</Tab>
|
||||
|
||||
<Tab :selected="tokenType === 'jwt'"
|
||||
@input="tokenType = 'jwt'">
|
||||
JWT Tokens
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<JwtToken v-if="tokenType === 'jwt'"
|
||||
:current-user="currentUser" />
|
||||
|
||||
<ApiToken v-else
|
||||
:current-user="currentUser" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApiToken from "./ApiToken"
|
||||
import JwtToken from "./JwtToken"
|
||||
import Loading from "@/components/Loading"
|
||||
import Tab from "@/components/elements/Tab"
|
||||
import Tabs from "@/components/elements/Tabs"
|
||||
import Utils from "@/Utils"
|
||||
|
||||
export default {
|
||||
mixins: [Utils],
|
||||
components: {
|
||||
ApiToken,
|
||||
JwtToken,
|
||||
Loading,
|
||||
Tab,
|
||||
Tabs,
|
||||
},
|
||||
|
||||
props: {
|
||||
currentUser: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
token: null,
|
||||
tokenType: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
refresh() {
|
||||
const args = this.getUrlArgs()
|
||||
this.$nextTick(() => {
|
||||
this.tokenType = args.type?.length ? args.type : 'api'
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
tokenType(value) {
|
||||
this.setUrlArgs({type: value})
|
||||
},
|
||||
|
||||
$route() {
|
||||
this.refresh()
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.setUrlArgs({type: null})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$header-height: 4em;
|
||||
|
||||
.tokens-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: $header-height;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: $header-height;
|
||||
margin-top: -0.2em;
|
||||
}
|
||||
|
||||
.body {
|
||||
height: calc(100% - #{$header-height} - 0.2em);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div class="token-container">
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<Modal ref="tokenModal">
|
||||
<div class="token-container">
|
||||
<label>
|
||||
This is your generated token. Treat it carefully and do not share it with untrusted parties.<br/>
|
||||
Also, make sure to save it - it WILL NOT be displayed again.
|
||||
</label>
|
||||
|
||||
<textarea class="token" v-text="token" @focus="copyToClipboard($event.target.value)" />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal title="Generate a JWT token"
|
||||
ref="tokenParamsModal"
|
||||
@open="$nextTick(() => $refs.password.focus())"
|
||||
@close="$refs.generateTokenForm.reset()">
|
||||
<div class="form-container">
|
||||
<p>Confirm your credentials in order to generate a new JWT token.</p>
|
||||
|
||||
<form @submit.prevent="generateToken" ref="generateTokenForm">
|
||||
<label>
|
||||
<span>Confirm password</span>
|
||||
<span>
|
||||
<input type="password" name="password" ref="password" placeholder="Password">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Token validity in days</span>
|
||||
<span>
|
||||
<input type="text" name="validityDays" placeholder="Validity in days">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<span class="note">
|
||||
Decimal values are also supported - e.g. <i>0.5</i> means half a
|
||||
day (12 hours). An empty or zero value means that the token has
|
||||
no expiry date.
|
||||
</span>
|
||||
|
||||
<label>
|
||||
<input type="submit" class="btn btn-primary" value="Generate JWT Token">
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div class="body">
|
||||
<label class="generate-btn-container">
|
||||
<button class="btn btn-primary" @click="$refs.tokenParamsModal.show()">
|
||||
Generate JWT Token
|
||||
</button>
|
||||
</label>
|
||||
|
||||
<p>
|
||||
<b>JWT tokens</b> are bearer-only, and they contain encrypted
|
||||
authentication information.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
They can be used as permanent or time-based tokens to authenticate
|
||||
with the Platypush API.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When compared to the standard
|
||||
<a href="/#settings?page=tokens&type=api">API tokens</a>, JWT tokens
|
||||
have the following pros:
|
||||
|
||||
<ul>
|
||||
<li>They are not stored on the server, so compromising the server
|
||||
does not necessarily compromise the tokens too.</li>
|
||||
</ul>
|
||||
|
||||
And the following cons:
|
||||
|
||||
<ul>
|
||||
<li>They are not revocable - once generated, they can be used
|
||||
indefinitely until they expire.</li>
|
||||
<li>The only way to revoke a JWT token is to change the user's
|
||||
password. However, if a user changes their password, all the
|
||||
JWT tokens generated with the old password will be
|
||||
invalidated.</li>
|
||||
<li>Their payload is the encrypted representation of the user's
|
||||
credentials, but without any OTP information, so an attacker
|
||||
gains access to the user's credentials and the server's
|
||||
encryption keys they can impersonate the user indefinitely
|
||||
bypassing 2FA.</li>
|
||||
</ul>
|
||||
|
||||
For these reasons, it is recommended to use generic API tokens over JWT
|
||||
tokens for most use cases.<br/><br/>
|
||||
|
||||
<Description />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Description from "./Description";
|
||||
import Loading from "@/components/Loading";
|
||||
import Utils from "@/Utils";
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
export default {
|
||||
name: "Token",
|
||||
components: {
|
||||
Description,
|
||||
Loading,
|
||||
Modal,
|
||||
},
|
||||
mixins: [Utils],
|
||||
|
||||
props: {
|
||||
currentUser: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
token: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async generateToken(event) {
|
||||
const username = this.currentUser.username
|
||||
const password = event.target.password.value
|
||||
let validityDays = event.target.validityDays?.length ? parseInt(event.target.validityDays.value) : 0
|
||||
if (!validityDays)
|
||||
validityDays = null
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
this.token = (await axios.post('/auth?type=jwt', {
|
||||
username: username,
|
||||
password: password,
|
||||
expiry_days: validityDays,
|
||||
})).data.token
|
||||
|
||||
if (this.token?.length)
|
||||
this.$refs.tokenModal.show()
|
||||
} catch (e) {
|
||||
console.error(e.toString())
|
||||
this.notify({
|
||||
text: e.toString(),
|
||||
error: true,
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "style.scss";
|
||||
</style>
|
|
@ -0,0 +1,124 @@
|
|||
.token-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: .15em;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
background: $background-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 1em .5em;
|
||||
|
||||
li {
|
||||
list-style: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 30em;
|
||||
|
||||
.note {
|
||||
display: block;
|
||||
font-size: .75em;
|
||||
margin: -.75em 0 2em 0;
|
||||
}
|
||||
|
||||
span {
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.generate-btn-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=password] {
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.modal {
|
||||
.content {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.token-container {
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10em;
|
||||
margin-top: 1em;
|
||||
border-radius: 1em;
|
||||
border: none;
|
||||
background: $active-glow-bg-1;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: calc(#{$desktop} - 1px)) {
|
||||
.form-container {
|
||||
justify-content: center;
|
||||
box-shadow: $border-shadow-top;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
.token-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.body {
|
||||
max-width: 650pt;
|
||||
justify-content: left;
|
||||
margin-top: 1.5em;
|
||||
border-radius: 1em;
|
||||
border: $default-border-2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,11 @@ button, .btn, .btn-default {
|
|||
border: $selected-border;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
border: 1px solid $default-hover-fg;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@ input[type=time],
|
|||
input[type=datetime-local],
|
||||
input[type=number] {
|
||||
border: $default-border-2;
|
||||
border-radius: .5em;
|
||||
border-radius: 1em;
|
||||
margin: .5em 0;
|
||||
padding: .25em;
|
||||
|
||||
&:hover {
|
||||
|
|
Loading…
Reference in a new issue