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:
parent
87c9c27fba
commit
606ef31b01
12 changed files with 216 additions and 43 deletions
|
@ -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%
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -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
7
client/js/util/date.js
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in a new issue