Added a Manage tokens tab to the user panel

This commit is contained in:
ReAnzu 2018-02-27 18:14:07 -06:00
parent 23268ded75
commit 05d2785ec6
11 changed files with 332 additions and 22 deletions

View file

@ -1,6 +1,6 @@
#user
width: 100%
max-width: 35em
max-width: 45em
nav.text-nav
margin-bottom: 1.5em
@ -37,6 +37,24 @@
height: 1px
clear: both
#user-tokens
.token-flex-container
width: 100%
display: flex;
flex-direction column;
padding-bottom: 0.5em;
.token-flex-row
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 0.25em;
padding-bottom: 0.25em;
border-bottom: black solid 1px;
form
width: auto;
#user-delete form
width: 100%

View file

@ -6,6 +6,9 @@
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'edit') %>'>Account settings</a></li><!--
--><% } %><!--
--><% if (ctx.canListTokens) { %><!--
--><li data-name='list-tokens'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'list-tokens') %>'>Manage tokens</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
--><li data-name='delete'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'delete') %>'>Account deletion</a></li><!--
--><% } %><!--

View file

@ -0,0 +1,29 @@
<div id='user-tokens'>
<div class='messages'></div>
<% if (ctx.tokens.length > 0) { %>
<div class="token-flex-container">
<div class="token-flex-row">
<div>Token</div>
<div>Actions</div>
</div>
<% _.each(ctx.tokens, function(token, index) { %>
<div class="token-flex-row">
<div><%= token.token %></div>
<div>
<form id='token<%= index %>'>
<input type='hidden' name='token' value='<%= token.token %>'/>
<input type='submit' value='Delete token'/>
</form>
</div>
</div>
<% }); %>
</div>
<% } else { %>
<h2>No Registered Tokens</h2>
<% } %>
<form id='create-token-form'>
<div class='buttons'>
<input type='submit' value='Create token'/>
</div>
</form>
</div>

View file

