-
<%= token.token %>
-
-
+
+
+
+
Token:
+
<%= token.token %>
+
+
+
Note:
+
<%= token.note %>
+
+
+
Created:
+
<%= new Date(token.creationTime).toLocaleDateString() %>
+
+ <% if (token.expirationTime) { %>
+
+
Expires:
+
<%= new Date(token.expirationTime).toLocaleDateString() %>
+
+ <% } %>
+
+
<% }); %>
@@ -21,9 +38,23 @@
<% } else { %>
No Registered Tokens
<% } %>
-
+
diff --git a/client/js/api.js b/client/js/api.js
index 637acdf9..ec1d9bcf 100644
--- a/client/js/api.js
+++ b/client/js/api.js
@@ -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(
diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js
index 4a7c0221..df044177 100644
--- a/client/js/controllers/user_controller.js
+++ b/client/js/controllers/user_controller.js
@@ -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.');
diff --git a/client/js/models/user_token.js b/client/js/models/user_token.js
index 0ca1b9b9..93e0693a 100644
--- a/client/js/models/user_token.js
+++ b/client/js/models/user_token.js
@@ -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);
diff --git a/client/js/util/date.js b/client/js/util/date.js
new file mode 100644
index 00000000..b9744c31
--- /dev/null
+++ b/client/js/util/date.js
@@ -0,0 +1,7 @@
+'use strict';
+
+Date.prototype.addDays = function(days) {
+ let dat = new Date(this.valueOf());
+ dat.setDate(dat.getDate() + days);
+ return dat;
+};
\ No newline at end of file
diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js
index f98f8d8c..fa3daff3 100644
--- a/client/js/views/user_tokens_view.js
+++ b/client/js/views/user_tokens_view.js
@@ -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;
diff --git a/server/szurubooru/api/user_token_api.py b/server/szurubooru/api/user_token_api.py
index 2323a97e..3f3031da 100644
--- a/server/szurubooru/api/user_token_api.py
+++ b/server/szurubooru/api/user_token_api.py
@@ -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)
diff --git a/server/szurubooru/func/auth.py b/server/szurubooru/func/auth.py
index 4e19f665..277d0b58 100644
--- a/server/szurubooru/func/auth.py
+++ b/server/szurubooru/func/auth.py
@@ -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:
diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py
index 398c54a2..09ae8962 100644
--- a/server/szurubooru/func/user_tokens.py
+++ b/server/szurubooru/func/user_tokens.py
@@ -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
diff --git a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py
index 95286bbf..ded7298d 100644
--- a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py
+++ b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py
@@ -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),
diff --git a/server/szurubooru/model/user.py b/server/szurubooru/model/user.py
index a8eb86e7..d5397189 100644
--- a/server/szurubooru/model/user.py
+++ b/server/szurubooru/model/user.py
@@ -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)