Make Kobo optional

move jsonschema dependency to optional-requirements.txt
Added version of jsonschema to about section
Added additional column to RemoteAuthToken table
Update configuration of Kobo sync protocol
This commit is contained in:
Ozzieisaacs 2020-01-11 19:10:39 +01:00
parent 2798dd5916
commit 79a9ef4859
14 changed files with 321 additions and 163 deletions

14
cps.py
View File

@ -41,8 +41,13 @@ from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
from cps.kobo import kobo
from cps.kobo_auth import kobo_auth
try:
from cps.kobo import kobo
from cps.kobo_auth import kobo_auth
kobo_available = True
except ImportError:
kobo_available = False
try:
from cps.oauth_bb import oauth
@ -61,8 +66,9 @@ def main():
app.register_blueprint(admi)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
if kobo_available:
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
if oauth_available:
app.register_blueprint(oauth)
success = web_server.start()

View File

@ -67,6 +67,7 @@ _VERSIONS = OrderedDict(
Unidecode = unidecode_version,
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed',
Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed',
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else u'not installed',
)
_VERSIONS.update(uploader.get_versions())

View File

@ -45,7 +45,8 @@ from .web import admin_required, render_title_template, before_request, unconfig
feature_support = {
'ldap': False, # bool(services.ldap),
'goodreads': bool(services.goodreads_support)
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo)
}
# try:
@ -63,6 +64,7 @@ except ImportError:
oauth_check = {}
feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__)
log = logger.create()
@ -568,7 +570,7 @@ def _configuration_update_helper():
# Remote login configuration
_config_checkbox("config_remote_login")
if not config.config_remote_login:
ub.session.query(ub.RemoteAuthToken).delete()
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type==0).delete()
# Goodreads configuration
_config_checkbox("config_use_goodreads")
@ -693,7 +695,8 @@ def new_user():
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
registered_oauth=oauth_check, title=_(u"Add new user"))
registered_oauth=oauth_check, feature_support=feature_support,
title=_(u"Add new user"))
content.password = generate_password_hash(to_save["password"])
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\
.first()
@ -704,14 +707,15 @@ def new_user():
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
registered_oauth=oauth_check, title=_(u"Add new user"))
registered_oauth=oauth_check, feature_support=feature_support,
title=_(u"Add new user"))
else:
content.email = to_save["email"]
else:
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
registered_oauth=oauth_check)
feature_support=feature_support, registered_oauth=oauth_check)
try:
ub.session.add(content)
ub.session.commit()
@ -729,7 +733,7 @@ def new_user():
# content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT)
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
registered_oauth=oauth_check)
feature_support=feature_support, registered_oauth=oauth_check)
@admi.route("/admin/mailsettings")
@ -850,8 +854,14 @@ def edit_user(user_id):
content.kobo_user_key_hash = kobo_user_key_hash
else:
flash(_(u"Found an existing account for this Kobo UserKey."), category="error")
return render_title_template("user_edit.html", translations=translations, languages=languages,
new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
new_user=0,
content=content,
downloads=downloads,
registered_oauth=oauth_check,
feature_support=feature_support,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
if to_save["email"] and to_save["email"] != content.email:
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \
@ -860,9 +870,15 @@ def edit_user(user_id):
content.email = to_save["email"]
else:
flash(_(u"Found an existing account for this e-mail address."), category="error")
return render_title_template("user_edit.html", translations=translations, languages=languages,
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
mail_configured = config.get_mail_server_configured(),
new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
feature_support=feature_support,
new_user=0,
content=content,
downloads=downloads,
registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
if "nickname" in to_save and to_save["nickname"] != content.nickname:
# Query User nickname, if not existing, change
@ -877,6 +893,7 @@ def edit_user(user_id):
new_user=0, content=content,
downloads=downloads,
registered_oauth=oauth_check,
feature_support=feature_support,
title=_(u"Edit User %(nick)s", nick=content.nickname),
page="edituser")
@ -888,9 +905,15 @@ def edit_user(user_id):
except IntegrityError:
ub.session.rollback()
flash(_(u"An unknown error occured."), category="error")
return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
content=content, downloads=downloads, registered_oauth=oauth_check,
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
new_user=0,
content=content,
downloads=downloads,
registered_oauth=oauth_check,
mail_configured=config.get_mail_server_configured(),
feature_support=feature_support,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")

View File

@ -17,10 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import uuid
from base64 import b64decode, b64encode
from datetime import datetime
from time import gmtime, strftime
try:
@ -28,14 +26,12 @@ try:
except ImportError:
from urllib.parse import unquote
from jsonschema import validate, exceptions
from flask import (
Blueprint,
request,
make_response,
jsonify,
json,
current_app,
url_for,
redirect,
)
@ -44,7 +40,7 @@ from werkzeug.datastructures import Headers
from sqlalchemy import func
import requests
from . import config, logger, kobo_auth, db, helper
from . import config, logger, kobo_auth, db, helper, services
from .web import download_required
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]}
@ -56,19 +52,6 @@ kobo_auth.register_url_value_preprocessor(kobo)
log = logger.create()
def b64encode_json(json_data):
if sys.version_info < (3, 0):
return b64encode(json.dumps(json_data))
else:
return b64encode(json.dumps(json_data).encode())
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
def to_epoch_timestamp(datetime_object):
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
def get_store_url_for_current_request():
# Programmatically modify the current url to point to the official Kobo store
base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
@ -110,117 +93,11 @@ def redirect_or_proxy_request():
)
class SyncToken:
""" The SyncToken is used to persist state accross requests.
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
Attributes:
books_last_created: Datetime representing the newest book that the device knows about.
books_last_modified: Datetime representing the last modified book that the device knows about.
"""
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
VERSION = "1-0-0"
MIN_VERSION = "1-0-0"
token_schema = {
"type": "object",
"properties": {"version": {"type": "string"}, "data": {"type": "object"},},
}
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
data_schema_v1 = {
"type": "object",
"properties": {
"raw_kobo_store_token": {"type": "string"},
"books_last_modified": {"type": "string"},
"books_last_created": {"type": "string"},
},
}
def __init__(
self,
raw_kobo_store_token="",
books_last_created=datetime.min,
books_last_modified=datetime.min,
):
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
self.books_last_modified = books_last_modified
@staticmethod
def from_headers(headers):
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
if sync_token_header == "":
return SyncToken()
# On the first sync from a Kobo device, we may receive the SyncToken
# from the official Kobo store. Without digging too deep into it, that
# token is of the form [b64encoded blob].[b64encoded blob 2]
if "." in sync_token_header:
return SyncToken(raw_kobo_store_token=sync_token_header)
try:
sync_token_json = json.loads(
b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
)
validate(sync_token_json, SyncToken.token_schema)
if sync_token_json["version"] < SyncToken.MIN_VERSION:
raise ValueError
data_json = sync_token_json["data"]
validate(sync_token_json, SyncToken.data_schema_v1)
except (exceptions.ValidationError, ValueError) as e:
log.error("Sync token contents do not follow the expected json schema.")
return SyncToken()
raw_kobo_store_token = data_json["raw_kobo_store_token"]
try:
books_last_modified = datetime.utcfromtimestamp(
data_json["books_last_modified"]
)
books_last_created = datetime.utcfromtimestamp(
data_json["books_last_created"]
)
except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
return SyncToken(
raw_kobo_store_token=raw_kobo_store_token,
books_last_created=books_last_created,
books_last_modified=books_last_modified,
)
def set_kobo_store_header(self, store_headers):
store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
def merge_from_store_response(self, store_response):
self.raw_kobo_store_token = store_response.headers.get(
SyncToken.SYNC_TOKEN_HEADER, ""
)
def to_headers(self, headers):
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
def build_sync_token(self):
token = {
"version": SyncToken.VERSION,
"data": {
"raw_kobo_store_token": self.raw_kobo_store_token,
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
"books_last_created": to_epoch_timestamp(self.books_last_created),
},
}
return b64encode_json(token)
@kobo.route("/v1/library/sync")
@login_required
@download_required
def HandleSyncRequest():
sync_token = SyncToken.from_headers(request.headers)
sync_token = services.SyncToken.from_headers(request.headers)
log.info("Kobo library sync request received.")
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header

