Added note and expiration fields to the user_token model

* Updated UI to show more information about the token.
* Updated the js API to note the client token when creating it.
* Added prototype override to do add day calculations on dates.
* Updated auth check against token to inspect the expiration date of the token if it possesses one.
This commit is contained in:
ReAnzu 2018-03-02 02:37:31 -06:00
parent 87c9c27fba
commit 606ef31b01
12 changed files with 216 additions and 43 deletions

View file

@ -38,23 +38,43 @@
clear: both
#user-tokens
.flex-centered
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-around;
.token-flex-container
width: 100%
display: flex;
flex-direction column;
padding-bottom: 0.5em;
.floor
border-bottom: black solid 1px;
.token-info
min-width: 75%;
.token-actions
max-width: 25%;
justify-content: end;
.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;
.token-flex-column
display: flex;
flex-direction: column;
#user-delete form
width: 100%

View file

@ -2,18 +2,35 @@
<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 class='token' data-token-id='<%= index %>'>
<input type='hidden' name='token' value='<%= token.token %>'/>
<input type='submit' value='Delete token'/>
</form>
<div class="token-flex-row floor">
<div class="token-flex-column token-info">
<div class="token-flex-row">
<div>Token:</div>
<div><%= token.token %></div>
</div>
<div class="token-flex-row">
<div>Note:</div>
<div><%= token.note %></div>
</div>
<div class="token-flex-row">
<div>Created:</div>
<div><%= new Date(token.creationTime).toLocaleDateString() %></div>
</div>
<% if (token.expirationTime) { %>
<div class="token-flex-row">
<div>Expires:</div>
<div><%= new Date(token.expirationTime).toLocaleDateString() %></div>
</div>
<% } %>
</div>
<div class="token-flex-column token-actions">
<div>
<form class='token' data-token-id='<%= index %>'>
<input type='hidden' name='token' value='<%= token.token %>'/>
<input type='submit' value='Delete token'/>
</form>
</div>
</div>
</div>
<% }); %>
@ -21,9 +38,23 @@
<% } else { %>
<h2>No Registered Tokens</h2>
<% } %>
<form id='create-token-form'>
<div class='buttons'>
<input type='submit' value='Create token'/>
</div>
</form>
<div class='flex-centered'>
<form id='create-token-form'>
<div class="token-flex-container">
<div class="token-flex-row">
<div>Note:</div>
<div><input name='note', type='textbox'/></div>
</div>
<div class="token-flex-row">
<div>Expiration:</div>
<div><input name='expirationTime' type='date'/></div>
</div>
<div class="token-flex-row" style='justify-content: end;'>
<div class='buttons'>
<input type='submit' value='Create token'/>
</div>
</div>
</div>
</form>
</div>
</div>

View file

