commit
c4b147c3e5
73
README.md
73
README.md
|
@ -42,11 +42,11 @@ You will need the following packages to get started:
|
||||||
|
|
||||||
#### Ubuntu
|
#### Ubuntu
|
||||||
|
|
||||||
sudo apt-get install -y python-pip python-dev libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libyaml-dev libssl-dev
|
sudo apt-get install -y python-pip python-dev libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libyaml-dev libssl-dev libsasl2-dev libldap2-dev
|
||||||
|
|
||||||
#### CentOS / RHEL
|
#### CentOS / RHEL
|
||||||
|
|
||||||
yum install -y python-pip python-devel.x86_64 libxslt-devel.x86_64 libxml2-devel.x86_64 libffi-devel.x86_64 libyaml-devel.x86_64 libxslt-devel.x86_64 zlib-devel.x86_64 openssl-devel.x86_64 python-pbr gcc
|
yum install -y python-pip python-devel.x86_64 libxslt-devel.x86_64 libxml2-devel.x86_64 libffi-devel.x86_64 libyaml-devel.x86_64 libxslt-devel.x86_64 zlib-devel.x86_64 openssl-devel.x86_64 openldap2-devel cyrus-sasl-devel python-pbr gcc
|
||||||
|
|
||||||
#### OSX / Windows
|
#### OSX / Windows
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ The easiest way. Install it using Python Package Index:
|
||||||
sudo apt-get install -y software-properties-common python-software-properties
|
sudo apt-get install -y software-properties-common python-software-properties
|
||||||
sudo add-apt-repository -y ppa:chris-lea/node.js
|
sudo add-apt-repository -y ppa:chris-lea/node.js
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y nodejs python-pip python-dev libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libyaml-dev libssl-dev
|
sudo apt-get install -y nodejs python-pip python-dev libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libyaml-dev libssl-dev libsasl2-dev libldap2-dev
|
||||||
sudo npm install -g bower
|
sudo npm install -g bower
|
||||||
bower install
|
bower install
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ You may want to customize your app and the easiest way is the setup command:
|
||||||
|
|
||||||
realms-wiki setup
|
realms-wiki setup
|
||||||
|
|
||||||
This will ask you questions and create a `realms-wiki.json` file in where you can find it.
|
This will ask you questions and create a `realms-wiki.json` file.
|
||||||
You can manually edit this file as well.
|
You can manually edit this file as well.
|
||||||
Any config value set in `realms-wiki.json` will override values set in `realms/config/__init__.py`.
|
Any config value set in `realms-wiki.json` will override values set in `realms/config/__init__.py`.
|
||||||
|
|
||||||
|
@ -273,6 +273,69 @@ WHOOSH_INDEX has to be a path readable and writeable by Realm's user. It will be
|
||||||
Whoosh is set up to use language optimization, so set WHOOSH_LANGUAGE to the language used in your wiki. For available languages, check `whoosh.lang.languages`.
|
Whoosh is set up to use language optimization, so set WHOOSH_LANGUAGE to the language used in your wiki. For available languages, check `whoosh.lang.languages`.
|
||||||
If your language is not supported, Realms will fall back to a simple text analyzer.
|
If your language is not supported, Realms will fall back to a simple text analyzer.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Local
|
||||||
|
|
||||||
|
Local default will be done using a backend database as defined in the config.
|
||||||
|
To disable local authentication, put the following your config.
|
||||||
|
|
||||||
|
"AUTH_LOCAL_ENABLE": false
|
||||||
|
|
||||||
|
|
||||||
|
### LDAP (beta)
|
||||||
|
|
||||||
|
Realms uses the following library to authenticate using LDAP. https://github.com/ContinuumIO/flask-ldap-login
|
||||||
|
It supports direct bind and bind by search.
|
||||||
|
Use these examples as a guide and place it in your realms-wiki.json config.
|
||||||
|
|
||||||
|
|
||||||
|
#### Bind By Search Example
|
||||||
|
|
||||||
|
In this example, BIND_DN and BIND_AUTH are used to search and authenticate. Leaving them blank implies anonymous authentication.
|
||||||
|
|
||||||
|
"LDAP": {
|
||||||
|
"URI": "ldap://localhost:8389",
|
||||||
|
"BIND_DN": "",
|
||||||
|
"BIND_AUTH": "",
|
||||||
|
"USER_SEARCH": {"base": "dc=realms,dc=io", "filter": "uid=%(username)s"},
|
||||||
|
"KEY_MAP": {
|
||||||
|
"username":"cn",
|
||||||
|
"email": "mail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#### Direct Bind Example
|
||||||
|
|
||||||
|
"LDAP": {
|
||||||
|
"URI": "ldap://localhost:8389",
|
||||||
|
"BIND_DN": "uid=%(username)s,ou=People,dc=realms,dc=io",
|
||||||
|
"KEY_MAP": {
|
||||||
|
"username":"cn",
|
||||||
|
"email": "mail",
|
||||||
|
},
|
||||||
|
"OPTIONS": {
|
||||||
|
"OPT_PROTOCOL_VERSION": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### OAuth (beta)
|
||||||
|
|
||||||
|
Realms currently supports Github, Twitter, Facebook and Google. Each provider requires a key and secret.
|
||||||
|
Put them in your `realms-wiki.json` config file. Use the example below.
|
||||||
|
|
||||||
|
"OAUTH": {
|
||||||
|
"twitter": {
|
||||||
|
"key": "",
|
||||||
|
"secret": ""
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"key": "",
|
||||||
|
"secret": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
realms-wiki start
|
realms-wiki start
|
||||||
|
@ -292,7 +355,7 @@ After your config is in place use the following commands:
|
||||||
sudo restart realms-wiki
|
sudo restart realms-wiki
|
||||||
|
|
||||||
|
|
||||||
### Developement mode
|
### Development mode
|
||||||
|
|
||||||
This will start the server in the foreground with auto reloaded enabled:
|
This will start the server in the foreground with auto reloaded enabled:
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
FROM realms/base
|
FROM realms/base
|
||||||
|
|
||||||
# Packages
|
# Packages
|
||||||
RUN apt-get update && apt-get install -y build-essential python-pip python-virtualenv python-dev zlib1g-dev libffi-dev libyaml-dev
|
RUN apt-get update && apt-get install -y build-essential python-pip python-virtualenv python-dev zlib1g-dev libffi-dev libyaml-dev libldap2-dev libsasl2-dev
|
||||||
|
|
||||||
# lxml deps
|
# lxml deps
|
||||||
# libxml2-dev libxslt1-dev
|
# libxml2-dev libxslt1-dev
|
||||||
|
|
|
@ -17,15 +17,15 @@ if ! type "add-apt-repository" > /dev/null; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Elastic Search
|
# Elastic Search
|
||||||
wget -qO - http://packages.elasticsearch.org/GPG-KEY-elasticsearch | sudo apt-key add -
|
# wget -qO - http://packages.elasticsearch.org/GPG-KEY-elasticsearch | sudo apt-key add -
|
||||||
echo 'deb http://packages.elasticsearch.org/elasticsearch/1.4/debian stable main' | sudo tee /etc/apt/sources.list.d/elastic.list
|
# echo 'deb http://packages.elasticsearch.org/elasticsearch/1.4/debian stable main' | sudo tee /etc/apt/sources.list.d/elastic.list
|
||||||
|
|
||||||
sudo add-apt-repository -y ppa:chris-lea/node.js
|
sudo add-apt-repository -y ppa:chris-lea/node.js
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
||||||
sudo apt-get install -y python build-essential pkg-config git \
|
sudo apt-get install -y python build-essential pkg-config git \
|
||||||
python-pip python-virtualenv python-dev zlib1g-dev \
|
python-pip python-virtualenv python-dev zlib1g-dev libldap2-dev libsasl2-dev \
|
||||||
libffi-dev libyaml-dev libssl-dev nodejs openjdk-7-jre-headless elasticsearch
|
libffi-dev libyaml-dev libssl-dev libldap2-dev libsasl2-dev nodejs
|
||||||
|
|
||||||
# Create swap file because ES eats up RAM and 14.04 doesn't have swap by default
|
# Create swap file because ES eats up RAM and 14.04 doesn't have swap by default
|
||||||
sudo fallocate -l 1G /swapfile
|
sudo fallocate -l 1G /swapfile
|
||||||
|
|
|
@ -15,6 +15,7 @@ from flask.ext.cache import Cache
|
||||||
from flask.ext.login import LoginManager, current_user
|
from flask.ext.login import LoginManager, current_user
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
from flask.ext.assets import Environment, Bundle
|
from flask.ext.assets import Environment, Bundle
|
||||||
|
from flask_ldap_login import LDAPLoginManager
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
@ -109,6 +110,11 @@ class Assets(Environment):
|
||||||
|
|
||||||
return super(Assets, self).register(name, Bundle(*args, filters=filters, output=output))
|
return super(Assets, self).register(name, Bundle(*args, filters=filters, output=output))
|
||||||
|
|
||||||
|
class MyLDAPLoginManager(LDAPLoginManager):
|
||||||
|
@property
|
||||||
|
def attrlist(self):
|
||||||
|
# the parent method doesn't always work
|
||||||
|
return None
|
||||||
|
|
||||||
class RegexConverter(BaseConverter):
|
class RegexConverter(BaseConverter):
|
||||||
""" Enables Regex matching on endpoints
|
""" Enables Regex matching on endpoints
|
||||||
|
@ -163,6 +169,7 @@ def create_app(config=None):
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
search.init_app(app)
|
search.init_app(app)
|
||||||
|
ldap.init_app(app)
|
||||||
|
|
||||||
db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin)
|
db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin)
|
||||||
|
|
||||||
|
@ -182,16 +189,17 @@ def create_app(config=None):
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html'), 404
|
||||||
|
|
||||||
if app.config['RELATIVE_PATH']:
|
if app.config.get('RELATIVE_PATH'):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def root():
|
def root():
|
||||||
return redirect(url_for(app.config['ROOT_ENDPOINT']))
|
return redirect(url_for(app.config.get('ROOT_ENDPOINT')))
|
||||||
|
|
||||||
app.discover()
|
app.discover()
|
||||||
|
|
||||||
# This will be removed at some point
|
# This will be removed at some point
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.metadata.create_all(db.get_engine(app))
|
if app.config.get('DB_URI'):
|
||||||
|
db.metadata.create_all(db.get_engine(app))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@ -202,6 +210,7 @@ db = SQLAlchemy()
|
||||||
cache = Cache()
|
cache = Cache()
|
||||||
assets = Assets()
|
assets = Assets()
|
||||||
search = Search()
|
search = Search()
|
||||||
|
ldap = MyLDAPLoginManager()
|
||||||
|
|
||||||
assets.register('main.js',
|
assets.register('main.js',
|
||||||
'vendor/jquery/dist/jquery.js',
|
'vendor/jquery/dist/jquery.js',
|
||||||
|
|
|
@ -83,6 +83,38 @@ DB_URI = 'sqlite:////tmp/wiki.db'
|
||||||
# DB_URI = 'oracle://scott:tiger@127.0.0.1:1521/sidname'
|
# DB_URI = 'oracle://scott:tiger@127.0.0.1:1521/sidname'
|
||||||
# DB_URI = 'crate://'
|
# DB_URI = 'crate://'
|
||||||
|
|
||||||
|
# LDAP = {
|
||||||
|
# 'URI': '',
|
||||||
|
#
|
||||||
|
# # This BIND_DN/BIND_PASSWORD default to '', this is shown here for demonstrative purposes
|
||||||
|
# # The values '' perform an anonymous bind so we may use search/bind method
|
||||||
|
# 'BIND_DN': '',
|
||||||
|
# 'BIND_AUTH': '',
|
||||||
|
#
|
||||||
|
# # Adding the USER_SEARCH field tells the flask-ldap-login that we are using
|
||||||
|
# # the search/bind method
|
||||||
|
# 'USER_SEARCH': {'base': 'dc=example,dc=com', 'filter': 'uid=%(username)s'},
|
||||||
|
#
|
||||||
|
# # Map ldap keys into application specific keys
|
||||||
|
# 'KEY_MAP': {
|
||||||
|
# 'name': 'cn',
|
||||||
|
# 'company': 'o',
|
||||||
|
# 'location': 'l',
|
||||||
|
# 'email': 'mail',
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# OAUTH = {
|
||||||
|
# 'twitter': {
|
||||||
|
# 'key': '',
|
||||||
|
# 'secret': ''
|
||||||
|
# },
|
||||||
|
# 'github': {
|
||||||
|
# 'key': '',
|
||||||
|
# 'secret': ''
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
CACHE_TYPE = 'simple'
|
CACHE_TYPE = 'simple'
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
|
@ -121,6 +153,7 @@ WIKI_PATH = '/tmp/wiki'
|
||||||
# Name of page that will act as home
|
# Name of page that will act as home
|
||||||
WIKI_HOME = 'home'
|
WIKI_HOME = 'home'
|
||||||
|
|
||||||
|
AUTH_LOCAL_ENABLE = True
|
||||||
ALLOW_ANON = True
|
ALLOW_ANON = True
|
||||||
REGISTRATION_ENABLED = True
|
REGISTRATION_ENABLED = True
|
||||||
PRIVATE_WIKI = False
|
PRIVATE_WIKI = False
|
||||||
|
@ -161,4 +194,13 @@ if ENV != "DEV":
|
||||||
ASSETS_DEBUG = False
|
ASSETS_DEBUG = False
|
||||||
SQLALCHEMY_ECHO = False
|
SQLALCHEMY_ECHO = False
|
||||||
|
|
||||||
MODULES = ['wiki', 'auth', 'search']
|
MODULES = ['wiki', 'search', 'auth']
|
||||||
|
|
||||||
|
if globals().get('AUTH_LOCAL_ENABLE'):
|
||||||
|
MODULES.append('auth.local')
|
||||||
|
|
||||||
|
if globals().get('OAUTH'):
|
||||||
|
MODULES.append('auth.oauth')
|
||||||
|
|
||||||
|
if globals().get('LDAP'):
|
||||||
|
MODULES.append('auth.ldap')
|
||||||
|
|
|
@ -119,7 +119,8 @@ def filename_to_cname(filename):
|
||||||
|
|
||||||
|
|
||||||
def gravatar_url(email):
|
def gravatar_url(email):
|
||||||
return "https://www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest()
|
email = hashlib.md5(email).hexdigest() if email else "default@realms.io"
|
||||||
|
return "https://www.gravatar.com/avatar/" + email
|
||||||
|
|
||||||
|
|
||||||
def in_virtualenv():
|
def in_virtualenv():
|
||||||
|
|
|
@ -2,6 +2,7 @@ from realms import login_manager
|
||||||
from flask import request, flash, redirect
|
from flask import request, flash, redirect
|
||||||
from flask.ext.login import login_url
|
from flask.ext.login import login_url
|
||||||
|
|
||||||
|
modules = set()
|
||||||
|
|
||||||
@login_manager.unauthorized_handler
|
@login_manager.unauthorized_handler
|
||||||
def unauthorized():
|
def unauthorized():
|
||||||
|
|
3
realms/modules/auth/ldap/__init__.py
Normal file
3
realms/modules/auth/ldap/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from ..models import Auth
|
||||||
|
|
||||||
|
Auth.register('ldap')
|
7
realms/modules/auth/ldap/forms.py
Normal file
7
realms/modules/auth/ldap/forms.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from flask_wtf import Form
|
||||||
|
from wtforms import StringField, PasswordField, validators
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(Form):
|
||||||
|
login = StringField('Username', [validators.DataRequired()])
|
||||||
|
password = PasswordField('Password', [validators.DataRequired()])
|
53
realms/modules/auth/ldap/models.py
Normal file
53
realms/modules/auth/ldap/models.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
from flask import render_template
|
||||||
|
from flask.ext.login import login_user
|
||||||
|
from realms import ldap
|
||||||
|
from flask_ldap_login import LDAPLoginForm
|
||||||
|
from ..models import BaseUser
|
||||||
|
|
||||||
|
|
||||||
|
users = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ldap.save_user
|
||||||
|
def save_user(username, userdata):
|
||||||
|
user = User(userdata.get('username'), userdata.get('email'))
|
||||||
|
users[user.id] = user
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseUser):
|
||||||
|
type = 'ldap'
|
||||||
|
|
||||||
|
def __init__(self, username, email='null@localhost.local', password=None):
|
||||||
|
self.id = username
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token_id(self):
|
||||||
|
return self.password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_user(*args, **kwargs):
|
||||||
|
return User.get_by_id(args[0])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(user_id):
|
||||||
|
return users.get(user_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login_form():
|
||||||
|
return render_template('auth/ldap/login.html', form=LDAPLoginForm())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def auth(user, password):
|
||||||
|
password = User.hash_password(password)
|
||||||
|
user.password = password
|
||||||
|
users[user.id] = user
|
||||||
|
if user:
|
||||||
|
login_user(user, remember=True)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
19
realms/modules/auth/ldap/views.py
Normal file
19
realms/modules/auth/ldap/views.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from flask import current_app, request, redirect, Blueprint, flash, url_for
|
||||||
|
from ..ldap.models import User
|
||||||
|
from flask_ldap_login import LDAPLoginForm
|
||||||
|
|
||||||
|
blueprint = Blueprint('auth.ldap', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/login/ldap", methods=['POST'])
|
||||||
|
def login():
|
||||||
|
form = LDAPLoginForm()
|
||||||
|
|
||||||
|
if not form.validate():
|
||||||
|
flash('Form invalid', 'warning')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
if User.auth(form.user, request.form['password']):
|
||||||
|
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('auth.login'))
|
3
realms/modules/auth/local/__init__.py
Normal file
3
realms/modules/auth/local/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from ..models import Auth
|
||||||
|
|
||||||
|
Auth.register('local')
|
|
@ -1,9 +1,10 @@
|
||||||
import click
|
import click
|
||||||
from realms.lib.util import random_string
|
from realms.lib.util import random_string
|
||||||
from realms.modules.auth.models import User
|
from realms.modules.auth.local.models import User
|
||||||
from realms.lib.util import green, red, yellow
|
from realms.lib.util import green, red, yellow
|
||||||
from realms import flask_cli
|
from realms import flask_cli
|
||||||
|
|
||||||
|
|
||||||
@flask_cli.group(short_help="Auth Module")
|
@flask_cli.group(short_help="Auth Module")
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
98
realms/modules/auth/local/models.py
Normal file
98
realms/modules/auth/local/models.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
from flask import current_app, render_template
|
||||||
|
from flask.ext.login import logout_user, login_user
|
||||||
|
from realms import login_manager, db
|
||||||
|
from realms.lib.model import Model
|
||||||
|
from ..models import BaseUser
|
||||||
|
from .forms import LoginForm
|
||||||
|
from itsdangerous import URLSafeSerializer, BadSignature
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
|
||||||
|
@login_manager.token_loader
|
||||||
|
def load_token(token):
|
||||||
|
# Load unsafe because payload is needed for sig
|
||||||
|
sig_okay, payload = URLSafeSerializer(current_app.config['SECRET_KEY']).loads_unsafe(token)
|
||||||
|
|
||||||
|
if not payload:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# User key *could* be stored in payload to avoid user lookup in db
|
||||||
|
user = User.get_by_id(payload.get('id'))
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if BaseUser.signer(sha256(user.password).hexdigest()).loads(token):
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except BadSignature:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class User(Model, BaseUser):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
type = 'local'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(128), unique=True)
|
||||||
|
email = db.Column(db.String(128), unique=True)
|
||||||
|
password = db.Column(db.String(60))
|
||||||
|
admin = False
|
||||||
|
|
||||||
|
hidden_fields = ['password']
|
||||||
|
readonly_fields = ['email', 'password']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token_id(self):
|
||||||
|
return self.password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_user(*args, **kwargs):
|
||||||
|
return User.get_by_id(args[0])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(username, email, password):
|
||||||
|
u = User()
|
||||||
|
u.username = username
|
||||||
|
u.email = email
|
||||||
|
u.password = User.hash_password(password)
|
||||||
|
u.save()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_username(username):
|
||||||
|
return User.query().filter_by(username=username).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_email(email):
|
||||||
|
return User.query().filter_by(email=email).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def signer(salt):
|
||||||
|
return URLSafeSerializer(current_app.config['SECRET_KEY'] + salt)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def auth(email, password):
|
||||||
|
user = User.get_by_email(email)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
# User doesn't exist
|
||||||
|
return False
|
||||||
|
|
||||||
|
if User.check_password(password, user.password):
|
||||||
|
# Password is good, log in user
|
||||||
|
login_user(user, remember=True)
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
# Password check failed
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def logout(cls):
|
||||||
|
logout_user()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login_form():
|
||||||
|
form = LoginForm()
|
||||||
|
return render_template('auth/local/login.html', form=form)
|
||||||
|
|
51
realms/modules/auth/local/views.py
Normal file
51
realms/modules/auth/local/views.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for
|
||||||
|
from realms.modules.auth.local.models import User
|
||||||
|
from realms.modules.auth.local.forms import LoginForm, RegistrationForm
|
||||||
|
|
||||||
|
blueprint = Blueprint('auth.local', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/login/local", methods=['POST'])
|
||||||
|
def login():
|
||||||
|
form = LoginForm()
|
||||||
|
|
||||||
|
if not form.validate():
|
||||||
|
flash('Form invalid', 'warning')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
if User.auth(request.form['email'], request.form['password']):
|
||||||
|
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
|
||||||
|
else:
|
||||||
|
flash('Email or Password Incorrect', 'warning')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/register", methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
|
||||||
|
if not current_app.config['REGISTRATION_ENABLED']:
|
||||||
|
flash("Registration is disabled")
|
||||||
|
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
|
||||||
|
|
||||||
|
form = RegistrationForm()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if not form.validate():
|
||||||
|
flash('Form invalid', 'warning')
|
||||||
|
return redirect(url_for('auth.local.register'))
|
||||||
|
|
||||||
|
if User.get_by_username(request.form['username']):
|
||||||
|
flash('Username is taken', 'warning')
|
||||||
|
return redirect(url_for('auth.local.register'))
|
||||||
|
|
||||||
|
if User.get_by_email(request.form['email']):
|
||||||
|
flash('Email is taken', 'warning')
|
||||||
|
return redirect(url_for('auth.local.register'))
|
||||||
|
|
||||||
|
User.create(request.form['username'], request.form['email'], request.form['password'])
|
||||||
|
User.auth(request.form['email'], request.form['password'])
|
||||||
|
|
||||||
|
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
|
||||||
|
|
||||||
|
return render_template("auth/register.html", form=form)
|
|
@ -1,39 +1,43 @@
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin
|
from flask.ext.login import UserMixin, logout_user, AnonymousUserMixin
|
||||||
from realms import login_manager, db
|
from realms import login_manager
|
||||||
from realms.lib.model import Model
|
|
||||||
from realms.lib.util import gravatar_url
|
from realms.lib.util import gravatar_url
|
||||||
from itsdangerous import URLSafeSerializer, BadSignature
|
from itsdangerous import URLSafeSerializer, BadSignature
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
from . import modules
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(auth_id):
|
||||||
return User.get_by_id(user_id)
|
return Auth.load_user(auth_id)
|
||||||
|
|
||||||
|
auth_users = {}
|
||||||
|
|
||||||
|
|
||||||
@login_manager.token_loader
|
class Auth(object):
|
||||||
def load_token(token):
|
|
||||||
# Load unsafe because payload is needed for sig
|
|
||||||
sig_okay, payload = URLSafeSerializer(current_app.config['SECRET_KEY']).loads_unsafe(token)
|
|
||||||
|
|
||||||
if not payload:
|
@staticmethod
|
||||||
return None
|
def register(module):
|
||||||
|
modules.add(module)
|
||||||
|
|
||||||
# User key *could* be stored in payload to avoid user lookup in db
|
@staticmethod
|
||||||
user = User.get_by_id(payload.get('id'))
|
def get_auth_user(auth_type):
|
||||||
|
mod = importlib.import_module('realms.modules.auth.%s.models' % auth_type)
|
||||||
|
return mod.User
|
||||||
|
|
||||||
if not user:
|
@staticmethod
|
||||||
return None
|
def load_user(auth_id):
|
||||||
|
auth_type, user_id = auth_id.split("/")
|
||||||
|
return Auth.get_auth_user(auth_type).load_user(user_id)
|
||||||
|
|
||||||
try:
|
@staticmethod
|
||||||
if User.signer(sha256(user.password).hexdigest()).loads(token):
|
def login_forms():
|
||||||
return user
|
forms = []
|
||||||
else:
|
for t in modules:
|
||||||
return None
|
forms.append(Auth.get_auth_user(t).login_form())
|
||||||
except BadSignature:
|
return "<hr />".join(forms)
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AnonUser(AnonymousUserMixin):
|
class AnonUser(AnonymousUserMixin):
|
||||||
|
@ -42,61 +46,35 @@ class AnonUser(AnonymousUserMixin):
|
||||||
admin = False
|
admin = False
|
||||||
|
|
||||||
|
|
||||||
class User(Model, UserMixin):
|
class BaseUser(UserMixin):
|
||||||
__tablename__ = 'users'
|
id = None
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
email = None
|
||||||
username = db.Column(db.String(128), unique=True)
|
username = None
|
||||||
email = db.Column(db.String(128), unique=True)
|
type = 'base'
|
||||||
password = db.Column(db.String(60))
|
|
||||||
admin = False
|
|
||||||
|
|
||||||
hidden_fields = ['password']
|
def get_id(self):
|
||||||
readonly_fields = ['email', 'password']
|
return unicode("%s/%s" % (self.type, self.id))
|
||||||
|
|
||||||
def get_auth_token(self):
|
def get_auth_token(self):
|
||||||
key = sha256(self.password).hexdigest()
|
key = sha256(self.auth_token_id).hexdigest()
|
||||||
return User.signer(key).dumps(dict(id=self.id))
|
return BaseUser.signer(key).dumps(dict(id=self.id))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token_id(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def avatar(self):
|
def avatar(self):
|
||||||
return gravatar_url(self.email)
|
return gravatar_url(self.email)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(username, email, password):
|
def load_user(*args, **kwargs):
|
||||||
u = User()
|
raise NotImplementedError
|
||||||
u.username = username
|
|
||||||
u.email = email
|
|
||||||
u.password = User.hash_password(password)
|
|
||||||
u.save()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_by_username(username):
|
|
||||||
return User.query().filter_by(username=username).first()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_by_email(email):
|
|
||||||
return User.query().filter_by(email=email).first()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def signer(salt):
|
def signer(salt):
|
||||||
return URLSafeSerializer(current_app.config['SECRET_KEY'] + salt)
|
return URLSafeSerializer(current_app.config['SECRET_KEY'] + salt)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def auth(email, password):
|
|
||||||
user = User.get_by_email(email)
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
# User doesn't exist
|
|
||||||
return False
|
|
||||||
|
|
||||||
if User.check_password(password, user.password):
|
|
||||||
# Password is good, log in user
|
|
||||||
login_user(user, remember=True)
|
|
||||||
return user
|
|
||||||
else:
|
|
||||||
# Password check failed
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hash_password(password):
|
def hash_password(password):
|
||||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))
|
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))
|
||||||
|
@ -109,4 +87,8 @@ class User(Model, UserMixin):
|
||||||
def logout(cls):
|
def logout(cls):
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login_form():
|
||||||
|
pass
|
||||||
|
|
||||||
login_manager.anonymous_user = AnonUser
|
login_manager.anonymous_user = AnonUser
|
||||||
|
|
3
realms/modules/auth/oauth/__init__.py
Normal file
3
realms/modules/auth/oauth/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from ..models import Auth
|
||||||
|
|
||||||
|
Auth.register('oauth')
|
173
realms/modules/auth/oauth/models.py
Normal file
173
realms/modules/auth/oauth/models.py
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
from flask import session
|
||||||
|
from flask_login import login_user
|
||||||
|
from flask_oauthlib.client import OAuth
|
||||||
|
|
||||||
|
from realms import config
|
||||||
|
from ..models import BaseUser
|
||||||
|
|
||||||
|
oauth = OAuth()
|
||||||
|
|
||||||
|
users = {}
|
||||||
|
|
||||||
|
providers = {
|
||||||
|
'twitter': {
|
||||||
|
'oauth': dict(
|
||||||
|
base_url='https://api.twitter.com/1.1/',
|
||||||
|
request_token_url='https://api.twitter.com/oauth/request_token',
|
||||||
|
access_token_url='https://api.twitter.com/oauth/access_token',
|
||||||
|
authorize_url='https://api.twitter.com/oauth/authenticate',
|
||||||
|
access_token_method='GET'),
|
||||||
|
'button': '<a href="/login/oauth/twitter" class="btn btn-twitter"><i class="fa fa-twitter"></i></a>',
|
||||||
|
'profile': None,
|
||||||
|
'field_map': {
|
||||||
|
'id': 'user_id',
|
||||||
|
'username': 'screen_name'
|
||||||
|
},
|
||||||
|
'token_name': 'oauth_token'
|
||||||
|
},
|
||||||
|
'github': {
|
||||||
|
'oauth': dict(
|
||||||
|
request_token_params={'scope': 'user:email'},
|
||||||
|
base_url='https://api.github.com/',
|
||||||
|
request_token_url=None,
|
||||||
|
access_token_method='POST',
|
||||||
|
access_token_url='https://github.com/login/oauth/access_token',
|
||||||
|
authorize_url='https://github.com/login/oauth/authorize'),
|
||||||
|
'button': '<a href="/login/oauth/github" class="btn btn-github"><i class="fa fa-github"></i></a>',
|
||||||
|
'profile': 'user',
|
||||||
|
'field_map': {
|
||||||
|
'id': 'id',
|
||||||
|
'username': 'login',
|
||||||
|
'email': 'email'
|
||||||
|
},
|
||||||
|
'token_name': 'access_token'
|
||||||
|
},
|
||||||
|
'facebook': {
|
||||||
|
'oauth': dict(
|
||||||
|
request_token_params={'scope': 'email'},
|
||||||
|
base_url='https://graph.facebook.com',
|
||||||
|
request_token_url=None,
|
||||||
|
access_token_url='/oauth/access_token',
|
||||||
|
access_token_method='GET',
|
||||||
|
authorize_url='https://www.facebook.com/dialog/oauth'
|
||||||
|
),
|
||||||
|
'button': '<a href="/login/oauth/facebook" class="btn btn-facebook"><i class="fa fa-facebook"></i></a>',
|
||||||
|
'profile': '/me',
|
||||||
|
'field_map': {
|
||||||
|
'id': 'id',
|
||||||
|
'username': 'name',
|
||||||
|
'email': 'email'
|
||||||
|
},
|
||||||
|
'token_name': 'access_token'
|
||||||
|
},
|
||||||
|
'google': {
|
||||||
|
'oauth': dict(
|
||||||
|
request_token_params={
|
||||||
|
'scope': 'https://www.googleapis.com/auth/userinfo.email'
|
||||||
|
},
|
||||||
|
base_url='https://www.googleapis.com/oauth2/v1/',
|
||||||
|
request_token_url=None,
|
||||||
|
access_token_method='POST',
|
||||||
|
access_token_url='https://accounts.google.com/o/oauth2/token',
|
||||||
|
authorize_url='https://accounts.google.com/o/oauth2/auth',
|
||||||
|
),
|
||||||
|
'button': '<a href="/login/oauth/google" class="btn btn-google"><i class="fa fa-google"></i></a>',
|
||||||
|
'profile': 'userinfo',
|
||||||
|
'field_map': {
|
||||||
|
'id': 'id',
|
||||||
|
'username': 'name',
|
||||||
|
'email': 'email'
|
||||||
|
},
|
||||||
|
'token_name': 'access_token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseUser):
|
||||||
|
type = 'oauth'
|
||||||
|
provider = None
|
||||||
|
|
||||||
|
def __init__(self, provider, user_id, username=None, token=None, email=None):
|
||||||
|
self.provider = provider
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.id = user_id
|
||||||
|
self.token = token
|
||||||
|
self.auth_id = "%s-%s" % (provider, username)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token_id(self):
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_user(*args, **kwargs):
|
||||||
|
return User.get_by_id(args[0])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(user_id):
|
||||||
|
return users.get(user_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def auth(provider, data, oauth_token):
|
||||||
|
field_map = providers.get(provider).get('field_map')
|
||||||
|
if not field_map:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_value(d, key):
|
||||||
|
if isinstance(key, basestring):
|
||||||
|
return d.get(key)
|
||||||
|
# key should be list here
|
||||||
|
val = d.get(key.pop(0))
|
||||||
|
if len(key) == 0:
|
||||||
|
# if empty we have our value
|
||||||
|
return val
|
||||||
|
# keep digging
|
||||||
|
return get_value(val, key)
|
||||||
|
|
||||||
|
fields = {}
|
||||||
|
for k, v in field_map.items():
|
||||||
|
fields[k] = get_value(data, v)
|
||||||
|
|
||||||
|
user = User(provider, fields['id'], username=fields.get('username'), email=fields.get('email'),
|
||||||
|
token=User.hash_password(oauth_token))
|
||||||
|
users[user.auth_id] = user
|
||||||
|
|
||||||
|
if user:
|
||||||
|
login_user(user, remember=True)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_app(cls, provider):
|
||||||
|
if oauth.remote_apps.get(provider):
|
||||||
|
return oauth.remote_apps.get(provider)
|
||||||
|
app = oauth.remote_app(
|
||||||
|
provider,
|
||||||
|
consumer_key=config.OAUTH.get(provider, {}).get('key'),
|
||||||
|
consumer_secret=config.OAUTH.get(provider, {}).get(
|
||||||
|
'secret'),
|
||||||
|
**providers[provider]['oauth'])
|
||||||
|
app.tokengetter(lambda: session.get(provider + "_token"))
|
||||||
|
return app
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_provider_value(cls, provider, key):
|
||||||
|
return providers.get(provider, {}).get(key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_token(cls, provider, resp):
|
||||||
|
return resp.get(cls.get_provider_value(provider, 'token_name'))
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return unicode("%s/%s" % (self.type, self.auth_id))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login_form():
|
||||||
|
buttons = []
|
||||||
|
for name, val in providers.items():
|
||||||
|
if not config.OAUTH.get(name, {}).get('key') or not config.OAUTH.get(name, {}).get('secret'):
|
||||||
|
continue
|
||||||
|
buttons.append(val.get('button'))
|
||||||
|
|
||||||
|
return "<h3>Social Login</h3>" + " ".join(buttons)
|
39
realms/modules/auth/oauth/views.py
Normal file
39
realms/modules/auth/oauth/views.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from flask import Blueprint, url_for, request, flash, redirect, session, current_app
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
blueprint = Blueprint('auth.oauth', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def oauth_failed(next_url):
|
||||||
|
flash('You denied the request to sign in.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/login/oauth/<provider>")
|
||||||
|
def login(provider):
|
||||||
|
return User.get_app(provider).authorize(callback=url_for('auth.oauth.callback', provider=provider, _external=True))
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/login/oauth/<provider>/callback')
|
||||||
|
def callback(provider):
|
||||||
|
next_url = request.args.get('next') or url_for(current_app.config['ROOT_ENDPOINT'])
|
||||||
|
try:
|
||||||
|
remote_app = User.get_app(provider)
|
||||||
|
resp = remote_app.authorized_response()
|
||||||
|
if resp is None:
|
||||||
|
flash('You denied the request to sign in.', 'error')
|
||||||
|
flash('Reason: ' + request.args['error_reason'] +
|
||||||
|
' ' + request.args['error_description'], 'error')
|
||||||
|
return redirect(next_url)
|
||||||
|
except Exception as e:
|
||||||
|
flash('Access denied: %s' % e.message)
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
oauth_token = resp.get(User.get_provider_value(provider, 'token_name'))
|
||||||
|
session[provider + "_token"] = (oauth_token, '')
|
||||||
|
profile = User.get_provider_value(provider, 'profile')
|
||||||
|
data = remote_app.get(profile).data if profile else resp
|
||||||
|
|
||||||
|
User.auth(provider, data, oauth_token)
|
||||||
|
|
||||||
|
return redirect(next_url)
|
|
@ -1,72 +1,22 @@
|
||||||
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for
|
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for
|
||||||
from realms.modules.auth.models import User
|
from flask.ext.login import logout_user
|
||||||
from realms.modules.auth.forms import LoginForm, RegistrationForm
|
from realms.modules.auth.models import Auth
|
||||||
|
|
||||||
blueprint = Blueprint('auth', __name__)
|
blueprint = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/logout")
|
|
||||||
def logout_page():
|
|
||||||
User.logout()
|
|
||||||
flash("You are now logged out")
|
|
||||||
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/login", methods=['GET', 'POST'])
|
@blueprint.route("/login", methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
form = LoginForm()
|
return render_template("auth/login.html", forms=Auth.login_forms())
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
if not form.validate():
|
|
||||||
flash('Form invalid', 'warning')
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
if User.auth(request.form['email'], request.form['password']):
|
|
||||||
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
|
|
||||||
else:
|
|
||||||
flash('Email or Password Incorrect', 'warning')
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
return render_template("auth/login.html", form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/register", methods=['GET', 'POST'])
|
@blueprint.route("/logout")
|
||||||
def register():
|
def logout():
|
||||||
|
logout_user()
|
||||||
if not current_app.config['REGISTRATION_ENABLED']:
|
flash("You are now logged out")
|
||||||
flash("Registration is disabled")
|
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
|
||||||
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
|
|
||||||
|
|
||||||
form = RegistrationForm()
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
|
|
||||||
if not form.validate():
|
|
||||||
flash('Form invalid', 'warning')
|
|
||||||
return redirect(url_for('auth.register'))
|
|
||||||
|
|
||||||
if User.get_by_username(request.form['username']):
|
|
||||||
flash('Username is taken', 'warning')
|
|
||||||
return redirect(url_for('auth.register'))
|
|
||||||
|
|
||||||
if User.get_by_email(request.form['email']):
|
|
||||||
flash('Email is taken', 'warning')
|
|
||||||
return redirect(url_for('auth.register'))
|
|
||||||
|
|
||||||
User.create(request.form['username'], request.form['email'], request.form['password'])
|
|
||||||
User.auth(request.form['email'], request.form['password'])
|
|
||||||
|
|
||||||
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
|
|
||||||
|
|
||||||
return render_template("auth/register.html", form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/settings", methods=['GET', 'POST'])
|
@blueprint.route("/settings", methods=['GET', 'POST'])
|
||||||
def settings():
|
def settings():
|
||||||
return render_template("auth/settings.html")
|
return render_template("auth/settings.html")
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/logout")
|
|
||||||
def logout():
|
|
||||||
User.logout()
|
|
||||||
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app
|
from flask import render_template, request, Blueprint
|
||||||
from realms import search as search_engine
|
from realms import search as search_engine
|
||||||
|
|
||||||
blueprint = Blueprint('search', __name__)
|
blueprint = Blueprint('search', __name__)
|
||||||
|
|
|
@ -224,6 +224,26 @@ a.label {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-facebook {
|
||||||
|
background-color: #325c99;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-github {
|
||||||
|
background-color: #4c4c4c;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-google {
|
||||||
|
background-color: #dd4b39;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-twitter {
|
||||||
|
background-color: #02acec;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width:1000px) {
|
@media (max-width:1000px) {
|
||||||
.ace_content {
|
.ace_content {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
@ -285,6 +305,5 @@ a.label {
|
||||||
background-color: #3498db;
|
background-color: #3498db;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
realms/templates/auth/ldap/login.html
Normal file
29
realms/templates/auth/ldap/login.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% from 'macros.html' import render_form, render_field %}
|
||||||
|
{% if config.get('AUTH_LOCAL_ENABLE') %}
|
||||||
|
<button type="button" class="btn btn-info" data-toggle="modal" data-target="#ldap-modal">
|
||||||
|
<i class="fa fa-folder-open-o"></i> Login with LDAP
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="modal fade" id="ldap-modal" tabindex="-1" role="dialog" aria-labelledby="ldap-login">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title" id="ldap-login">LDAP Login</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{% call render_form(form, action_url=url_for('auth.ldap.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||||
|
{{ render_field(form.username, placeholder='Username', type='text', required=1) }}
|
||||||
|
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h3><i class="fa fa-folder-open-o"></i> LDAP Login</h3>
|
||||||
|
{% call render_form(form, action_url=url_for('auth.ldap.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||||
|
{{ render_field(form.username, placeholder='Username', type='text', required=1) }}
|
||||||
|
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||||
|
{% endcall %}
|
||||||
|
{% endif %}
|
5
realms/templates/auth/local/login.html
Normal file
5
realms/templates/auth/local/login.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% from 'macros.html' import render_form, render_field %}
|
||||||
|
{% call render_form(form, action_url=url_for('auth.local.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||||
|
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
||||||
|
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||||
|
{% endcall %}
|
|
@ -1,8 +1,4 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% from 'macros.html' import render_form, render_field %}
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% call render_form(form, action_url=url_for('auth.login'), action_text='Login', btn_class='btn btn-primary') %}
|
{{ forms|safe }}
|
||||||
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
|
||||||
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
|
||||||
{% endcall %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% from 'macros.html' import render_form, render_field %}
|
{% from 'macros.html' import render_form, render_field %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% call render_form(form, action_url=url_for('auth.register'), action_text='Register', btn_class='btn btn-primary') %}
|
{% call render_form(form, action_url=url_for('auth.local.register'), action_text='Register', btn_class='btn btn-primary') %}
|
||||||
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
|
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
|
||||||
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
||||||
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
|
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
|
||||||
|
|
|
@ -41,12 +41,12 @@
|
||||||
<div class="navbar-collapse collapse navbar-inverse-collapse">
|
<div class="navbar-collapse collapse navbar-inverse-collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
{% if config.get('ROOT_ENDPOINT') != 'wiki.index' %}
|
{% if config.get('ROOT_ENDPOINT') != 'wiki.index' %}
|
||||||
<li><a href="{{ url_for('wiki.index') }}"><i class="fa fa-list"></i> Index</a></li>
|
<li><a href="{{ url_for('wiki.index') }}"><i class="fa fa-list"></i><span class="hidden-sm"> Index</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ url_for('wiki.create') }}"><i class="fa fa-plus"></i> New</a></li>
|
<li><a href="{{ url_for('wiki.create') }}"><i class="fa fa-plus"></i><span class="hidden-sm"> New</span></a></li>
|
||||||
{% if name %}
|
{% if name %}
|
||||||
<li><a href="{{ url_for('wiki.edit', name=name) }}"><i class="fa fa-pencil"></i> Edit</a></li>
|
<li><a href="{{ url_for('wiki.edit', name=name) }}"><i class="fa fa-pencil"></i><span class="hidden-sm"> Edit</span></a></li>
|
||||||
<li><a href="{{ url_for('wiki.history', name=name) }}"><i class="fa fa-clock-o"></i> History</a></li>
|
<li><a href="{{ url_for('wiki.history', name=name) }}"><i class="fa fa-clock-o"></i><span class="hidden-sm"> History</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -73,8 +73,8 @@
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ url_for('auth.login') }}"><i class="fa fa-user"></i> Login</a></li>
|
<li><a href="{{ url_for('auth.login') }}"><i class="fa fa-user"></i> Login</a></li>
|
||||||
{% if config.REGISTRATION_ENABLED %}
|
{% if config.REGISTRATION_ENABLED and 'auth.local' in config.MODULES %}
|
||||||
<li><a href="{{ url_for('auth.register') }}"><i class="fa fa-users"></i> Register</a></li>
|
<li><a href="{{ url_for('auth.local.register') }}"><i class="fa fa-users"></i> Register</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
<button class="btn btn-default btn-sm dropdown-toggle" type="button" id="editor-actions"
|
<button class="btn btn-default btn-sm dropdown-toggle" type="button" id="editor-actions"
|
||||||
data-toggle="dropdown" title="Actions">
|
data-toggle="dropdown" title="Actions">
|
||||||
<i class="fa fa-cog"></i>
|
<i class="fa fa-cog"></i>
|
||||||
<span class="hidden-xs">Actions <i class="fa fa-caret-down"></i></span>
|
<span class="hidden-xs hidden-sm">Actions <i class="fa fa-caret-down"></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="editor-actions">
|
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="editor-actions">
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
<button id="theme-list-btn" type="button" class="dropdown-toggle btn btn-default btn-sm"
|
<button id="theme-list-btn" type="button" class="dropdown-toggle btn btn-default btn-sm"
|
||||||
data-toggle="dropdown" title="Change Theme">
|
data-toggle="dropdown" title="Change Theme">
|
||||||
<i class="fa fa-paint-brush"></i>
|
<i class="fa fa-paint-brush"></i>
|
||||||
<span class="hidden-xs">Theme <i class="fa fa-caret-down"></i></span>
|
<span class="hidden-xs hidden-sm">Theme <i class="fa fa-caret-down"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<ul id="theme-list" class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="theme-list">
|
<ul id="theme-list" class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="theme-list">
|
||||||
<li><a tabindex="-1" href="#" data-value="chrome" >Chrome</a></li>
|
<li><a tabindex="-1" href="#" data-value="chrome" >Chrome</a></li>
|
||||||
|
@ -141,7 +141,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<a id="submit-btn" class="btn btn-primary btn-sm">
|
<a id="submit-btn" class="btn btn-primary btn-sm">
|
||||||
<i class="fa fa-save"></i>
|
<i class="fa fa-save"></i>
|
||||||
<span class="hidden-xs">Publish</span>
|
<span class="hidden-xs">Save</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -28,18 +28,21 @@ setup(name='realms-wiki',
|
||||||
'Flask-Cache==0.13.1',
|
'Flask-Cache==0.13.1',
|
||||||
'Flask-Elastic==0.2',
|
'Flask-Elastic==0.2',
|
||||||
'Flask-Login==0.2.11',
|
'Flask-Login==0.2.11',
|
||||||
|
'Flask-OAuthlib==0.9.1',
|
||||||
'Flask-SQLAlchemy==2.0',
|
'Flask-SQLAlchemy==2.0',
|
||||||
'Flask-WTF==0.10.2',
|
'Flask-WTF==0.10.2',
|
||||||
'PyYAML==3.11',
|
'PyYAML==3.11',
|
||||||
'bcrypt==1.0.2',
|
'bcrypt==1.0.2',
|
||||||
'beautifulsoup4==4.3.2',
|
'beautifulsoup4==4.3.2',
|
||||||
'click==3.3',
|
'click==3.3',
|
||||||
|
'flask-ldap-login==0.3.0',
|
||||||
'gevent==1.0.2',
|
'gevent==1.0.2',
|
||||||
'ghdiff==0.4',
|
'ghdiff==0.4',
|
||||||
'gittle==0.5.0',
|
'gittle==0.5.0',
|
||||||
'gunicorn==19.3',
|
'gunicorn==19.3',
|
||||||
'itsdangerous==0.24',
|
'itsdangerous==0.24',
|
||||||
'markdown2==2.3.0',
|
'markdown2==2.3.0',
|
||||||
|
'python-ldap==2.4.22',
|
||||||
'simplejson==3.6.3'
|
'simplejson==3.6.3'
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
|
|
Loading…
Reference in a new issue