View File

@ -23,13 +23,13 @@ This module also includes research notes into the auth protocol used by Kobo dev
Log-in:
When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
Upon successful sign-in, the user is redirected to
Upon successful sign-in, the user is redirected to
https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
which serves the following response:
<script type='text/javascript'>location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com';</script>.
And triggers the insertion of a userKey into the device's User table.
Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is
required to authorize the API call.
@ -95,7 +95,7 @@ def load_user_from_kobo_request(request):
user = (
ub.session.query(ub.User)
.join(ub.RemoteAuthToken)
.filter(ub.RemoteAuthToken.auth_token == auth_token)
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
.first()
)
if user is not None:
@ -108,21 +108,23 @@ def load_user_from_kobo_request(request):
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
@kobo_auth.route("/generate_auth_token")
@kobo_auth.route("/generate_auth_token/<int:user_id>")
@login_required
def generate_auth_token():
def generate_auth_token(user_id):
# Invalidate any prevously generated Kobo Auth token for this user.
ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == current_user.id
).delete()
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == user_id
).filter(ub.RemoteAuthToken.token_type==1).first()
auth_token = ub.RemoteAuthToken()
auth_token.user_id = current_user.id
auth_token.expiration = datetime.max
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
if not auth_token:
auth_token = ub.RemoteAuthToken()
auth_token.user_id = user_id
auth_token.expiration = datetime.max
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
auth_token.token_type = 1
ub.session.add(auth_token)
ub.session.commit()
ub.session.add(auth_token)
ub.session.commit()
return render_title_template(
"generate_kobo_auth_url.html",
@ -131,3 +133,13 @@ def generate_auth_token():
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
),
)
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
@login_required
def delete_auth_token(user_id):
# Invalidate any prevously generated Kobo Auth token for this user.
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
.filter(ub.RemoteAuthToken.token_type==1).delete()
ub.session.commit()
return ""

