add user settings; support for various reply mechanisms
This commit is contained in:
parent
dc90b729d4
commit
8d0d6da450
11 changed files with 386 additions and 71 deletions
|
@ -1,4 +1,5 @@
|
|||
import bleach
|
||||
import json
|
||||
from .extensions import db
|
||||
|
||||
|
||||
|
@ -12,6 +13,24 @@ bleach.ALLOWED_ATTRIBUTES.update({
|
|||
})
|
||||
|
||||
|
||||
class JsonType(db.TypeDecorator):
|
||||
"""Represents an immutable structure as a json-encoded string.
|
||||
http://docs.sqlalchemy.org/en/rel_0_9/core/types.html#marshal-json-strings
|
||||
"""
|
||||
impl = db.Text
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
users_to_feeds = db.Table(
|
||||
'users_to_feeds', db.Model.metadata,
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), index=True),
|
||||
|
@ -24,6 +43,7 @@ class User(db.Model):
|
|||
domain = db.Column(db.String(256))
|
||||
micropub_endpoint = db.Column(db.String(512))
|
||||
access_token = db.Column(db.String(512))
|
||||
settings = db.Column(JsonType)
|
||||
|
||||
# Flask-Login integration
|
||||
def is_authenticated(self):
|
||||
|
|
70
woodwind/static/indieconfig.js
Normal file
70
woodwind/static/indieconfig.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*jslint browser: true, plusplus: true, vars: true, indent: 2 */
|
||||
window.loadIndieConfig = (function () {
|
||||
'use strict';
|
||||
|
||||
// Indie-Config Loading script
|
||||
// by Pelle Wessman, voxpelli.com
|
||||
// MIT-licensed
|
||||
// http://indiewebcamp.com/indie-config
|
||||
|
||||
var config, configFrame, configTimeout,
|
||||
callbacks = [],
|
||||
handleConfig, parseConfig;
|
||||
|
||||
// When the configuration has been loaded – deregister all loading mechanics and call all callbacks
|
||||
handleConfig = function () {
|
||||
config = config || {};
|
||||
|
||||
configFrame.parentNode.removeChild(configFrame);
|
||||
configFrame = undefined;
|
||||
|
||||
window.removeEventListener('message', parseConfig);
|
||||
|
||||
clearTimeout(configTimeout);
|
||||
|
||||
while (callbacks[0]) {
|
||||
callbacks.shift()(config);
|
||||
}
|
||||
};
|
||||
|
||||
// When we receive a message, check if the source is right and try to parse it
|
||||
parseConfig = function (message) {
|
||||
var correctSource = (configFrame && message.source === configFrame.contentWindow);
|
||||
|
||||
if (correctSource && config === undefined) {
|
||||
try {
|
||||
config = JSON.parse(message.data);
|
||||
} catch (ignore) {}
|
||||
|
||||
handleConfig();
|
||||
}
|
||||
};
|
||||
|
||||
return function (callback) {
|
||||
// If the config is already loaded, call callback right away
|
||||
if (config) {
|
||||
callback(config);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise add the callback to the queue
|
||||
callbacks.push(callback);
|
||||
|
||||
// Are we already trying to load the Indie-Config, then wait
|
||||
if (configFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the iframe that will load the Indie-Config
|
||||
configFrame = document.createElement('iframe');
|
||||
configFrame.src = 'web+action:load';
|
||||
document.getElementsByTagName('body')[0].appendChild(configFrame);
|
||||
configFrame.style.display = 'none';
|
||||
|
||||
// Listen for messages so we will catch the Indie-Config message
|
||||
window.addEventListener('message', parseConfig);
|
||||
|
||||
// And if no such Indie-Config message has been loaded in a while, abort the loading
|
||||
configTimeout = setTimeout(handleConfig, 3000);
|
||||
};
|
||||
}());
|
|
@ -415,11 +415,32 @@ article {
|
|||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block; }
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
max-width: 100%; }
|
||||
|
||||
textarea, input[type="text"], input[type="url"] {
|
||||
width: 100%;
|
||||
margin: 0.25em 0; }
|
||||
textarea.input-25, input[type="text"].input-25, input[type="url"].input-25 {
|
||||
width: 23%; }
|
||||
textarea.input-50, input[type="text"].input-50, input[type="url"].input-50 {
|
||||
width: 48%; }
|
||||
textarea.input-75, input[type="text"].input-75, input[type="url"].input-75 {
|
||||
width: 73%; }
|
||||
|
||||
.reply-area {
|
||||
text-align: center; }
|
||||
.reply-area a {
|
||||
display: inline-block;
|
||||
padding: 0.2em;
|
||||
border: 1px solid #687D77;
|
||||
border-radius: 4px;
|
||||
background-color: #ECEBF0;
|
||||
text-decoration: none;
|
||||
color: #484A47;
|
||||
min-width: 50px;
|
||||
text-align: center; }
|
||||
|
||||
button {
|
||||
padding: 0.25em; }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"version": 3,
|
||||
"mappings": "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",
|
||||
"mappings": "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",
|
||||
"sources": ["normalize.scss","style.scss"],
|
||||
"names": [],
|
||||
"file": "style.css"
|
||||
|
|
|
@ -92,12 +92,39 @@ article {
|
|||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
textarea, input[type="text"], input[type="url"] {
|
||||
width: 100%;
|
||||
margin: 0.25em 0 ;
|
||||
margin: 0.25em 0;
|
||||
|
||||
&.input-25 {
|
||||
width: 23%;
|
||||
}
|
||||
&.input-50 {
|
||||
width: 48%;
|
||||
}
|
||||
&.input-75 {
|
||||
width: 73%;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-area {
|
||||
text-align: center;
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 0.2em;
|
||||
border: 1px solid $sirocco;
|
||||
border-radius: 4px;
|
||||
background-color: $athens-gray;
|
||||
text-decoration: none;
|
||||
color: $lunar-green;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
63
woodwind/static/webaction.js
Normal file
63
woodwind/static/webaction.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*jslint browser: true, plusplus: true, vars: true, indent: 2 */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var loadingClassRegexp = /(^|\s)indieconfig-loading(\s|$)/;
|
||||
|
||||
var doTheAction = function (indieConfig) {
|
||||
var href, action, anchors;
|
||||
|
||||
// Don't block the tag anymore as the queued action is now handled
|
||||
this.className = this.className.replace(loadingClassRegexp, ' ');
|
||||
|
||||
// Pick the correct endpoint for the correct action
|
||||
action = this.getAttribute('do');
|
||||
href = indieConfig[action];
|
||||
|
||||
// If no endpoint is found, try the URL of the first a-tag within it
|
||||
if (!href) {
|
||||
anchors = this.getElementsByTagName('a');
|
||||
if (anchors[0]) {
|
||||
href = anchors[0].href;
|
||||
}
|
||||
}
|
||||
|
||||
// We have found an endpoint!
|
||||
if (href) {
|
||||
//Resolve a relative target
|
||||
var target = document.createElement('a');
|
||||
target.href = this.getAttribute('with');
|
||||
target = target.href;
|
||||
|
||||
// Insert the target into the endpoint
|
||||
href = href.replace('{url}', encodeURIComponent(target || window.location.href));
|
||||
|
||||
// And redirect to it
|
||||
window.open( href, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
// Event handler for a click on an indie-action tag
|
||||
var handleTheAction = function (e) {
|
||||
// Prevent the default of eg. any a-tag fallback within the indie-action tag
|
||||
e.preventDefault();
|
||||
|
||||
// Make sure this tag hasn't already been queued for the indieconfig-load
|
||||
if (!loadingClassRegexp.test(this.className)) {
|
||||
this.className += ' indieconfig-loading';
|
||||
// Set "doTheAction" to be called when the indie-config has been loaded
|
||||
window.loadIndieConfig(doTheAction.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
// Once the page is loased add click event listeners to all indie-action tags
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var actions = document.querySelectorAll('indie-action'),
|
||||
i,
|
||||
length = actions.length;
|
||||
|
||||
for (i = 0; i < length; i++) {
|
||||
actions[i].addEventListener('click', handleTheAction);
|
||||
}
|
||||
});
|
||||
}());
|
34
woodwind/templates/_reply.jinja2
Normal file
34
woodwind/templates/_reply.jinja2
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% set settings = current_user.settings or {} %}
|
||||
{% set replyMethod = settings.get('reply-method') %}
|
||||
|
||||
{% if replyMethod == 'micropub' and current_user.micropub_endpoint %}
|
||||
<form class="like-form" action="{{ current_user.micropub_endpoint }}" method="POST" style="display:inline">
|
||||
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
|
||||
<input type="hidden" name="h" value="entry"/>
|
||||
<input type="hidden" name="like-of" value="{{ entry.permalink }}"/>
|
||||
<button type="submit" class="like-button">Like</button>
|
||||
<span class="submit-response"></span>
|
||||
</form>
|
||||
|
||||
<button class="show-reply-form">Reply</button>
|
||||
|
||||
<form class="reply-form" action="{{ current_user.micropub_endpoint }}" method="POST">
|
||||
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
|
||||
<input type="hidden" name="h" value="entry"/>
|
||||
<input type="hidden" name="in-reply-to" value="{{ entry.permalink }}"/>
|
||||
<textarea name="content"></textarea>
|
||||
<button type="submit" class="reply-button">Reply</button>
|
||||
<span class="submit-response"></span>
|
||||
</form>
|
||||
|
||||
{% elif replyMethod == 'indie-config' %}
|
||||
{% for action in settings.get('indie-config-actions', []) %}
|
||||
<indie-action with="{{ entry.permalink }}" do="{{ action }}">
|
||||
<a class="reply-link" href="#">{{ action | capitalize }}</a>
|
||||
</indie-action>
|
||||
{% endfor %}
|
||||
{% elif replyMethod == 'action-urls' %}
|
||||
{% for action, url in settings.get('action-urls', []) %}
|
||||
<a class="reply-link" href="{{ url | replace('{url}', entry.permalink) }}" target="_blank">{{ action | capitalize }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
|
@ -1,6 +1,13 @@
|
|||
{% extends "base.jinja2" %}
|
||||
{% block head %}
|
||||
<script src="{{url_for('static', filename='feed.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='feed.js')}}"></script>
|
||||
|
||||
{% if current_user and current_user.settings
|
||||
and current_user.settings.get('reply-method') == 'indie-config' %}
|
||||
<script src="{{url_for('static', filename='indieconfig.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='webaction.js')}}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock head %}
|
||||
|
||||
{% block header %}
|
||||
|
@ -34,28 +41,9 @@
|
|||
{% endif %}
|
||||
<footer>
|
||||
<a href="{{ entry.permalink }}">{{ entry.published }}</a>
|
||||
|
||||
{% if current_user.micropub_endpoint %}
|
||||
<form class="like-form" action="{{ current_user.micropub_endpoint }}" method="POST" style="display:inline">
|
||||
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
|
||||
<input type="hidden" name="h" value="entry"/>
|
||||
<input type="hidden" name="like-of" value="{{ entry.permalink }}"/>
|
||||
<button type="submit" class="like-button">Like</button>
|
||||
<span class="submit-response"></span>
|
||||
</form>
|
||||
|
||||
<button class="show-reply-form">Reply</button>
|
||||
|
||||
<form class="reply-form" action="{{ current_user.micropub_endpoint }}" method="POST">
|
||||
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
|
||||
<input type="hidden" name="h" value="entry"/>
|
||||
<input type="hidden" name="in-reply-to" value="{{ entry.permalink }}"/>
|
||||
<textarea name="content"></textarea>
|
||||
<button type="submit" class="reply-button">Reply</button>
|
||||
<span class="submit-response"></span>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="reply-area">
|
||||
{% include '_reply.jinja2' with context %}
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{% extends "base.jinja2" %}
|
||||
{% block body %}
|
||||
<form action="{{ url_for('.login') }}" method="GET">
|
||||
<input type="text" name="me" placeholder="mydomain.com" />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -2,45 +2,123 @@
|
|||
{% block body %}
|
||||
<main>
|
||||
|
||||
<!-- reply via micropub -->
|
||||
<form name="settings" action="{{ url_for('.settings') }}" method="POST">
|
||||
<button type="submit">Save Changes</button>
|
||||
|
||||
{% if current_user.micropub_endpoint or current_user.access_token %}
|
||||
<form>
|
||||
<label>Micropub Endpoint</label>
|
||||
<input type="text" value="{{ current_user.micropub_endpoint }}" readonly />
|
||||
<div id="reply-mechanism-settings">
|
||||
{% set reply_method = settings.get('reply-method') %}
|
||||
|
||||
<label>Access Token</label>
|
||||
<input type="text" value="{{ current_user.access_token }}" readonly />
|
||||
</form>
|
||||
<h2>Reply Mechanism</h2>
|
||||
<p>
|
||||
<input type="radio" id="reply-method-micropub"
|
||||
name="reply-method" value="micropub"
|
||||
{% if reply_method == 'micropub' %}checked{% endif %}/>
|
||||
<label for="reply-method-micropub">Micropub.</label>
|
||||
Each post will have a Like and Reply button that will post content to your site directly via micropub. See <a href="https://indiewebcamp.com/Micropub">Micropub</a> for details.
|
||||
</p>
|
||||
|
||||
<form action="{{ url_for('.authorize') }}" method="POST">
|
||||
<button type="submit">
|
||||
Reauthorize Micropub
|
||||
</button>
|
||||
<input type="hidden" name="next" value="{{ request.path }}" />
|
||||
</form>
|
||||
<p>
|
||||
<input type="radio" id="reply-method-indie-config"
|
||||
name="reply-method" value="indie-config"
|
||||
{% if reply_method == 'indie-config' %}checked{% endif %}/>
|
||||
<label for="reply-method-indie-config">Indie-config.</label>
|
||||
Clicking Like or Reply will invoke your <code>web+action</code> handler if registered. See <a href="https://indiewebcamp.com/indie-config">indie-config</a> for details.
|
||||
</p>
|
||||
|
||||
<form action="{{ url_for('.deauthorize') }}" method="POST">
|
||||
<button type="submit">Revoke Credentials</button>
|
||||
<input type="hidden" name="next" value="{{ request.path }}" />
|
||||
</form>
|
||||
<p>
|
||||
<input type="radio" id="reply-method-action-urls"
|
||||
name="reply-method" value="action-urls"
|
||||
{% if reply_method == 'action-urls' %}checked{% endif %}/>
|
||||
<label for="reply-method-action-urls">Configurable action urls.</label>
|
||||
Configure Woodwind with your preferred web action handlers. The placeholder <code>{url}</code> will be replaced with the permalink URL of each entry.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<form action="{{ url_for('.authorize') }}" method="POST">
|
||||
<button type="submit">
|
||||
Authorize Micropub
|
||||
</button>
|
||||
<input type="hidden" name="next" value="{{ request.path }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
<!-- reply via micropub -->
|
||||
|
||||
<!-- reply via indie-config -->
|
||||
<div id="micropub-settings">
|
||||
<h2>Micropub</h2>
|
||||
<p>
|
||||
Configure micropub credentials.
|
||||
</p>
|
||||
{% if current_user.micropub_endpoint or current_user.access_token %}
|
||||
<input type="text" value="{{ current_user.micropub_endpoint }}" readonly />
|
||||
<input type="text" value="{{ current_user.access_token }}" readonly />
|
||||
<p>
|
||||
<a href="{{url_for('.authorize', next=request.path)}}">Reauthorize Micropub</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{url_for('.deauthorize', next=request.path)}}">Revoke Credentials</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{{url_for('.authorize', next=request.path)}}">Authorize Micropub</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- reply via indie-config -->
|
||||
|
||||
<div id="indie-config-settings">
|
||||
<h2>Indie-Config</h2>
|
||||
<p>
|
||||
Select indie-config actions.
|
||||
</p>
|
||||
{% set selectedActions = settings.get('indie-config-actions', []) %}
|
||||
|
||||
<!-- configure endpoints manually -->
|
||||
{% for action in ['like', 'favorite', 'reply', 'repost', 'bookmark'] %}
|
||||
<label>
|
||||
<input type="checkbox" name="indie-config-action" value="{{ action }}"
|
||||
{% if action in selectedActions %}checked{% endif %} />
|
||||
{{ action | capitalize }}
|
||||
</label><br/>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- configure endpoints manually -->
|
||||
|
||||
<div id="action-urls-settings">
|
||||
<h2>Action URLs</h2>
|
||||
<div id="action-urls-inputs">
|
||||
{% set actionUrls = settings.get('action-urls', {}) %}
|
||||
{% for action, url in actionUrls %}
|
||||
<input type="text" name="action" class="input-25" value="{{action}}" />
|
||||
<input type="text" name="action-url" class="input-75" value="{{url}}" />
|
||||
{% endfor %}
|
||||
<input type="text" id="action-1" name="action" class="input-25" placeholder="Like" />
|
||||
<input type="text" id="action-url-1" name="action-url" class="input-75" placeholder="http://example.com/new/like?url={url}" />
|
||||
|
||||
</div>
|
||||
<button id="add-action">Add</button>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
|
||||
$('#add-action').click(function(evt) {
|
||||
evt.preventDefault();
|
||||
$('#action-1').clone().appendTo('#action-urls-inputs');
|
||||
$('#action-url-1').clone().appendTo('#action-urls-inputs');
|
||||
});
|
||||
|
||||
function showRelevantSection() {
|
||||
var replyMethod = $('input[name=reply-method]:checked').val();
|
||||
$('#micropub-settings').hide();
|
||||
$('#indie-config-settings').hide();
|
||||
$('#action-urls-settings').hide();
|
||||
if (replyMethod == 'micropub') {
|
||||
$('#micropub-settings').show();
|
||||
} else if (replyMethod == 'indie-config') {
|
||||
$('#indie-config-settings').show();
|
||||
} else if (replyMethod == 'action-urls') {
|
||||
$('#action-urls-settings').show();
|
||||
}
|
||||
}
|
||||
|
||||
$('input[name=reply-method]').change(showRelevantSection);
|
||||
$(showRelevantSection);
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock body %}
|
||||
|
|
|
@ -43,10 +43,31 @@ def feeds():
|
|||
return flask.render_template('feeds.jinja2', feeds=sorted_feeds)
|
||||
|
||||
|
||||
@views.route('/settings')
|
||||
@views.route('/settings', methods=['GET', 'POST'])
|
||||
@flask_login.login_required
|
||||
def settings():
|
||||
return flask.render_template('settings.jinja2')
|
||||
settings = flask_login.current_user.settings or {}
|
||||
if flask.request.method == 'GET':
|
||||
return flask.render_template('settings.jinja2', settings=settings)
|
||||
|
||||
settings = dict(settings)
|
||||
reply_method = flask.request.form.get('reply-method')
|
||||
settings['reply-method'] = reply_method
|
||||
|
||||
if reply_method == 'micropub':
|
||||
pass
|
||||
elif reply_method == 'indie-config':
|
||||
settings['indie-config-actions'] = flask.request.form.getlist(
|
||||
'indie-config-action')
|
||||
elif reply_method == 'action-urls':
|
||||
zipped = zip(
|
||||
flask.request.form.getlist('action'),
|
||||
flask.request.form.getlist('action-url'))
|
||||
settings['action-urls'] = [[k, v] for k, v in zipped if k and v]
|
||||
|
||||
flask_login.current_user.settings = settings
|
||||
db.session.commit()
|
||||
return flask.render_template('settings.jinja2', settings=settings)
|
||||
|
||||
|
||||
@views.route('/update_feed')
|
||||
|
@ -103,7 +124,7 @@ def login():
|
|||
def login_callback(resp):
|
||||
if not resp.me:
|
||||
flask.flash(cgi.escape('Login error: ' + resp.error))
|
||||
return flask.redirect(flask.url_for('.login'))
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
|
||||
if resp.error:
|
||||
flask.flash(cgi.escape('Warning: ' + resp.error))
|
||||
|
@ -121,12 +142,12 @@ def login_callback(resp):
|
|||
return flask.redirect(resp.next_url or flask.url_for('.index'))
|
||||
|
||||
|
||||
@views.route('/authorize', methods=['POST'])
|
||||
@views.route('/authorize')
|
||||
@flask_login.login_required
|
||||
def authorize():
|
||||
return micropub.authorize(
|
||||
me=flask_login.current_user.url,
|
||||
next_url=flask.request.form.get('next'),
|
||||
next_url=flask.request.args.get('next'),
|
||||
scope='post')
|
||||
|
||||
|
||||
|
@ -135,13 +156,13 @@ def authorize():
|
|||
def micropub_callback(resp):
|
||||
if not resp.me or resp.error:
|
||||
flask.flash(cgi.escape('Authorize error: ' + resp.error))
|
||||
return flask.redirect(flask.url_for('.login'))
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
|
||||
domain = urllib.parse.urlparse(resp.me).netloc
|
||||
user = load_user(domain)
|
||||
if not user:
|
||||
flask.flash(cgi.escape('Unknown user for domain: ' + domain))
|
||||
return flask.redirect(flask.url_for('.login'))
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
|
||||
user.micropub_endpoint = resp.micropub_endpoint
|
||||
user.access_token = resp.access_token
|
||||
|
@ -149,13 +170,13 @@ def micropub_callback(resp):
|
|||
return flask.redirect(resp.next_url or flask.url_for('.index'))
|
||||
|
||||
|
||||
@views.route('/deauthorize', methods=['POST'])
|
||||
@views.route('/deauthorize')
|
||||
@flask_login.login_required
|
||||
def deauthorize():
|
||||
flask_login.current_user.micropub_endpoint = None
|
||||
flask_login.current_user.access_token = None
|
||||
db.session.commit()
|
||||
return flask.redirect(flask.request.form.get('next')
|
||||
return flask.redirect(flask.request.args.get('next')
|
||||
or flask.url_for('.index'))
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue