Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
D
douban-api-proxy
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
captainwong
douban-api-proxy
Commits
5a6ad970
Commit
5a6ad970
authored
Jul 08, 2017
by
OzzieIsaacs
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'button_padding/magic-link'
parents
93dfb961
623f5c8e
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
221 additions
and
2 deletions
+221
-2
redirect.py
cps/redirect.py
+25
-0
admin.html
cps/templates/admin.html
+2
-0
config_edit.html
cps/templates/config_edit.html
+4
-0
login.html
cps/templates/login.html
+4
-0
remote_login.html
cps/templates/remote_login.html
+40
-0
ub.py
cps/ub.py
+33
-0
web.py
cps/web.py
+109
-2
readme.md
readme.md
+4
-0
No files found.
cps/redirect.py
0 → 100644
View file @
5a6ad970
# http://flask.pocoo.org/snippets/62/
from
urlparse
import
urlparse
,
urljoin
from
flask
import
request
,
url_for
,
redirect
def
is_safe_url
(
target
):
ref_url
=
urlparse
(
request
.
host_url
)
test_url
=
urlparse
(
urljoin
(
request
.
host_url
,
target
))
return
test_url
.
scheme
in
(
'http'
,
'https'
)
and
ref_url
.
netloc
==
test_url
.
netloc
def
get_redirect_target
():
for
target
in
request
.
values
.
get
(
'next'
),
request
.
referrer
:
if
not
target
:
continue
if
is_safe_url
(
target
):
return
target
def
redirect_back
(
endpoint
,
**
values
):
target
=
request
.
form
[
'next'
]
if
not
target
or
not
is_safe_url
(
target
):
target
=
url_for
(
endpoint
,
**
values
)
return
redirect
(
target
)
cps/templates/admin.html
View file @
5a6ad970
...
@@ -64,6 +64,7 @@
...
@@ -64,6 +64,7 @@
<th>
{{_('Uploading')}}
</th>
<th>
{{_('Uploading')}}
</th>
<th>
{{_('Public registration')}}
</th>
<th>
{{_('Public registration')}}
</th>
<th>
{{_('Anonymous browsing')}}
</th>
<th>
{{_('Anonymous browsing')}}
</th>
<th>
{{_('Remote Login')}}
</th>
</tr>
</tr>
<tr>
<tr>
<td>
{{config.config_calibre_dir}}
</td>
<td>
{{config.config_calibre_dir}}
</td>
...
@@ -73,6 +74,7 @@
...
@@ -73,6 +74,7 @@
<td>
{% if config.config_uploading %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
<td>
{% if config.config_uploading %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
<td>
{% if config.config_public_reg %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
<td>
{% if config.config_public_reg %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
<td>
{% if config.config_anonbrowse %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
<td>
{% if config.config_anonbrowse %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
<td>
{% if config.config_remote_login %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
</table>
</table>
<div
class=
"btn btn-default"
><a
href=
"{{url_for('configuration')}}"
>
{{_('Configuration')}}
</a></div>
<div
class=
"btn btn-default"
><a
href=
"{{url_for('configuration')}}"
>
{{_('Configuration')}}
</a></div>
<h2>
{{_('Administration')}}
</h2>
<h2>
{{_('Administration')}}
</h2>
...
...
cps/templates/config_edit.html
View file @
5a6ad970
...
@@ -93,6 +93,10 @@
...
@@ -93,6 +93,10 @@
<input
type=
"checkbox"
id=
"config_public_reg"
name=
"config_public_reg"
{%
if
content
.
config_public_reg
%}
checked
{%
endif
%}
>
<input
type=
"checkbox"
id=
"config_public_reg"
name=
"config_public_reg"
{%
if
content
.
config_public_reg
%}
checked
{%
endif
%}
>
<label
for=
"config_public_reg"
>
{{_('Enable public registration')}}
</label>
<label
for=
"config_public_reg"
>
{{_('Enable public registration')}}
</label>
</div>
</div>
<div
class=
"form-group"
>
<input
type=
"checkbox"
id=
"config_remote_login"
name=
"config_remote_login"
{%
if
content
.
config_remote_login
%}
checked
{%
endif
%}
>
<label
for=
"config_remote_login"
>
{{_('Enable remote login ("magic link")')}}
</label>
</div>
<h2>
{{_('Default Settings for new users')}}
</h2>
<h2>
{{_('Default Settings for new users')}}
</h2>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<input
type=
"checkbox"
name=
"admin_role"
id=
"admin_role"
{%
if
content
.
role_admin
()
%}
checked
{%
endif
%}
>
<input
type=
"checkbox"
name=
"admin_role"
id=
"admin_role"
{%
if
content
.
role_admin
()
%}
checked
{%
endif
%}
>
...
...
cps/templates/login.html
View file @
5a6ad970
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
<div
class=
"well col-sm-6 col-sm-offset-2"
>
<div
class=
"well col-sm-6 col-sm-offset-2"
>
<h2
style=
"margin-top: 0"
>
{{_('Login')}}
</h2>
<h2
style=
"margin-top: 0"
>
{{_('Login')}}
</h2>
<form
method=
"POST"
role=
"form"
>
<form
method=
"POST"
role=
"form"
>
<input
type=
"hidden"
name=
"next"
value=
"{{next_url}}"
>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
for=
"username"
>
{{_('Username')}}
</label>
<label
for=
"username"
>
{{_('Username')}}
</label>
<input
type=
"text"
class=
"form-control"
id=
"username"
name=
"username"
placeholder=
"{{_('Username')}}"
>
<input
type=
"text"
class=
"form-control"
id=
"username"
name=
"username"
placeholder=
"{{_('Username')}}"
>
...
@@ -17,6 +18,9 @@
...
@@ -17,6 +18,9 @@
</label>
</label>
</div>
</div>
<button
type=
"submit"
name=
"submit"
class=
"btn btn-default"
>
{{_('Submit')}}
</button>
<button
type=
"submit"
name=
"submit"
class=
"btn btn-default"
>
{{_('Submit')}}
</button>
{% if remote_login %}
<a
href=
"{{url_for('remote_login')}}"
class=
"pull-right"
>
{{_('Log in with magic link')}}
</a>
{% endif %}
</form>
</form>
</div>
</div>
{% if error %}
{% if error %}
...
...
cps/templates/remote_login.html
0 → 100644
View file @
5a6ad970
{% extends "layout.html" %}
{% block body %}
<div
class=
"well"
>
<h2
style=
"margin-top: 0"
>
{{_('Remote Login')}}
</h2>
<p>
{{_('Using your another device, visit')}}
<a
href=
"{{verify_url}}"
>
{{verify_url}}
</a>
{{_('and log in')}}.
</p>
<p>
{{_('Once you do so, you will automatically get logged in on this device.')}}
</p>
<p>
{{_('The link will expire after %s minutes.' % 10)}}
</p>
</div>
{% endblock %}
{% block js %}
<script
type=
"text/javascript"
>
(
function
()
{
// Poll the server to check if the user has authenticated
var
t
=
setInterval
(
function
()
{
$
.
post
(
'{{url_for("token_verified")}}'
,
{
token
:
'{{token}}'
})
.
done
(
function
(
response
)
{
if
(
response
.
status
===
'success'
)
{
// Wait a tick so cookies are updated
setTimeout
(
function
()
{
window
.
location
.
href
=
'{{url_for("index")}}'
;
},
0
);
}
})
.
fail
(
function
(
xhr
)
{
clearInterval
(
t
);
var
response
=
JSON
.
parse
(
xhr
.
responseText
);
alert
(
response
.
message
);
});
},
5000
);
})()
</script>
{% endblock %}
cps/ub.py
View file @
5a6ad970
...
@@ -11,6 +11,8 @@ import logging
...
@@ -11,6 +11,8 @@ import logging
from
werkzeug.security
import
generate_password_hash
from
werkzeug.security
import
generate_password_hash
from
flask_babel
import
gettext
as
_
from
flask_babel
import
gettext
as
_
import
json
import
json
import
datetime
from
binascii
import
hexlify
dbpath
=
os
.
path
.
join
(
os
.
path
.
normpath
(
os
.
getenv
(
"CALIBRE_DBPATH"
,
os
.
path
.
dirname
(
os
.
path
.
realpath
(
__file__
))
+
os
.
sep
+
".."
+
os
.
sep
)),
"app.db"
)
dbpath
=
os
.
path
.
join
(
os
.
path
.
normpath
(
os
.
getenv
(
"CALIBRE_DBPATH"
,
os
.
path
.
dirname
(
os
.
path
.
realpath
(
__file__
))
+
os
.
sep
+
".."
+
os
.
sep
)),
"app.db"
)
engine
=
create_engine
(
'sqlite:///{0}'
.
format
(
dbpath
),
echo
=
False
)
engine
=
create_engine
(
'sqlite:///{0}'
.
format
(
dbpath
),
echo
=
False
)
...
@@ -260,11 +262,29 @@ class Settings(Base):
...
@@ -260,11 +262,29 @@ class Settings(Base):
config_google_drive_calibre_url_base
=
Column
(
String
)
config_google_drive_calibre_url_base
=
Column
(
String
)
config_google_drive_watch_changes_response
=
Column
(
String
)
config_google_drive_watch_changes_response
=
Column
(
String
)
config_columns_to_ignore
=
Column
(
String
)
config_columns_to_ignore
=
Column
(
String
)
config_remote_login
=
Column
(
Boolean
)
def
__repr__
(
self
):
def
__repr__
(
self
):
pass
pass
class
RemoteAuthToken
(
Base
):
__tablename__
=
'remote_auth_token'
id
=
Column
(
Integer
,
primary_key
=
True
)
auth_token
=
Column
(
String
(
8
),
unique
=
True
)
user_id
=
Column
(
Integer
,
ForeignKey
(
'user.id'
))
verified
=
Column
(
Boolean
,
default
=
False
)
expiration
=
Column
(
DateTime
)
def
__init__
(
self
):
self
.
auth_token
=
hexlify
(
os
.
urandom
(
4
))
self
.
expiration
=
datetime
.
datetime
.
now
()
+
datetime
.
timedelta
(
minutes
=
10
)
# 10 min from now
def
__repr__
(
self
):
return
'<Token
%
r>'
%
self
.
id
# Class holds all application specific settings in calibre-web
# Class holds all application specific settings in calibre-web
class
Config
:
class
Config
:
def
__init__
(
self
):
def
__init__
(
self
):
...
@@ -299,6 +319,7 @@ class Config:
...
@@ -299,6 +319,7 @@ class Config:
self
.
config_columns_to_ignore
=
data
.
config_columns_to_ignore
self
.
config_columns_to_ignore
=
data
.
config_columns_to_ignore
self
.
db_configured
=
bool
(
self
.
config_calibre_dir
is
not
None
and
self
.
db_configured
=
bool
(
self
.
config_calibre_dir
is
not
None
and
(
not
self
.
config_use_google_drive
or
os
.
path
.
exists
(
self
.
config_calibre_dir
+
'/metadata.db'
)))
(
not
self
.
config_use_google_drive
or
os
.
path
.
exists
(
self
.
config_calibre_dir
+
'/metadata.db'
)))
self
.
config_remote_login
=
data
.
config_remote_login
@
property
@
property
def
get_main_dir
(
self
):
def
get_main_dir
(
self
):
...
@@ -449,6 +470,16 @@ def migrate_Database():
...
@@ -449,6 +470,16 @@ def migrate_Database():
session
.
commit
()
session
.
commit
()
if
session
.
query
(
User
)
.
filter
(
User
.
role
.
op
(
'&'
)(
ROLE_ANONYMOUS
)
==
ROLE_ANONYMOUS
)
.
first
()
is
None
:
if
session
.
query
(
User
)
.
filter
(
User
.
role
.
op
(
'&'
)(
ROLE_ANONYMOUS
)
==
ROLE_ANONYMOUS
)
.
first
()
is
None
:
create_anonymous_user
()
create_anonymous_user
()
try
:
session
.
query
(
exists
()
.
where
(
Settings
.
config_remote_login
))
.
scalar
()
except
exc
.
OperationalError
:
conn
=
engine
.
connect
()
conn
.
execute
(
"ALTER TABLE Settings ADD column `config_remote_login` INTEGER DEFAULT 0"
)
def
clean_database
():
# Remove expired remote login tokens
now
=
datetime
.
datetime
.
now
()
session
.
query
(
RemoteAuthToken
)
.
filter
(
now
>
RemoteAuthToken
.
expiration
)
.
delete
()
def
create_default_config
():
def
create_default_config
():
settings
=
Settings
()
settings
=
Settings
()
...
@@ -529,7 +560,9 @@ if not os.path.exists(dbpath):
...
@@ -529,7 +560,9 @@ if not os.path.exists(dbpath):
except
Exception
:
except
Exception
:
raise
raise
else
:
else
:
Base
.
metadata
.
create_all
(
engine
)
migrate_Database
()
migrate_Database
()
clean_database
()
# Generate global Settings Object accecable from every file
# Generate global Settings Object accecable from every file
config
=
Config
()
config
=
Config
()
cps/web.py
View file @
5a6ad970
...
@@ -58,6 +58,7 @@ import shutil
...
@@ -58,6 +58,7 @@ import shutil
import
gdriveutils
import
gdriveutils
import
tempfile
import
tempfile
import
hashlib
import
hashlib
from
redirect
import
redirect_back
,
is_safe_url
from
tornado
import
version
as
tornadoVersion
from
tornado
import
version
as
tornadoVersion
...
@@ -372,6 +373,21 @@ def login_required_if_no_ano(func):
...
@@ -372,6 +373,21 @@ def login_required_if_no_ano(func):
return
login_required
(
func
)
return
login_required
(
func
)
def
remote_login_required
(
f
):
@
wraps
(
f
)
def
inner
(
*
args
,
**
kwargs
):
if
config
.
config_remote_login
:
return
f
(
*
args
,
**
kwargs
)
if
request
.
is_xhr
:
data
=
{
'status'
:
'error'
,
'message'
:
'Forbidden'
}
response
=
make_response
(
json
.
dumps
(
data
,
ensure_ascii
=
false
))
response
.
headers
[
"Content-Type"
]
=
"application/json; charset=utf-8"
return
response
,
403
abort
(
403
)
return
inner
# custom jinja filters
# custom jinja filters
@
app
.
template_filter
(
'shortentitle'
)
@
app
.
template_filter
(
'shortentitle'
)
def
shortentitle_filter
(
s
):
def
shortentitle_filter
(
s
):
...
@@ -1809,14 +1825,20 @@ def login():
...
@@ -1809,14 +1825,20 @@ def login():
if
user
and
check_password_hash
(
user
.
password
,
form
[
'password'
]):
if
user
and
check_password_hash
(
user
.
password
,
form
[
'password'
]):
login_user
(
user
,
remember
=
True
)
login_user
(
user
,
remember
=
True
)
flash
(
_
(
u"you are now logged in as: '
%(nickname)
s'"
,
nickname
=
user
.
nickname
),
category
=
"success"
)
flash
(
_
(
u"you are now logged in as: '
%(nickname)
s'"
,
nickname
=
user
.
nickname
),
category
=
"success"
)
return
redirect
(
url_for
(
"index"
))
return
redirect
_back
(
url_for
(
"index"
))
else
:
else
:
ipAdress
=
request
.
headers
.
get
(
'X-Forwarded-For'
,
request
.
remote_addr
)
ipAdress
=
request
.
headers
.
get
(
'X-Forwarded-For'
,
request
.
remote_addr
)
app
.
logger
.
info
(
'Login failed for user "'
+
form
[
'username'
]
+
'" IP-adress: '
+
ipAdress
)
app
.
logger
.
info
(
'Login failed for user "'
+
form
[
'username'
]
+
'" IP-adress: '
+
ipAdress
)
flash
(
_
(
u"Wrong Username or Password"
),
category
=
"error"
)
flash
(
_
(
u"Wrong Username or Password"
),
category
=
"error"
)
return
render_title_template
(
'login.html'
,
title
=
_
(
u"login"
))
next_url
=
request
.
args
.
get
(
'next'
)
if
next_url
is
None
or
not
is_safe_url
(
next_url
):
next_url
=
url_for
(
'index'
)
return
render_title_template
(
'login.html'
,
title
=
_
(
u"login"
),
next_url
=
next_url
,
remote_login
=
config
.
config_remote_login
)
@
app
.
route
(
'/logout'
)
@
app
.
route
(
'/logout'
)
...
@@ -1827,6 +1849,87 @@ def logout():
...
@@ -1827,6 +1849,87 @@ def logout():
return
redirect
(
url_for
(
'login'
))
return
redirect
(
url_for
(
'login'
))
@
app
.
route
(
'/remote/login'
)
@
remote_login_required
def
remote_login
():
auth_token
=
ub
.
RemoteAuthToken
()
ub
.
session
.
add
(
auth_token
)
ub
.
session
.
commit
()
verify_url
=
url_for
(
'verify_token'
,
token
=
auth_token
.
auth_token
,
_external
=
true
)
return
render_title_template
(
'remote_login.html'
,
title
=
_
(
u"login"
),
token
=
auth_token
.
auth_token
,
verify_url
=
verify_url
)
@
app
.
route
(
'/verify/<token>'
)
@
remote_login_required
@
login_required
def
verify_token
(
token
):
auth_token
=
ub
.
session
.
query
(
ub
.
RemoteAuthToken
)
.
filter
(
ub
.
RemoteAuthToken
.
auth_token
==
token
)
.
first
()
# Token not found
if
auth_token
is
None
:
flash
(
_
(
u"Token not found"
),
category
=
"error"
)
return
redirect
(
url_for
(
'index'
))
# Token expired
if
datetime
.
datetime
.
now
()
>
auth_token
.
expiration
:
ub
.
session
.
delete
(
auth_token
)
ub
.
session
.
commit
()
flash
(
_
(
u"Token has expired"
),
category
=
"error"
)
return
redirect
(
url_for
(
'index'
))
# Update token with user information
auth_token
.
user_id
=
current_user
.
id
auth_token
.
verified
=
True
ub
.
session
.
commit
()
flash
(
_
(
u"Success! Please return to your device"
),
category
=
"success"
)
return
redirect
(
url_for
(
'index'
))
@
app
.
route
(
'/ajax/verify_token'
,
methods
=
[
'POST'
])
@
remote_login_required
def
token_verified
():
token
=
request
.
form
[
'token'
]
auth_token
=
ub
.
session
.
query
(
ub
.
RemoteAuthToken
)
.
filter
(
ub
.
RemoteAuthToken
.
auth_token
==
token
)
.
first
()
data
=
{}
# Token not found
if
auth_token
is
None
:
data
[
'status'
]
=
'error'
data
[
'message'
]
=
_
(
u"Token not found"
)
# Token expired
elif
datetime
.
datetime
.
now
()
>
auth_token
.
expiration
:
ub
.
session
.
delete
(
auth_token
)
ub
.
session
.
commit
()
data
[
'status'
]
=
'error'
data
[
'message'
]
=
_
(
u"Token has expired"
)
elif
not
auth_token
.
verified
:
data
[
'status'
]
=
'not_verified'
else
:
user
=
ub
.
session
.
query
(
ub
.
User
)
.
filter
(
ub
.
User
.
id
==
auth_token
.
user_id
)
.
first
()
login_user
(
user
)
ub
.
session
.
delete
(
auth_token
)
ub
.
session
.
commit
()
data
[
'status'
]
=
'success'
flash
(
_
(
u"you are now logged in as: '
%(nickname)
s'"
,
nickname
=
user
.
nickname
),
category
=
"success"
)
response
=
make_response
(
json
.
dumps
(
data
,
ensure_ascii
=
false
))
response
.
headers
[
"Content-Type"
]
=
"application/json; charset=utf-8"
return
response
@
app
.
route
(
'/send/<int:book_id>'
)
@
app
.
route
(
'/send/<int:book_id>'
)
@
login_required
@
login_required
@
download_required
@
download_required
...
@@ -2186,6 +2289,10 @@ def configuration_helper(origin):
...
@@ -2186,6 +2289,10 @@ def configuration_helper(origin):
content
.
config_anonbrowse
=
1
content
.
config_anonbrowse
=
1
if
"config_public_reg"
in
to_save
and
to_save
[
"config_public_reg"
]
==
"on"
:
if
"config_public_reg"
in
to_save
and
to_save
[
"config_public_reg"
]
==
"on"
:
content
.
config_public_reg
=
1
content
.
config_public_reg
=
1
content
.
config_remote_login
=
(
"config_remote_login"
in
to_save
and
to_save
[
"config_remote_login"
]
==
"on"
)
if
not
content
.
config_remote_login
:
ub
.
session
.
query
(
ub
.
RemoteAuthToken
)
.
delete
()
content
.
config_default_role
=
0
content
.
config_default_role
=
0
if
"admin_role"
in
to_save
:
if
"admin_role"
in
to_save
:
...
...
readme.md
View file @
5a6ad970
...
@@ -26,6 +26,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
...
@@ -26,6 +26,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
-
Support for Calibre custom columns
-
Support for Calibre custom columns
-
Fine grained per-user permissions
-
Fine grained per-user permissions
-
Self update capability
-
Self update capability
-
"Magic Link" login to make it easy to log on eReaders
## Quick start
## Quick start
...
@@ -56,6 +57,9 @@ Tick to allow not logged in users to browse the catalog, anonymous user permissi
...
@@ -56,6 +57,9 @@ Tick to allow not logged in users to browse the catalog, anonymous user permissi
Enable uploading:
Enable uploading:
Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed.
Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed.
Enable remote login ("magic link"):
Tick to enable remote login, i.e. a link that allows user to log in via a different device.
## Requirements
## Requirements
Python 2.7+
Python 2.7+
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment