Discourse SSO (#560)

* Alter Discourse support to act as SSO provider.
Correct service group sync retry queueing.

* Correct default database enviroment variable names.

* Redirect to requested page after succesful login.

* Correct default redirect handling.
Correct attribute used to logout users on Discourse.
Improve logging messages to use parsed path on Discourse.

* Correct task retry syntax using bind=True.
Inherit from base exception so can catch TeamspeakErrors.
This commit is contained in:
Adarnof 2016-10-25 14:52:12 -04:00 committed by GitHub
parent 1daf77709d
commit 4ff21b25c3
14 changed files with 252 additions and 166 deletions

View File

@ -125,7 +125,7 @@ WSGI_APPLICATION = 'alliance_auth.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'aa_test',
'NAME': 'alliance_auth',
'USER': os.environ.get('AA_DB_DEFAULT_USER', 'allianceserver'),
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', 'password'),
'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'),
@ -477,11 +477,12 @@ DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False')
# DISCOURSE_URL - Web address of the forums (no trailing slash)
# DISCOURSE_API_USERNAME - API account username
# DISCOURSE_API_KEY - API Key
# DISCOURSE_SSO_SECRET - SSO secret key
######################################
DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', '')
DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '')
DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '')
DISCOURSE_SSO_SECRET = os.environ.get('AA_DISCOURSE_SSO_SECRET', '')
#####################################
# IPS4 Configuration

View File

