Compare commits

..

5 Commits

Author SHA1 Message Date
1f4ca52a10 add: markdown deletion 2025-06-27 12:06:28 +01:00
62c33c47c6 add: markdown deletion 2025-06-23 15:41:03 +01:00
692c0794c5 upgrade react-query to v5 2025-05-09 00:44:53 +01:00
848c4b8fd8 fix: fix endpoints 2025-05-08 12:25:47 +01:00
1a160c9415 add: backend api auth by apikey/apikey gen/apikey revoke 2025-05-06 18:54:10 +01:00
13 changed files with 218 additions and 68 deletions

View File

@@ -1,22 +1,25 @@
import base64
import os
import pkgutil
from functools import wraps from functools import wraps
from datetime import datetime, UTC
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from flask import jsonify, Blueprint, request, make_response from flask import jsonify, Blueprint, request, make_response
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from jwt import decode, ExpiredSignatureError, InvalidTokenError, get_unverified_header from jwt import decode, ExpiredSignatureError, InvalidTokenError, get_unverified_header
import importlib
import requests
from threading import Lock from threading import Lock
from db.models.APIKey import APIKey
from db import get_db
import base64
import os
import pkgutil
import secrets
import string
import env_provider import env_provider
import hashlib import hashlib
import json import json
import importlib
import requests
_public_key_cache = {} _public_key_cache = {}
_lock = Lock() _lock = Lock()
@@ -96,18 +99,42 @@ def require_auth(roles=[]):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if request.method == "OPTIONS": if request.method == "OPTIONS":
return '', 200 return '', 200
auth_header = request.headers.get('Authorization') auth_header = request.headers.get('Authorization')
api_key_header = request.headers.get('X-API-Key')
if auth_header and api_key_header:
return jsonify({"error": "Cannot use both Bearer token and API Key authentication"}), 403
if api_key_header:
api_key = get_api_key(api_key_header)
if not api_key:
return jsonify({"error": "Invalid API key"}), 401
expire_time = api_key.expire.replace(tzinfo=UTC) if api_key.expire.tzinfo is None else api_key.expire
if datetime.now(UTC) > expire_time:
return jsonify({"error": "API key has expired"}), 401
if roles and not (set(roles) & set(api_key.roles)):
return jsonify({"error": "Forbidden, permission denied"}), 403
update_last_used(api_key)
return func(*args, **kwargs)
if not auth_header or not auth_header.startswith('Bearer'): if not auth_header or not auth_header.startswith('Bearer'):
return jsonify({"error": "Unauthorized"}), 401 return jsonify({"error": "Unauthorized"}), 401
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
decoded = verify_token(token) decoded = verify_token(token)
if not decoded: if not decoded:
return jsonify({"error": "Invalid or expired token"}), 401 return jsonify({"error": "Invalid or expired token"}), 401
user_roles = decoded.get("resource_access", {}).get(env_provider.KC_CLIENT_ID, {}).get("roles", []) user_roles = decoded.get("resource_access", {}).get(env_provider.KC_CLIENT_ID, {}).get("roles", [])
if roles and not (set(roles) & set(user_roles)): if roles and not (set(roles) & set(user_roles)):
print("auth failed") print("auth failed")
return jsonify({"error": "Forbidden, permission denied"}), 403 return jsonify({"error": "Forbidden, permission denied"}), 403
print("auth success") print("auth success")
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
@@ -169,3 +196,17 @@ def etag_response(f):
return resp return resp
return response return response
return decorator return decorator
def generate_api_key(length=32):
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
def get_api_key(key):
with get_db() as session:
return session.query(APIKey).filter_by(key=key, is_active=True).first()
def update_last_used(api_key):
with get_db() as session:
api_key.last_used_at = datetime.now(UTC)
session.commit()

