Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev
This commit is contained in:
commit
f3e47bd082
|
@ -1,6 +1,6 @@
|
|||
# Bibliothèques nécessaires
|
||||
|
||||
Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas sur votre config, quitte
|
||||
Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas sur votre config, quitte
|
||||
à faire un virtual environment.
|
||||
|
||||
La liste de paquets fourni est pour Archlinux, les paquets peuvent avoir des noms légèrement différents dans votre distribution.
|
||||
|
@ -12,6 +12,7 @@ python-flask-migrate
|
|||
python-flask-script
|
||||
python-flask-sqlalchemy
|
||||
python-flask-wtf
|
||||
python-ldap
|
||||
python-uwsgi
|
||||
python-psycopg2
|
||||
python-pyyaml
|
||||
|
|
|
@ -32,7 +32,7 @@ from app.models.forum import Forum
|
|||
from app.models.topic import Topic
|
||||
from app.models.notification import Notification
|
||||
|
||||
from app.routes import index, search, users # To load routes at initialization
|
||||
from app.routes import index, search, users, tools # To load routes at initialization
|
||||
from app.routes.account import login, account, notification
|
||||
from app.routes.admin import index, groups, account, trophies, forums
|
||||
from app.routes.forum import index
|
||||
|
|
|
@ -9,6 +9,8 @@ from app.models.notification import Notification
|
|||
import app.utils.unicode_names as unicode_names
|
||||
from app.utils.notify import notify
|
||||
from config import V5Config
|
||||
from local_config import USE_LDAP
|
||||
import app.utils.ldap as ldap
|
||||
|
||||
import werkzeug.security
|
||||
import re
|
||||
|
@ -108,7 +110,9 @@ class Member(User):
|
|||
self.name = name
|
||||
self.norm = unicode_names.normalize(name)
|
||||
self.email = email
|
||||
self.set_password(password)
|
||||
if not USE_LDAP:
|
||||
self.set_password(password)
|
||||
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password
|
||||
self.xp = 0
|
||||
|
||||
self.bio = ""
|
||||
|
@ -163,8 +167,11 @@ class Member(User):
|
|||
data = {key: data[key] for key in data if data[key] is not None}
|
||||
|
||||
# TODO: verify good type of those args, think about the password mgt
|
||||
# Beware of LDAP injections
|
||||
if "email" in data:
|
||||
self.email = data["email"]
|
||||
if USE_LDAP:
|
||||
ldap.set_email(self.norm, self.email)
|
||||
if "password" in data:
|
||||
self.set_password(data["password"])
|
||||
if "bio" in data:
|
||||
|
@ -203,13 +210,19 @@ class Member(User):
|
|||
Set the user's password. Check whether the request sender has the right
|
||||
to do this!
|
||||
"""
|
||||
self.password_hash = werkzeug.security.generate_password_hash(password,
|
||||
method='pbkdf2:sha512', salt_length=10)
|
||||
if USE_LDAP:
|
||||
ldap.set_password(self, password)
|
||||
else:
|
||||
self.password_hash = werkzeug.security.generate_password_hash(
|
||||
password, method='pbkdf2:sha512', salt_length=10)
|
||||
|
||||
def check_password(self, password):
|
||||
"""Compares password against member hash."""
|
||||
return werkzeug.security.check_password_hash(self.password_hash,
|
||||
password)
|
||||
if USE_LDAP:
|
||||
return ldap.check_password(self, password)
|
||||
else:
|
||||
return werkzeug.security.check_password_hash(self.password_hash,
|
||||
password)
|
||||
|
||||
def notify(self, message, href=None):
|
||||
""" Notify a user with a message.
|
||||
|
|
|
@ -4,6 +4,8 @@ from app import app, db
|
|||
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
|
||||
from app.models.users import Member
|
||||
from app.utils.render import render
|
||||
import app.utils.ldap as ldap
|
||||
from local_config import USE_LDAP
|
||||
|
||||
|
||||
@app.route('/account', methods=['GET', 'POST'])
|
||||
|
@ -30,7 +32,7 @@ def edit_account():
|
|||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('account.html', form=form)
|
||||
return render('account/account.html', form=form)
|
||||
|
||||
|
||||
@app.route('/account/delete', methods=['GET', 'POST'])
|
||||
|
@ -47,7 +49,7 @@ def delete_account():
|
|||
else:
|
||||
flash('Erreur lors de la suppression du compte', 'error')
|
||||
del_form.delete.data = False # Force to tick to delete the account
|
||||
return render('delete_account.html', del_form=del_form)
|
||||
return render('account/delete_account.html', del_form=del_form)
|
||||
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
|
@ -60,9 +62,13 @@ def register():
|
|||
member.newsletter = form.newsletter.data
|
||||
db.session.add(member)
|
||||
db.session.commit()
|
||||
# Workflow with LDAP is User → Postgresql → LDAP → Change password
|
||||
if USE_LDAP:
|
||||
ldap.add_member(member)
|
||||
ldap.set_password(member, form.password.data)
|
||||
flash('Inscription réussie', 'ok')
|
||||
return redirect(url_for('validation') + "?email=" + form.email.data)
|
||||
return render('register.html', title='Register', form=form)
|
||||
return render('account/register.html', title='Register', form=form)
|
||||
|
||||
|
||||
@app.route('/register/validation/', methods=['GET', 'POST'])
|
||||
|
@ -70,4 +76,4 @@ def validation():
|
|||
mail = request.args['email']
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
return render('validation.html', mail=mail)
|
||||
return render('account/validation.html', mail=mail)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from flask import redirect, url_for, request, flash
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from app import app
|
||||
from app.forms.login import LoginForm
|
||||
from app.models.users import Member
|
||||
|
@ -15,6 +15,9 @@ def login():
|
|||
return redirect(url_for('index'))
|
||||
|
||||
form = LoginForm()
|
||||
lateral = LoginForm(prefix="menu_")
|
||||
if lateral.validate_on_submit():
|
||||
form = lateral
|
||||
if form.validate_on_submit():
|
||||
member = Member.query.filter_by(name=form.username.data).first()
|
||||
|
||||
|
@ -51,7 +54,7 @@ def login():
|
|||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render('login.html', form=form)
|
||||
return render('account/login.html', form=form)
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
from app import app
|
||||
|
||||
from app.utils.render import render
|
||||
|
||||
|
||||
@app.route('/tools')
|
||||
def tools():
|
||||
return render('tools.html')
|
|
@ -9,7 +9,7 @@ from app.utils.render import render
|
|||
def user(username):
|
||||
norm = unicode_names.normalize(username)
|
||||
member = Member.query.filter_by(norm=norm).first_or_404()
|
||||
return render('user.html', member=member)
|
||||
return render('account/user.html', member=member)
|
||||
|
||||
|
||||
@app.route('/user/id/<int:user_id>')
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
/* Trigger actions for the menu */
|
||||
|
||||
/* Initialization */
|
||||
var b = document.getElementById('light-menu').getElementsByTagName('a')
|
||||
var b = document.querySelectorAll('#light-menu a');
|
||||
for(var i = 1; i < b.length; i++) {
|
||||
b[i].setAttribute('onfocus', "this.setAttribute('f', 'true');");
|
||||
b[i].setAttribute('onblur', "this.setAttribute('f', 'false');");
|
||||
b[i].removeAttribute('href');
|
||||
console.log("Removed");
|
||||
}
|
||||
|
||||
var trigger_menu = function(active) {
|
||||
|
@ -15,8 +17,8 @@ var trigger_menu = function(active) {
|
|||
element.classList.remove('opened');
|
||||
}
|
||||
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var menu = document.querySelector('#menu');
|
||||
var buttons = document.querySelectorAll('#light-menu li');
|
||||
var menus = document.querySelectorAll('#menu > div');
|
||||
|
||||
if(active == -1 || buttons[active].classList.contains('opened')) {
|
||||
|
@ -39,8 +41,8 @@ var trigger_menu = function(active) {
|
|||
}
|
||||
|
||||
var mouse_trigger = function(event) {
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var menu = document.querySelector('#menu');
|
||||
var buttons = document.querySelectorAll('#light-menu li');
|
||||
|
||||
if(!menu.contains(event.target)) {
|
||||
var active = -1;
|
||||
|
@ -48,7 +50,7 @@ var mouse_trigger = function(event) {
|
|||
for(i = 0; i < buttons.length; i++) {
|
||||
if(buttons[i].contains(event.target))
|
||||
active = i;
|
||||
buttons[i].getElementsByTagName('a')[0].blur();
|
||||
buttons[i].querySelector('a').blur();
|
||||
}
|
||||
|
||||
trigger_menu(active);
|
||||
|
@ -57,11 +59,11 @@ var mouse_trigger = function(event) {
|
|||
|
||||
var keyboard_trigger = function(event) {
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var buttons = document.querySelectorAll('#light-menu li');
|
||||
|
||||
if(event.keyCode == 13) {
|
||||
for(var i = 0; i < buttons.length; i++) {
|
||||
if(buttons[i].getElementsByTagName('a')[0].getAttribute('f') == 'true') {
|
||||
if(buttons[i].querySelector('a').getAttribute('f') == 'true') {
|
||||
trigger_menu(i);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<section>
|
||||
{{ widget_member.profile(member) }}
|
||||
|
||||
{% if current_user.is_authenticated and current_user.priv('access-admin-panel') %}
|
||||
{% if current_user.is_authenticated and (current_user == member or current_user.priv('access-admin-panel')) %}
|
||||
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier</a></div>
|
||||
{% endif %}
|
||||
|
|
@ -5,7 +5,11 @@
|
|||
</a>
|
||||
|
||||
<li>
|
||||
<a role="button" label="Compte" tabindex="0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('user', username=current_user.name) }}" role="button" label="Compte" tabindex="0">
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" role="button" label="Compte" tabindex="0">
|
||||
{% endif %}
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path>
|
||||
</svg>
|
||||
|
@ -23,7 +27,7 @@
|
|||
</li>
|
||||
|
||||
<li>
|
||||
<a role="button" label="Forum" tabindex="0">
|
||||
<a href="{{ url_for('forum_index') }}" role="button" label="Forum" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z"></path>
|
||||
</svg>
|
||||
|
@ -59,7 +63,7 @@
|
|||
</li>
|
||||
|
||||
<li>
|
||||
<a role="button" label="Outils" tabindex="0">
|
||||
<a href="{{ url_for('tools') }}" role="button" label="Outils" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"></path>
|
||||
</svg>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Outils</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<p>Planète Casio met à votre disposition divers outils. Pour vous connecter,
|
||||
utilisez votre identifiant unique et votre mot de passe habituel.
|
||||
<ul>
|
||||
<li><a href="https://gitea.planet-casio.com">Gitea</a> (forge Git)</li>
|
||||
<li><a href="https://wiki.planet-casio.com">Wiki</a> (wiki répétoriant tout un tas de trucs)</li>
|
||||
<li><a href="https://bible.planet-casio.com">Bible</a> (la bible du programmeur Casio bas niveau)</li>
|
||||
</ul>
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,83 @@
|
|||
import ldap
|
||||
from ldap.modlist import addModlist, modifyModlist
|
||||
from local_config import LDAP_PASSWORD, LDAP_ORGANIZATION
|
||||
|
||||
|
||||
def get_member(username):
|
||||
""" Get informations about member. Username must be normalized! """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Search for user
|
||||
r = conn.search_s(LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE, f'(cn={username})')
|
||||
if len(r) > 0:
|
||||
return r[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def edit(user, fields):
|
||||
""" Edit a user. Fields is {'name': ['value'], …} """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
# conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
|
||||
# old_value = {"userPassword": ["my_old_password"]}
|
||||
# new_value = {"userPassword": ["my_new_password"]}
|
||||
|
||||
modlist = ldap.modlist.modifyModlist(old_value, new_value)
|
||||
con.modify_s(dn, modlist)
|
||||
|
||||
|
||||
def set_email(user, email):
|
||||
pass
|
||||
|
||||
|
||||
def set_password(user, password):
|
||||
""" Set password for a user. """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}',
|
||||
LDAP_PASSWORD)
|
||||
conn.passwd_s(f"cn={user.norm},{LDAP_ORGANIZATION}", None, password)
|
||||
|
||||
|
||||
def check_password(user, password):
|
||||
""" Try to login a user through LDAP register. """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
try:
|
||||
conn.simple_bind_s(f"cn={user.norm},{LDAP_ORGANIZATION}", password)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def add_member(member):
|
||||
""" Add a member to LDAP register. Fields must have been sanitized! """
|
||||
if get_member(member.norm) is not None:
|
||||
print("User already exists")
|
||||
return
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
|
||||
# Create fields
|
||||
dn = f'cn={member.norm},{LDAP_ORGANIZATION}'
|
||||
modlist = addModlist({
|
||||
'objectClass': [bytes('inetOrgPerson', 'UTF8')],
|
||||
'cn': [bytes(member.norm, 'UTF8')],
|
||||
'sn': [bytes(member.norm, 'UTF8')],
|
||||
'displayName': [bytes(member.name, 'UTF8')],
|
||||
'mail': [bytes(member.email, 'UTF8')],
|
||||
'uid': [bytes(str(member.id), 'UTF8')],
|
||||
'userPassword': [bytes("", 'UTF8')]
|
||||
})
|
||||
# Add the member
|
||||
conn.add_s(dn, modlist)
|
||||
|
||||
|
||||
def delete_member(member):
|
||||
""" Remove a member from LDAP register """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
|
||||
# Create fields
|
||||
dn = f'cn={member.norm},{LDAP_ORGANIZATION}'
|
||||
# Delete the user
|
||||
conn.delete_s(dn)
|
|
@ -31,7 +31,7 @@ def render(*args, styles=[], **kwargs):
|
|||
if s[0] == '+':
|
||||
styles_.append(s[1:])
|
||||
|
||||
login_form = LoginForm()
|
||||
login_form = LoginForm(prefix="menu_")
|
||||
search_form = SearchForm()
|
||||
return render_template(*args, **kwargs,
|
||||
login_form=login_form, search_form=search_form, styles=styles_)
|
||||
|
|
|
@ -27,7 +27,7 @@ def valid_name(name, msg=False):
|
|||
|
||||
# Rule 2
|
||||
try:
|
||||
normalize(name)
|
||||
normalized_name = normalize(name)
|
||||
except ValueError:
|
||||
errors.append("cant-normalize")
|
||||
|
||||
|
@ -36,7 +36,7 @@ def valid_name(name, msg=False):
|
|||
errors.append("no-letter")
|
||||
|
||||
# Rule 4
|
||||
if name in V5Config.FORBIDDEN_USERNAMES:
|
||||
if normalized_name in V5Config.FORBIDDEN_USERNAMES:
|
||||
errors.append("forbidden")
|
||||
|
||||
return True if errors == [] else errors
|
||||
|
|
|
@ -3,7 +3,9 @@ from wtforms.validators import ValidationError
|
|||
from app.models.users import Member
|
||||
from app.utils.valid_name import valid_name
|
||||
from app.utils.unicode_names import normalize
|
||||
import app.utils.ldap as ldap
|
||||
from config import V5Config
|
||||
from local_config import USE_LDAP
|
||||
|
||||
|
||||
def name_valid(form, name):
|
||||
|
@ -42,6 +44,13 @@ def name_available(form, name):
|
|||
if member is not None:
|
||||
raise ValidationError("Ce nom d'utilisateur est indisponible.")
|
||||
|
||||
# Double check with LDAP if needed
|
||||
if USE_LDAP:
|
||||
member = ldap.get_member(norm)
|
||||
if member is not None:
|
||||
raise ValidationError("Ce nom d'utilisateur est indisponible.")
|
||||
|
||||
|
||||
|
||||
def email(form, email):
|
||||
member = Member.query.filter_by(email=email.data).first()
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
DB_NAME = "pcv5"
|
||||
USE_LDAP = False
|
||||
LDAP_PASSWORD = "openldap"
|
||||
LDAP_ORGANIZATION = "o=planet-casio"
|
Loading…
Reference in New Issue