API v9: première esquisse

This commit is contained in:
Emmanuel Viennet 2021-09-09 12:49:23 +02:00
parent 4ac076ec6c
commit be224b9576
11 changed files with 184 additions and 1 deletions

View File

@ -117,6 +117,7 @@ def create_app(config_class=DevConfig):
from app.views import notes_bp from app.views import notes_bp
from app.views import users_bp from app.views import users_bp
from app.views import absences_bp from app.views import absences_bp
from app.api import bp as api_bp
# https://scodoc.fr/ScoDoc # https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp) app.register_blueprint(scodoc_bp)
@ -130,6 +131,7 @@ def create_app(config_class=DevConfig):
app.register_blueprint( app.register_blueprint(
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences" absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
) )
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
scodoc_exc_formatter = RequestFormatter( scodoc_exc_formatter = RequestFormatter(
"[%(asctime)s] %(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(remote_addr)s requested %(url)s\n"
"%(levelname)s in %(module)s: %(message)s" "%(levelname)s in %(module)s: %(message)s"

8
app/api/__init__.py Normal file
View File

@ -0,0 +1,8 @@
"""api.__init__
"""
from flask import Blueprint
bp = Blueprint("api", __name__)
from app.api import sco_api

53
app/api/auth.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: UTF-8 -*
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial
# (see https://github.com/miguelgrinberg/microblog)
# Under The MIT License (MIT)
# Copyright (c) 2017 Miguel Grinberg
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from app.auth.models import User
from app.api.errors import error_response
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()
@basic_auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
return user
@basic_auth.error_handler
def basic_auth_error(status):
return error_response(status)
@token_auth.verify_token
def verify_token(token):
return User.check_token(token) if token else None
@token_auth.error_handler
def token_auth_error(status):
return error_response(status)

37
app/api/errors.py Normal file
View File

@ -0,0 +1,37 @@
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial
# (see https://github.com/miguelgrinberg/microblog)
# Under The MIT License (MIT)
# Copyright (c) 2017 Miguel Grinberg
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES
def error_response(status_code, message=None):
payload = {"error": HTTP_STATUS_CODES.get(status_code, "Unknown error")}
if message:
payload["message"] = message
response = jsonify(payload)
response.status_code = status_code
return response
def bad_request(message):
return error_response(400, message)

46
app/api/sco_api.py Normal file
View File

@ -0,0 +1,46 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""API ScoDoc 9
"""
# PAS ENCORE IMPLEMENTEE, juste un essai
from flask import jsonify, request, url_for, abort
from app import db
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import bad_request
from app import models
@bp.route("/ScoDoc/api/list_depts", methods=["GET"])
@token_auth.login_required
def list_depts():
depts = models.Departement.query.filter_by(visible=True).all()
data = {"items": [d.to_dict() for d in depts]}
return jsonify(data)

20
app/api/tokens.py Normal file
View File

@ -0,0 +1,20 @@
from flask import jsonify
from app import db
from app.api import bp
from app.api.auth import basic_auth, token_auth
@bp.route("/tokens", methods=["POST"])
@basic_auth.login_required
def get_token():
token = basic_auth.current_user().get_token()
db.session.commit()
return jsonify({"token": token})
@bp.route("/tokens", methods=["DELETE"])
@token_auth.login_required
def revoke_token():
token_auth.current_user().revoke_token()
db.session.commit()
return "", 204

View File

@ -37,9 +37,11 @@ def login():
if form.validate_on_submit(): if form.validate_on_submit():
user = User.query.filter_by(user_name=form.user_name.data).first() user = User.query.filter_by(user_name=form.user_name.data).first()
if user is None or not user.check_password(form.password.data): if user is None or not user.check_password(form.password.data):
current_app.logger.info("login: invalid (%s)", form.user_name.data)
flash(_("Invalid user name or password")) flash(_("Invalid user name or password"))
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
login_user(user, remember=form.remember_me.data) login_user(user, remember=form.remember_me.data)
current_app.logger.info("login: success (%s)", form.user_name.data)
next_page = request.args.get("next") next_page = request.args.get("next")
if not next_page or url_parse(next_page).netloc != "": if not next_page or url_parse(next_page).netloc != "":
next_page = url_for("scodoc.index") next_page = url_for("scodoc.index")

View File

@ -34,3 +34,13 @@ class Departement(db.Model):
def __repr__(self): def __repr__(self):
return f"<Departement {self.acronym}>" return f"<Departement {self.acronym}>"
def to_dict(self):
data = {
"id": self.id,
"acronym": self.acronym,
"description": self.description,
"visible": self.visible,
"date_creation": self.date_creation,
}
return data

View File

@ -52,6 +52,10 @@ def POST(s, path, data, errmsg=None):
# --- Ouverture session (login) # --- Ouverture session (login)
s = requests.Session() s = requests.Session()
s.post(
"https://deb11.viennet.net/api/auth/login",
data={"user_name": USER, "password": PASSWORD},
)
r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE) r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE)
if r.status_code != 200: if r.status_code != 200:
raise ScoError("erreur de connection: vérifier adresse et identifiants") raise ScoError("erreur de connection: vérifier adresse et identifiants")

View File

@ -18,6 +18,7 @@ Flask==2.0.1
Flask-Babel==2.0.0 Flask-Babel==2.0.0
Flask-Bootstrap==3.3.7.1 Flask-Bootstrap==3.3.7.1
Flask-Caching==1.10.1 Flask-Caching==1.10.1
Flask-HTTPAuth==4.4.0
Flask-Login==0.5.0 Flask-Login==0.5.0
Flask-Mail==0.9.1 Flask-Mail==0.9.1
Flask-Migrate==3.1.0 Flask-Migrate==3.1.0

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.0.11" SCOVERSION = "9.0.12"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"