@ -1,5 +1,6 @@
'use strict';
require('./util/date.js');
const cookies = require('js-cookie');
const request = require('superagent');
const config = require('./config.js');
@ -119,8 +120,15 @@ class Api extends events.EventTarget {
}
createToken(userName, options) {
let userTokenRequest = {
enabled: true,
note: 'Client 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, {})
this.post('/user-token/' + userName, userTokenRequest)
.then(response => {
cookies.set(
'auth',
@ -327,7 +335,8 @@ class Api extends events.EventTarget {
try {
if (this.userName && this.userToken) {
req.auth = null;
req.set('Authorization', 'Token ' + new Buffer(this.userName + ":" + this.userToken).toString('base64'))
req.set('Authorization', 'Token '
+ new Buffer(this.userName + ":" + this.userToken).toString('base64'))
}
else if (this.userName && this.userPassword) {
req.auth(

View file

@ -199,7 +199,7 @@ class UserController {
_evtCreateToken(e) {
this._view.clearMessages();
this._view.disableForm();
UserToken.create(e.detail.user.name)
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.');

View file

@ -11,10 +11,12 @@ class UserToken extends events.EventTarget {
this._updateFromResponse({});
}
get token() { return this._token; }
get enabled() { return this._enabled; }
get version() { return this._version; }
get creationTime() { return this._creationTime; }
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; }
static fromResponse(response) {
if (typeof response.results !== 'undefined') {
@ -39,8 +41,17 @@ class UserToken extends events.EventTarget {
});
}
static create(userName) {
return api.post(uri.formatApiLink('user-token', userName))
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))
});
@ -62,10 +73,12 @@ class UserToken extends events.EventTarget {
_updateFromResponse(response) {
const map = {
_token: response.token,
_enabled: response.enabled,
_version: response.version,
_creationTime: response.creationTime,
_token: response.token,
_note: response.note,
_enabled: response.enabled,
_expirationTime: response.expirationTime,
_version: response.version,
_creationTime: response.creationTime,
};
Object.assign(this, map);

7
client/js/util/date.js Normal file
View file

@ -0,0 +1,7 @@
'use strict';
Date.prototype.addDays = function(days) {
let dat = new Date(this.valueOf());
dat.setDate(dat.getDate() + days);
return dat;
};

View file

@ -72,7 +72,16 @@ class UserTokenView extends events.EventTarget {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
user: this._user
user: this._user,
note: this._userTokenNoteInputNode ?
this._userTokenNoteInputNode.value :
undefined,
expirationTime: this._userTokenExpirationTimeInputNode && this._userTokenExpirationTimeInputNode.value.length > 0 ?
new Date(this._userTokenExpirationTimeInputNode.value).toISOString() :
undefined,
},
}));
}
@ -80,6 +89,14 @@ class UserTokenView extends events.EventTarget {
get _formNode() {
return this._hostNode.querySelector('#create-token-form');
}
get _userTokenNoteInputNode() {
return this._formNode.querySelector('[name=note]');
}
get _userTokenExpirationTimeInputNode() {
return this._formNode.querySelector('[name=expirationTime]');
}
}
module.exports = UserTokenView;

View file

@ -30,7 +30,17 @@ def create_user_token(
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_tokens:create:%s' % infix)
user_token = user_tokens.create_user_token(user)
enabled = ctx.get_param_as_bool('enabled', True)
user_token = user_tokens.create_user_token(user, enabled)
if ctx.has_param('note'):
note = ctx.get_param_as_string('note')
user_tokens.update_user_token_note(user_token, note)
if ctx.has_param('expirationTime'):
expiration_time = ctx.get_param_as_string('expirationTime')
user_tokens.update_user_token_expiration_time(user_token,
expiration_time)
ctx.session.add(user_token)
ctx.session.commit()
return _serialize(ctx, user_token)
@ -47,6 +57,15 @@ def update_user_token(
auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix)
user_tokens.update_user_token_enabled(user_token,
ctx.get_param_as_bool('enabled'))
if ctx.has_param('note'):
auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix)
note = ctx.get_param_as_string('note')
user_tokens.update_user_token_note(user_token, note)
if ctx.has_param('expirationTime'):
auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix)
expiration_time = ctx.get_param_as_string('expirationTime')
user_tokens.update_user_token_expiration_time(user_token,
expiration_time)
user_tokens.update_user_token_edit_time(user_token)
ctx.session.commit()
return _serialize(ctx, user_token)

View file

@ -1,12 +1,13 @@
from typing import Tuple
import hashlib
import random
import uuid
from collections import OrderedDict
from datetime import datetime
from nacl import pwhash
from nacl.exceptions import InvalidkeyError
from szurubooru import config, model, errors, db
from szurubooru import config, db, model, errors
from szurubooru.func import util
import uuid
RANK_MAP = OrderedDict([
@ -80,7 +81,15 @@ def is_valid_password(user: model.User, password: str) -> bool:
def is_valid_token(user_token: model.UserToken) -> bool:
return user_token is not None and user_token.enabled
''' Token must be enabled and if it has an expiration,
it must be greater than now. '''
assert user_token
if not user_token.enabled:
return False
if (user_token.expiration_time is not None
and user_token.expiration_time < datetime.utcnow()):
return False
return True
def has_privilege(user: model.User, privilege_name: str) -> bool:

View file

@ -1,10 +1,20 @@
import pytz
from dateutil import parser as dateutil_parser
from datetime import datetime
from typing import Any, Optional, List, Dict, Callable
from szurubooru import db, model, rest, errors
from szurubooru.func import auth, serialization, users
from szurubooru.func import auth, serialization, users, util
class InvalidEnabledFieldError(errors.ValidationError):
class InvalidEnabledError(errors.ValidationError):
pass
class InvalidExpirationError(errors.ValidationError):
pass
class InvalidNoteError(errors.ValidationError):
pass
@ -20,10 +30,12 @@ class UserTokenSerializer(serialization.BaseSerializer):
return {
'user': self.serialize_user,
'token': self.serialize_token,
'note': self.serialize_note,
'enabled': self.serialize_enabled,
'version': self.serialize_version,
'expirationTime': self.serialize_expiration_time,
'creationTime': self.serialize_creation_time,
'lastEditTime': self.serialize_last_edit_time,
'version': self.serialize_version,
}
def serialize_user(self) -> Any:
@ -38,9 +50,15 @@ class UserTokenSerializer(serialization.BaseSerializer):
def serialize_token(self) -> Any:
return self.user_token.token
def serialize_note(self) -> Any:
return self.user_token.note
def serialize_enabled(self) -> Any:
return self.user_token.enabled
def serialize_expiration_time(self) -> Any:
return self.user_token.expiration_time
def serialize_version(self) -> Any:
return self.user_token.version
@ -69,15 +87,13 @@ def get_user_tokens(user: model.User) -> List[model.UserToken]:
.all())
def create_user_token(user: model.User) -> model.UserToken:
def create_user_token(user: model.User, enabled: bool) -> model.UserToken:
assert user
user_token = model.UserToken()
user_token.user = user
user_token.token = auth.generate_authorization_token()
user_token.enabled = True
user_token.enabled = enabled
user_token.creation_time = datetime.utcnow()
db.session.add(user_token)
db.session.commit()
return user_token
@ -85,10 +101,38 @@ def update_user_token_enabled(
user_token: model.UserToken, enabled: bool) -> None:
assert user_token
if enabled is None:
raise InvalidEnabledFieldError('Enabled cannot be empty.')
raise InvalidEnabledError('Enabled cannot be empty.')
user_token.enabled = enabled
def update_user_token_edit_time(user_token: model.UserToken) -> None:
assert user_token
user_token.last_edit_time = datetime.utcnow()
def update_user_token_expiration_time(
user_token: model.UserToken, expiration_time: str) -> None:
assert user_token
if expiration_time is not None:
try:
expiration_time = dateutil_parser.parse(expiration_time)
except ValueError:
raise InvalidExpirationError(
'Expiration is in invalid format {}'.format(expiration_time))
if expiration_time.tzinfo is None:
raise InvalidExpirationError(
'Expiration cannot be missing timezone')
else:
expiration_time = expiration_time.astimezone(pytz.UTC)
if expiration_time < datetime.utcnow().astimezone(pytz.UTC):
raise InvalidExpirationError(
'Expiration cannot happen in the past')
user_token.expiration_time = expiration_time
def update_user_token_note(user_token: model.UserToken, note: str) -> None:
assert user_token
note = note.strip() if note is not None else ''
if util.value_exceeds_column_size(note, model.UserToken.note):
raise InvalidNoteError('Note is too long.')
user_token.note = note

View file

@ -20,7 +20,9 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token', sa.Unicode(length=36), nullable=False),
sa.Column('note', sa.Unicode(length=128), nullable=True),
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('expiration_time', sa.DateTime(), nullable=True),
sa.Column('creation_time', sa.DateTime(), nullable=False),
sa.Column('last_edit_time', sa.DateTime(), nullable=True),
sa.Column('version', sa.Integer(), nullable=False),

View file

@ -99,7 +99,9 @@ class UserToken(Base):
nullable=False,
index=True)
token = sa.Column('token', sa.Unicode(36), nullable=False)
note = sa.Column('note', sa.Unicode(128), nullable=True)
enabled = sa.Column('enabled', sa.Boolean, nullable=False, default=True)
expiration_time = sa.Column('expiration_time', sa.DateTime, nullable=True)
creation_time = sa.Column('creation_time', sa.DateTime, nullable=False)
last_edit_time = sa.Column('last_edit_time', sa.DateTime)
version = sa.Column('version', sa.Integer, default=1, nullable=False)