From b520d929ca617b633151292641661765ae9883d8 Mon Sep 17 00:00:00 2001 From: MooGoo Date: Fri, 5 Jul 2024 08:33:22 +0800 Subject: [PATCH] fix: admin use jwt for api and adjust login process --- app/application.py | 2 - app/blueprints/admin.py | 110 +++++++++++++++--- app/blueprints/admin_static/common.js | 57 ++++----- app/blueprints/api.py | 18 +-- app/blueprints/base.py | 57 --------- app/config.py | 7 ++ app/templates/admin/base.html | 2 +- app/templates/login.html | 80 ------------- .../sites/ppi/admin/record-form-view.html | 7 ++ requirements/base.txt | 1 + 10 files changed, 142 insertions(+), 199 deletions(-) delete mode 100644 app/templates/login.html diff --git a/app/application.py b/app/application.py index 2488c78..598ad3a 100644 --- a/app/application.py +++ b/app/application.py @@ -132,8 +132,6 @@ def create_app(): apply_blueprints(flask_app) -flask_app.config['JWT_SECRET_KEY'] = 'no-secret' # TODO - # flask extensions babel = Babel(flask_app, locale_selector=get_locale) flask_app.jinja_env.globals['get_locale'] = get_locale diff --git a/app/blueprints/admin.py b/app/blueprints/admin.py index 68d5d20..10c8e6b 100644 --- a/app/blueprints/admin.py +++ b/app/blueprints/admin.py @@ -19,6 +19,9 @@ ) from flask.views import View from jinja2.exceptions import TemplateNotFound +from werkzeug.security import ( + check_password_hash, +) from flask_login import ( login_required, current_user, @@ -38,6 +41,18 @@ from sqlalchemy.orm import ( aliased, ) +from flask_login import ( + login_required, + login_user, + logout_user, +) +from flask_jwt_extended import ( + jwt_required, + create_access_token, + get_jwt_identity, + set_access_cookies, + unset_jwt_cookies, +) from app.models.collection import ( Collection, Record, @@ -80,6 +95,7 @@ from app.helpers_query import ( make_admin_record_query, ) + from app.helpers_data import ( export_specimen_dwc_csv, ) @@ -88,11 +104,60 @@ admin = Blueprint('admin', __name__, static_folder='admin_static', static_url_path='static') +@admin.route('/login', methods=['GET', 'POST']) +def login(): + site = get_current_site(request) + if not site: + return abort(404) -@admin.before_request -def check_auth(): - if not current_user.is_authenticated: - return abort(401) + if request.method == 'GET': + return render_template('admin/login.html', site=site) + elif request.method == 'POST': + username = request.json.get('username', '') + passwd = request.json.get('passwd', '') + print(username, passwd, request.json, request.form, flush=True) + if u := User.query.filter(User.username==username, User.site_id==site.id).first(): + if check_password_hash(u.passwd, passwd): + login_user(u) + + access_token = create_access_token(identity=u.id) + #flash('已登入') + + #next_url = flask.request.args.get('next') + # is_safe_url should check if the url is safe for redirects. + # See http://flask.pocoo.org/snippets/62/ for an example. + #if not is_safe_url(next): + # return flask.abort(400) + + #return redirect(url_for('admin.index')) + response = jsonify(access_token=access_token) + set_access_cookies(response, access_token) + return response + + #flash('帳號或密碼錯誤') + #return redirect(url_for('admin.login')) + return jsonify({'msg': '帳號或密碼錯誤'}), 401 + +@admin.route("/protected", methods=["GET"]) +@jwt_required() +def protected(): + # Access the identity of the current user with get_jwt_identity + current_user = get_jwt_identity() + return jsonify(logged_in_as=current_user), 200 + +@admin.route('/logout') +def logout(): + logout_user() + + response = jsonify({"msg": "logout successful"}) + unset_jwt_cookies(response) + #return response + return redirect(url_for('admin.login')) + +#@admin.before_request +#def check_auth(): +# if not current_user.is_authenticated: +# return abort(401) def save_record(record, payload, collection): #print(record, payload, flush=True) @@ -395,13 +460,17 @@ def modify_frontend_collection_record(collection_id, record_id): @admin.route('/collections//records') +@login_required def create_frontend_collection_record(collection_id): + site = current_user.site #return send_from_directory('blueprints/admin_static/record-form', 'index.html') - return send_from_directory('/build/admin-record-form', 'index.html') + #return send_from_directory('/build/admin-record-form', 'index.html') + #return send_from_directory('aa', 'index.html') + try: + return render_template(f'sites/{site.name}/admin/record-form-view.html') + except TemplateNotFound: + return send_from_directory('/build/admin-record-form', 'index.html') -@admin.route('/static_build/') -def static_build(filename): - return send_from_directory('/build', filename) @admin.route('/reset_password', methods=('GET', 'POST')) @login_required @@ -423,7 +492,7 @@ def reset_password(): def index(): if not current_user.is_authenticated: #return current_app.login_manager.unauthorized() - return redirect(url_for('base.login')) + return redirect(url_for('admin.login')) site = current_user.site collection_ids = [x.id for x in site.collections] @@ -848,14 +917,23 @@ def export_data(): export_specimen_dwc_csv() return '' -# auth error -# @admin.route('/api/collections//options') -# def api_get_collection_options(collection_id): -# if collection := session.get(Collection, collection_id): -# data = get_all_options(collection) -# return jsonify(data) +@admin.route('/collections//options') +@jwt_required() +def api_get_collection_options(collection_id): + if collection := session.get(Collection, collection_id): + + uid = request.args.get('uid') + data = get_all_options(collection) + uid = get_jwt_identity() + print(uid, flush=True) + if user := session.get(User, uid): + data['current_user'] = { + 'uid': uid, + 'uname': user.username, + } + return jsonify(data) -# return abort(404) + return abort(404) @admin.route('/api/units/', methods=['DELETE']) diff --git a/app/blueprints/admin_static/common.js b/app/blueprints/admin_static/common.js index c3430bf..27bb0e0 100644 --- a/app/blueprints/admin_static/common.js +++ b/app/blueprints/admin_static/common.js @@ -1,30 +1,35 @@ - UIkit.util.on('.item-delete-confirm', 'click', function (e) { +(function() { + 'use strict'; + + UIkit.util.on('.item-delete-confirm', 'click', function (e) { e.preventDefault(); e.target.blur(); - UIkit.modal.confirm('確定要刪除?').then(function () { - fetch(e.target.dataset.deleteurl, { method: 'DELETE' }) - .then(resp => resp.json()) - .then(json => { - //console.log('ok', json); - if ('next_url' in json) { - location.href = json.next_url; - }}); - }, function () { - // console.log('Rejected.') - }); - }); + UIkit.modal.confirm('確定要刪除?').then(function () { + fetch(e.target.dataset.deleteurl, { method: 'DELETE' }) + .then(resp => resp.json()) + .then(json => { + //console.log('ok', json); + if ('next_url' in json) { + location.href = json.next_url; + }}); + }, function () { + // console.log('Rejected.') + }); + }); -UIkit.util.on('#delete-favorites', 'click', function (e) { - e.preventDefault(); - const url = e.target.dataset['link']; - fetch(url, { - method: 'DELETE', - }) - .then(res => res.json()) - .then(res => { - console.log(res); - if (res.message === 'ok') { - UIkit.notification({message: '已清除我的最愛'}); - } + UIkit.util.on('#delete-favorites', 'click', function (e) { + e.preventDefault(); + const url = e.target.dataset['link']; + fetch(url, { + method: 'DELETE', }) -}); + .then(res => res.json()) + .then(res => { + //console.log(res); + if (res.message === 'ok') { + UIkit.notification({message: '已清除我的最愛'}); + } + }); + }); + +})(); diff --git a/app/blueprints/api.py b/app/blueprints/api.py index 13efd89..708ead8 100644 --- a/app/blueprints/api.py +++ b/app/blueprints/api.py @@ -794,22 +794,6 @@ def get_occurrence(): api.add_url_rule('/record//', 'get-record-parts', get_record_parts, ('GET')) api.add_url_rule('/occurrence', 'get-occurrence', get_occurrence) # for TBIA -# TODO: auth problem, should move to blueprint:admin -@api.route('/admin/collections//options') -def api_get_collection_options(collection_id): - from app.blueprints.admin import get_all_options - if collection := session.get(Collection, collection_id): - - uid = request.args.get('uid') - data = get_all_options(collection) - user = session.get(User, uid) - data['current_user'] = { - 'uid': uid, - 'uname': user.username, - } - return jsonify(data) - - return abort(404) @api.route('/admin/collections//records/', methods=['GET', 'POST', 'OPTIONS', 'PUT']) def api_modify_admin_record(collection_id, record_id): @@ -834,7 +818,7 @@ def api_modify_admin_record(collection_id, record_id): else: return abort(404) -@api.route('/admin/collections//records', methods=['POST', 'OPTIONS']) +@api.route('/admin2/collections//records', methods=['POST', 'OPTIONS']) def api_create_admin_record(collection_id): from app.blueprints.admin import save_record if request.method == 'OPTIONS': diff --git a/app/blueprints/base.py b/app/blueprints/base.py index 4c8610c..34ce5b0 100644 --- a/app/blueprints/base.py +++ b/app/blueprints/base.py @@ -17,19 +17,6 @@ redirect, url_for, ) -from flask_login import ( - login_required, - login_user, - logout_user, -) -from flask_jwt_extended import ( - jwt_required, - create_access_token, - get_jwt_identity, -) -from werkzeug.security import ( - check_password_hash, -) from sqlalchemy import ( select, func, @@ -173,50 +160,6 @@ def portal_search(): return render_template('portal-search.html', site_list=site_list, items=items, total=total, pagination=pagination) -@base.route('/login', methods=['GET', 'POST']) -def login(): - site = get_current_site(request) - if not site: - return abort(404) - - if request.method == 'GET': - return render_template('login.html', site=site) - elif request.method == 'POST': - username = request.form.get('username', '') - passwd = request.form.get('passwd', '') - - if u := User.query.filter(User.username==username, User.site_id==site.id).first(): - if check_password_hash(u.passwd, passwd): - login_user(u) - - # TODO - access_token = create_access_token(identity=username) - flash('已登入') - #next_url = flask.request.args.get('next') - # is_safe_url should check if the url is safe for redirects. - # See http://flask.pocoo.org/snippets/62/ for an example. - #if not is_safe_url(next): - # return flask.abort(400) - return redirect(url_for('admin.index')) - - flash('帳號或密碼錯誤') - - return redirect(url_for('base.login')) - -@base.route("/protected", methods=["GET"]) -@jwt_required() -def protected(): - # Access the identity of the current user with get_jwt_identity - current_user = get_jwt_identity() - return jsonify(logged_in_as=current_user), 200 - -@base.route('/logout') -@login_required -def logout(): - logout_user() - return redirect(url_for('base.login')) - - @base.route('/favicon.ico') def favicon(): return send_from_directory(os.path.join(current_app.root_path, 'static'), diff --git a/app/config.py b/app/config.py index 04263a3..a39ca90 100644 --- a/app/config.py +++ b/app/config.py @@ -12,10 +12,17 @@ class Config(object): DATABASE_URI = 'postgresql+psycopg2://postgres:example@postgres:5432/naturedb' PORTAL_SITE = os.getenv('PORTAL_SITE') SECRET_KEY = 'no secret' + WEB_ENV = os.getenv('WEB_ENV') + JWT_COOKIE_SECURE = False + JWT_TOKEN_LOCATION = ['headers', 'query_string'] + JWT_SECRET_KEY = SECRET_KEY + class ProductionConfig(Config): SECRET_KEY = os.getenv('SECRET_KEY') + JWT_COOKIE_SECURE = True + JWT_SECRET_KEY = SECRET_KEY class DevelopmentConfig(Config): DEBUG = True diff --git a/app/templates/admin/base.html b/app/templates/admin/base.html index e48ae98..372b6d8 100644 --- a/app/templates/admin/base.html +++ b/app/templates/admin/base.html @@ -41,7 +41,7 @@ diff --git a/app/templates/login.html b/app/templates/login.html deleted file mode 100644 index a52c66e..0000000 --- a/app/templates/login.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - {{ site.name}}::login - {##} - - - - -
- © 2023 naturedb -
-
- -