36
api/apikey/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
from flask import Blueprint, request, jsonify
from api import get_api_key, generate_api_key
from db import get_db
from api import require_auth
from db.models.APIKey import APIKey
api_key_bp = Blueprint('apikey', __name__, url_prefix='/api/apikey')
@api_key_bp.route('/', methods=['POST'])
@require_auth(roles=['admin'])
def create_key():
data = request.get_json()
if not data or 'name' not in data:
return jsonify({"error": "Name is required"}), 400
roles = data.get('roles', [])
try:
with get_db() as session:
apikey = APIKey(key=generate_api_key(),name=data['name'], roles=roles)
session.add(apikey);
session.commit
return jsonify(apikey.to_dict()), 201
except Exception as e:
return jsonify({"error": str(e)}), 500
@api_key_bp.route('/<key>', methods=['DELETE'])
@require_auth(roles=['admin'])
def revoke_key(key):
api_key = get_api_key(key)
with get_db() as session:
if not api_key:
return jsonify({"error": "API key not found"}), 404
api_key.is_active = False
session.commit()
return jsonify({"message": "API key revoked successfully"}), 200

View File

@@ -268,7 +268,8 @@ def delete_markdown(markdown_id):
""" """
Delete a markdown document. Delete a markdown document.
This endpoint deletes an existing markdown document. This endpoint deletes an existing markdown document and cascades the deletion
to related settings to avoid foreign key conflicts.
It requires authentication with the 'admin' role. It requires authentication with the 'admin' role.
Request: Request:
@@ -280,18 +281,60 @@ def delete_markdown(markdown_id):
Response Codes: Response Codes:
- 200: Deleted successfully - 200: Deleted successfully
- 404: Markdown not found - 404: Markdown not found
- 500: Server error during cascade deletion
""" """
with get_db() as session: with get_db() as session:
markdown = session.get(Markdown, markdown_id) try:
if markdown is None: markdown = session.get(Markdown, markdown_id)
logger.error(f"failed to delete markdown: {markdown_id}") if markdown is None:
logger.error(f"failed to delete markdown: {markdown_id}")
errno = RequestContext.get_error_id()
return jsonify({"error": f"file not found - {errno}"}), 404
md = markdown.to_dict()
if markdown.setting_id:
markdown_setting = session.query(MarkdownSetting).get(markdown.setting_id)
if markdown_setting:
template_setting_id = markdown_setting.template_setting_id
permission_setting_id = markdown_setting.permission_setting_id
markdown_setting.template_setting_id = None
markdown_setting.permission_setting_id = None
session.flush()
if template_setting_id:
from db.models.MarkdownTemplateSetting import MarkdownTemplateSetting
template_setting = session.query(MarkdownTemplateSetting).get(template_setting_id)
if template_setting:
session.delete(template_setting)
if permission_setting_id:
permission_setting = session.query(MarkdownPermissionSetting).get(permission_setting_id)
if permission_setting:
session.delete(permission_setting)
session.delete(markdown_setting)
# Send webhook event before committing the transaction
# This ensures webhook handlers can still access related data
markdown_deleted.send(None, payload=md)
session.delete(markdown)
session.commit()
logger.info(f"Successfully deleted markdown {markdown_id} with cascade deletion")
return jsonify(md), 200
except Exception as e:
import traceback
logger.error(f"Failed to delete markdown {markdown_id}: {e}")
logger.error(f"Exception type: {type(e).__name__}")
logger.error(f"Full traceback:\n{traceback.format_exc()}")
errno = RequestContext.get_error_id() errno = RequestContext.get_error_id()
return jsonify({"error": f"file not found - {errno}"}), 404 session.rollback()
md = markdown.to_dict() return jsonify({"error": f"delete failed - {errno}"}), 500
session.delete(markdown)
session.commit()
markdown_deleted.send(None, payload=md)
return jsonify(md), 200
@markdown_bp.route('/move_forward/<int:markdown_id>', methods=['PATCH']) @markdown_bp.route('/move_forward/<int:markdown_id>', methods=['PATCH'])

View File