148
cps/services/SyncToken.py Normal file
View File

@ -0,0 +1,148 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
from datetime import datetime
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import json
from .. import logger as log
def b64encode_json(json_data):
if sys.version_info < (3, 0):
return b64encode(json.dumps(json_data))
else:
return b64encode(json.dumps(json_data).encode())
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
def to_epoch_timestamp(datetime_object):
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
class SyncToken:
""" The SyncToken is used to persist state accross requests.
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
Attributes:
books_last_created: Datetime representing the newest book that the device knows about.
books_last_modified: Datetime representing the last modified book that the device knows about.
"""
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
VERSION = "1-0-0"
MIN_VERSION = "1-0-0"
token_schema = {
"type": "object",
"properties": {"version": {"type": "string"}, "data": {"type": "object"},},
}
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
data_schema_v1 = {
"type": "object",
"properties": {
"raw_kobo_store_token": {"type": "string"},
"books_last_modified": {"type": "string"},
"books_last_created": {"type": "string"},
},
}
def __init__(
self,
raw_kobo_store_token="",
books_last_created=datetime.min,
books_last_modified=datetime.min,
):
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
self.books_last_modified = books_last_modified
@staticmethod
def from_headers(headers):
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
if sync_token_header == "":
return SyncToken()
# On the first sync from a Kobo device, we may receive the SyncToken
# from the official Kobo store. Without digging too deep into it, that
# token is of the form [b64encoded blob].[b64encoded blob 2]
if "." in sync_token_header:
return SyncToken(raw_kobo_store_token=sync_token_header)
try:
sync_token_json = json.loads(
b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
)
validate(sync_token_json, SyncToken.token_schema)
if sync_token_json["version"] < SyncToken.MIN_VERSION:
raise ValueError
data_json = sync_token_json["data"]
validate(sync_token_json, SyncToken.data_schema_v1)
except (exceptions.ValidationError, ValueError) as e:
log.error("Sync token contents do not follow the expected json schema.")
return SyncToken()
raw_kobo_store_token = data_json["raw_kobo_store_token"]
try:
books_last_modified = datetime.utcfromtimestamp(
data_json["books_last_modified"]
)
books_last_created = datetime.utcfromtimestamp(
data_json["books_last_created"]
)
except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
return SyncToken(
raw_kobo_store_token=raw_kobo_store_token,
books_last_created=books_last_created,
books_last_modified=books_last_modified,
)
def set_kobo_store_header(self, store_headers):
store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
def merge_from_store_response(self, store_response):
self.raw_kobo_store_token = store_response.headers.get(
SyncToken.SYNC_TOKEN_HEADER, ""
)
def to_headers(self, headers):
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
def build_sync_token(self):
token = {
"version": SyncToken.VERSION,
"data": {
"raw_kobo_store_token": self.raw_kobo_store_token,
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
"books_last_created": to_epoch_timestamp(self.books_last_created),
},
}
return b64encode_json(token)

View File