@ -94,8 +94,7 @@ urlpatterns = [
url(r'^discord_add_bot/$', services.views.discord_add_bot, name='auth_discord_add_bot'),
# Discourse Service Control
url(r'^activate_discourse/$', services.views.activate_discourse, name='auth_activate_discourse'),
url(r'^deactivate_discourse/$', services.views.deactivate_discourse, name='auth_deactivate_discourse'),
url(r'^discourse_sso$', services.views.discourse_sso, name='auth_discourse_sso'),
# IPS4 Service Control
url(r'^activate_ips4/$', services.views.activate_ips4,

View File

@ -110,17 +110,6 @@ class AuthServicesInfoManager:
else:
logger.error("Failed to update user %s discord info: user does not exist." % user)
@staticmethod
def update_user_discourse_info(username, user):
if User.objects.filter(username=user.username).exists():
logger.debug("Updating user %s discourse info: username %s" % (user, username))
authserviceinfo = AuthServicesInfo.objects.get_or_create(user=user)[0]
authserviceinfo.discourse_username = username
authserviceinfo.save(update_fields=['discourse_username'])
logger.info("Updated user %s discourse info in authservicesinfo model." % user)
else:
logger.error("Failed to update user %s discourse info: user does not exist." % user)
@staticmethod
def update_user_ips4_info(username, id, user):
if User.objects.filter(username=user.username).exists():

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-21 02:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0008_set_state'),
]
operations = [
migrations.RemoveField(
model_name='authservicesinfo',
name='discourse_username',
),
migrations.AddField(
model_name='authservicesinfo',
name='discourse_enabled',
field=models.BooleanField(default=False),
),
]

View File

@ -21,7 +21,7 @@ class AuthServicesInfo(models.Model):
teamspeak3_uid = models.CharField(max_length=254, blank=True, default="")
teamspeak3_perm_key = models.CharField(max_length=254, blank=True, default="")
discord_uid = models.CharField(max_length=254, blank=True, default="")
discourse_username = models.CharField(max_length=254, blank=True, default="")
discourse_enabled = models.BooleanField(default=False, blank=True)
ips4_username = models.CharField(max_length=254, blank=True, default="")
ips4_id = models.CharField(max_length=254, blank=True, default="")
smf_username = models.CharField(max_length=254, blank=True, default="")

View File

@ -28,7 +28,10 @@ def login_user(request):
if user.is_active:
logger.info("Successful login attempt from user %s" % user)
login(request, user)
return redirect("auth_dashboard")
redirect_to = request.POST.get('next', request.GET.get('next', ''))
if not redirect_to:
redirect_to = 'auth_dashboard'
return redirect(redirect_to)
else:
logger.info("Login attempt failed for user %s: user marked inactive." % user)
messages.warning(request, 'Your account has been disabled.')

View File

@ -5,7 +5,7 @@ dnspython
passlib
requests>=2.9.1
bcrypt
#zeroc-ice
zeroc-ice
slugify
requests-oauthlib
sleekxmpp

View File

@ -11,6 +11,13 @@ from services.models import GroupCache
logger = logging.getLogger(__name__)
class DiscourseError(Exception):
def __init__(self, endpoint, errors):
self.endpoint = endpoint
self.errors = errors
def __str__(self):
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
# not exhaustive, only the ones we need
ENDPOINTS = {
'groups': {
@ -112,6 +119,22 @@ ENDPOINTS = {
'optional': [],
},
},
'logout': {
'path': "/admin/users/%s/log_out",
'method': requests.post,
'args': {
'required': [],
'optional': [],
},
},
'external': {
'path': "/users/by-external/%s.json",
'method': requests.get,
'args': {
'required': [],
'optional': [],
},
},
},
}
@ -131,10 +154,9 @@ class DiscourseManager:
'api_key': settings.DISCOURSE_API_KEY,
'api_username': settings.DISCOURSE_API_USERNAME,
}
silent = kwargs.pop('silent', False)
if args:
path = endpoint['path'] % args
else:
path = endpoint['path']
endpoint['path'] = endpoint['path'] % args
data = {}
for arg in endpoint['args']['required']:
data[arg] = kwargs[arg]
@ -142,21 +164,24 @@ class DiscourseManager:
if arg in kwargs:
data[arg] = kwargs[arg]
for arg in kwargs:
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional']:
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
r = endpoint['method'](settings.DISCOURSE_URL + path, params=params, json=data)
out = r.text
r = endpoint['method'](settings.DISCOURSE_URL + endpoint['path'], params=params, json=data)
try:
if 'errors' in r.json():
if 'errors' in r.json() and not silent:
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
r.raise_for_status()
raise DiscourseError(endpoint, r.json()['errors'])
if 'success' in r.json():
if not r.json()['success']:
raise Exception("Execution failed")
if not r.json()['success'] and not silent:
raise DiscourseError(endpoint, None)
out = r.json()
except ValueError:
logger.warn("No json data received for endpoint %s" % endpoint)
r.raise_for_status()
out = r.text
finally:
try:
r.raise_for_status()
except requests.exceptions.HTTPError as e:
raise DiscourseError(endpoint, e.response.status_code)
return out
@staticmethod
@ -235,8 +260,8 @@ class DiscourseManager:
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
@staticmethod
def __user_name_to_id(name):
data = DiscourseManager.__get_user(name)
def __user_name_to_id(name, silent=False):
data = DiscourseManager.__get_user(name, silent=silent)
return data['user']['id']
@staticmethod
@ -244,9 +269,9 @@ class DiscourseManager:
raise NotImplementedError
@staticmethod
def __get_user(username):
def __get_user(username, silent=False):
endpoint = ENDPOINTS['users']['get']
return DiscourseManager.__exc(endpoint, username)
return DiscourseManager.__exc(endpoint, username, silent=silent)
@staticmethod
def __activate_user(username):
@ -268,7 +293,7 @@ class DiscourseManager:
@staticmethod
def __check_if_user_exists(username):
try:
DiscourseManager.__user_name_to_id(username)
DiscourseManager.__user_name_to_id(username, silent=True)
return True
except:
return False
@ -292,11 +317,26 @@ class DiscourseManager:
return DiscourseManager.__exc(endpoint, username, email=email)
@staticmethod
def _sanatize_username(username):
sanatized = username.replace(" ", "_")
sanatized = sanatized.strip(' _')
sanatized = sanatized.replace("'", "")
return sanatized
def __logout(id):
endpoint = ENDPOINTS['users']['logout']
return DiscourseManager.__exc(endpoint, id)
@staticmethod
def __get_user_by_external(id):
endpoint = ENDPOINTS['users']['external']
return DiscourseManager.__exc(endpoint, id)
@staticmethod
def __user_id_by_external_id(id):
data = DiscourseManager.__get_user_by_external(id)
return data['user']['id']
@staticmethod
def _sanitize_username(username):
sanitized = username.replace(" ", "_")
sanitized = sanitized.strip(' _')
sanitized = sanitized.replace("'", "")
return sanitized
@staticmethod
def _sanitize_groupname(name):
@ -304,42 +344,14 @@ class DiscourseManager:
return re.sub('[^\w]', '', name)
@staticmethod
def add_user(username, email):
logger.debug("Adding new discourse user %s" % username)
password = DiscourseManager.__generate_random_pass()
safe_username = DiscourseManager._sanatize_username(username)
try:
if DiscourseManager.__check_if_user_exists(safe_username):
logger.debug("Discourse user %s already exists. Reactivating" % safe_username)
DiscourseManager.__unsuspend(safe_username)
else:
logger.debug("Creating new user account for %s" % username)
DiscourseManager.__create_user(safe_username, email, password)
logger.info("Added new discourse user %s" % username)
return safe_username, password
except:
logger.exception("Failed to add new discourse user %s" % username)
return "", ""
@staticmethod
def delete_user(username):
logger.debug("Deleting discourse user %s" % username)
try:
DiscourseManager.__suspend_user(username)
logger.info("Deleted discourse user %s" % username)
return True
except:
logger.exception("Failed to delete discourse user %s" % username)
return False
@staticmethod
def update_groups(username, raw_groups):
def update_groups(user):
groups = []
for g in raw_groups:
groups.append(DiscourseManager._sanitize_groupname(g[:20]))
logger.debug("Updating discourse user %s groups to %s" % (username, groups))
for g in user.groups.all():
groups.append(DiscourseManager._sanitize_groupname(str(g)[:20]))
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
group_dict = DiscourseManager.__generate_group_dict(groups)
inv_group_dict = {v: k for k, v in group_dict.items()}
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
user_groups = DiscourseManager.__get_user_groups(username)
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
rem_groups = [x for x in user_groups if not x in inv_group_dict]
@ -350,3 +362,12 @@ class DiscourseManager:
DiscourseManager.__add_user_to_group(g, username)
for g in rem_groups:
DiscourseManager.__remove_user_from_group(g, username)
@staticmethod
def disable_user(user):
logger.debug("Disabling user %s Discourse access." % user)
d_user = DiscourseManager.__get_user_by_external(user.pk)
DiscourseManager.__logout(d_user['user']['id'])
DiscourseManager.__suspend_user(d_user['user']['username'])
logger.info("Disabled user %s Discourse access." % user)
return True