@@ -1,13 +1,12 @@
from flask import jsonify, request from flask import jsonify, request, Blueprint
import api import api
from api import limiter, etag_response, require_auth from api import limiter, etag_response, require_auth
from api.setting import setting_bp
from db import get_db from db import get_db
from db.models.MarkdownSetting import MarkdownSetting from db.models.MarkdownSetting import MarkdownSetting
markdown_setting_bp = Blueprint('markdown_setting', __name__, url_prefix='/api/setting/markdown')
@setting_bp.route('/markdown/<int:setting_id>', methods=['GET']) @markdown_setting_bp.route('/<int:setting_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response @etag_response
def get_markdown_path(setting_id): def get_markdown_path(setting_id):
@@ -33,7 +32,7 @@ def get_markdown_path(setting_id):
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/markdown/', methods=['POST']) @markdown_setting_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_markdown_setting(): def create_markdown_setting():
""" """
@@ -68,7 +67,7 @@ def create_markdown_setting():
except Exception: except Exception:
return jsonify({"error": "failed to create setting"}), 500 return jsonify({"error": "failed to create setting"}), 500
@setting_bp.route('/markdown/<int:setting_id>', methods=['PUT', 'PATCH']) @markdown_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_markdown_setting(setting_id): def update_markdown_setting(setting_id):
""" """
@@ -106,7 +105,7 @@ def update_markdown_setting(setting_id):
return jsonify({"error": "failed to update setting"}), 500 return jsonify({"error": "failed to update setting"}), 500
@setting_bp.route('/markdown/<int:setting_id>', methods=['DELETE']) @markdown_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_markdown_setting(setting_id): def delete_markdown_setting(setting_id):
""" """

View File

@@ -1,12 +1,11 @@
from flask import jsonify, request from flask import jsonify, request, Blueprint
import api import api
from api import etag_response, limiter, require_auth from api import etag_response, limiter, require_auth
from api.setting import setting_bp
from db import get_db from db import get_db
from db.models.MarkdownPermissionSetting import MarkdownPermissionSetting from db.models.MarkdownPermissionSetting import MarkdownPermissionSetting
markdown_permission_setting_bp = Blueprint('markdown_permission_setting', __name__, url_prefix='/api/setting/markdown/permission')
@setting_bp.route('/markdown/permission/<int:setting_id>/', methods=['GET']) @markdown_permission_setting_bp.route('/<int:setting_id>', methods=['GET'])
@etag_response @etag_response
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
def get_permission_setting(setting_id): def get_permission_setting(setting_id):
@@ -32,7 +31,7 @@ def get_permission_setting(setting_id):
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/markdown/permission/', methods=['POST']) @markdown_permission_setting_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_permission_setting(): def create_permission_setting():
""" """
@@ -59,7 +58,7 @@ def create_permission_setting():
return jsonify(new_setting.to_dict()), 201 return jsonify(new_setting.to_dict()), 201
@setting_bp.route('/markdown/permission/<int:setting_id>', methods=['PUT', 'PATCH']) @markdown_permission_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_permission_setting(setting_id): def update_permission_setting(setting_id):
""" """
@@ -95,7 +94,7 @@ def update_permission_setting(setting_id):
session.commit() session.commit()
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/markdown/permission/<int:setting_id>', methods=['DELETE']) @markdown_permission_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_permission_setting(setting_id): def delete_permission_setting(setting_id):
""" """

View File

@@ -1,12 +1,12 @@
from flask import jsonify, request from flask import jsonify, request, Blueprint
import api import api
from api import etag_response, limiter, require_auth from api import etag_response, limiter, require_auth
from api.setting import setting_bp
from db import get_db from db import get_db
from db.models.MarkdownTemplateSetting import MarkdownTemplateSetting from db.models.MarkdownTemplateSetting import MarkdownTemplateSetting
@setting_bp.route('/markdown/template/', methods=['GET']) markdown_template_setting_bp = Blueprint('markdown_template_setting', __name__, url_prefix='/api/setting/markdown/template')
@markdown_template_setting_bp.route('/', methods=['GET'])
@etag_response @etag_response
def list_template_settings(): def list_template_settings():
""" """
@@ -24,7 +24,7 @@ def list_template_settings():
settings = session.query(MarkdownTemplateSetting).all() settings = session.query(MarkdownTemplateSetting).all()
return jsonify([s.to_dict() for s in settings]), 200 return jsonify([s.to_dict() for s in settings]), 200
@setting_bp.route('/markdown/template/<int:setting_id>/', methods=['GET']) @markdown_template_setting_bp.route('/<int:setting_id>', methods=['GET'])
@etag_response @etag_response
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
def get_template_setting(setting_id): def get_template_setting(setting_id):
@@ -50,7 +50,7 @@ def get_template_setting(setting_id):
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/markdown/template/', methods=['POST']) @markdown_template_setting_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_template_setting(): def create_template_setting():
""" """
@@ -77,7 +77,7 @@ def create_template_setting():
return jsonify(new_setting.to_dict()), 201 return jsonify(new_setting.to_dict()), 201
@setting_bp.route('/markdown/template/<int:setting_id>', methods=['PUT', 'PATCH']) @markdown_template_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_template_setting(setting_id): def update_template_setting(setting_id):
""" """
@@ -113,7 +113,7 @@ def update_template_setting(setting_id):
session.commit() session.commit()
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/markdown/template/<int:setting_id>', methods=['DELETE']) @markdown_template_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_template_setting(setting_id): def delete_template_setting(setting_id):
""" """

View File

@@ -1,15 +1,16 @@
from flask import jsonify, request from flask import jsonify, request, Blueprint
import api import api
from api import limiter, require_auth, etag_response from api import limiter, require_auth, etag_response
from api.setting import setting_bp
from db import get_db from db import get_db
from db.models.PathSetting import PathSetting from db.models.PathSetting import PathSetting
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@setting_bp.route('/path/<int:setting_id>', methods=['GET']) path_setting_bp = Blueprint('path_setting', __name__, url_prefix='/api/setting/path')
@path_setting_bp.route('/<int:setting_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response @etag_response
def get_path_setting(setting_id): def get_path_setting(setting_id):
@@ -34,7 +35,7 @@ def get_path_setting(setting_id):
return jsonify({}), 204 return jsonify({}), 204
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/path/', methods=['POST']) @path_setting_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_path_setting(): def create_path_setting():
""" """
@@ -71,7 +72,7 @@ def create_path_setting():
return jsonify({"error": "failed to create setting"}), 500 return jsonify({"error": "failed to create setting"}), 500
@setting_bp.route('/path/<int:setting_id>', methods=['PUT', 'PATCH']) @path_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_path_setting(setting_id): def update_path_setting(setting_id):
""" """
@@ -108,7 +109,7 @@ def update_path_setting(setting_id):
except Exception: except Exception:
return jsonify({"error": "failed to update path setting"}), 500 return jsonify({"error": "failed to update path setting"}), 500
@setting_bp.route('/path/<int:setting_id>', methods=['DELETE']) @path_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_path_setting(setting_id): def delete_path_setting(setting_id):
""" """

View File

@@ -1,11 +1,10 @@
from flask import jsonify, request from flask import jsonify, request, Blueprint
from api import require_auth from api import require_auth
from api.setting import setting_bp
from db import get_db from db import get_db
from db.models.WebhookSetting import WebhookSetting from db.models.WebhookSetting import WebhookSetting
path_webhook_setting_bp = Blueprint('path_webhook_setting', __name__, url_prefix='/api/setting/path/webhook')
@setting_bp.route('/path/webhook/', methods=['GET']) @path_webhook_setting_bp.route('/', methods=['GET'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def list_webhook_settings(): def list_webhook_settings():
""" """
@@ -25,7 +24,7 @@ def list_webhook_settings():
return jsonify([s.to_dict() for s in settings]), 200 return jsonify([s.to_dict() for s in settings]), 200
@setting_bp.route('/path/webhook/<int:setting_id>', methods=['GET']) @path_webhook_setting_bp.route('/<int:setting_id>', methods=['GET'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def webhook_setting(setting_id): def webhook_setting(setting_id):
""" """
@@ -51,7 +50,7 @@ def webhook_setting(setting_id):
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/path/webhook/', methods=['POST']) @path_webhook_setting_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_webhook_setting(): def create_webhook_setting():
""" """
@@ -87,7 +86,7 @@ def create_webhook_setting():
return jsonify(setting.to_dict()), 201 return jsonify(setting.to_dict()), 201
@setting_bp.route('/path/webhook/<int:setting_id>', methods=['PUT', 'PATCH']) @path_webhook_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_webhook_setting(setting_id): def update_webhook_setting(setting_id):
""" """
@@ -131,7 +130,7 @@ def update_webhook_setting(setting_id):
return jsonify(setting.to_dict()), 200 return jsonify(setting.to_dict()), 200
@setting_bp.route('/path/webhook/<int:setting_id>', methods=['DELETE']) @path_webhook_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_webhook_setting(setting_id): def delete_webhook_setting(setting_id):
""" """

View File

@@ -1,10 +1,12 @@
from flask import jsonify, request from flask import jsonify, request, Blueprint
from api import etag_response, require_auth from api import etag_response, require_auth
from api.template import template_bp
from db import get_db from db import get_db
from db.models.MarkdownTemplate import MarkdownTemplate from db.models.MarkdownTemplate import MarkdownTemplate
cached_templates = {} cached_templates = {}
markdown_template_bp = Blueprint('markdown_template', __name__, url_prefix='/api/template/markdown')
def inflate_template(template): def inflate_template(template):
for parameter in template.get('parameters'): for parameter in template.get('parameters'):
if parameter.get('type', {}).get('base_type') == 'template': if parameter.get('type', {}).get('base_type') == 'template':
@@ -19,7 +21,7 @@ def inflate_template(template):
return template return template
@template_bp.route('/markdown/<int:template_id>', methods=['GET']) @markdown_template_bp.route('/<int:template_id>', methods=['GET'])
@etag_response @etag_response
def get_markdown_template(template_id): def get_markdown_template(template_id):
""" """
@@ -42,10 +44,9 @@ def get_markdown_template(template_id):
template = session.query(MarkdownTemplate).get(template_id) template = session.query(MarkdownTemplate).get(template_id)
if template is None: if template is None:
return jsonify({}), 204 return jsonify({}), 204
print(inflate_template(template.to_dict()))
return jsonify(inflate_template(template.to_dict())), 200 return jsonify(inflate_template(template.to_dict())), 200
@template_bp.route('/markdown/', methods=['GET']) @markdown_template_bp.route('/', methods=['GET'])
@etag_response @etag_response
def get_markdown_templates(): def get_markdown_templates():
""" """
@@ -62,11 +63,10 @@ def get_markdown_templates():
""" """
with get_db() as session: with get_db() as session:
templates = session.query(MarkdownTemplate).all() templates = session.query(MarkdownTemplate).all()
print(templates)
return jsonify([inflate_template(template.to_dict()) for template in templates]), 200 return jsonify([inflate_template(template.to_dict()) for template in templates]), 200
@template_bp.route('/markdown/', methods=['POST']) @markdown_template_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_markdown_template(): def create_markdown_template():
""" """
@@ -103,7 +103,7 @@ def create_markdown_template():
return jsonify({"error": "failed to create markdown template"}), 400 return jsonify({"error": "failed to create markdown template"}), 400
@template_bp.route('/markdown/<int:template_id>', methods=['PUT', 'PATCH']) @markdown_template_bp.route('/<int:template_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_markdown_template(template_id): def update_markdown_template(template_id):
""" """
@@ -142,7 +142,7 @@ def update_markdown_template(template_id):
return jsonify(template.to_dict()), 200 return jsonify(template.to_dict()), 200
@template_bp.route('/markdown/<int:template_id>', methods=['DELETE']) @markdown_template_bp.route('/<int:template_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_markdown_template(template_id): def delete_markdown_template(template_id):
""" """

View File

@@ -1,12 +1,13 @@
from flask import jsonify, request from flask import jsonify, request
from flask.sansio.blueprints import Blueprint
from api import etag_response, require_auth from api import etag_response, require_auth
from api.template import template_bp from api.template import template_bp
from db import get_db from db import get_db
from db.models.PathTemplate import PathTemplate from db.models.PathTemplate import PathTemplate
path_template_bp = Blueprint('path_template', __name__, url_prefix='/api/template/path')
@template_bp.route('/path/<int:template_id>', methods=['GET']) @path_template_bp.route('/<int:template_id>', methods=['GET'])
@etag_response @etag_response
def get_path_template(template_id): def get_path_template(template_id):
""" """
@@ -30,7 +31,7 @@ def get_path_template(template_id):
return jsonify({}), 204 return jsonify({}), 204
return jsonify(template.to_dict()), 200 return jsonify(template.to_dict()), 200
@template_bp.route('/path/', methods=['GET']) @path_template_bp.route('/', methods=['GET'])
@etag_response @etag_response
def get_path_templates(): def get_path_templates():
""" """
@@ -49,7 +50,7 @@ def get_path_templates():
return jsonify([template.to_dict() for template in templates]), 200 return jsonify([template.to_dict() for template in templates]), 200
@template_bp.route('/path/', methods=['POST']) @path_template_bp.route('/', methods=['POST'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def create_path_template(): def create_path_template():
""" """
@@ -84,7 +85,7 @@ def create_path_template():
return jsonify({"error": "failed to create path template"}), 400 return jsonify({"error": "failed to create path template"}), 400
@template_bp.route('/path/<int:template_id>', methods=['PUT', 'PATCH']) @path_template_bp.route('/<int:template_id>', methods=['PUT', 'PATCH'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def update_path_template(template_id): def update_path_template(template_id):
""" """
@@ -118,7 +119,7 @@ def update_path_template(template_id):
return jsonify(template.to_dict()), 200 return jsonify(template.to_dict()), 200
@template_bp.route('/path/<int:template_id>', methods=['DELETE']) @path_template_bp.route('/<int:template_id>', methods=['DELETE'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
def delete_path_template(template_id): def delete_path_template(template_id):
""" """

4
app.py
View File

@@ -35,7 +35,8 @@ CORS(app, resources={
r"https?://localhost:\d+", r"https?://localhost:\d+",
r"https?://127\.0\.0\.1:\d+", r"https?://127\.0\.0\.1:\d+",
r"https?://localhost" r"https?://localhost"
] ],
"supports_credentials": True
} }
}, },
expose_headers=['Content-Disposition'] expose_headers=['Content-Disposition']
@@ -54,5 +55,6 @@ def log_request():
if __name__ == '__main__': if __name__ == '__main__':
api.init_rate_limits(app) api.init_rate_limits(app)
print("env")
pprint(env_provider.summerize()) pprint(env_provider.summerize())
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True) app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True)

25
db/models/APIKey.py Normal file
View File

@@ -0,0 +1,25 @@
from datetime import datetime, timedelta, UTC
from sqlalchemy import Column, String, DateTime, Boolean, JSON
from db.models import Base
class APIKey(Base):
__tablename__ = 'apikey'
key = Column(String(64), primary_key=True)
name = Column(String(255), nullable=False)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
last_used_at = Column(DateTime)
is_active = Column(Boolean, default=True)
roles = Column(JSON, nullable=False, default=list)
expire = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC) + timedelta(days=15))
def to_dict(self):
return {
"key": self.key,
"name": self.name,
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
"is_active": self.is_active,
"roles": self.roles,
"expire": self.expire.isoformat() if self.expire else None
}

View File

@@ -60,6 +60,10 @@ class WebhookEventHandler(abc.ABC):
if webhook_setting is None and p["parent_id"] != 1: if webhook_setting is None and p["parent_id"] != 1:
return self.get_setting(session, p["parent_id"]) return self.get_setting(session, p["parent_id"])
# Check if webhook_setting is still None (e.g., when parent_id == 1 or no parent found)
if webhook_setting is None:
return None
setting = webhook_setting.to_dict() setting = webhook_setting.to_dict()
if not setting["enabled"] or setting["on_events"] & self.event_type == 0: if not setting["enabled"] or setting["on_events"] & self.event_type == 0:
return None return None