@ -35,4 +35,9 @@ except ImportError as err:
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
ldap = None
try:
from . import SyncToken as SyncToken
kobo = True
except ImportError as err:
log.debug("cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
kobo = None

View File

@ -228,6 +228,41 @@ $(function() {
$(this).find(".modal-body").html("...");
});
$("#modal_kobo_token")
.on("show.bs.modal", function(e) {
var $modalBody = $(this).find(".modal-body");
// Prevent static assets from loading multiple times
var useCache = function(options) {
options.async = true;
options.cache = true;
};
preFilters.add(useCache);
$.get(e.relatedTarget.href).done(function(content) {
$modalBody.html(content);
preFilters.remove(useCache);
});
})
.on("hidden.bs.modal", function() {
$(this).find(".modal-body").html("...");
$("#config_delete_kobo_token").show();
});
$("#btndeletetoken").click(function() {
//get data-id attribute of the clicked element
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src;
var path = src.substring(0,src.lastIndexOf("/"));
// var domainId = $(this).value("domainId");
$.ajax({
method:"get",
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
});
$("#modalDeleteToken").modal("hide");
$("#config_delete_kobo_token").hide();
});
$(window).resize(function() {
$(".discover .row").isotope("layout");
});

View File

@ -1,7 +1,6 @@
{% extends "layout.html" %}
{% extends "fragment.html" %}
{% block body %}
<div class="well">
<h2 style="margin-top: 0">{{_('Generate Kobo Auth URL')}}</h2>
<p>
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>.
</p>
@ -12,4 +11,4 @@
{{_('Please note that every visit to this current page invalidates any previously generated Authentication url for this user.')}}</a>.
</p>
</div>
{% endblock %}
{% endblock %}

View File

@ -59,6 +59,13 @@
{% endfor %}
</div>
{% endif %}
{% if feature_support['kobo'] %}
<label>{{ _('Kobo Sync Token')}}</label>
<div class="form-group col">
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
<div class="btn btn-danger" id="config_delete_kobo_token" data-toggle="modal" data-target="#modalDeleteToken" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
</div>
{% endif %}
<div class="col-sm-6">
{% for element in sidebar %}
{% if element['config_show'] %}
@ -146,6 +153,35 @@
</div>
{% endif %}
</div>
<div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="kobo_tokenModalLabel">{{_('Generate Kobo Auth URL')}}</h4>
</div>
<div class="modal-body">...</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div>
</div>
</div>
</div>
<div id="modalDeleteToken" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger">
</div>
<div class="modal-body text-center">
<p>{{_('Do you really want to delete the Kobo Token?')}}</p>
<button type="button" class="btn btn-danger" id="btndeletetoken" value="{{content.id}}">{{_('Delete')}}</button>
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Back')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block modal %}
{{ restrict_modal() }}

View File

@ -199,6 +199,7 @@ class User(UserBase, Base):
allowed_tags = Column(String, default="")
restricted_column_value = Column(String, default="")
allowed_column_value = Column(String, default="")
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
if oauth_support:
@ -333,6 +334,7 @@ class RemoteAuthToken(Base):
user_id = Column(Integer, ForeignKey('user.id'))
verified = Column(Boolean, default=False)
expiration = Column(DateTime)
token_type = Column(Integer, default=0)
def __init__(self):
self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
@ -364,6 +366,15 @@ def migrate_Database(session):
conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
conn.execute("update registration set 'allow' = 1")
session.commit()
try:
session.query(exists().where(RemoteAuthToken.token_type)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some columns are missing
conn = engine.connect()
conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0")
conn.execute("update remote_auth_token set 'token_type' = 0")
session.commit()
# Handle table exists, but no content
cnt = session.query(Registration).count()
if not cnt:

View File

@ -55,7 +55,8 @@ from .redirect import redirect_back
feature_support = {
'ldap': False, # bool(services.ldap),
'goodreads': bool(services.goodreads_support)
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo)
}
try:
@ -1319,6 +1320,7 @@ def profile():
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
feature_support=feature_support,
registered_oauth=oauth_check, oauth_status=oauth_status)
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
# Query User nickname, if not existing, change
@ -1329,6 +1331,7 @@ def profile():
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
feature_support=feature_support,
new_user=0, content=current_user,
downloads=downloads,
registered_oauth=oauth_check,
@ -1360,13 +1363,13 @@ def profile():
flash(_(u"Found an existing account for this e-mail address."), category="error")
log.debug(u"Found an existing account for this e-mail address.")
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
translations=translations,
translations=translations, feature_support=feature_support,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
registered_oauth=oauth_check, oauth_status=oauth_status)
flash(_(u"Profile updated"), category="success")
log.debug(u"Profile updated")
return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
content=current_user, downloads=downloads,
content=current_user, downloads=downloads, feature_support=feature_support,
title= _(u"%(name)s's profile", name=current_user.nickname),
page="me", registered_oauth=oauth_check, oauth_status=oauth_status)

View File

@ -32,3 +32,6 @@ rarfile>=2.7
# other
natsort>=2.2.0
git+https://github.com/OzzieIsaacs/comicapi.git@5346716578b2843f54d522f44d01bc8d25001d24#egg=comicapi
#kobo integration
jsonschema>=3.2.0

View File

@ -13,4 +13,3 @@ SQLAlchemy>=1.1.0
tornado>=4.1
Wand>=0.4.4
unidecode>=0.04.19
jsonschema>=3.2.0