View File

@ -249,7 +249,7 @@ class TS3Server(TS3Proto):
self.send_command('use', keys={'sid': id})
class TeamspeakError:
class TeamspeakError(Exception):
def __init__(self, code, msg=None):
self.code = str(code)
if not msg:

View File

@ -43,7 +43,7 @@ def m2m_changed_user_groups(sender, instance, action, *args, **kwargs):
update_discord_groups.delay(instance.pk)
if auth.mumble_username:
update_mumble_groups.delay(instance.pk)
if auth.discourse_username:
if auth.discourse_enabled:
update_discourse_groups.delay(instance.pk)
if auth.smf_username:
update_smf_groups.delay(instance.pk)

View File

@ -181,10 +181,11 @@ def deactivate_services(user):
marketManager.disable_user(authinfo.market_username)
AuthServicesInfoManager.update_user_market_info("", user)
change = True
if authinfo.discourse_username and authinfo.discourse_username != "":
logger.debug("User %s has a Discourse account %s. Deleting." % (user, authinfo.discourse_username))
DiscourseManager.delete_user(authinfo.discourse_username)
AuthServicesInfoManager.update_user_discourse_info("", user)
if authinfo.discourse_enabled:
logger.debug("User %s has a Discourse account. Disabling login." % user)
DiscourseManager.disable_user(user)
authinfo.discourse_enabled = False
authinfo.save()
change = True
if authinfo.smf_username and authinfo.smf_username != "":
logger.debug("User %s has a SMF account %s. Deleting." % (user, authinfo.smf_username))
@ -195,8 +196,8 @@ def deactivate_services(user):
notify(user, "Services Disabled", message="Your services accounts have been disabled.", level="danger")
@task
def validate_services(user, state):
@task(bind=True)
def validate_services(self, user, state):
if state == MEMBER_STATE:
setting_string = 'AUTH'
elif state == BLUE_STATE:
@ -238,9 +239,10 @@ def validate_services(user, state):
marketManager.disable_user(auth.market_username)
AuthServicesInfoManager.update_user_market_info("", user)
notify(user, 'Alliance Market Account Disabled', level='danger')
if auth.discourse_username and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False):
DiscourseManager.delete_user(auth.discourse_username)
AuthServicesInfoManager.update_user_discourse_info("", user)
if auth.discourse_enabled and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False):
DiscourseManager.disable_user(user)
authinfo.discourse_enabled = False
autninfo.save()
notify(user, 'Discourse Account Disabled', level='danger')
if auth.smf_username and not getattr(settings, 'ENABLE_%s_SMF' % setting_string, False):
smfManager.disable_user(auth.smf_username)
@ -248,8 +250,8 @@ def validate_services(user, state):
notify(user, "SMF Account Disabled", level='danger')
@task
def update_jabber_groups(pk):
@task(bind=True)
def update_jabber_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating jabber groups for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -274,8 +276,8 @@ def update_all_jabber_groups():
update_jabber_groups.delay(user.user_id)
@task
def update_mumble_groups(pk):
@task(bind=True)
def update_mumble_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating mumble groups for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -300,8 +302,8 @@ def update_all_mumble_groups():
update_mumble_groups.delay(user.user_id)
@task
def update_forum_groups(pk):
@task(bind=True)
def update_forum_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating forum groups for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -326,8 +328,8 @@ def update_all_forum_groups():
update_forum_groups.delay(user.user_id)
@task
def update_smf_groups(pk):
@task(bind=True)
def update_smf_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating smf groups for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -352,8 +354,8 @@ def update_all_smf_groups():
update_smf_groups.delay(user.user_id)
@task
def update_ipboard_groups(pk):
@task(bind=True)
def update_ipboard_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating user %s ipboard groups." % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -378,8 +380,8 @@ def update_all_ipboard_groups():
update_ipboard_groups.delay(user.user_id)
@task
def update_teamspeak3_groups(pk):
@task(bind=True)
def update_teamspeak3_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating user %s teamspeak3 groups" % user)
usergroups = user.groups.all()
@ -407,8 +409,8 @@ def update_all_teamspeak3_groups():
update_teamspeak3_groups.delay(user.user_id)
@task
def update_discord_groups(pk):
@task(bind=True)
def update_discord_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discord groups for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -434,8 +436,8 @@ def update_all_discord_groups():
update_discord_groups.delay(user.user_id)
@task
def update_discord_nickname(pk):
@task(bind=True)
def update_discord_nickname(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discord nickname for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
@ -456,22 +458,14 @@ def update_all_discord_nicknames():
update_discord_nickname(user.user_id)
@task
def update_discourse_groups(pk):
@task(bind=True)
def update_discourse_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discourse groups for user %s" % user)
authserviceinfo = AuthServicesInfo.objects.get(user=user)
groups = []
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
logger.debug("No syncgroups found for user. Adding empty group.")
groups.append('empty')
logger.debug("Updating user %s discourse groups to %s" % (user, groups))
try:
DiscourseManager.update_groups(authserviceinfo.discourse_username, groups)
DiscourseManager.update_groups(user)
except:
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user, exc_info=True)
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10)
logger.debug("Updated user %s discourse groups." % user)

