From 21d5c0be0226bea7a349b184250f71e615132a9e Mon Sep 17 00:00:00 2001 From: orertrr Date: Tue, 25 Jun 2024 17:17:13 +0800 Subject: [PATCH 1/9] feat: add api for COSCUP-Volunteer Signed-off-by: orertrr --- main.py | 2 ++ view/volunteer.py | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 view/volunteer.py diff --git a/main.py b/main.py index d00fb2b..4e4d498 100644 --- a/main.py +++ b/main.py @@ -21,6 +21,7 @@ from view.subscribe import VIEW_SUBSCRIBE from view.subscriber import VIEW_SUBSCRIBER from view.trello import VIEW_TRELLO +from view.volunteer import VIEW_VOLUNTEER logging.basicConfig( filename='./log/log.log', @@ -38,6 +39,7 @@ app.register_blueprint(VIEW_SUBSCRIBE) app.register_blueprint(VIEW_SUBSCRIBER) app.register_blueprint(VIEW_TRELLO) +app.register_blueprint(VIEW_VOLUNTEER) if app.debug: app.config['TEMPLATES_AUTO_RELOAD'] = True diff --git a/view/volunteer.py b/view/volunteer.py new file mode 100644 index 0000000..5c8c048 --- /dev/null +++ b/view/volunteer.py @@ -0,0 +1,6 @@ +''' +API for COSCUP-Volunteer +''' +from flask import Blueprint + +VIEW_VOLUNTEER = Blueprint('volunteer', __name__, url_prefix='/coscup') From 2ed095d83a3e79b0dbbda14e7d39eb6d2e4cc608 Mon Sep 17 00:00:00 2001 From: orertrr Date: Tue, 2 Jul 2024 23:04:05 +0800 Subject: [PATCH 2/9] feat: add token management page prototype Signed-off-by: orertrr --- main.py | 2 + templates/base.html | 3 + templates/index.html | 13 +++ templates/settings_token.html | 164 ++++++++++++++++++++++++++++++++++ view/token.py | 9 ++ 5 files changed, 191 insertions(+) create mode 100644 templates/settings_token.html create mode 100644 view/token.py diff --git a/main.py b/main.py index 4e4d498..36c1455 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,7 @@ from view.subscriber import VIEW_SUBSCRIBER from view.trello import VIEW_TRELLO from view.volunteer import VIEW_VOLUNTEER +from view.token import VIEW_TOKEN logging.basicConfig( filename='./log/log.log', @@ -40,6 +41,7 @@ app.register_blueprint(VIEW_SUBSCRIBER) app.register_blueprint(VIEW_TRELLO) app.register_blueprint(VIEW_VOLUNTEER) +app.register_blueprint(VIEW_TOKEN) if app.debug: app.config['TEMPLATES_AUTO_RELOAD'] = True diff --git a/templates/base.html b/templates/base.html index b9ac90b..2b13d7a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -38,6 +38,9 @@ {{session.u.name}} +
+
+
+

電子報系統設定

+
+ +
+
diff --git a/templates/settings_token.html b/templates/settings_token.html new file mode 100644 index 0000000..64d1f1b --- /dev/null +++ b/templates/settings_token.html @@ -0,0 +1,164 @@ +{% extends 'base.html' %} +{%block head_title%}Subscriber API Management - {%endblock%} +{% block body %} +
+
+
+

+ 電子報系統設定 +

+

+ API Token 設定 +

+
+
+
+ +
+
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + +
勾選編號標籤
[[ token.id ]][[ token.label ]]
+
+ + +
+
+{% endblock %} +{% block js %} + +{% endblock %} diff --git a/view/token.py b/view/token.py new file mode 100644 index 0000000..2d0c458 --- /dev/null +++ b/view/token.py @@ -0,0 +1,9 @@ +''' API token management ''' +from flask import Blueprint, render_template + +VIEW_TOKEN = Blueprint('token', __name__, url_prefix='/token') + +@VIEW_TOKEN.route('/', methods=('GET',)) +def index() -> str: + ''' index page ''' + return render_template('settings_token.html') From 21f51e40e1a93b34fee027297879397655dae44e Mon Sep 17 00:00:00 2001 From: orertrr Date: Sun, 14 Jul 2024 19:53:59 +0800 Subject: [PATCH 3/9] feat: add API for accessing API tokens Signed-off-by: orertrr --- models/api_token.py | 17 ++++++++++++++ models/index.py | 3 +++ module/api_token.py | 38 ++++++++++++++++++++++++++++++ poetry.lock | 21 +++++++++++++++-- pyproject.toml | 1 + view/token.py | 57 ++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 models/api_token.py create mode 100644 module/api_token.py diff --git a/models/api_token.py b/models/api_token.py new file mode 100644 index 0000000..458b1c5 --- /dev/null +++ b/models/api_token.py @@ -0,0 +1,17 @@ +from models.base import DBBase + +class APITokenDB(DBBase): + ''' Token Collection + + Schema: + { + serial_no: string, + token: string, + label: string + } + ''' + def __init__(self) -> None: + super().__init__('api_token') + + def index(self) -> None: + self.create_index([('serial_no', 1)]) diff --git a/models/index.py b/models/index.py index 2c58b25..abc77d9 100644 --- a/models/index.py +++ b/models/index.py @@ -1,8 +1,11 @@ ''' index ''' from models.subscriberdb import (SubscriberDB, SubscriberLoginTokenDB, SubscriberReadDB) +from models.api_token import APITokenDB if __name__ == '__main__': SubscriberDB().index() SubscriberLoginTokenDB().index() SubscriberReadDB().index() + + APITokenDB().index() diff --git a/module/api_token.py b/module/api_token.py new file mode 100644 index 0000000..5270ec6 --- /dev/null +++ b/module/api_token.py @@ -0,0 +1,38 @@ +from typing import Any +from uuid import uuid4 +from dataclasses import dataclass, asdict, field + +from passlib.context import CryptContext # type: ignore + +from models.api_token import APITokenDB + +@dataclass +class APITokenSchema: + token: str = field(default_factory=lambda: uuid4().hex) + serial_no: str = field(default_factory=lambda: f'{uuid4().node:08x}') + label: str = '' + +class APIToken: + @staticmethod + def create(label: str) -> APITokenSchema: + new_token = APITokenSchema(label=label) + + hash_context = CryptContext(schemes=['bcrypt'], deprecated='auto') + hashed_token = asdict(new_token) + hashed_token['token'] = hash_context.hash(hashed_token['token']) + + APITokenDB().insert_one(hashed_token) + + new_token.token = f'{new_token.serial_no}|{new_token.token}' + + return new_token + + @staticmethod + def get_list() -> list[dict[str, Any]]: + return list(APITokenDB().find({}, { 'label': 1, 'serial_no': 1, '_id': 0 })) + + @staticmethod + def delete(tokens: list[str]) -> None: + APITokenDB().delete_many({ + 'serial_no': { '$in': tokens } + }) diff --git a/poetry.lock b/poetry.lock index 029482a..cb8628f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "amqp" @@ -816,6 +816,23 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = "*" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] +totp = ["cryptography"] + [[package]] name = "phonenumbers" version = "8.13.30" @@ -1321,4 +1338,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "2b9b81f0b5f3ab613fa87a8a14c0bce8df54cb00f378aabe619253d58930dd57" +content-hash = "eda4423ce149ab181639946e116b278ab6c22d6980495cbc2d4cfcf47a000b28" diff --git a/pyproject.toml b/pyproject.toml index 49c7221..c5ccb35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ pymongo = "^4.3.3" requests = "^2.31.0" uWSGI = "^2.0.21" certifi = "*" +passlib = "^1.7.4" [tool.poetry.group.dev.dependencies] diff --git a/view/token.py b/view/token.py index 2d0c458..59b1192 100644 --- a/view/token.py +++ b/view/token.py @@ -1,9 +1,64 @@ ''' API token management ''' -from flask import Blueprint, render_template +from flask import Blueprint, Response, make_response, render_template, request, jsonify +from werkzeug.exceptions import BadRequest + +from module.api_token import APIToken VIEW_TOKEN = Blueprint('token', __name__, url_prefix='/token') +@VIEW_TOKEN.errorhandler(BadRequest) +def handle_bad_request(e: BadRequest) -> Response: + return make_response({ 'message': e.description }, e.code) + @VIEW_TOKEN.route('/', methods=('GET',)) def index() -> str: ''' index page ''' return render_template('settings_token.html') + +@VIEW_TOKEN.route('/', methods=('POST',)) +def create_token() -> str | Response: + '''API for creating token + + Request Body: + { + "label": string + } + + Response Body (200): + { + "label": string, + "token": string, + } + ''' + create_token_dto = request.get_json() + + if 'label' not in create_token_dto or \ + type(create_token_dto['label']) is not str or \ + len(create_token_dto['label']) == 0: + raise BadRequest('label should not be empty string.') + + new_token = APIToken.create(create_token_dto['label']) + return jsonify({ + 'label': new_token.label, + 'token': new_token.token, + }) + +@VIEW_TOKEN.route('/list', methods=('GET',)) +def get_list() -> Response: + tokens = APIToken.get_list() + return jsonify({ 'tokens': tokens }) + +@VIEW_TOKEN.route('/', methods=('DELETE',)) +def delete() -> Response: + delete_token_dto = request.get_json() + + if not isinstance(delete_token_dto['tokens'], list): + raise BadRequest('tokens should be in the body and an array of string') + + for token in delete_token_dto['tokens']: + if type(token) is not str: + raise BadRequest('tokens should be an array of string') + + APIToken.delete(delete_token_dto['tokens']) + + return jsonify({ 'success': True }) From 3d1280eca37d11524ac84ddc09df38dca67357a6 Mon Sep 17 00:00:00 2001 From: orertrr Date: Sun, 14 Jul 2024 19:56:30 +0800 Subject: [PATCH 4/9] feat: add token management page Signed-off-by: orertrr --- templates/settings_token.html | 118 ++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/templates/settings_token.html b/templates/settings_token.html index 64d1f1b..73467d7 100644 --- a/templates/settings_token.html +++ b/templates/settings_token.html @@ -25,33 +25,31 @@

- - + +
- - - +
勾選編號 標籤
[[ token.id ]] [[ token.label ]]
-
{% endblock %} {% block js %} +