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
|
clear: both
|
||||||
|
|
||||||
#user-tokens
|
#user-tokens
|
||||||
|
|
||||||
|
.flex-centered
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
.token-flex-container
|
.token-flex-container
|
||||||
width: 100%
|
width: 100%
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction column;
|
flex-direction column;
|
||||||
padding-bottom: 0.5em;
|
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
|
.token-flex-row
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-top: 0.25em;
|
padding-top: 0.25em;
|
||||||
padding-bottom: 0.25em;
|
padding-bottom: 0.25em;
|
||||||
border-bottom: black solid 1px;
|
|
||||||
|
|
||||||
form
|
form
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
|
.token-flex-column
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
#user-delete form
|
#user-delete form
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,29 @@
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
<% if (ctx.tokens.length > 0) { %>
|
<% if (ctx.tokens.length > 0) { %>
|
||||||
<div class="token-flex-container">
|
<div class="token-flex-container">
|
||||||
<div class="token-flex-row">
|
|
||||||
<div>Token</div>
|
|
||||||
<div>Actions</div>
|
|
||||||
</div>
|
|
||||||
<% _.each(ctx.tokens, function(token, index) { %>
|
<% _.each(ctx.tokens, function(token, index) { %>
|
||||||
|
<div class="token-flex-row floor">
|
||||||
|
<div class="token-flex-column token-info">
|
||||||
<div class="token-flex-row">
|
<div class="token-flex-row">
|
||||||
|
<div>Token:</div>
|
||||||
<div><%= token.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>
|
<div>
|
||||||
<form class='token' data-token-id='<%= index %>'>
|
<form class='token' data-token-id='<%= index %>'>
|
||||||
<input type='hidden' name='token' value='<%= token.token %>'/>
|
<input type='hidden' name='token' value='<%= token.token %>'/>
|
||||||
|
@ -16,14 +32,29 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</div>
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<h2>No Registered Tokens</h2>
|
<h2>No Registered Tokens</h2>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<div class='flex-centered'>
|
||||||
<form id='create-token-form'>
|
<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'>
|
<div class='buttons'>
|
||||||
<input type='submit' value='Create token'/>
|
<input type='submit' value='Create token'/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
require('./util/date.js');
|
||||||
const cookies = require('js-cookie');
|
const cookies = require('js-cookie');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
@ -119,8 +120,15 @@ class Api extends events.EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
createToken(userName, options) {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.post('/user-token/' + userName, {})
|
this.post('/user-token/' + userName, userTokenRequest)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
cookies.set(
|
cookies.set(
|
||||||
'auth',
|
'auth',
|
||||||
|
@ -327,7 +335,8 @@ class Api extends events.EventTarget {
|
||||||
try {
|
try {
|
||||||
if (this.userName && this.userToken) {
|
if (this.userName && this.userToken) {
|
||||||
req.auth = null;
|
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) {
|
else if (this.userName && this.userPassword) {
|
||||||
req.auth(
|
req.auth(
|
||||||
|
|
|
@ -199,7 +199,7 @@ class UserController {
|
||||||
_evtCreateToken(e) {
|
_evtCreateToken(e) {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
UserToken.create(e.detail.user.name)
|
UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
|
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
|
||||||
ctx.controller.showSuccess('Token ' + response.token + ' created.');
|
ctx.controller.showSuccess('Token ' + response.token + ' created.');
|
||||||
|
|
|
@ -12,8 +12,10 @@ class UserToken extends events.EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
get token() { return this._token; }
|
get token() { return this._token; }
|
||||||
|
get note() { return this._note; }
|
||||||
get enabled() { return this._enabled; }
|
get enabled() { return this._enabled; }
|
||||||
get version() { return this._version; }
|
get version() { return this._version; }
|
||||||
|
get expirationTime() { return this._expirationTime; }
|
||||||
get creationTime() { return this._creationTime; }
|
get creationTime() { return this._creationTime; }
|
||||||
|
|
||||||
static fromResponse(response) {
|
static fromResponse(response) {
|
||||||
|
@ -39,8 +41,17 @@ class UserToken extends events.EventTarget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(userName) {
|
static create(userName, note, expirationTime) {
|
||||||
return api.post(uri.formatApiLink('user-token', userName))
|
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 => {
|
.then(response => {
|
||||||
return Promise.resolve(UserToken.fromResponse(response))
|
return Promise.resolve(UserToken.fromResponse(response))
|
||||||
});
|
});
|
||||||
|
@ -63,7 +74,9 @@ class UserToken extends events.EventTarget {
|
||||||
_updateFromResponse(response) {
|
_updateFromResponse(response) {
|
||||||
const map = {
|
const map = {
|
||||||
_token: response.token,
|
_token: response.token,
|
||||||
|
_note: response.note,
|
||||||
_enabled: response.enabled,
|
_enabled: response.enabled,
|
||||||
|
_expirationTime: response.expirationTime,
|
||||||
_version: response.version,
|
_version: response.version,
|
||||||
_creationTime: response.creationTime,
|
_creationTime: response.creationTime,
|
||||||
};
|
};
|
||||||
|
|
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();
|
e.preventDefault();
|
||||||
this.dispatchEvent(new CustomEvent('submit', {
|
this.dispatchEvent(new CustomEvent('submit', {
|
||||||
detail: {
|
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() {
|
get _formNode() {
|
||||||
return this._hostNode.querySelector('#create-token-form');
|
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;
|
module.exports = UserTokenView;
|
||||||
|
|
|
@ -30,7 +30,17 @@ def create_user_token(
|
||||||
user = users.get_user_by_name(params['user_name'])
|
user = users.get_user_by_name(params['user_name'])
|
||||||
infix = 'self' if ctx.user.user_id == user.user_id else 'any'
|
infix = 'self' if ctx.user.user_id == user.user_id else 'any'
|
||||||
auth.verify_privilege(ctx.user, 'user_tokens:create:%s' % infix)
|
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)
|
return _serialize(ctx, user_token)
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,6 +57,15 @@ def update_user_token(
|
||||||
auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix)
|
auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix)
|
||||||
user_tokens.update_user_token_enabled(user_token,
|
user_tokens.update_user_token_enabled(user_token,
|
||||||
ctx.get_param_as_bool('enabled'))
|
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)
|
user_tokens.update_user_token_edit_time(user_token)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
return _serialize(ctx, user_token)
|
return _serialize(ctx, user_token)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import hashlib
|
import hashlib
|
||||||
import random
|
import random
|
||||||
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from datetime import datetime
|
||||||
from nacl import pwhash
|
from nacl import pwhash
|
||||||
from nacl.exceptions import InvalidkeyError
|
from nacl.exceptions import InvalidkeyError
|
||||||
from szurubooru import config, model, errors, db
|
from szurubooru import config, db, model, errors
|
||||||
from szurubooru.func import util
|
from szurubooru.func import util
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
RANK_MAP = OrderedDict([
|
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:
|
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:
|
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 datetime import datetime
|
||||||
from typing import Any, Optional, List, Dict, Callable
|
from typing import Any, Optional, List, Dict, Callable
|
||||||
from szurubooru import db, model, rest, errors
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,10 +30,12 @@ class UserTokenSerializer(serialization.BaseSerializer):
|
||||||
return {
|
return {
|
||||||
'user': self.serialize_user,
|
'user': self.serialize_user,
|
||||||
'token': self.serialize_token,
|
'token': self.serialize_token,
|
||||||
|
'note': self.serialize_note,
|
||||||
'enabled': self.serialize_enabled,
|
'enabled': self.serialize_enabled,
|
||||||
'version': self.serialize_version,
|
'expirationTime': self.serialize_expiration_time,
|
||||||
'creationTime': self.serialize_creation_time,
|
'creationTime': self.serialize_creation_time,
|
||||||
'lastEditTime': self.serialize_last_edit_time,
|
'lastEditTime': self.serialize_last_edit_time,
|
||||||
|
'version': self.serialize_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
def serialize_user(self) -> Any:
|
def serialize_user(self) -> Any:
|
||||||
|
@ -38,9 +50,15 @@ class UserTokenSerializer(serialization.BaseSerializer):
|
||||||
def serialize_token(self) -> Any:
|
def serialize_token(self) -> Any:
|
||||||
return self.user_token.token
|
return self.user_token.token
|
||||||
|
|
||||||
|
def serialize_note(self) -> Any:
|
||||||
|
return self.user_token.note
|
||||||
|
|
||||||
def serialize_enabled(self) -> Any:
|
def serialize_enabled(self) -> Any:
|
||||||
return self.user_token.enabled
|
return self.user_token.enabled
|
||||||
|
|
||||||
|
def serialize_expiration_time(self) -> Any:
|
||||||
|
return self.user_token.expiration_time
|
||||||
|
|
||||||
def serialize_version(self) -> Any:
|
def serialize_version(self) -> Any:
|
||||||
return self.user_token.version
|
return self.user_token.version
|
||||||
|
|
||||||
|
@ -69,15 +87,13 @@ def get_user_tokens(user: model.User) -> List[model.UserToken]:
|
||||||
.all())
|
.all())
|
||||||
|
|
||||||
|
|
||||||
def create_user_token(user: model.User) -> model.UserToken:
|
def create_user_token(user: model.User, enabled: bool) -> model.UserToken:
|
||||||
assert user
|
assert user
|
||||||
user_token = model.UserToken()
|
user_token = model.UserToken()
|
||||||
user_token.user = user
|
user_token.user = user
|
||||||
user_token.token = auth.generate_authorization_token()
|
user_token.token = auth.generate_authorization_token()
|
||||||
user_token.enabled = True
|
user_token.enabled = enabled
|
||||||
user_token.creation_time = datetime.utcnow()
|
user_token.creation_time = datetime.utcnow()
|
||||||
db.session.add(user_token)
|
|
||||||
db.session.commit()
|
|
||||||
return user_token
|
return user_token
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,10 +101,38 @@ def update_user_token_enabled(
|
||||||
user_token: model.UserToken, enabled: bool) -> None:
|
user_token: model.UserToken, enabled: bool) -> None:
|
||||||
assert user_token
|
assert user_token
|
||||||
if enabled is None:
|
if enabled is None:
|
||||||
raise InvalidEnabledFieldError('Enabled cannot be empty.')
|
raise InvalidEnabledError('Enabled cannot be empty.')
|
||||||
user_token.enabled = enabled
|
user_token.enabled = enabled
|
||||||
|
|
||||||
|
|
||||||
def update_user_token_edit_time(user_token: model.UserToken) -> None:
|
def update_user_token_edit_time(user_token: model.UserToken) -> None:
|
||||||
assert user_token
|
assert user_token
|
||||||
user_token.last_edit_time = datetime.utcnow()
|
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('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('token', sa.Unicode(length=36), 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('enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('expiration_time', sa.DateTime(), nullable=True),
|
||||||
sa.Column('creation_time', sa.DateTime(), nullable=False),
|
sa.Column('creation_time', sa.DateTime(), nullable=False),
|
||||||
sa.Column('last_edit_time', sa.DateTime(), nullable=True),
|
sa.Column('last_edit_time', sa.DateTime(), nullable=True),
|
||||||
sa.Column('version', sa.Integer(), nullable=False),
|
sa.Column('version', sa.Integer(), nullable=False),
|
||||||
|
|
|
@ -99,7 +99,9 @@ class UserToken(Base):
|
||||||
nullable=False,
|
nullable=False,
|
||||||
index=True)
|
index=True)
|
||||||
token = sa.Column('token', sa.Unicode(36), nullable=False)
|
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)
|
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)
|
creation_time = sa.Column('creation_time', sa.DateTime, nullable=False)
|
||||||
last_edit_time = sa.Column('last_edit_time', sa.DateTime)
|
last_edit_time = sa.Column('last_edit_time', sa.DateTime)
|
||||||
version = sa.Column('version', sa.Integer, default=1, nullable=False)
|
version = sa.Column('version', sa.Integer, default=1, nullable=False)
|
||||||
|
|
Reference in a new issue