aboutsummaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/css/core-forms.styl32
-rw-r--r--client/css/user-view.styl43
-rw-r--r--client/html/user.tpl7
-rw-r--r--client/html/user_tokens.tpl74
-rw-r--r--client/js/api.js94
-rw-r--r--client/js/controllers/user_controller.js107
-rw-r--r--client/js/models/user_token.js116
-rw-r--r--client/js/util/polyfill.js7
-rw-r--r--client/js/util/views.js6
-rw-r--r--client/js/views/user_tokens_view.js134
-rw-r--r--client/js/views/user_view.js13
11 files changed, 621 insertions, 12 deletions
diff --git a/client/css/core-forms.styl b/client/css/core-forms.styl
index f083ffb..bed63e3 100644
--- a/client/css/core-forms.styl
+++ b/client/css/core-forms.styl
@@ -138,6 +138,38 @@ input[type=checkbox]:focus + .checkbox:before
/*
+ * Date and time inputs
+ */
+
+input[type=date],
+input[type=time]
+ vertical-align: top
+ font-family: 'Droid Sans', sans-serif
+ font-size: 100%
+ padding: 0.2em 0.3em
+ box-sizing: border-box
+ border: 2px solid $input-enabled-border-color
+ background: $input-enabled-background-color
+ color: $input-enabled-text-color
+ box-shadow: none /* :-moz-submit-invalid on FF */
+ transition: border-color 0.1s linear, background-color 0.1s linear
+
+ &:disabled
+ border: 2px solid $input-disabled-border-color
+ background: $input-disabled-background-color
+ color: $input-disabled-text-color
+
+ &:focus
+ border-color: $main-color
+
+ &[readonly]
+ border: 2px solid $input-disabled-border-color
+ background: $input-disabled-background-color
+ color: $input-disabled-text-color
+
+
+
+/*
* Regular inputs
*/
select,
diff --git a/client/css/user-view.styl b/client/css/user-view.styl
index 12cba75..3cdd29c 100644
--- a/client/css/user-view.styl
+++ b/client/css/user-view.styl
@@ -1,3 +1,6 @@
+@import colors
+$token-border-color = $active-tab-background-color
+
#user
width: 100%
max-width: 35em
@@ -37,7 +40,43 @@
height: 1px
clear: both
- #user-delete form
- width: 100%
+ #user-tokens
+
+ .token-flex-container
+ width: 100%
+ display: flex;
+ flex-direction column;
+ padding-bottom: 0.5em;
+
+ .full-width
+ width: 100%
+
+ .token-flex-row
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 0.2em;
+
+ .no-wrap
+ white-space: nowrap;
+ .token-input
+ min-height: 2em;
+ line-height: 2em;
+ text-align: center;
+ .token-flex-column
+ display: flex;
+ flex-direction: column;
+
+ .token-flex-labels
+ padding-right: 0.5em
+
+ hr
+ border-top: 3px solid $token-border-color
+
+ form
+ width: 100%;
+
+ #user-delete form
+ width: 100%
diff --git a/client/html/user.tpl b/client/html/user.tpl
index 28e34e6..75c721f 100644
--- a/client/html/user.tpl
+++ b/client/html/user.tpl
@@ -4,10 +4,13 @@
--><ul><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
- --><li data-name='edit'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'edit') %>'>Account settings</a></li><!--
+ --><li data-name='edit'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'edit') %>'>Settings</a></li><!--
+ --><% } %><!--
+ --><% if (ctx.canListTokens) { %><!--
+ --><li data-name='list-tokens'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'list-tokens') %>'>Login tokens</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
- --><li data-name='delete'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'delete') %>'>Account deletion</a></li><!--
+ --><li data-name='delete'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'delete') %>'>Delete</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>
diff --git a/client/html/user_tokens.tpl b/client/html/user_tokens.tpl
new file mode 100644
index 0000000..73db7a1
--- /dev/null
+++ b/client/html/user_tokens.tpl
@@ -0,0 +1,74 @@
+<div id='user-tokens'>
+ <div class='messages'></div>
+ <% if (ctx.tokens.length > 0) { %>
+ <div class='token-flex-container'>
+ <% _.each(ctx.tokens, function(token, index) { %>
+ <div class='token-flex-row'>
+ <div class='token-flex-column token-flex-labels'>
+ <div class='token-flex-row'>Token:</div>
+ <div class='token-flex-row'>Note:</div>
+ <div class='token-flex-row'>Created:</div>
+ <div class='token-flex-row'>Expires:</div>
+ <div class='token-flex-row no-wrap'>Last used:</div>
+ </div>
+ <div class='token-flex-column full-width'>
+ <div class='token-flex-row'><%= token.token %></div>
+ <div class='token-flex-row'>
+ <% if (token.note !== null) { %>
+ <%= token.note %>
+ <% } else { %>
+ No note
+ <% } %>
+ <a class='token-change-note' data-token-id='<%= index %>' href='#'>(change)</a>
+ </div>
+ <div class='token-flex-row'><%= ctx.makeRelativeTime(token.creationTime) %></div>
+ <div class='token-flex-row'>
+ <% if (token.expirationTime) { %>
+ <%= ctx.makeRelativeTime(token.expirationTime) %>
+ <% } else { %>
+ No expiration
+ <% } %>
+ </div>
+ <div class='token-flex-row'><%= ctx.makeRelativeTime(token.lastUsageTime) %></div>
+ </div>
+ </div>
+ <div class='token-flex-row'>
+ <div class='token-flex-column full-width'>
+ <div class='token-flex-row'>
+ <form class='token' data-token-id='<%= index %>'>
+ <% if (token.isCurrentAuthToken) { %>
+ <input type='submit' value='Delete and logout'
+ title='This token is used to authenticate this client, deleting it will force a logout.'/>
+ <% } else { %>
+ <input type='submit' value='Delete'/>
+ <% } %>
+ </form>
+ </div>
+ </div>
+ </div>
+ <hr/>
+ <% }); %>
+ </div>
+ <% } else { %>
+ <h2>No Registered Tokens</h2>
+ <% } %>
+ <form id='create-token-form'>
+ <ul class='input'>
+ <li class='note'>
+ <%= ctx.makeTextInput({
+ text: 'Note',
+ id: 'note',
+ }) %>
+ </li>
+ <li class='expirationTime'>
+ <%= ctx.makeDateInput({
+ text: 'Expires',
+ id: 'expirationTime',
+ }) %>
+ </li>
+ </ul>
+ <div class='buttons'>
+ <input type='submit' value='Create token'/>
+ </div>
+ </form>
+</div>
diff --git a/client/js/api.js b/client/js/api.js
index abfeb6f..3623045 100644
--- a/client/js/api.js
+++ b/client/js/api.js
@@ -15,6 +15,7 @@ class Api extends events.EventTarget {
this.user = null;
this.userName = null;
this.userPassword = null;
+ this.token = null;
this.cache = {};
this.allRanks = [
'anonymous',
@@ -87,16 +88,16 @@ class Api extends events.EventTarget {
loginFromCookies() {
const auth = cookies.getJSON('auth');
- return auth && auth.user && auth.password ?
- this.login(auth.user, auth.password, true) :
+ return auth && auth.user && auth.token ?
+ this.loginWithToken(auth.user, auth.token, true) :
Promise.resolve();
}
- login(userName, userPassword, doRemember) {
+ loginWithToken(userName, token, doRemember) {
this.cache = {};
return new Promise((resolve, reject) => {
this.userName = userName;
- this.userPassword = userPassword;
+ this.token = token;
this.get('/user/' + userName + '?bump-login=true')
.then(response => {
const options = {};
@@ -105,8 +106,70 @@ class Api extends events.EventTarget {
}
cookies.set(
'auth',
- {'user': userName, 'password': userPassword},
+ {'user': userName, 'token': token},
+ options);
+ this.user = response;
+ resolve();
+ this.dispatchEvent(new CustomEvent('login'));
+ }, error => {
+ reject(error);
+ this.logout();
+ });
+ });
+ }
+
+ createToken(userName, options) {
+ let userTokenRequest = {
+ enabled: true,
+ note: 'Web Login Token'
+ };
+ if (typeof options.expires !== 'undefined') {
+ userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString()
+ }
+ return new Promise((resolve, reject) => {
+ this.post('/user-token/' + userName, userTokenRequest)
+ .then(response => {
+ cookies.set(
+ 'auth',
+ {'user': userName, 'token': response.token},
+ options);
+ this.userName = userName;
+ this.token = response.token;
+ this.userPassword = null;
+ }, error => {
+ reject(error);
+ });
+ });
+ }
+
+ deleteToken(userName, userToken) {
+ return new Promise((resolve, reject) => {
+ this.delete('/user-token/' + userName + '/' + userToken, {})
+ .then(response => {
+ const options = {};
+ cookies.set(
+ 'auth',
+ {'user': userName, 'token': null},
options);
+ resolve();
+ }, error => {
+ reject(error);
+ });
+ });
+ }
+
+ login(userName, userPassword, doRemember) {
+ this.cache = {};
+ return new Promise((resolve, reject) => {
+ this.userName = userName;
+ this.userPassword = userPassword;
+ this.get('/user/' + userName + '?bump-login=true')
+ .then(response => {
+ const options = {};
+ if (doRemember) {
+ options.expires = 365;
+ }
+ this.createToken(this.userName, options);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
@@ -118,9 +181,20 @@ class Api extends events.EventTarget {
}
logout() {
+ let self = this;
+ this.deleteToken(this.userName, this.token)
+ .then(response => {
+ self._logout();
+ }, error => {
+ self._logout();
+ });
+ }
+
+ _logout() {
this.user = null;
this.userName = null;
this.userPassword = null;
+ this.token = null;
this.dispatchEvent(new CustomEvent('logout'));
}
@@ -137,6 +211,10 @@ class Api extends events.EventTarget {
}
}
+ isCurrentAuthToken(userToken) {
+ return userToken.token === this.token;
+ }
+
_getFullUrl(url) {
const fullUrl =
(config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
@@ -258,7 +336,11 @@ class Api extends events.EventTarget {
}
try {
- if (this.userName && this.userPassword) {
+ if (this.userName && this.token) {
+ req.auth = null;
+ req.set('Authorization', 'Token '
+ + new Buffer(this.userName + ":" + this.token).toString('base64'))
+ } else if (this.userName && this.userPassword) {
req.auth(
this.userName,
encodeURIComponent(this.userPassword)
diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js
index 46020f3..d042e41 100644
--- a/client/js/controllers/user_controller.js
+++ b/client/js/controllers/user_controller.js
@@ -7,6 +7,7 @@ const misc = require('../util/misc.js');
const config = require('../config.js');
const views = require('../util/views.js');
const User = require('../models/user.js');
+const UserToken = require('../models/user_token.js');
const topNavigation = require('../models/top_navigation.js');
const UserView = require('../views/user_view.js');
const EmptyView = require('../views/empty_view.js');
@@ -21,8 +22,28 @@ class UserController {
return;
}
+ this._successMessages = [];
+ this._errorMessages = [];
+
+ let userTokenPromise = Promise.resolve([]);
+ if (section === 'list-tokens') {
+ userTokenPromise = UserToken.get(userName)
+ .then(userTokens => {
+ return userTokens.map(token => {
+ token.isCurrentAuthToken = api.isCurrentAuthToken(token);
+ return token;
+ });
+ }, error => {
+ return [];
+ });
+ }
+
topNavigation.setTitle('User ' + userName);
- User.get(userName).then(user => {
+ Promise.all([
+ userTokenPromise,
+ User.get(userName)
+ ]).then(responses => {
+ const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
@@ -48,6 +69,7 @@ class UserController {
} else {
topNavigation.activate('users');
}
+
this._view = new UserView({
user: user,
section: section,
@@ -58,18 +80,51 @@ class UserController {
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
+ canListTokens: api.hasPrivilege(`userTokens:list:${infix}`),
+ canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`),
+ canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
+ canDeleteToken: api.hasPrivilege(`userTokens:delete:${infix}`),
canDelete: api.hasPrivilege(`users:delete:${infix}`),
ranks: ranks,
+ tokens: userTokens,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
+ this._view.addEventListener('create-token', e => this._evtCreateToken(e));
+ this._view.addEventListener('delete-token', e => this._evtDeleteToken(e));
+ this._view.addEventListener('update-token', e => this._evtUpdateToken(e));
+
+ for (let message of this._successMessages) {
+ this.showSuccess(message);
+ }
+
+ for (let message of this._errorMessages) {
+ this.showError(message);
+ }
+
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
}
+ showSuccess(message) {
+ if (typeof this._view === 'undefined') {
+ this._successMessages.push(message)
+ } else {
+ this._view.showSuccess(message);
+ }
+ }
+
+ showError(message) {
+ if (typeof this._view === 'undefined') {
+ this._errorMessages.push(message)
+ } else {
+ this._view.showError(message);
+ }
+ }
+
_evtChange(e) {
misc.enableExitConfirmation();
}
@@ -148,6 +203,53 @@ class UserController {
this._view.enableForm();
});
}
+
+ _evtCreateToken(e) {
+ this._view.clearMessages();
+ this._view.disableForm();
+ UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
+ .then(response => {
+ const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
+ ctx.controller.showSuccess('Token ' + response.token + ' created.');
+ }, error => {
+ this._view.showError(error.message);
+ this._view.enableForm();
+ });
+ }
+
+ _evtDeleteToken(e) {
+ this._view.clearMessages();
+ this._view.disableForm();
+ if (api.isCurrentAuthToken(e.detail.userToken)) {
+ router.show(uri.formatClientLink('logout'));
+ } else {
+ e.detail.userToken.delete(e.detail.user.name)
+ .then(() => {
+ const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
+ ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.');
+ }, error => {
+ this._view.showError(error.message);
+ this._view.enableForm();
+ });
+ }
+ }
+
+ _evtUpdateToken(e) {
+ this._view.clearMessages();
+ this._view.disableForm();
+
+ if (e.detail.note !== undefined) {
+ e.detail.userToken.note = e.detail.note;
+ }
+
+ e.detail.userToken.save(e.detail.user.name).then(response => {
+ const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
+ ctx.controller.showSuccess('Token ' + response.token + ' updated.');
+ }, error => {
+ this._view.showError(error.message);
+ this._view.enableForm();
+ });
+ }
}
module.exports = router => {
@@ -157,6 +259,9 @@ module.exports = router => {
router.enter(['user', ':name', 'edit'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit');
});
+ router.enter(['user', ':name', 'list-tokens'], (ctx, next) => {
+ ctx.controller = new UserController(ctx, 'list-tokens');
+ });
router.enter(['user', ':name', 'delete'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete');
});
diff --git a/client/js/models/user_token.js b/client/js/models/user_token.js
new file mode 100644
index 0000000..6e70a94
--- /dev/null
+++ b/client/js/models/user_token.js
@@ -0,0 +1,116 @@
+'use strict';
+
+const api = require('../api.js');
+const uri = require('../util/uri.js');
+const events = require('../events.js');
+
+class UserToken extends events.EventTarget {
+ constructor() {
+ super();
+ this._orig = {};
+ this._updateFromResponse({});
+ }
+
+ get token() { return this._token; }
+ get note() { return this._note; }
+ get enabled() { return this._enabled; }
+ get version() { return this._version; }
+ get expirationTime() { return this._expirationTime; }
+ get creationTime() { return this._creationTime; }
+ get lastEditTime() { return this._lastEditTime; }
+ get lastUsageTime() { return this._lastUsageTime; }
+
+ set note(value) { this._note = value; }
+
+ static fromResponse(response) {
+ if (typeof response.results !== 'undefined') {
+ let tokenList = [];
+ for (let responseToken of response.results) {
+ const token = new UserToken();
+ token._updateFromResponse(responseToken);
+ tokenList.push(token)
+ }
+ return tokenList;
+ } else {
+ const ret = new UserToken();
+ ret._updateFromResponse(response);
+ return ret;
+ }
+ }
+
+ static get(userName) {
+ return api.get(uri.formatApiLink('user-tokens', userName))
+ .then(response => {
+ return Promise.resolve(UserToken.fromResponse(response));
+ });
+ }
+
+ static create(userName, note, expirationTime) {
+ let userTokenRequest = {
+ enabled: true
+ };
+ if (note) {
+ userTokenRequest.note = note;
+ }
+ if (expirationTime) {
+ userTokenRequest.expirationTime = expirationTime;
+ }
+ return api.post(uri.formatApiLink('user-token', userName), userTokenRequest)
+ .then(response => {
+ return Promise.resolve(UserToken.fromResponse(response))
+ });
+ }
+
+ save(userName) {
+ const detail = {version: this._version};
+
+ if (this._note !== this._orig._note) {
+ detail.note = this._note;
+ }
+
+ return api.put(
+ uri.formatApiLink('user-token', userName, this._orig._token),
+ detail)
+ .then(response => {
+ this._updateFromResponse(response);
+ this.dispatchEvent(new CustomEvent('change', {
+ detail: {
+ userToken: this,
+ },
+ }));
+ return Promise.resolve(this);
+ });
+ }
+
+ delete(userName) {
+ return api.delete(
+ uri.formatApiLink('user-token', userName, this._orig._token),
+ {version: this._version})
+ .then(response => {
+ this.dispatchEvent(new CustomEvent('delete', {
+ detail: {
+ userToken: this,
+ },
+ }));
+ return Promise.resolve();
+ });
+ }
+
+ _updateFromResponse(response) {
+ const map = {
+ _token: response.token,
+ _note: response.note,
+ _enabled: response.enabled,
+ _expirationTime: response.expirationTime,
+ _version: response.version,
+ _creationTime: response.creationTime,
+ _lastEditTime: response.lastEditTime,
+ _lastUsageTime: response.lastUsageTime,
+ };
+
+ Object.assign(this, map);
+ Object.assign(this._orig, map);
+ }
+}
+
+module.exports = UserToken;
diff --git a/client/js/util/polyfill.js b/client/js/util/polyfill.js
index 91186b2..71ee972 100644
--- a/client/js/util/polyfill.js
+++ b/client/js/util/polyfill.js
@@ -59,3 +59,10 @@ Number.prototype.between = function(a, b, inclusive) {
// non standard
Promise.prototype.abort = () => {};
+
+// non standard
+Date.prototype.addDays = function(days) {
+ let dat = new Date(this.valueOf());
+ dat.setDate(dat.getDate() + days);
+ return dat;
+};
diff --git a/client/js/util/views.js b/client/js/util/views.js
index b0b7cce..9f238b1 100644
--- a/client/js/util/views.js
+++ b/client/js/util/views.js
@@ -168,6 +168,11 @@ function makeNumericInput(options) {
return makeInput(options);
}
+function makeDateInput(options) {
+ options.type = 'date';
+ return makeInput(options)
+}
+
function getPostUrl(id, parameters) {
return uri.formatClientLink(
'post', id,
@@ -392,6 +397,7 @@ function getTemplate(templatePath) {
makePasswordInput: makePasswordInput,
makeEmailInput: makeEmailInput,
makeColorInput: makeColorInput,
+ makeDateInput: makeDateInput,
makePostLink: makePostLink,
makeTagLink: makeTagLink,
makeUserLink: makeUserLink,
diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js
new file mode 100644
index 0000000..f6c8480
--- /dev/null
+++ b/client/js/views/user_tokens_view.js
@@ -0,0 +1,134 @@
+'use strict';
+
+const events = require('../events.js');
+const views = require('../util/views.js');
+
+const template = views.getTemplate('user-tokens');
+
+class UserTokenView extends events.EventTarget {
+ constructor(ctx) {
+ super();
+
+ this._user = ctx.user;
+ this._tokens = ctx.tokens;
+ this._hostNode = ctx.hostNode;
+ this._tokenFormNodes = [];
+ views.replaceContent(this._hostNode, template(ctx));
+ views.decorateValidator(this._formNode);
+
+ this._formNode.addEventListener('submit', e => this._evtSubmit(e));
+
+ this._decorateTokenForms();
+ this._decorateTokenNoteChangeLinks();
+ }
+
+ _decorateTokenForms() {
+ this._tokenFormNodes = [];
+ for (let i = 0; i < this._tokens.length; i++) {
+ let formNode = this._hostNode.querySelector(
+ '.token[data-token-id=\"' + i + '\"]');
+ formNode.addEventListener('submit', e => this._evtDelete(e));
+ this._tokenFormNodes.push(formNode);
+ }
+ }
+
+ _decorateTokenNoteChangeLinks() {
+ for (let i = 0; i < this._tokens.length; i++) {
+ let linkNode = this._hostNode.querySelector(
+ '.token-change-note[data-token-id=\"' + i + '\"]');
+ linkNode.addEventListener(
+ 'click', e => this._evtChangeNoteClick(e));
+ }
+ }
+
+ clearMessages() {
+ views.clearMessages(this._hostNode);
+ }
+
+ showSuccess(message) {
+ views.showSuccess(this._hostNode, message);
+ }
+
+ showError(message) {
+ views.showError(this._hostNode, message);
+ }
+
+ enableForm() {
+ views.enableForm(this._formNode);
+ for (let formNode of this._tokenFormNodes) {
+ views.enableForm(formNode);
+ }
+ }
+
+ disableForm() {
+ views.disableForm(this._formNode);
+ for (let formNode of this._tokenFormNodes) {
+ views.disableForm(formNode);
+ }
+ }
+
+ _evtDelete(e) {
+ e.preventDefault();
+ const userToken = this._tokens[parseInt(
+ e.target.getAttribute('data-token-id'))];
+ this.dispatchEvent(new CustomEvent('delete', {
+ detail: {
+ user: this._user,
+ userToken: userToken,
+ },
+ }));
+ }
+
+ _evtSubmit(e) {
+ e.preventDefault();
+ this.dispatchEvent(new CustomEvent('submit', {
+ detail: {
+ user: this._user,
+
+ note: this._userTokenNoteInputNode ?
+ this._userTokenNoteInputNode.value :
+ undefined,
+
+ expirationTime:
+ (this._userTokenExpirationTimeInputNode
+ && this._userTokenExpirationTimeInputNode.value) ?
+ new Date(this._userTokenExpirationTimeInputNode.value)
+ .toISOString() :
+ undefined,
+ },
+ }));
+ }
+
+ _evtChangeNoteClick(e) {
+ e.preventDefault();
+ const userToken = this._tokens[
+ parseInt(e.target.getAttribute('data-token-id'))];
+ const text = window.prompt(
+ 'Please enter the new name:',
+ userToken.note !== null ? userToken.note : undefined);
+ if (!text) {
+ return;
+ }
+ this.dispatchEvent(new CustomEvent('update', {
+ detail: {
+ user: this._user,
+ userToken: userToken,
+ note: text ? text : undefined,
+ },
+ }));
+ }
+
+ get _formNode() {
+ return this._hostNode.querySelector('#create-token-form');
+ }
+
+ get _userTokenNoteInputNode() {
+ return this._formNode.querySelector('.note input');
+ }
+
+ get _userTokenExpirationTimeInputNode() {
+ return this._formNode.querySelector('.expirationTime input');
+ }
+}
+
+module.exports = UserTokenView;
diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js
index d52e899..75fd154 100644
--- a/client/js/views/user_view.js
+++ b/client/js/views/user_view.js
@@ -3,6 +3,7 @@
const events = require('../events.js');
const views = require('../util/views.js');
const UserDeleteView = require('./user_delete_view.js');
+const UserTokensView = require('./user_tokens_view.js');
const UserSummaryView = require('./user_summary_view.js');
const UserEditView = require('./user_edit_view.js');
const EmptyView = require('../views/empty_view.js');
@@ -45,7 +46,17 @@ class UserView extends events.EventTarget {
this._view = new UserEditView(ctx);
events.proxyEvent(this._view, this, 'submit');
}
-
+ } else if (ctx.section == 'list-tokens') {
+ if (!this._ctx.canListTokens) {
+ this._view = new EmptyView();
+ this._view.showError(
+ 'You don\'t have privileges to view user tokens.');
+ } else {
+ this._view = new UserTokensView(ctx);
+ events.proxyEvent(this._view, this, 'delete', 'delete-token');
+ events.proxyEvent(this._view, this, 'submit', 'create-token');
+ events.proxyEvent(this._view, this, 'update', 'update-token');
+ }
} else if (ctx.section == 'delete') {
if (!this._ctx.canDelete) {
this._view = new EmptyView();

© 2015 - 2026 Jakob L. Kreuze