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 @@
<th>
{{_('Uploading')}}
</th>
<th>
{{_('Public registration')}}
</th>
<th>
{{_('Anonymous browsing')}}
</th>
<th>
{{_('Remote Login')}}
</th>
</tr>
<tr>
<td>
{{config.config_calibre_dir}}
</td>
...
...
@@ -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_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_remote_login %}
<span
class=
"glyphicon glyphicon-ok"
></span>
{% else %}
<span
class=
"glyphicon glyphicon-remove"
></span>
{% endif %}
</td>
</table>
<div
class=
"btn btn-default"
><a
href=
"{{url_for('configuration')}}"
>
{{_('Configuration')}}
</a></div>
<h2>
{{_('Administration')}}
</h2>
...
...
cps/templates/config_edit.html
View file @
5a6ad970
...
...
@@ -93,6 +93,10 @@
<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>
</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>
<div
class=
"form-group"
>
<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 @@
<div
class=
"well col-sm-6 col-sm-offset-2"
>
<h2
style=
"margin-top: 0"
>
{{_('Login')}}
</h2>
<form
method=
"POST"
role=
"form"
>
<input
type=
"hidden"
name=
"next"
value=
"{{next_url}}"
>
<div
class=
"form-group"
>
<label
for=
"username"
>
{{_('Username')}}
</label>
<input
type=
"text"
class=
"form-control"
id=
"username"
name=
"username"
placeholder=
"{{_('Username')}}"
>
...
...
@@ -17,6 +18,9 @@
</label>
</div>
<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>
</div>
{% 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
from
werkzeug.security
import
generate_password_hash
from
flask_babel
import
gettext
as
_
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"
)
engine
=
create_engine
(
'sqlite:///{0}'
.
format
(
dbpath
),
echo
=
False
)
...
...
@@ -260,11 +262,29 @@ class Settings(Base):
config_google_drive_calibre_url_base
=
Column
(
String
)
config_google_drive_watch_changes_response
=
Column
(
String
)
config_columns_to_ignore
=
Column
(
String
)
config_remote_login
=
Column
(
Boolean
)
def
__repr__
(
self
):
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
Config
:
def
__init__
(
self
):
...
...
@@ -299,6 +319,7 @@ class Config:
self
.
config_columns_to_ignore
=
data
.
config_columns_to_ignore
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'
)))
self
.
config_remote_login
=
data
.
config_remote_login
@
property
def
get_main_dir
(
self
):
...
...
@@ -449,6 +470,16 @@ def migrate_Database():
session
.
commit
()
if
session
.
query
(
User
)
.
filter
(
User
.
role
.
op
(
'&'
)(
ROLE_ANONYMOUS
)
==
ROLE_ANONYMOUS
)
.
first
()
is
None
:
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
():
settings
=
Settings
()
...
...
@@ -529,7 +560,9 @@ if not os.path.exists(dbpath):
except
Exception
:
raise
else
:
Base
.
metadata
.
create_all
(
engine
)
migrate_Database
()
clean_database
()
# Generate global Settings Object accecable from every file
config
=
Config
()
cps/web.py
View file @
5a6ad970
...
...
@@ -58,6 +58,7 @@ import shutil
import
gdriveutils
import
tempfile
import
hashlib
from
redirect
import
redirect_back
,
is_safe_url
from
tornado
import
version
as
tornadoVersion
...
...
@@ -372,6 +373,21 @@ def login_required_if_no_ano(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
@
app
.
template_filter
(
'shortentitle'
)
def
shortentitle_filter
(
s
):
...
...
@@ -1809,14 +1825,20 @@ def login():
if
user
and
check_password_hash
(
user
.
password
,
form
[
'password'
]):
login_user
(
user
,
remember
=
True
)
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
:
ipAdress
=
request
.
headers
.
get
(
'X-Forwarded-For'
,
request
.
remote_addr
)
app
.
logger
.
info
(
'Login failed for user "'
+
form
[
'username'
]
+
'" IP-adress: '
+
ipAdress
)
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'
)
...
...
@@ -1827,6 +1849,87 @@ def logout():
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>'
)
@
login_required
@
download_required
...
...
@@ -2186,6 +2289,10 @@ def configuration_helper(origin):
content
.
config_anonbrowse
=
1
if
"config_public_reg"
in
to_save
and
to_save
[
"config_public_reg"
]
==
"on"
:
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
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
-
Support for Calibre custom columns
-
Fine grained per-user permissions
-
Self update capability
-
"Magic Link" login to make it easy to log on eReaders
## Quick start
...
...
@@ -56,6 +57,9 @@ Tick to allow not logged in users to browse the catalog, anonymous user permissi
Enable uploading:
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
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