commit
c1093a54fe
@ -0,0 +1,18 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
||||
local_python
|
||||
lopy
|
||||
static_files
|
||||
log_files
|
||||
venv
|
||||
|
||||
*.sw[op]
|
||||
*.pyc
|
||||
|
||||
settings_local.py
|
||||
docker-compose.yml
|
@ -0,0 +1,45 @@
|
||||
virtualenv -p python3.5 venv
|
||||
|
||||
source ./venv/bin/activate
|
||||
|
||||
pip install Django==2.0.3 # 2.0.3
|
||||
|
||||
# s binarne distribuovanou libssl
|
||||
pip install psycopg2
|
||||
|
||||
pip install django-statici18n # preklad Javascript souboru
|
||||
|
||||
# nahrada za SUDS, zalozeno na lxml
|
||||
#pip install zeep
|
||||
|
||||
pip install bleach
|
||||
|
||||
|
||||
##
|
||||
# Keycloak SSO
|
||||
##
|
||||
|
||||
#pip install git+https://github.com/jhuapl-boss/django-oidc.git
|
||||
#pip install git+https://github.com/jhuapl-boss/drf-oidc-auth.git
|
||||
#pip install git+https://github.com/jhuapl-boss/boss-oidc.git
|
||||
|
||||
pip install -U django-auth-oidc pyjwkest
|
||||
#pip install -U service_identity
|
||||
#pip install -U openid_connect
|
||||
|
||||
|
||||
##
|
||||
# Database setup
|
||||
##
|
||||
psql <<SQL
|
||||
CREATE USER piratinalodeni;
|
||||
CREATE DATABASE piratinalodeni OWNER=piratinalodeni ENCODING='UTF8' LC_COLLATE='cs_CZ.UTF-8' template template0;
|
||||
SQL
|
||||
|
||||
##
|
||||
# App Setup
|
||||
##
|
||||
./src/manage.py makemigrations nalodeni
|
||||
./src/manage.py migrate
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
nalodeni.env
|
||||
|
@ -0,0 +1,32 @@
|
||||
FROM debian:stretch
|
||||
#FROM python:3
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
WORKDIR /
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
RUN apt-get install -y python-virtualenv python-pip
|
||||
|
||||
RUN virtualenv -p python3 venv
|
||||
|
||||
# ponechano kvuli vyuziti cache
|
||||
#RUN bash -c 'source /venv/bin/activate; pip install -r pip-requirements.txt'
|
||||
RUN bash -c 'source /venv/bin/activate; pip install Django==2.0.3 psycopg2==2.7.4 django-statici18n==1.7.0 django-auth-oidc==0.4.5 pyjwkest==1.4.0 bleach==2.1.4 django-anymail==3.0 html2text==2018.1.9'
|
||||
|
||||
ADD . /nalodeni/src/
|
||||
|
||||
#RUN bash -c 'touch /nalodeni/src/main/settings_local.py' # this file needs to exist
|
||||
#RUN adduser --disabled-login --quiet --gecos nalodeni nalodeni
|
||||
#RUN chown -R nalodeni:nalodeni /nalodeni/
|
||||
#RUN chmod u+x /nalodeni/src/docker-entrypoint.sh
|
||||
RUN bash -c 'adduser --disabled-login --quiet --gecos nalodeni nalodeni && \
|
||||
chmod -R o+r /nalodeni/ && \
|
||||
chown -R nalodeni:nalodeni /nalodeni/src/static_files && \
|
||||
chmod o+x /nalodeni/src/docker-entrypoint.sh && \
|
||||
touch /nalodeni/src/main/settings_local.py '
|
||||
|
||||
USER nalodeni
|
||||
|
||||
ENTRYPOINT /nalodeni/src/docker-entrypoint.sh
|
@ -0,0 +1,22 @@
|
||||
upstream nalodeni.pirati.io {
|
||||
ip_hash;
|
||||
server nalodeni:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name nalodeni.pirati.io;
|
||||
listen 8001;
|
||||
|
||||
location / {
|
||||
proxy_pass http://nalodeni.pirati.io/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /static_nalodeni/;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
nalodeni.conf
|
@ -0,0 +1,27 @@
|
||||
version: '2'
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nalodeni-nginx
|
||||
ports:
|
||||
- "80:8001"
|
||||
volumes:
|
||||
- ./deploy/nginx:/etc/nginx/conf.d
|
||||
- vol_static_nalodeni:/static_nalodeni
|
||||
depends_on:
|
||||
- nalodeni
|
||||
nalodeni:
|
||||
build: .
|
||||
container_name: nalodeni
|
||||
command: bash -c "/nalodeni/src/docker-entrypoint.sh"
|
||||
volumes:
|
||||
- vol_static_nalodeni:/nalodeni/src/static_files
|
||||
ports:
|
||||
- "8000"
|
||||
environment:
|
||||
- NALODENI_DEBUG=off
|
||||
- NALODENI_DEBUG_LOCAL=off
|
||||
|
||||
|
||||
volumes:
|
||||
vol_static_nalodeni:
|
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
trap "echo TRAPed signal" HUP INT QUIT KILL TERM
|
||||
|
||||
source /venv/bin/activate
|
||||
|
||||
cd /nalodeni/src
|
||||
|
||||
python manage.py migrate
|
||||
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
python manage.py loaddata nalodeni_newsletter nalodeni_topics nalodeni_skills
|
||||
|
||||
PYTHONIOENCODING=utf-8 python manage.py runserver 0.0.0.0:8000
|
||||
|
@ -0,0 +1,57 @@
|
||||
import os
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from openid_connect import connect, connect_url
|
||||
from openid_connect._oidc import OpenIDClient, TokenResponse
|
||||
|
||||
class OpenIDClientCustom(OpenIDClient):
|
||||
|
||||
def refresh_session(self, refresh_token):
|
||||
r = requests.post(self.token_endpoint, auth=self.auth, data=dict(
|
||||
grant_type="refresh_token",
|
||||
refresh_token=refresh_token
|
||||
), headers={'Accept': 'application/json'})
|
||||
if r.status_code != 200:
|
||||
#r.raise_for_status()
|
||||
return None
|
||||
|
||||
resp = TokenResponse(r.json(), self)
|
||||
|
||||
if "scope" in resp._data:
|
||||
resp.scope = set(self.translate_scope_out(set(resp._data["scope"].split(" "))))
|
||||
if not hasattr(resp, "scope") or "openid" in resp.scope:
|
||||
resp.id = self.get_id(resp)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
server = None
|
||||
|
||||
def get_server_orig():
|
||||
global server
|
||||
|
||||
if server is not None:
|
||||
return server
|
||||
|
||||
AUTH_URL = os.environ.get("AUTH_URL")
|
||||
if AUTH_URL:
|
||||
server = connect_url(AUTH_URL)
|
||||
else:
|
||||
server = connect(settings.AUTH_SERVER, settings.AUTH_CLIENT_ID,
|
||||
settings.AUTH_CLIENT_SECRET,
|
||||
getattr(settings, 'AUTH_PROTOCOL', None))
|
||||
|
||||
return server
|
||||
|
||||
def get_server():
|
||||
global server
|
||||
|
||||
if server is not None:
|
||||
return server
|
||||
|
||||
server = OpenIDClientCustom( settings.AUTH_SERVER, settings.AUTH_CLIENT_ID,
|
||||
settings.AUTH_CLIENT_SECRET)
|
||||
|
||||
return server
|
@ -0,0 +1,13 @@
|
||||
# encoding: utf8
|
||||
|
||||
class MissingSsoInfo(Exception):
|
||||
pass
|
||||
|
||||
class EmailVersusIdentityMismatch(Exception):
|
||||
pass
|
||||
|
||||
class UsernameAlreadyTaken(Exception):
|
||||
pass
|
||||
|
||||
class EmailAlreadyTaken(Exception):
|
||||
pass
|
@ -0,0 +1,29 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import time
|
||||
|
||||
import keycloak_oidc.views
|
||||
|
||||
class KeycloakSessionRefreshMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
# One-time configuration and initialization.
|
||||
|
||||
def __call__(self, request):
|
||||
# Code to be executed for each request before
|
||||
# the view (and later middleware) are called.
|
||||
|
||||
refresh_expires_at = request.session.get('refresh_expires_at',None)
|
||||
#print("refresh_expires_at", refresh_expires_at, time.time())
|
||||
|
||||
if refresh_expires_at is not None:
|
||||
if refresh_expires_at - time.time() < 5*60: # 5 minutes before expiration
|
||||
rslt = keycloak_oidc.views.renew_session(request)
|
||||
|
||||
if rslt != 0:
|
||||
# renew not successful
|
||||
return keycloak_oidc.views.logout(request, multiSessionFound=False)
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
app_name = 'keycloak_oidc'
|
||||
urlpatterns = [
|
||||
url(r'^$', views.login, name='login-base'),
|
||||
url(r'^login/$', views.login, name='login'),
|
||||
url(r'^login/(?P<idp_hint>[a-z]+)/$', views.login, name='login_idp'),
|
||||
url(r'^done/$', views.callback, name='login-done'),
|
||||
url(r'^logout/$', views.logout, name='logout', kwargs={'ssoLogout':True}),
|
||||
url(r'^logoutLocal/$', views.logout, name='logout'),
|
||||
url(r'^account/$', views.account, name='account'),
|
||||
url(r'^refresh_session/$', views.refresh_session, name='refresh-session'),
|
||||
]
|
@ -0,0 +1,337 @@
|
||||
# encoding: utf8
|
||||
from importlib import import_module
|
||||
|
||||
import logging
|
||||
|
||||
from jwkest.jwt import JWT
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.contrib import auth
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import redirect, resolve_url
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
|
||||
from django.conf import settings as appSettings
|
||||
|
||||
from . import auth as _auth
|
||||
from . import exceptions as sso_exc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
LOGIN_REDIRECT_URL = appSettings.LOGIN_REDIRECT_URL
|
||||
except AttributeError:
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
try:
|
||||
LOGOUT_REDIRECT_URL = appSettings.LOGOUT_REDIRECT_URL
|
||||
except AttributeError:
|
||||
LOGOUT_REDIRECT_URL = '/sso/logout/'
|
||||
|
||||
if LOGOUT_REDIRECT_URL is None:
|
||||
LOGOUT_REDIRECT_URL = '/sso/logout/'
|
||||
|
||||
try:
|
||||
AUTH_SCOPE = appSettings.AUTH_SCOPE
|
||||
except AttributeError:
|
||||
AUTH_SCOPE = ('openid',)
|
||||
|
||||
try:
|
||||
AUTH_AVAIL_IDP = appSettings.AUTH_AVAIL_IDP
|
||||
except AttributeError:
|
||||
AUTH_AVAIL_IDP = []
|
||||
|
||||
try:
|
||||
GET_USER_FUNCTION = appSettings.AUTH_GET_USER_FUNCTION
|
||||
except AttributeError:
|
||||
GET_USER_FUNCTION = 'keycloak_oidc:get_user_by_username'
|
||||
|
||||
try:
|
||||
LOGOUT_REDIRECT_VIEW = appSettings.AUTH_LOGOUT_REDIRECT_VIEW
|
||||
except AttributeError:
|
||||
LOGOUT_REDIRECT_VIEW = 'nalodeni:about'
|
||||
|
||||
try:
|
||||
LOGIN_ROLE_REQUIRED = appSettings.AUTH_LOGIN_ROLE_REQUIRED
|
||||
except AttributeError:
|
||||
LOGIN_ROLE_REQUIRED = None
|
||||
|
||||
try:
|
||||
LOGIN_ROLE_REQUIRED_ANY = appSettings.AUTH_LOGIN_ROLE_REQUIRED_ANY
|
||||
except AttributeError:
|
||||
LOGIN_ROLE_REQUIRED_ANY = False
|
||||
|
||||
|
||||
jwt_singleton = JWT()
|
||||
|
||||
|
||||
def _import_object(path, def_name):
|
||||
try:
|
||||
mod, cls = path.split(':', 1)
|
||||
except ValueError:
|
||||
mod = path
|
||||
cls = def_name
|
||||
|
||||
return getattr(import_module(mod), cls)
|
||||
|
||||
get_user = _import_object(GET_USER_FUNCTION, 'get_user')
|
||||
|
||||
|
||||
def login(request, idp_hint=None):
|
||||
return_path = request.GET.get(auth.REDIRECT_FIELD_NAME, "/")
|
||||
|
||||
if idp_hint and idp_hint not in AUTH_AVAIL_IDP:
|
||||
idp_hint = None
|
||||
|
||||
try:
|
||||
srv = _auth.get_server()
|
||||
except Exception as e:
|
||||
messages.error(request, "Bohužel nastaly potíže s centrálním přihlášením uživatele.")
|
||||
logger.error('Chyba SSO login: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
|
||||
return redirect(_auth.get_server().authorize(
|
||||
redirect_uri = request.build_absolute_uri(reverse("keycloak_oidc:login-done")),
|
||||
state = return_path,
|
||||
scope = AUTH_SCOPE,
|
||||
) + "&kc_locale=" + appSettings.AUTH_SSO_LOCALE + (
|
||||
('&kc_idp_hint=' + idp_hint) if idp_hint is not None else ""
|
||||
)
|
||||
)
|
||||
|
||||
def renew_session(request, returnHttp=False):
|
||||
"""
|
||||
Refresh the session using a refresh_token.
|
||||
"""
|
||||
|
||||
reftoken = request.session.get('refresh_token', None)
|
||||
|
||||
if reftoken is None:
|
||||
return -3
|
||||
|
||||
#print("Refresh token IAT:", jwt_singleton.unpack(reftoken).payload()['iat'])
|
||||
#print(jwt_singleton.unpack(reftoken).payload())
|
||||
|
||||
try:
|
||||
srv = _auth.get_server()
|
||||
except Exception as e:
|
||||
messages.error(request,"Bohužel nastaly potíže s centrálním SSO serverem.")
|
||||
logger.error('Chyba SSO renew: %s' % e)
|
||||
if returnHttp:
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
else:
|
||||
return -1
|
||||
|
||||
new_token = srv.refresh_session(reftoken)
|
||||
if new_token is None:
|
||||
if returnHttp:
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
else:
|
||||
return -2
|
||||
|
||||
rt = new_token._data['refresh_token']
|
||||
#print("Refresh token NEW IAT:", jwt_singleton.unpack(rt).payload()['iat'])
|
||||
rt_payload = jwt_singleton.unpack(rt).payload()
|
||||
|
||||
# save the new refresh token
|
||||
request.session['refresh_token'] = rt
|
||||
request.session['refresh_expires_at'] = rt_payload['exp']
|
||||
|
||||
if returnHttp:
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def callback(request):
|
||||
return_path = request.GET.get("state",'/')
|
||||
sso_code = request.GET.get('code','')
|
||||
|
||||
if sso_code == "" and return_path == "":
|
||||
messages.error(request, "Chyba při přihlášení na centrálním serveru SSO.")
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
else:
|
||||
try:
|
||||
res = _auth.get_server().request_token(
|
||||
redirect_uri = request.build_absolute_uri(reverse("keycloak_oidc:login-done")),
|
||||
code = sso_code,
|
||||
)
|
||||
except HTTPError as e:
|
||||
messages.error(request, "Přihlášení uživatele se nezdařilo. Chyba komunikace s centrálním serverem přihlášení.")
|
||||
logger.error('Chyba SSO callback: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
|
||||
#print(res)
|
||||
#print(dir(res))
|
||||
#print(res.id_token)
|
||||
#print(res.access_token)
|
||||
|
||||
id_token_data = jwt_singleton.unpack(res.id_token).payload()
|
||||
|
||||
reftoken = jwt_singleton.unpack(res._data['refresh_token']).payload()
|
||||
|
||||
# Get the user from local DB
|
||||
#
|
||||
try:
|
||||
user = get_user(id_token_data)
|
||||
user.backend = 'django.contrib.auth.backends.ModelBackend'
|
||||
except sso_exc.MissingSsoInfo as e:
|
||||
messages.error(request, "Účet na Pirátské identitě není řádně vyplněn. %s" % e )
|
||||
messages.info(request,"Zkontrolujte údaje na <a href='%s' target='_blank'>auth.pirati.cz</a>." % (appSettings.AUTH_SERVER + "account/"), extra_tags="safe")
|
||||
logger.error('SSO account incomplete 1: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
except sso_exc.EmailVersusIdentityMismatch as e:
|
||||
messages.error(request, "Účet v nalodění a na Pirátské identitě nesouhlasí.")
|
||||
messages.info(request,"Obraťte se na technický odbor Pirátů.")
|
||||
logger.error('SSO account email vs. identity mismatch: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
except sso_exc.UsernameAlreadyTaken as e:
|
||||
messages.error(request, "%s" % e)
|
||||
messages.info(request,'Pokud se jedná o vaše uživatelské jméno,'
|
||||
+ 'obraťte se na technický odbor Pirátů.')
|
||||
logger.error('Username already taken in Nalodeni: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
except sso_exc.EmailAlreadyTaken as e:
|
||||
messages.error(request, "%s" % e)
|
||||
messages.info(request,'Pokud se jedná o váš email,'
|
||||
+ 'obraťte se na technický odbor Pirátů.')
|
||||
logger.error('Email already taken in Nalodeni: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
|
||||
|
||||
# Process the user roles, if any ( taken from bossoidc.backend )
|
||||
#
|
||||
jwt = jwt_singleton.unpack(res.access_token).payload()
|
||||
#print("access_token:", jwt_singleton.unpack(res.access_token).payload())
|
||||
#print("id_token:", jwt_singleton.unpack(res.id_token).payload())
|
||||
#### if 'realm_access' in jwt: # Session logins and Bearer tokens from password Grant Types
|
||||
#### roles = jwt['realm_access']['roles']
|
||||
#### else:
|
||||
#### roles = []
|
||||
resource_roles = []
|
||||
if 'resource_access' in jwt:
|
||||
if jwt['azp'] in jwt['resource_access']:
|
||||
if 'roles' in jwt['resource_access'][jwt['azp']]:
|
||||
resource_roles = jwt['resource_access'][jwt['azp']]['roles']
|
||||
if (LOGIN_ROLE_REQUIRED_ANY and len(resource_roles) == 0) or (LOGIN_ROLE_REQUIRED is not None and LOGIN_ROLE_REQUIRED not in resource_roles):
|
||||
messages.error(request, "Váš pirátský účet nemá přidělené potřebné přístupové role. Přístup odmítnut.")
|
||||
logger.error('SSO account %s: no roles assigned' % (user.ssoUid,))
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
|
||||
# Update user information
|
||||
#
|
||||
if 'given_name' in jwt and 'family_name' in jwt:
|
||||
user.first_name = jwt['given_name']
|
||||
user.last_name = jwt['family_name']
|
||||
else:
|
||||
messages.error(request, "Účet na Pirátské identitě není řádně vyplněn.")
|
||||
messages.info(request,"Zkontrolujte údaje na <a href='%s' target='_blank'>auth.pirati.cz</a>." % (appSettings.AUTH_SERVER + "account/"), extra_tags="safe")
|
||||
logger.error('SSO account incomplete 2: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
|
||||
user.loginSession = jwt['session_state']
|
||||
user.save()
|
||||
|
||||
# Authenticate the user
|
||||
#
|
||||
auth.login(request, user)
|
||||
|
||||
request.session['openid_token'] = res.id_token
|
||||
request.session['openid'] = id_token_data
|
||||
request.session['refresh_token'] = res._data['refresh_token']
|
||||
request.session['refresh_expires_at'] = reftoken['exp']
|
||||
request.session['loginSession'] = user.loginSession
|
||||
|
||||
|
||||
##
|
||||
# Set client-system roles based on SSO roles
|
||||
#
|
||||
# After setting permissions, the User object should be reloaded,
|
||||
# but we do a redirect, so it is fine.
|
||||
##
|
||||
#print(roles)
|
||||
#print(resource_roles)
|
||||
user.user_permissions.clear()
|
||||
|
||||
request.session['site_perms'] = []
|
||||
|
||||
# prepend "sso_" not to mix with other perms
|
||||
for rr in resource_roles:
|
||||
perm = 'sso_'+rr
|
||||
request.session['site_perms'].append(perm)
|
||||
|
||||
# tell the User to calculate needed permissions
|
||||
request.session['spc'] = user.do_site_perms_calc(request.session['site_perms'])
|
||||
|
||||
# tell us, what roles do we have
|
||||
if appSettings.DEBUG:
|
||||
print("Roles assigned:", request.session['site_perms'])
|
||||
print("Roles calculated:", request.session['spc'])
|
||||
|
||||
url_is_safe = is_safe_url(
|
||||
url = return_path,
|
||||
host = request.get_host(),
|
||||
allowed_hosts = set(request.get_host()),
|
||||
require_https = request.is_secure(),
|
||||
)
|
||||
|
||||
if not url_is_safe:
|
||||
return redirect(resolve_url(LOGIN_REDIRECT_URL))
|
||||
return redirect(return_path)
|
||||
|
||||
|
||||
def logout(request, multiSessionFound=False, ssoLogout=False):
|
||||
id_token = request.session.get('openid_token', None)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
if not multiSessionFound:
|
||||
# reset the DB session, because this is
|
||||
# a user requested logout
|
||||
request.user.loginSession = None
|
||||
request.user.save()
|
||||
|
||||
request.session['loginSession'] = None
|
||||
|
||||
auth.logout(request)
|
||||
|
||||
if ssoLogout:
|
||||
try:
|
||||
srv = _auth.get_server()
|
||||
except Exception as e:
|
||||
messages.error(request,"Bohužel nastaly potíže s centrálním odhlášením uživatele.")
|
||||
logger.error('Chyba SSO logout: %s' % e)
|
||||
return redirect(request.build_absolute_uri("/"))
|
||||
|
||||
if _auth.get_server().end_session_endpoint and id_token is not None:
|
||||
messages.info(request,"Byli jste odhlášení z aplikace i Centrální identity.")
|
||||
return redirect(_auth.get_server().end_session(
|
||||
post_logout_redirect_uri = request.build_absolute_uri(LOGOUT_REDIRECT_URL),
|
||||
state = '',
|
||||
id_token_hint = id_token,
|
||||
))
|
||||
else:
|
||||
if id_token:
|
||||
messages.info(request,"Byli jste odhlášení z aplikace. Vaše centrální identita na <a href='%s' target='_blank'>auth.pirati.cz</a> zůstává přihlášena." % (appSettings.AUTH_SERVER + "account/"), extra_tags="safe")
|
||||
else:
|
||||
messages.info(request,"Byli jste odhlášení z aplikace.")
|
||||
|
||||
return redirect(LOGOUT_REDIRECT_VIEW)
|
||||
|
||||
def account(request):
|
||||
return redirect(
|
||||
request.build_absolute_uri(appSettings.AUTH_SERVER+"account/")
|
||||
+ "?kc_locale=" + appSettings.AUTH_SSO_LOCALE
|
||||
)
|
||||
|
||||
def refresh_session(request):
|
||||
"""
|
||||
Does a keycloak session refresh via middleware.
|
||||
"""
|
||||
resp = { 'exp' : request.session.get('refresh_expires_at', None) }
|
||||
|
||||
return HttpResponse(json.dumps(resp), content_type="application/json")
|
||||
|
@ -0,0 +1,160 @@
|
||||
# -*- encoding:utf-8 -*-
|
||||
"""
|
||||
Main settings file for one domain.
|
||||
|
||||
Domain: .CZ
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from .settings_global import *
|
||||
from .settings_local import *
|
||||
|
||||
##
|
||||
# Load ENV variables with prefix NALODENI_ to UPDATE_CONFIGS
|
||||
#
|
||||
# Docker deployment needs setting to be supplied externally.
|
||||
#
|
||||
# ENV_APPROVED_UPDATES contains known variables we can set,
|
||||
# the tuple contains "path" to the setting.
|
||||
ENV_APPROVED_UPDATES = {
|
||||
'AUTH_SERVER' : ('AUTH_SERVER',),
|
||||
'AUTH_CLIENT_ID' : ('AUTH_CLIENT_ID',),
|
||||
'AUTH_CLIENT_SECRET' : ('AUTH_CLIENT_SECRET',),
|
||||
'AUTH_AVAIL_IDP' : ('AUTH_AVAIL_IDP',),
|
||||
|
||||
'HTTP_PROTOCOL' : ('HTTP_PROTOCOL',),
|
||||
'BASE_DOMAIN' : ('BASE_DOMAIN',),
|
||||
'BASE_SUBDOMAIN' : ('BASE_SUBDOMAIN',),
|
||||
'BASE_PORT' : ('BASE_PORT',),
|
||||
|
||||
'PSQL_USER' : ('DATABASES','default','USER'),
|
||||
'PSQL_PASSWORD' : ('DATABASES','default','PASSWORD'),
|
||||
'PSQL_DBNAME' : ('DATABASES','default','NAME'),
|
||||
'PSQL_HOST' : ('DATABASES','default','HOST'),
|
||||
'PSQL_PORT' : ('DATABASES','default','PORT'),
|
||||
|
||||
'APP_REG_LIMIT_HARD' : ('APP_REG_LIMIT_HARD', ),
|
||||
'APP_REG_LIMIT_SOFT' : ('APP_REG_LIMIT_SOFT', ),
|
||||
'TOKEN_VALID_SEC' : ('TOKEN_VALID_SEC', ),
|
||||
|
||||
'EMAIL_BACKEND' : ('EMAIL_BACKEND',),
|
||||
'EMAIL_HOST' : ('EMAIL_HOST',),
|
||||
'EMAIL_PORT' : ('EMAIL_PORT',),
|
||||
'EMAIL_HOST_USER' : ('EMAIL_HOST_USER',),
|
||||
'EMAIL_HOST_PASSWORD' : ('EMAIL_HOST_PASSWORD',),
|
||||
'EMAIL_USE_TLS' : ('EMAIL_USE_TLS',),
|
||||
'EMAIL_USE_SSL' : ('EMAIL_USE_SSL',),
|
||||
'EMAIL_SSL_CERTFILE' : ('EMAIL_SSL_CERTFILE',),
|
||||
'EMAIL_SSL_KEYFILE' : ('EMAIL_SSL_KEYFILE',),
|
||||
|
||||
'DEBUG' : ('DEBUG',),
|
||||
'DEBUG_LOCAL' : ('DEBUG_LOCAL',),
|
||||
}
|
||||
for evk in os.environ:
|
||||
if evk[0:9] == "NALODENI_":
|
||||
var_key = evk[9:]
|
||||
if var_key in ENV_APPROVED_UPDATES:
|
||||
val_type = os.environ[evk][0:2]
|
||||
val = str(os.environ[evk][2:])
|
||||
|
||||
if val_type == "s-":
|
||||
val = (val, ) # tuple
|
||||
elif val_type == "b-":
|
||||
val = (val == 'on', ) # tuple
|
||||
elif val_type == "a-":
|
||||
# nested tuple, one gets eaten by + operator
|
||||
val = ( tuple(val.split(',')) ,)
|
||||
else:
|
||||
print("Wrong ENV value for '%s', skipping" % evk)
|
||||
continue
|
||||
|
||||
if val == "":
|
||||
val = (None, )
|
||||
|
||||
UPDATE_CONFIGS.append( ENV_APPROVED_UPDATES[var_key] + val )
|
||||
|
||||
##
|
||||
# Update configs from settings_local.UPDATE_CONFIGS
|
||||
#
|
||||
# Each item is a list-like path to the setting to be
|
||||
# updated, the last value of the list is the value of
|
||||
# the setting updated. E.g.:
|
||||
# ('CACHES', 'default', 'KEY_PREFIX', 'cz:'),
|
||||
# is the same as
|
||||
# CACHES['default']['KEY_PREFIX'] = 'cz:'
|
||||
# written directly in the settings file.
|
||||
#
|
||||
# Use array references to update the existing array.
|
||||
# [:-2] used to preserve the last array-like link, so
|
||||
# we can update the value there, and not the local variable.
|
||||
for item in UPDATE_CONFIGS:
|
||||
p = vars()
|
||||
for i in item[:-2]:
|
||||
if not i in p:
|
||||
p[i] = {}
|
||||
p = p[i]
|
||||
p[item[-2]]=item[-1]
|
||||
|
||||
#
|
||||
##
|
||||
|
||||
|
||||
## ##
|
||||
# DO NOT EDIT BELOW !!! #
|
||||
## ##
|
||||
|
||||
##
|
||||
# SECURITY - do not change.
|
||||
##
|
||||
#CSRF_USE_SESSIONS = False
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
CSRF_COOKIE_SECURE = (HTTP_PROTOCOL == 'https') # fix to HTTPS when available
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SECURE = (HTTP_PROTOCOL == 'https') # fix to HTTPS when available
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', HTTP_PROTOCOL)
|
||||
|
||||
##
|
||||
# Domain setup
|
||||
##
|
||||
BASE_URL = HTTP_PROTOCOL + '://' + BASE_SUBDOMAIN + BASE_DOMAIN + ((':' + BASE_PORT) if BASE_PORT != "" else "")
|
||||
SESSION_COOKIE_DOMAIN= BASE_SUBDOMAIN + BASE_DOMAIN
|
||||
CSRF_COOKIE_DOMAIN=BASE_SUBDOMAIN + BASE_DOMAIN
|
||||
CSRF_TRUSTED_ORIGINS = [ SESSION_COOKIE_DOMAIN , ]
|
||||
ALLOWED_HOSTS = [ BASE_SUBDOMAIN + BASE_DOMAIN, ]
|
||||
|
||||
##
|
||||
# Internationalization, https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
##
|
||||
LANGUAGE_CODE = 'cs'
|
||||
TIME_ZONE = 'Europe/Prague'
|
||||
|
||||
##
|
||||
# User model
|
||||
##
|
||||
AUTH_USER_MODEL = "nalodeni.AppUser"
|
||||
|
||||
##
|
||||
# Logger config
|
||||
##
|
||||
LOG_FILES = os.path.join(BASE_DIR, 'log_files')
|
||||
LOG_INCOMING_REQUESTS_FILE = os.path.join(
|
||||
LOG_FILES, 'incoming_requests_%s.log' % BASE_DOMAIN
|
||||
)
|
||||
|
||||
##
|
||||
# Media files path setup
|
||||
##
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR,'media_files')
|
||||
|
||||
##
|
||||
# Debugging settings
|
||||
##
|
||||
if DEBUG_LOCAL:
|
||||
ALLOWED_HOSTS += [
|
||||
'localhost',
|
||||
]
|
||||
|
||||
CSRF_COOKIE_DOMAIN = "localhost"
|
||||
SESSION_COOKIE_DOMAIN= CSRF_COOKIE_DOMAIN
|
||||
|
@ -0,0 +1,208 @@
|
||||
"""
|
||||
Django settings for main project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.0.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Used to update config values _after_ importing settings_local
|
||||
UPDATE_CONFIGS = []
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '-replace-this-in-install-settings-3243v432'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
DEBUG_LOCAL=False
|
||||
DEBUG_PROPAGATE_EXCEPTIONS = True
|
||||
LOG_INCOMING_REQUESTS = False
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
HTTP_PROTOCOL = 'https'
|
||||
BASE_DOMAIN = "localhost"
|
||||
BASE_SUBDOMAIN = ""
|
||||
BASE_PORT = ""
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
#'statici18n', # e.g. for javascript translation
|
||||
'keycloak_oidc',
|
||||
|
||||
'anymail',
|
||||
|
||||
'nalodeni',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
|
||||
'keycloak_oidc.middleware.KeycloakSessionRefreshMiddleware', # added
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
|
||||
'django.middleware.gzip.GZipMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'main.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
#'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.template.context_processors.i18n', # for statici18n
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'main.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': '',
|
||||
'USER': '',
|
||||
'PASSWORD': '',
|
||||
'HOST': '', # Set to empty string for localhost.
|
||||
'PORT': '', # Set to empty string for default.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Specify in settings
|
||||
AUTH_USER_MODEL = None
|
||||
|
||||
# Other auth backends
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'nalodeni.auth.EmailTokenAuthBackend',
|
||||
]
|
||||
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
# },
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
# },
|
||||
# {
|
||||
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
# },
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'cs'
|
||||
|
||||
TIME_ZONE = 'Europe/Prague'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = False
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR,'static_files')
|
||||
|
||||
##
|
||||
# Locale, translation files
|
||||
##
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR + "/locale",
|
||||
]
|
||||
|
||||
##
|
||||
# Email setup
|
||||
##
|
||||
EMAIL_HOST="localhost"
|
||||
EMAIL_PORT=25
|
||||
#DEFAULT_FROM_EMAIL = 'nalodeni@pirati.cz'
|
||||
ANYMAIL = {
|
||||
"MAILGUN_API_KEY": "",
|
||||
"MAILGUN_SENDER_DOMAIN": '',
|
||||
"MAILGUN_API_URL": 'https://api.eu.mailgun.net/v3',
|
||||
}
|
||||
#EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
|
||||
|
||||
##
|
||||
# SingleSignOn
|
||||
#
|
||||
# AUTH_SCOPE - list of scopes to request from the auth server
|
||||
# AUTH_GET_USER_FUNCTION - name of a function that takes the user info dict,
|
||||
# and returns an user object representing that user
|
||||
##
|
||||
AUTH_SERVER = "https://pttest1.kouzelnakrabicka.cz/auth/realms/pirati/"
|
||||
AUTH_CLIENT_ID = "" # view setting_local.py
|
||||
AUTH_CLIENT_SECRET = "" # view setting_local.py
|
||||
AUTH_SCOPE = ['openid',]
|
||||
AUTH_GET_USER_FUNCTION = 'nalodeni.models:get_user_by_keycloak_email'
|
||||
AUTH_SSO_LOCALE = 'cs'
|
||||
AUTH_AVAIL_IDP = []
|
||||
|
||||
#LOGIN_REDIRECT_URL = '/sso/login'
|
||||
#LOGOUT_REDIRECT_URL = '/sso/logout'
|
||||
|
||||
##
|
||||
# Email registration
|
||||
##
|
||||
APP_REG_LIMIT_HARD = 50
|
||||
APP_REG_LIMIT_SOFT = 0
|
||||
TOKEN_VALID_SEC = 30*60
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Defaults for settings_local variables
|
||||
##
|
@ -0,0 +1,57 @@
|
||||
# -*- encoding:utf-8 -*-
|
||||
"""
|
||||
Application local settings:
|
||||
- database
|
||||
- server connections
|
||||
"""
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '5k@dbpq_3+t7g+ylt5h1*ox79lnwp-qijry3y60^1r_9q*m5b('
|
||||
|
||||
##
|
||||
# Database setup
|
||||
##
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql', # Add 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': 'mainApp', # Or path to database file if using sqlite3.
|
||||
'USER': 'user', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
##
|
||||
# Email setup
|
||||
##
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
ANYMAIL = {
|
||||
"MAILGUN_API_KEY": "",
|
||||
"MAILGUN_SENDER_DOMAIN": '',
|
||||
"MAILGUN_API_URL": 'https://api.eu.mailgun.net/v3',
|
||||
|
||||
"FROM_NAME" : "",
|
||||
"FROM_EMAIL" : "",
|
||||
"PUBLIC_TO_EMAIL" : "",
|
||||
}
|
||||
|
||||
##
|
||||
# Update config array with these values
|
||||
##
|
||||
UPDATE_CONFIGS = [
|
||||
]
|
||||
|
||||
UPDATE_CONFIGS = [
|
||||
#('CACHES', 'default', 'LOCATION', '/var/run/memcached/memcached.sock'),
|
||||
('CACHES', 'default', 'KEY_PREFIX', 'mainApp:'),
|
||||
|
||||
('AUTH_CLIENT_ID','mainApp'),
|
||||
('AUTH_CLIENT_SECRET',''),
|
||||
('AUTH_SSO_LOCALE', 'cs'),
|
||||
('AUTH_AVAIL_IDP',('facebook',)), # tuple
|
||||
]
|
||||
|
||||
LOG_INCOMING_REQUESTS = False
|
||||
|
||||
DEBUG_LOCAL=False
|
@ -0,0 +1,96 @@
|
||||
<!doctype html>
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang=""> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang=""> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang=""> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" lang=""> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title></title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
||||
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="static/css/bootstrap-theme.min.css">
|
||||
<link rel="stylesheet" href="static/css/main.css">
|
||||
|
||||
<script src="static/js/vendor/modernizr-2.8.3-respond-1.4.2.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 8]>
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="#">Project name</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<form class="navbar-form navbar-right" role="form">
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="Email" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" placeholder="Password" class="form-control">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Sign in</button>
|
||||
</form>
|
||||
</div><!--/.navbar-collapse -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Hello, world!</h1>
|
||||
<p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>
|
||||
<p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more »</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Example row of columns -->
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h2>Heading</h2>
|
||||
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
|
||||
<p><a class="btn btn-default" href="#" role="button">View details »</a></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h2>Heading</h2>
|
||||
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
|
||||
<p><a class="btn btn-default" href="#" role="button">View details »</a></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h2>Heading</h2>
|
||||
<p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
|
||||
<p><a class="btn btn-default" href="#" role="button">View details »</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<footer>
|
||||
<p>© Company 2015</p>
|
||||
</footer>
|
||||
</div> <!-- /container -->
|
||||
<script>window.jQuery || document.write('<script src="static/js/vendor/jquery-1.11.2.min.js"><\/script>')</script>
|
||||
|
||||
<script src="static/js/vendor/bootstrap.min.js"></script>
|
||||
|
||||
<script src="static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,38 @@
|
||||
"""main URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/2.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
import nalodeni.urls
|
||||
|
||||
urlpatterns = [
|
||||
#path('admin/', admin.site.urls),
|
||||
path('', include(nalodeni.urls)),
|
||||
|
||||
# SSO
|
||||
path('sso/', include('keycloak_oidc.urls')),
|
||||
]
|
||||
|
||||
|
||||
# Javascript translations
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
urlpatterns += [
|
||||
path('jsi18n/nalodeni/',
|
||||
JavaScriptCatalog.as_view(packages=['nalodeni']),
|
||||
name='js-cat-website'),
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for main project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings")
|
||||
|
||||
application = get_wsgi_application()
|
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
@ -0,0 +1,262 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.conf import settings as appSettings
|
||||
|
||||
from . import models
|
||||
|
||||
# Get an instance of a logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Our user model
|
||||
USER_MODEL = get_user_model()
|
||||
|
||||
class EmailTokenAuthBackend:
|
||||
"""
|
||||
Provides authorization via email workflow.
|
||||
|
||||
The user is verified by a token previously sent by email.
|
||||
"""
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return USER_MODEL.objects.get(pk=user_id)
|
||||
except USER_MODEL.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def authenticate(self, request, emailToken=None):
|
||||
if emailToken is None:
|
||||
return None
|
||||
|
||||
# do we have a user with this valid token ?
|
||||
try:
|
||||
u = USER_MODEL.objects.get(emailToken=emailToken)
|
||||
|
||||
if u.etStamp and (datetime.now() - u.etStamp).total_seconds() < int(appSettings.TOKEN_VALID_SEC):
|
||||
u.emailToken = None
|
||||
u.etStamp = None
|
||||
u.save()
|
||||
return u
|
||||
else:
|
||||
u.emailToken = None
|
||||
u.etStamp = None
|
||||
u.save()
|
||||
raise ValidationError("Přihlašovací odkaz vypršel, nechte si poslat nový.")
|
||||
|
||||
except USER_MODEL.DoesNotExist:
|
||||
pass
|
||||
|
||||
# is this a first-time login, so the user has to be created ?
|
||||
rslt = models.AppRegEmail.objects.filter(emailToken=emailToken)
|
||||
if len(rslt) == 0:
|
||||
return None
|
||||
|
||||
elif len(rslt) == 1:
|
||||
reg = rslt[0]
|
||||
if reg.etStamp and (datetime.now() - reg.etStamp).total_seconds() < int(appSettings.TOKEN_VALID_SEC):
|
||||
rsltUsers = models.AppUser.objects.filter(username=reg.email)
|
||||
if len(rsltUsers) != 0 :
|
||||
raise ValidationError("Uživatelské jméno je již obsazeno.")
|
||||
|
||||
rsltUsers = models.AppUser.objects.filter(email=reg.email)
|
||||
if len(rsltUsers) != 0 :
|
||||
raise ValidationError("E-mailová adresa je již obsazena.")
|
||||
|
||||
# token valid, create the user
|
||||
u = USER_MODEL()
|
||||
u.username = reg.email
|
||||
u.email = reg.email
|
||||
u.postcode = reg.postcode
|
||||
u.kind = reg.kind
|
||||
u.interestedIn = reg.interestedIn
|
||||
u.userform = reg.userform
|
||||
u.dc_stamp = datetime.now()
|
||||
u.save()
|
||||
|
||||
# remove the approved email from registration
|
||||
reg.userform = None
|
||||
reg.save()
|
||||
reg.delete()
|
||||
|
||||
return u
|
||||
else:
|
||||
raise ValidationError("Registrační odkaz vypršel, nechte si poslat nový.")
|
||||
|
||||
else:
|
||||
# multiple tokens, which should not happen
|
||||
raise Exception("Multiple records with the same token.")
|
||||
|
||||
return None
|
||||
|
||||
def sendLoginToken(user):
|
||||
"""
|
||||
Generate and send a token to the user.
|
||||
"""
|
||||
|
||||
emailToken = get_random_string(120)
|
||||
|
||||
user.emailToken = emailToken
|
||||
user.etStamp = datetime.now()
|
||||
user.save()
|
||||
|
||||
emailSubj = 'Piráti - nalodění - přihlašovací odkaz'
|
||||
emailBody = """\
|
||||
Dobrý den,
|
||||
|
||||
níže zasíláme přihlašovací odkaz do aplikace Pirátů "Nalodění":
|
||||
|
||||
{baseUrl}/prihlaseni/?t={emailToken}
|
||||
|
||||
Přihlásíte se kliknutím na odkaz, nebo jeho překopírováním do prohlížeče internetových stránek.
|
||||
Odkaz je možné použít pouze jednou, v případě potřeby si nechte zaslat nový odkaz.
|
||||
|
||||
S pozdravem
|
||||
Piráti
|
||||
"""
|
||||
|
||||
send_mail(
|
||||
emailSubj,
|
||||
emailBody.format(
|
||||
emailToken=emailToken,
|
||||
baseUrl=appSettings.BASE_URL),
|
||||
"nalodeni@pirati.cz",
|
||||
[user.email], # email to ...
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def sendRegisterToken(f_email):
|
||||
"""
|
||||
Generate and send a token to the registered email.
|
||||
"""
|
||||
|
||||
# Check for user non-existence with this email
|
||||
f_email = f_email.strip().lower()
|
||||
rslt = models.AppUser.objects.filter(email__iexact=f_email)
|
||||
if len(rslt) == 0:
|
||||
pass
|
||||
elif len(rslt) == 1:
|
||||
# send login token instead
|
||||
return sendLoginToken(rslt[0])
|
||||
else:
|
||||
logger.error("More AppUser objects with the same email.")
|
||||
return
|
||||
|
||||
|
||||
# check registrations
|
||||
rslt = models.AppRegEmail.objects.filter(email__iexact=f_email)
|
||||
if len(rslt) == 1:
|
||||
rt = rslt[0]
|
||||
elif len(rslt) == 0:
|
||||
rt = models.AppRegEmail()
|
||||
rt.email = f_email
|
||||
else:
|
||||
logger.error("More AppRegEmail objects with the same email.")
|
||||
return
|
||||
|
||||
sendRegisterTokenReg(rt)
|
||||
|
||||
|
||||
def sendRegisterTokenReg(rt):
|
||||
""" Send registration email to an already existing AppRegEmail instance 'rt'. """
|
||||
# create token
|
||||
emailToken = get_random_string(120)
|
||||
|
||||
rt.emailToken = emailToken
|
||||
rt.etStamp = datetime.now()
|
||||
rt.save()
|
||||
|
||||
emailSubj = 'Piráti - nalodění - registrační odkaz'
|
||||
emailBody = """\
|
||||
Dobrý den,
|
||||
|
||||
níže Vám zasíláme registrační odkaz do aplikace Pirátů "Nalodění":
|
||||
|
||||
{baseUrl}/prihlaseni/?t={emailToken}
|
||||
|
||||
V registraci pokračujte kliknutím na odkaz, nebo jeho překopírováním do prohlížeče internetových stránek.
|
||||
Odkaz je možné použít pouze jednou, v případě potřeby si nechte zaslat nový odkaz.
|
||||
|
||||
S pozdravem
|
||||
Piráti
|
||||
"""
|
||||
|
||||
send_mail(
|
||||
emailSubj,
|
||||
emailBody.format(
|
||||
emailToken=emailToken,
|
||||
baseUrl=appSettings.BASE_URL),
|
||||
"nalodeni@pirati.cz",
|
||||
[rt.email], # email to ...
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def sendEmailContactVerificationToken(user):
|
||||
""" Send validation email to user.email_contact. """
|
||||
# The wanted new value, save it here so we do not accidentally change it,
|
||||
# or someone else through the user-object.
|
||||
ecw = user.email_contact
|
||||
|
||||
if ecw is None or ecw == "":
|
||||
raise ValidationError("Chybí kontaktní email.")
|
||||
|
||||
# ValidationError se odchytava vyse
|
||||
validate_email(ecw)
|
||||
|
||||
# create token
|
||||
rand_str = get_random_string(120)
|
||||
|
||||
user.email_contact_token = "%s-%s" % ( int(datetime.now().timestamp()), rand_str )
|
||||
user.email_contact_verified = False
|
||||
user.save()
|
||||
|
||||
sha256hash = hashlib.sha256()
|
||||
sha256hash.update(rand_str.encode('utf-8'))
|
||||
sha256hash.update(ecw.encode('utf-8'))
|
||||
|
||||
emailToken = sha256hash.hexdigest()
|
||||
emailSubj = 'Piráti - nalodění - ověření kontaktního emailu'
|
||||