{{ site.code }} Login

- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
- -

{{ message }}

-
- {% endfor %} - {% endif %} - {% endwith %} -
-
-
-
- - -
-
-
-
- - -
-
- {#
- -
#} -
- -
-
-
- - - - - - - - - -
- - - - - - diff --git a/app/templates/sites/ppi/admin/record-form-view.html b/app/templates/sites/ppi/admin/record-form-view.html index a22c05a..57d56d7 100644 --- a/app/templates/sites/ppi/admin/record-form-view.html +++ b/app/templates/sites/ppi/admin/record-form-view.html @@ -1,6 +1,12 @@ {% extends "admin/base.html" %} {% block script %} + {% endblock %} {% macro widget(name, label, value='', width='1-4@s', type='input') -%} @@ -21,6 +27,7 @@ {%- endmacro %} {% block main %} +aoeuao
{{ widget('collector', '採集者', '', '1-4@s') }} {{ widget('field_number', '採集號', '', '1-4@s') }} diff --git a/requirements/base.txt b/requirements/base.txt index a258a29..2c19ead 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,6 +3,7 @@ Flask==3.0.3 Flask-Login==0.6.3 Flask-Babel==4.0.0 Flask-JWT-Extended==4.6.0 + # database SQLAlchemy==2.0.30 GeoAlchemy2==0.15.1