@ -120,7 +120,7 @@ class Api extends events.EventTarget {
create_token(userName, options) {
return new Promise((resolve, reject) => {
this.post('/user-token', {})
this.post('/user-token/' + userName, {})
.then(response => {
cookies.set(
'auth',
@ -137,7 +137,7 @@ class Api extends events.EventTarget {
delete_token(userName, userToken) {
return new Promise((resolve, reject) => {
this.delete('/user-token/' + userToken, {})
this.delete('/user-token/' + userName + '/' + userToken, {})
.then(response => {
const options = {};
cookies.set(

View file

@ -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,11 @@ class UserController {
return;
}
this._successMessages = [];
this._errorMessages = [];
topNavigation.setTitle('User ' + userName);
User.get(userName).then(user => {
User.get(userName).then(async user => {
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
@ -48,6 +52,17 @@ class UserController {
} else {
topNavigation.activate('users');
}
let userTokens = [];
if (section === 'list-tokens') {
userTokens = await UserToken.get(userName)
.then(response => {
return response;
}, error => {
return [];
});
}
this._view = new UserView({
user: user,
section: section,
@ -58,18 +73,50 @@ 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(`userToken:list:${infix}`),
canCreateToken: api.hasPrivilege(`userToken:create:${infix}`),
canEditToken: api.hasPrivilege(`userToken:edit:${infix}`),
canDeleteToken: api.hasPrivilege(`userToken: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));
for (let i = 0; i < this._successMessages.length; i++) {
this.showSuccess(this._successMessages[i]);
}
for (let i = 0; i < this._errorMessages.length; i++) {
this.showError(this._errorMessages[i]);
}
}, 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 +195,32 @@ class UserController {
this._view.enableForm();
});
}
_evtCreateToken(e) {
this._view.clearMessages();
this._view.disableForm();
UserToken.create(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 + ' created.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
}
_evtDeleteToken(e) {
this._view.clearMessages();
this._view.disableForm();
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();
});
}
}
module.exports = router => {
@ -157,6 +230,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');
});

View file

@ -0,0 +1,76 @@
'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 enabled() { return this._enabled; }
get version() { return this._version; }
get creation_time() { return this._creation_time; }
static fromResponse(response) {
if (typeof response.results !== 'undefined') {
let token_list = [];
for (let i = 0; i < response.results.length; i++) {
const token = new UserToken();
token._updateFromResponse(response.results[i]);
token_list.push(token)
}
return token_list;
} 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) {
return api.post(uri.formatApiLink('user-token', userName))
.then(response => {
return Promise.resolve(UserToken.fromResponse(response))
});
}
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,
_enabled: response.enabled,
_version: response.version,
_creation_time: response.creationTime,
};
Object.assign(this, map);
Object.assign(this._orig, map);
}
}
module.exports = UserToken;

View file

@ -0,0 +1,86 @@
'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()
}
_decorateTokenForms() {
for (let i = 0; i < this._tokens.length; i++) {
let formNode = this._hostNode.querySelector('#token' + i);
views.decorateValidator(formNode);
formNode.addEventListener('submit', e => this._evtDelete(e));
this._tokenFormNodes.push(formNode)
}
}
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 i = 0; i < this._tokenFormNodes.length; i++) {
let formNode = this._tokenFormNodes[i];
views.enableForm(formNode);
}
}
disableForm() {
views.disableForm(this._formNode);
for (let i = 0; i < this._tokenFormNodes.length; i++) {
let formNode = this._tokenFormNodes[i];
views.disableForm(formNode);
}
}
_evtDelete(e) {
e.preventDefault();
const userToken = this._tokens[parseInt(e.target.id.replace('token', ''))];
this.dispatchEvent(new CustomEvent('delete', {
detail: {
user: this._user,
userToken: userToken,
},
}));
}
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
user: this._user
},
}));
}
get _formNode() {
return this._hostNode.querySelector('#create-token-form');
}
}
module.exports = UserTokenView;

View file

@ -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');
@ -42,7 +43,16 @@ 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');
}
} else if (ctx.section == 'delete') {
if (!this._ctx.canDelete) {
this._view = new EmptyView();

View file

@ -86,10 +86,14 @@ privileges:
'users:delete:any': administrator
'users:delete:self': regular
'user_token:list': regular
'user_token:create': regular
'user_token:edit': regular
'user_token:delete': regular
'user_token:list:any': administrator
'user_token:list:self': regular
'user_token:create:any': administrator
'user_token:create:self': regular
'user_token:edit:any': administrator
'user_token:edit:self': regular
'user_token:delete:any': administrator
'user_token:delete:self': regular
'posts:create:anonymous': regular
'posts:create:identified': regular

View file

@ -1,7 +1,7 @@
from typing import Dict
from szurubooru import model, rest
from szurubooru.func import auth, user_tokens, serialization, versions
from szurubooru.func import auth, users, user_tokens, serialization, versions
def _serialize(
@ -12,34 +12,42 @@ def _serialize(
options=serialization.get_serialization_options(ctx))
@rest.routes.get('/user-tokens/?')
def get_user_tokens(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'user_token:list')
@rest.routes.get('/user-tokens/(?P<user_name>[^/]+)/?')
def get_user_tokens(ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response:
user = users.get_user_by_name(params['user_name'])
infix = 'self' if ctx.user.user_id == user.user_id else 'any'
auth.verify_privilege(ctx.user, 'user_token:list:%s' % infix)
user_token_list = user_tokens.get_user_tokens(ctx.user)
return {
"results": [_serialize(ctx, token) for token in user_token_list]
}
@rest.routes.post('/user-token/?')
def create_user_token(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'user_token:create')
@rest.routes.post('/user-token/(?P<user_name>[^/]+)/?')
def create_user_token(ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response:
user = users.get_user_by_name(params['user_name'])
infix = 'self' if ctx.user.user_id == user.user_id else 'any'
auth.verify_privilege(ctx.user, 'user_token:create:%s' % infix)
user_token = user_tokens.create_user_token(ctx.user)
return _serialize(ctx, user_token)
@rest.routes.put('/user-token/(?P<user_token>[^/]+)/?')
def edit_user_token(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'user_token:edit')
@rest.routes.put('/user-token/(?P<user_name>[^/]+)/(?P<user_token>[^/]+)/?')
def edit_user_token(ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response:
user = users.get_user_by_name(params['user_name'])
infix = 'self' if ctx.user.user_id == user.user_id else 'any'
auth.verify_privilege(ctx.user, 'user_token:edit:%s' % infix)
user_token = user_tokens.get_user_token_by_user_and_token(ctx.user, params['user_token'])
versions.verify_version(user_token, ctx)
versions.bump_version(user_token)
return _serialize(ctx, user_token)
@rest.routes.delete('/user-token/(?P<user_token>[^/]+)/?')
@rest.routes.delete('/user-token/(?P<user_name>[^/]+)/(?P<user_token>[^/]+)/?')
def delete_user_token(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
auth.verify_privilege(ctx.user, 'user_token:delete')
user = users.get_user_by_name(params['user_name'])
infix = 'self' if ctx.user.user_id == user.user_id else 'any'
auth.verify_privilege(ctx.user, 'user_token:delete:%s' % infix)
user_token = user_tokens.get_user_token_by_user_and_token(ctx.user, params['user_token'])
if user_token is not None:
ctx.session.delete(user_token)

View file

@ -62,7 +62,7 @@ def get_user_token_by_user_and_token(user: model.User, token: str) -> model.User
def get_user_tokens(user: model.User) -> List[model.UserToken]:
assert user
return (db.session.query(model.UserToken)
.filter(sa.func.lower(model.UserToken.user_id) == sa.func.lower(user.user_id))
.filter(model.UserToken.user_id == user.user_id)
.all())