View File

@ -38,6 +38,18 @@ from services.forms import TeamspeakJoinForm
from authentication.decorators import members_and_blues
from authentication.states import MEMBER_STATE, BLUE_STATE
import base64
import hmac
import hashlib
try:
from urllib import unquote, urlencode
except ImportError: #py3
from urllib.parse import unquote, urlencode
try:
from urlparse import parse_qs
except ImportError: #py3
from urllib.parse import parse_qs
import datetime
import logging
@ -131,6 +143,12 @@ def jabber_broadcast_view(request):
def services_view(request):
logger.debug("services_view called by user %s" % request.user)
auth = AuthServicesInfo.objects.get_or_create(user=request.user)[0]
char = None
if auth.main_char_id:
try:
char = EveCharacter.objects.get(character_id=auth.main_char_id)
except EveCharacter.DoesNotExist:
messages.warning(request, "There's a problem with your main character. Please select a new one.")
services = [
'FORUM',
@ -146,7 +164,10 @@ def services_view(request):
'XENFORO',
]
context = {'authinfo': auth}
context = {
'authinfo': auth,
'char': char,
}
for s in services:
context['SHOW_' + s] = (getattr(settings, 'ENABLE_AUTH_' + s) and (
@ -817,49 +838,6 @@ def set_ipboard_password(request):
return render(request, 'registered/service_password.html', context=context)
@login_required
@members_and_blues()
def activate_discourse(request):
logger.debug("activate_discourse called by user %s" % request.user)
authinfo = AuthServicesInfo.objects.get_or_create(user=request.user)[0]
character = EveManager.get_character_by_id(authinfo.main_char_id)
logger.debug("Adding discourse user for user %s with main character %s" % (request.user, character))
result = DiscourseManager.add_user(character.character_name, request.user.email)
if result[0] != "":
AuthServicesInfoManager.update_user_discourse_info(result[0], request.user)
logger.debug("Updated authserviceinfo for user %s with discourse credentials. Updating groups." % request.user)
update_discourse_groups.delay(request.user.pk)
logger.info("Successfully activated discourse for user %s" % request.user)
messages.success(request, 'Activated Discourse account.')
messages.warning(request, 'Do not lose your Discourse password. It cannot be reset through auth.')
credentials = {
'username': result[0],
'password': result[1],
}
return render(request, 'registered/service_credentials.html',
context={'credentials': credentials, 'service': 'Discourse'})
else:
logger.error("Unsuccessful attempt to activate discourse for user %s" % request.user)
messages.error(request, 'An error occurred while processing your Discourse account.')
return redirect("auth_services")
@login_required
@members_and_blues()
def deactivate_discourse(request):
logger.debug("deactivate_discourse called by user %s" % request.user)
authinfo = AuthServicesInfo.objects.get_or_create(user=request.user)[0]
result = DiscourseManager.delete_user(authinfo.discourse_username)
if result:
AuthServicesInfoManager.update_user_discourse_info("", request.user)
logger.info("Successfully deactivated discourse for user %s" % request.user)
messages.success(request, 'Deactivated Discourse account.')
else:
logger.error("Unsuccessful attempt to activate discourse for user %s" % request.user)
messages.error(request, 'An error occurred while processing your Discourse account.')
return redirect("auth_services")
@login_required
@members_and_blues()
def activate_ips4(request):
@ -1145,3 +1123,87 @@ def set_market_password(request):
logger.debug("Rendering form for user %s" % request.user)
context = {'form': form, 'service': 'Market'}
return render(request, 'registered/service_password.html', context=context)
@login_required
def discourse_sso(request):
## Check if user has access
auth, c = AuthServicesInfo.objects.get_or_create(user=request.user)
if not request.user.is_superuser:
if auth.state == MEMBER_STATE and not settings.ENABLE_AUTH_DISCOURSE:
messages.error(request, 'You are not authorized to access Discourse.')
return redirect('auth_dashboard')
elif auth.state == BLUE_STATE and not settings.ENABLE_BLUE_DISCOURSE:
messages.error(request, 'You are not authorized to access Discourse.')
return redirect('auth_dashboard')
else:
messages.error(request, 'You are not authorized to access Discourse.')
return redirect('auth_dashboard')
if not auth.main_char_id:
messages.error(request, "You must have a main character set to access Discourse.")
return redirect('auth_characters')
try:
main_char = EveCharacter.objects.get(character_id=auth.main_char_id)
except EveCharacter.DoesNotExist:
messages.error(request, "Your main character is missing a database model. Please select a new one.")
return redirect('auth_characters')
payload = request.GET.get('sso')
signature = request.GET.get('sig')
if None in [payload, signature]:
messages.error(request, 'No SSO payload or signature. Please contact support if this problem persists.')
return redirect('auth_dashboard')
## Validate the payload
try:
payload = unquote(payload).encode('utf-8')
decoded = base64.decodestring(payload).decode('utf-8')
assert 'nonce' in decoded
assert len(payload) > 0
except AssertionError:
messages.error(request, 'Invalid payload. Please contact support if this problem persists.')
return redirect('auth_dashboard')
key = str(settings.DISCOURSE_SSO_SECRET).encode('utf-8')
h = hmac.new(key, payload, digestmod=hashlib.sha256)
this_signature = h.hexdigest()
if this_signature != signature:
messages.error(request, 'Invalid payload. Please contact support if this problem persists.')
return redirect('auth_dashboard')
## Build the return payload
username = DiscourseManager._sanitize_username(main_char.character_name)
qs = parse_qs(decoded)
params = {
'nonce': qs['nonce'][0],
'email': request.user.email,
'external_id': request.user.pk,
'username': username,
'name': username,
}
if auth.main_char_id:
params['avatar_url'] = 'https://image.eveonline.com/Character/%s_256.jpg' % auth.main_char_id
return_payload = base64.encodestring(urlencode(params).encode('utf-8'))
h = hmac.new(key, return_payload, digestmod=hashlib.sha256)
query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()})
## Record activation and queue group sync
auth.discourse_enabled = True
auth.save()
update_discourse_groups.delay(request.user.pk)
## Redirect back to Discourse
url = '%s/session/sso_login' % settings.DISCOURSE_URL
return redirect('%s?%s' % (url, query_string))

View File

@ -60,6 +60,7 @@
{% csrf_token %}
<h2 class="form-signin-heading text-center">{% trans "Please sign in" %}</h2>
{{ form|bootstrap }}
<input type="hidden" name="next" value="{{ request.GET.next }}" />
<div class="row">
<div class="col-md-12">
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Sign in" %}</button>

View File

@ -238,18 +238,10 @@
{% endif %}
{% if SHOW_DISCOURSE %}
<td class="text-center">Discourse</td>
<td class="text-center">{{ authinfo.discourse_username }}</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td>
<td class="text-center">
{% ifequal authinfo.discourse_username "" %}
<a href="{% url 'auth_activate_discourse' %}" class="btn btn-warning">
<span class="glyphicon glyphicon-ok"></span>
</a>
{% else %}
<a href="{% url 'auth_deactivate_discourse' %}" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% endifequal %}
<a class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a>
</td>
{% endif %}
{% if SHOW_TEAMSPEAK3 %}