Added a Manage tokens tab to the user panel
This commit is contained in:
parent
23268ded75
commit
05d2785ec6
11 changed files with 332 additions and 22 deletions
|
@ -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%
|
||||
|
||||
|
|
|
@ -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><!--
|
||||
--><% } %><!--
|
||||
|
|
29
client/html/user_tokens.tpl
Normal file
29
client/html/user_tokens.tpl
Normal 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>
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
76
client/js/models/user_token.js
Normal file
76
client/js/models/user_token.js
Normal 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;
|
86
client/js/views/user_tokens_view.js
Normal file
86
client/js/views/user_tokens_view.js
Normal 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;
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue