mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 14:04:20 +01:00
API Documentation structure (#1480)
Implement a basic working idea for how to start adding better documentation on the REST API * Works on #821
This commit is contained in:
@@ -9,6 +9,11 @@ from CTFd.api.v1.flags import flags_namespace
|
|||||||
from CTFd.api.v1.hints import hints_namespace
|
from CTFd.api.v1.hints import hints_namespace
|
||||||
from CTFd.api.v1.notifications import notifications_namespace
|
from CTFd.api.v1.notifications import notifications_namespace
|
||||||
from CTFd.api.v1.pages import pages_namespace
|
from CTFd.api.v1.pages import pages_namespace
|
||||||
|
from CTFd.api.v1.schemas import (
|
||||||
|
APIDetailedSuccessResponse,
|
||||||
|
APISimpleErrorResponse,
|
||||||
|
APISimpleSuccessResponse,
|
||||||
|
)
|
||||||
from CTFd.api.v1.scoreboard import scoreboard_namespace
|
from CTFd.api.v1.scoreboard import scoreboard_namespace
|
||||||
from CTFd.api.v1.statistics import statistics_namespace
|
from CTFd.api.v1.statistics import statistics_namespace
|
||||||
from CTFd.api.v1.submissions import submissions_namespace
|
from CTFd.api.v1.submissions import submissions_namespace
|
||||||
@@ -21,6 +26,12 @@ from CTFd.api.v1.users import users_namespace
|
|||||||
api = Blueprint("api", __name__, url_prefix="/api/v1")
|
api = Blueprint("api", __name__, url_prefix="/api/v1")
|
||||||
CTFd_API_v1 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI"))
|
CTFd_API_v1 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI"))
|
||||||
|
|
||||||
|
CTFd_API_v1.schema_model("APISimpleErrorResponse", APISimpleErrorResponse.schema())
|
||||||
|
CTFd_API_v1.schema_model(
|
||||||
|
"APIDetailedSuccessResponse", APIDetailedSuccessResponse.schema()
|
||||||
|
)
|
||||||
|
CTFd_API_v1.schema_model("APISimpleSuccessResponse", APISimpleSuccessResponse.schema())
|
||||||
|
|
||||||
CTFd_API_v1.add_namespace(challenges_namespace, "/challenges")
|
CTFd_API_v1.add_namespace(challenges_namespace, "/challenges")
|
||||||
CTFd_API_v1.add_namespace(tags_namespace, "/tags")
|
CTFd_API_v1.add_namespace(tags_namespace, "/tags")
|
||||||
CTFd_API_v1.add_namespace(awards_namespace, "/awards")
|
CTFd_API_v1.add_namespace(awards_namespace, "/awards")
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.models import Awards, Users, db
|
from CTFd.models import Awards, Users, db
|
||||||
from CTFd.schemas.awards import AwardSchema
|
from CTFd.schemas.awards import AwardSchema
|
||||||
@@ -9,10 +13,39 @@ from CTFd.utils.decorators import admins_only
|
|||||||
|
|
||||||
awards_namespace = Namespace("awards", description="Endpoint to retrieve Awards")
|
awards_namespace = Namespace("awards", description="Endpoint to retrieve Awards")
|
||||||
|
|
||||||
|
AwardModel = sqlalchemy_to_pydantic(Awards)
|
||||||
|
|
||||||
|
|
||||||
|
class AwardDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: AwardModel
|
||||||
|
|
||||||
|
|
||||||
|
class AwardListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[AwardModel]
|
||||||
|
|
||||||
|
|
||||||
|
awards_namespace.schema_model(
|
||||||
|
"AwardDetailedSuccessResponse", AwardDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
awards_namespace.schema_model(
|
||||||
|
"AwardListSuccessResponse", AwardListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@awards_namespace.route("")
|
@awards_namespace.route("")
|
||||||
class AwardList(Resource):
|
class AwardList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@awards_namespace.doc(
|
||||||
|
description="Endpoint to create an Award object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "AwardListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
|
|
||||||
@@ -57,6 +90,16 @@ class AwardList(Resource):
|
|||||||
@awards_namespace.param("award_id", "An Award ID")
|
@awards_namespace.param("award_id", "An Award ID")
|
||||||
class Award(Resource):
|
class Award(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@awards_namespace.doc(
|
||||||
|
description="Endpoint to get a specific Award object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "AwardDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, award_id):
|
def get(self, award_id):
|
||||||
award = Awards.query.filter_by(id=award_id).first_or_404()
|
award = Awards.query.filter_by(id=award_id).first_or_404()
|
||||||
response = AwardSchema().dump(award)
|
response = AwardSchema().dump(award)
|
||||||
@@ -66,6 +109,10 @@ class Award(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@awards_namespace.doc(
|
||||||
|
description="Endpoint to delete an Award object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, award_id):
|
def delete(self, award_id):
|
||||||
award = Awards.query.filter_by(id=award_id).first_or_404()
|
award = Awards.query.filter_by(id=award_id).first_or_404()
|
||||||
db.session.delete(award)
|
db.session.delete(award)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import abort, render_template, request, url_for
|
from flask import abort, render_template, request, url_for
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
from sqlalchemy.sql import and_
|
from sqlalchemy.sql import and_
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.models import ChallengeFiles as ChallengeFilesModel
|
from CTFd.models import ChallengeFiles as ChallengeFilesModel
|
||||||
from CTFd.models import (
|
from CTFd.models import (
|
||||||
@@ -47,12 +50,42 @@ challenges_namespace = Namespace(
|
|||||||
"challenges", description="Endpoint to retrieve Challenges"
|
"challenges", description="Endpoint to retrieve Challenges"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ChallengeModel = sqlalchemy_to_pydantic(Challenges)
|
||||||
|
TransientChallengeModel = sqlalchemy_to_pydantic(Challenges, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class ChallengeDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: ChallengeModel
|
||||||
|
|
||||||
|
|
||||||
|
class ChallengeListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[ChallengeModel]
|
||||||
|
|
||||||
|
|
||||||
|
challenges_namespace.schema_model(
|
||||||
|
"ChallengeDetailedSuccessResponse", ChallengeDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
challenges_namespace.schema_model(
|
||||||
|
"ChallengeListSuccessResponse", ChallengeListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("")
|
@challenges_namespace.route("")
|
||||||
class ChallengeList(Resource):
|
class ChallengeList(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
|
@challenges_namespace.doc(
|
||||||
|
description="Endpoint to get Challenge objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ChallengeListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
# This can return None (unauth) if visibility is set to public
|
# This can return None (unauth) if visibility is set to public
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
@@ -132,6 +165,16 @@ class ChallengeList(Resource):
|
|||||||
return {"success": True, "data": response}
|
return {"success": True, "data": response}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@challenges_namespace.doc(
|
||||||
|
description="Endpoint to create a Challenge object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ChallengeDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
challenge_type = data["type"]
|
challenge_type = data["type"]
|
||||||
@@ -162,11 +205,20 @@ class ChallengeTypes(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("/<challenge_id>")
|
@challenges_namespace.route("/<challenge_id>")
|
||||||
@challenges_namespace.param("challenge_id", "A Challenge ID")
|
|
||||||
class Challenge(Resource):
|
class Challenge(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
|
@challenges_namespace.doc(
|
||||||
|
description="Endpoint to get a specific Challenge object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ChallengeDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
if is_admin():
|
if is_admin():
|
||||||
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
|
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
|
||||||
@@ -311,6 +363,16 @@ class Challenge(Resource):
|
|||||||
return {"success": True, "data": response}
|
return {"success": True, "data": response}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@challenges_namespace.doc(
|
||||||
|
description="Endpoint to edit a specific Challenge object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ChallengeDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self, challenge_id):
|
def patch(self, challenge_id):
|
||||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||||
challenge_class = get_chal_class(challenge.type)
|
challenge_class = get_chal_class(challenge.type)
|
||||||
@@ -319,6 +381,10 @@ class Challenge(Resource):
|
|||||||
return {"success": True, "data": response}
|
return {"success": True, "data": response}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@challenges_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific Challenge object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, challenge_id):
|
def delete(self, challenge_id):
|
||||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||||
chal_class = get_chal_class(challenge.type)
|
chal_class = get_chal_class(challenge.type)
|
||||||
@@ -529,7 +595,6 @@ class ChallengeAttempt(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("/<challenge_id>/solves")
|
@challenges_namespace.route("/<challenge_id>/solves")
|
||||||
@challenges_namespace.param("id", "A Challenge ID")
|
|
||||||
class ChallengeSolves(Resource):
|
class ChallengeSolves(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@check_score_visibility
|
@check_score_visibility
|
||||||
@@ -577,7 +642,6 @@ class ChallengeSolves(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("/<challenge_id>/files")
|
@challenges_namespace.route("/<challenge_id>/files")
|
||||||
@challenges_namespace.param("id", "A Challenge ID")
|
|
||||||
class ChallengeFiles(Resource):
|
class ChallengeFiles(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
@@ -593,7 +657,6 @@ class ChallengeFiles(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("/<challenge_id>/tags")
|
@challenges_namespace.route("/<challenge_id>/tags")
|
||||||
@challenges_namespace.param("id", "A Challenge ID")
|
|
||||||
class ChallengeTags(Resource):
|
class ChallengeTags(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
@@ -609,7 +672,6 @@ class ChallengeTags(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("/<challenge_id>/hints")
|
@challenges_namespace.route("/<challenge_id>/hints")
|
||||||
@challenges_namespace.param("id", "A Challenge ID")
|
|
||||||
class ChallengeHints(Resource):
|
class ChallengeHints(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
@@ -624,7 +686,6 @@ class ChallengeHints(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route("/<challenge_id>/flags")
|
@challenges_namespace.route("/<challenge_id>/flags")
|
||||||
@challenges_namespace.param("id", "A Challenge ID")
|
|
||||||
class ChallengeFlags(Resource):
|
class ChallengeFlags(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_config, clear_standings
|
from CTFd.cache import clear_config, clear_standings
|
||||||
from CTFd.models import Configs, db
|
from CTFd.models import Configs, db
|
||||||
from CTFd.schemas.config import ConfigSchema
|
from CTFd.schemas.config import ConfigSchema
|
||||||
@@ -9,10 +13,39 @@ from CTFd.utils.decorators import admins_only
|
|||||||
|
|
||||||
configs_namespace = Namespace("configs", description="Endpoint to retrieve Configs")
|
configs_namespace = Namespace("configs", description="Endpoint to retrieve Configs")
|
||||||
|
|
||||||
|
ConfigModel = sqlalchemy_to_pydantic(Configs)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: ConfigModel
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[ConfigModel]
|
||||||
|
|
||||||
|
|
||||||
|
configs_namespace.schema_model(
|
||||||
|
"ConfigDetailedSuccessResponse", ConfigDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
configs_namespace.schema_model(
|
||||||
|
"ConfigListSuccessResponse", ConfigListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@configs_namespace.route("")
|
@configs_namespace.route("")
|
||||||
class ConfigList(Resource):
|
class ConfigList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@configs_namespace.doc(
|
||||||
|
description="Endpoint to get Config objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ConfigListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
configs = Configs.query.all()
|
configs = Configs.query.all()
|
||||||
schema = ConfigSchema(many=True)
|
schema = ConfigSchema(many=True)
|
||||||
@@ -23,6 +56,16 @@ class ConfigList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@configs_namespace.doc(
|
||||||
|
description="Endpoint to get create a Config object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ConfigDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = ConfigSchema()
|
schema = ConfigSchema()
|
||||||
@@ -43,6 +86,10 @@ class ConfigList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@configs_namespace.doc(
|
||||||
|
description="Endpoint to get patch Config objects in bulk",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def patch(self):
|
def patch(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
|
|
||||||
@@ -58,11 +105,13 @@ class ConfigList(Resource):
|
|||||||
@configs_namespace.route("/<config_key>")
|
@configs_namespace.route("/<config_key>")
|
||||||
class Config(Resource):
|
class Config(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
# TODO: This returns weirdly structured data. It should more closely match ConfigDetailedSuccessResponse #1506
|
||||||
def get(self, config_key):
|
def get(self, config_key):
|
||||||
|
|
||||||
return {"success": True, "data": get_config(config_key)}
|
return {"success": True, "data": get_config(config_key)}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
# TODO: This returns weirdly structured data. It should more closely match ConfigDetailedSuccessResponse #1506
|
||||||
def patch(self, config_key):
|
def patch(self, config_key):
|
||||||
config = Configs.query.filter_by(key=config_key).first()
|
config = Configs.query.filter_by(key=config_key).first()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -89,6 +138,10 @@ class Config(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@configs_namespace.doc(
|
||||||
|
description="Endpoint to delete a Config object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, config_key):
|
def delete(self, config_key):
|
||||||
config = Configs.query.filter_by(key=config_key).first_or_404()
|
config = Configs.query.filter_by(key=config_key).first_or_404()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.models import Files, db
|
from CTFd.models import Files, db
|
||||||
from CTFd.schemas.files import FileSchema
|
from CTFd.schemas.files import FileSchema
|
||||||
from CTFd.utils import uploads
|
from CTFd.utils import uploads
|
||||||
@@ -8,10 +12,39 @@ from CTFd.utils.decorators import admins_only
|
|||||||
|
|
||||||
files_namespace = Namespace("files", description="Endpoint to retrieve Files")
|
files_namespace = Namespace("files", description="Endpoint to retrieve Files")
|
||||||
|
|
||||||
|
FileModel = sqlalchemy_to_pydantic(Files)
|
||||||
|
|
||||||
|
|
||||||
|
class FileDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: FileModel
|
||||||
|
|
||||||
|
|
||||||
|
class FileListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[FileModel]
|
||||||
|
|
||||||
|
|
||||||
|
files_namespace.schema_model(
|
||||||
|
"FileDetailedSuccessResponse", FileDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
files_namespace.schema_model(
|
||||||
|
"FileListSuccessResponse", FileListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@files_namespace.route("")
|
@files_namespace.route("")
|
||||||
class FilesList(Resource):
|
class FilesList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@files_namespace.doc(
|
||||||
|
description="Endpoint to get file objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FileListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
file_type = request.args.get("type")
|
file_type = request.args.get("type")
|
||||||
files = Files.query.filter_by(type=file_type).all()
|
files = Files.query.filter_by(type=file_type).all()
|
||||||
@@ -24,6 +57,16 @@ class FilesList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@files_namespace.doc(
|
||||||
|
description="Endpoint to get file objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FileDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
files = request.files.getlist("file")
|
files = request.files.getlist("file")
|
||||||
# challenge_id
|
# challenge_id
|
||||||
@@ -47,6 +90,16 @@ class FilesList(Resource):
|
|||||||
@files_namespace.route("/<file_id>")
|
@files_namespace.route("/<file_id>")
|
||||||
class FilesDetail(Resource):
|
class FilesDetail(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@files_namespace.doc(
|
||||||
|
description="Endpoint to get a specific file object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FileDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, file_id):
|
def get(self, file_id):
|
||||||
f = Files.query.filter_by(id=file_id).first_or_404()
|
f = Files.query.filter_by(id=file_id).first_or_404()
|
||||||
schema = FileSchema()
|
schema = FileSchema()
|
||||||
@@ -58,6 +111,10 @@ class FilesDetail(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@files_namespace.doc(
|
||||||
|
description="Endpoint to delete a file object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, file_id):
|
def delete(self, file_id):
|
||||||
f = Files.query.filter_by(id=file_id).first_or_404()
|
f = Files.query.filter_by(id=file_id).first_or_404()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.models import Flags, db
|
from CTFd.models import Flags, db
|
||||||
from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class
|
from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class
|
||||||
from CTFd.schemas.flags import FlagSchema
|
from CTFd.schemas.flags import FlagSchema
|
||||||
@@ -8,10 +12,39 @@ from CTFd.utils.decorators import admins_only
|
|||||||
|
|
||||||
flags_namespace = Namespace("flags", description="Endpoint to retrieve Flags")
|
flags_namespace = Namespace("flags", description="Endpoint to retrieve Flags")
|
||||||
|
|
||||||
|
FlagModel = sqlalchemy_to_pydantic(Flags)
|
||||||
|
|
||||||
|
|
||||||
|
class FlagDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: FlagModel
|
||||||
|
|
||||||
|
|
||||||
|
class FlagListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[FlagModel]
|
||||||
|
|
||||||
|
|
||||||
|
flags_namespace.schema_model(
|
||||||
|
"FlagDetailedSuccessResponse", FlagDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
flags_namespace.schema_model(
|
||||||
|
"FlagListSuccessResponse", FlagListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@flags_namespace.route("")
|
@flags_namespace.route("")
|
||||||
class FlagList(Resource):
|
class FlagList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@flags_namespace.doc(
|
||||||
|
description="Endpoint to list Flag objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FlagListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
flags = Flags.query.all()
|
flags = Flags.query.all()
|
||||||
schema = FlagSchema(many=True)
|
schema = FlagSchema(many=True)
|
||||||
@@ -22,6 +55,16 @@ class FlagList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@flags_namespace.doc(
|
||||||
|
description="Endpoint to create a Flag object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FlagDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = FlagSchema()
|
schema = FlagSchema()
|
||||||
@@ -62,6 +105,16 @@ class FlagTypes(Resource):
|
|||||||
@flags_namespace.route("/<flag_id>")
|
@flags_namespace.route("/<flag_id>")
|
||||||
class Flag(Resource):
|
class Flag(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@flags_namespace.doc(
|
||||||
|
description="Endpoint to get a specific Flag object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FlagDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, flag_id):
|
def get(self, flag_id):
|
||||||
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
||||||
schema = FlagSchema()
|
schema = FlagSchema()
|
||||||
@@ -75,6 +128,10 @@ class Flag(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@flags_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific Flag object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, flag_id):
|
def delete(self, flag_id):
|
||||||
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
||||||
|
|
||||||
@@ -85,6 +142,16 @@ class Flag(Resource):
|
|||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@flags_namespace.doc(
|
||||||
|
description="Endpoint to edit a specific Flag object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "FlagDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self, flag_id):
|
def patch(self, flag_id):
|
||||||
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
||||||
schema = FlagSchema()
|
schema = FlagSchema()
|
||||||
|
|||||||
0
CTFd/api/v1/helpers/__init__.py
Normal file
0
CTFd/api/v1/helpers/__init__.py
Normal file
49
CTFd/api/v1/helpers/request.py
Normal file
49
CTFd/api/v1/helpers/request.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
from pydantic import create_model
|
||||||
|
|
||||||
|
ARG_LOCATIONS = {
|
||||||
|
"query": lambda: request.args,
|
||||||
|
"json": lambda: request.get_json(),
|
||||||
|
"form": lambda: request.form,
|
||||||
|
"headers": lambda: request.headers,
|
||||||
|
"cookies": lambda: request.cookies,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_args(spec, location):
|
||||||
|
"""
|
||||||
|
A rough implementation of webargs using pydantic schemas. You can pass a
|
||||||
|
pydantic schema as spec or create it on the fly as follows:
|
||||||
|
|
||||||
|
@validate_args({"name": (str, None), "id": (int, None)}, location="query")
|
||||||
|
"""
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
spec = create_model("", **spec)
|
||||||
|
|
||||||
|
schema = spec.schema()
|
||||||
|
props = schema.get("properties", {})
|
||||||
|
required = schema.get("required", [])
|
||||||
|
|
||||||
|
for k in props:
|
||||||
|
if k in required:
|
||||||
|
props[k]["required"] = True
|
||||||
|
props[k]["in"] = location
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
# Inject parameters information into the Flask-Restx apidoc attribute.
|
||||||
|
# Not really a good solution. See https://github.com/CTFd/CTFd/issues/1504
|
||||||
|
apidoc = getattr(func, "__apidoc__", {"params": {}})
|
||||||
|
apidoc["params"].update(props)
|
||||||
|
func.__apidoc__ = apidoc
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
data = ARG_LOCATIONS[location]()
|
||||||
|
loaded = spec(**data).dict(exclude_unset=True)
|
||||||
|
return func(*args, loaded, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
31
CTFd/api/v1/helpers/schemas.py
Normal file
31
CTFd/api/v1/helpers/schemas.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from typing import Container, Type
|
||||||
|
|
||||||
|
from pydantic import BaseModel, create_model
|
||||||
|
from sqlalchemy.inspection import inspect
|
||||||
|
from sqlalchemy.orm.properties import ColumnProperty
|
||||||
|
|
||||||
|
|
||||||
|
def sqlalchemy_to_pydantic(
|
||||||
|
db_model: Type, *, exclude: Container[str] = []
|
||||||
|
) -> Type[BaseModel]:
|
||||||
|
"""
|
||||||
|
Mostly copied from https://github.com/tiangolo/pydantic-sqlalchemy
|
||||||
|
"""
|
||||||
|
mapper = inspect(db_model)
|
||||||
|
fields = {}
|
||||||
|
for attr in mapper.attrs:
|
||||||
|
if isinstance(attr, ColumnProperty):
|
||||||
|
if attr.columns:
|
||||||
|
column = attr.columns[0]
|
||||||
|
python_type = column.type.python_type
|
||||||
|
name = attr.key
|
||||||
|
if name in exclude:
|
||||||
|
continue
|
||||||
|
default = None
|
||||||
|
if column.default is None and not column.nullable:
|
||||||
|
default = ...
|
||||||
|
fields[name] = (python_type, default)
|
||||||
|
pydantic_model = create_model(
|
||||||
|
db_model.__name__, **fields # type: ignore
|
||||||
|
)
|
||||||
|
return pydantic_model
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.models import Hints, HintUnlocks, db
|
from CTFd.models import Hints, HintUnlocks, db
|
||||||
from CTFd.schemas.hints import HintSchema
|
from CTFd.schemas.hints import HintSchema
|
||||||
from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only
|
from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only
|
||||||
@@ -8,10 +12,39 @@ from CTFd.utils.user import get_current_user, is_admin
|
|||||||
|
|
||||||
hints_namespace = Namespace("hints", description="Endpoint to retrieve Hints")
|
hints_namespace = Namespace("hints", description="Endpoint to retrieve Hints")
|
||||||
|
|
||||||
|
HintModel = sqlalchemy_to_pydantic(Hints)
|
||||||
|
|
||||||
|
|
||||||
|
class HintDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: HintModel
|
||||||
|
|
||||||
|
|
||||||
|
class HintListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[HintModel]
|
||||||
|
|
||||||
|
|
||||||
|
hints_namespace.schema_model(
|
||||||
|
"HintDetailedSuccessResponse", HintDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
hints_namespace.schema_model(
|
||||||
|
"HintListSuccessResponse", HintListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hints_namespace.route("")
|
@hints_namespace.route("")
|
||||||
class HintList(Resource):
|
class HintList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@hints_namespace.doc(
|
||||||
|
description="Endpoint to list Hint objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "HintListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
hints = Hints.query.all()
|
hints = Hints.query.all()
|
||||||
response = HintSchema(many=True).dump(hints)
|
response = HintSchema(many=True).dump(hints)
|
||||||
@@ -22,6 +55,16 @@ class HintList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@hints_namespace.doc(
|
||||||
|
description="Endpoint to create a Hint object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "HintDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = HintSchema("admin")
|
schema = HintSchema("admin")
|
||||||
@@ -42,6 +85,16 @@ class HintList(Resource):
|
|||||||
class Hint(Resource):
|
class Hint(Resource):
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@hints_namespace.doc(
|
||||||
|
description="Endpoint to get a specific Hint object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "HintDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, hint_id):
|
def get(self, hint_id):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||||
@@ -67,6 +120,16 @@ class Hint(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@hints_namespace.doc(
|
||||||
|
description="Endpoint to edit a specific Hint object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "HintDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self, hint_id):
|
def patch(self, hint_id):
|
||||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
@@ -85,6 +148,10 @@ class Hint(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@hints_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific Tag object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, hint_id):
|
def delete(self, hint_id):
|
||||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||||
db.session.delete(hint)
|
db.session.delete(hint)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import current_app, request
|
from flask import current_app, request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.models import Notifications, db
|
from CTFd.models import Notifications, db
|
||||||
from CTFd.schemas.notifications import NotificationSchema
|
from CTFd.schemas.notifications import NotificationSchema
|
||||||
from CTFd.utils.decorators import admins_only
|
from CTFd.utils.decorators import admins_only
|
||||||
@@ -9,9 +13,39 @@ notifications_namespace = Namespace(
|
|||||||
"notifications", description="Endpoint to retrieve Notifications"
|
"notifications", description="Endpoint to retrieve Notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NotificationModel = sqlalchemy_to_pydantic(Notifications)
|
||||||
|
TransientNotificationModel = sqlalchemy_to_pydantic(Notifications, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: NotificationModel
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[NotificationModel]
|
||||||
|
|
||||||
|
|
||||||
|
notifications_namespace.schema_model(
|
||||||
|
"NotificationDetailedSuccessResponse", NotificationDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
notifications_namespace.schema_model(
|
||||||
|
"NotificationListSuccessResponse", NotificationListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@notifications_namespace.route("")
|
@notifications_namespace.route("")
|
||||||
class NotificantionList(Resource):
|
class NotificantionList(Resource):
|
||||||
|
@notifications_namespace.doc(
|
||||||
|
description="Endpoint to get notification objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "NotificationListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
notifications = Notifications.query.all()
|
notifications = Notifications.query.all()
|
||||||
schema = NotificationSchema(many=True)
|
schema = NotificationSchema(many=True)
|
||||||
@@ -21,6 +55,16 @@ class NotificantionList(Resource):
|
|||||||
return {"success": True, "data": result.data}
|
return {"success": True, "data": result.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@notifications_namespace.doc(
|
||||||
|
description="Endpoint to create a notification object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "NotificationDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
|
|
||||||
@@ -49,6 +93,16 @@ class NotificantionList(Resource):
|
|||||||
@notifications_namespace.route("/<notification_id>")
|
@notifications_namespace.route("/<notification_id>")
|
||||||
@notifications_namespace.param("notification_id", "A Notification ID")
|
@notifications_namespace.param("notification_id", "A Notification ID")
|
||||||
class Notification(Resource):
|
class Notification(Resource):
|
||||||
|
@notifications_namespace.doc(
|
||||||
|
description="Endpoint to get a specific notification object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "NotificationDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, notification_id):
|
def get(self, notification_id):
|
||||||
notif = Notifications.query.filter_by(id=notification_id).first_or_404()
|
notif = Notifications.query.filter_by(id=notification_id).first_or_404()
|
||||||
schema = NotificationSchema()
|
schema = NotificationSchema()
|
||||||
@@ -59,6 +113,10 @@ class Notification(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@notifications_namespace.doc(
|
||||||
|
description="Endpoint to delete a notification object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, notification_id):
|
def delete(self, notification_id):
|
||||||
notif = Notifications.query.filter_by(id=notification_id).first_or_404()
|
notif = Notifications.query.filter_by(id=notification_id).first_or_404()
|
||||||
db.session.delete(notif)
|
db.session.delete(notif)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.request import validate_args
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_pages
|
from CTFd.cache import clear_pages
|
||||||
from CTFd.models import Pages, db
|
from CTFd.models import Pages, db
|
||||||
from CTFd.schemas.pages import PageSchema
|
from CTFd.schemas.pages import PageSchema
|
||||||
@@ -9,11 +14,56 @@ from CTFd.utils.decorators import admins_only
|
|||||||
pages_namespace = Namespace("pages", description="Endpoint to retrieve Pages")
|
pages_namespace = Namespace("pages", description="Endpoint to retrieve Pages")
|
||||||
|
|
||||||
|
|
||||||
|
PageModel = sqlalchemy_to_pydantic(Pages)
|
||||||
|
TransientPageModel = sqlalchemy_to_pydantic(Pages, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class PageDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: PageModel
|
||||||
|
|
||||||
|
|
||||||
|
class PageListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[PageModel]
|
||||||
|
|
||||||
|
|
||||||
|
pages_namespace.schema_model(
|
||||||
|
"PageDetailedSuccessResponse", PageDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
pages_namespace.schema_model(
|
||||||
|
"PageListSuccessResponse", PageListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pages_namespace.route("")
|
@pages_namespace.route("")
|
||||||
|
@pages_namespace.doc(
|
||||||
|
responses={200: "Success", 400: "An error occured processing your data"}
|
||||||
|
)
|
||||||
class PageList(Resource):
|
class PageList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
@pages_namespace.doc(
|
||||||
pages = Pages.query.all()
|
description="Endpoint to get page objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "PageListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@validate_args(
|
||||||
|
{
|
||||||
|
"id": (int, None),
|
||||||
|
"title": (str, None),
|
||||||
|
"route": (str, None),
|
||||||
|
"draft": (bool, None),
|
||||||
|
"hidden": (bool, None),
|
||||||
|
"auth_required": (bool, None),
|
||||||
|
},
|
||||||
|
location="query",
|
||||||
|
)
|
||||||
|
def get(self, query):
|
||||||
|
pages = Pages.query.filter_by(**query).all()
|
||||||
schema = PageSchema(exclude=["content"], many=True)
|
schema = PageSchema(exclude=["content"], many=True)
|
||||||
response = schema.dump(pages)
|
response = schema.dump(pages)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
@@ -22,8 +72,19 @@ class PageList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
@pages_namespace.doc(
|
||||||
req = request.get_json()
|
description="Endpoint to create a page object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "PageDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@validate_args(TransientPageModel, location="json")
|
||||||
|
def post(self, json_args):
|
||||||
|
req = json_args
|
||||||
schema = PageSchema()
|
schema = PageSchema()
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
|
|
||||||
@@ -42,8 +103,19 @@ class PageList(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@pages_namespace.route("/<page_id>")
|
@pages_namespace.route("/<page_id>")
|
||||||
|
@pages_namespace.doc(
|
||||||
|
params={"page_id": "ID of a page object"},
|
||||||
|
responses={
|
||||||
|
200: ("Success", "PageDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
class PageDetail(Resource):
|
class PageDetail(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@pages_namespace.doc(description="Endpoint to read a page object")
|
||||||
def get(self, page_id):
|
def get(self, page_id):
|
||||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||||
schema = PageSchema()
|
schema = PageSchema()
|
||||||
@@ -55,6 +127,7 @@ class PageDetail(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@pages_namespace.doc(description="Endpoint to edit a page object")
|
||||||
def patch(self, page_id):
|
def patch(self, page_id):
|
||||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
@@ -75,6 +148,10 @@ class PageDetail(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@pages_namespace.doc(
|
||||||
|
description="Endpoint to delete a page object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, page_id):
|
def delete(self, page_id):
|
||||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||||
db.session.delete(page)
|
db.session.delete(page)
|
||||||
|
|||||||
60
CTFd/api/v1/schemas/__init__.py
Normal file
60
CTFd/api/v1/schemas/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class APISimpleSuccessResponse(BaseModel):
|
||||||
|
success: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class APIDetailedSuccessResponse(APISimpleSuccessResponse):
|
||||||
|
data: Optional[Any]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apidoc(cls):
|
||||||
|
"""
|
||||||
|
Helper to inline references from the generated schema
|
||||||
|
"""
|
||||||
|
schema = cls.schema()
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = schema["properties"]["data"]["$ref"]
|
||||||
|
ref = key.split("/").pop()
|
||||||
|
definition = schema["definitions"][ref]
|
||||||
|
schema["properties"]["data"] = definition
|
||||||
|
del schema["definitions"][ref]
|
||||||
|
if bool(schema["definitions"]) is False:
|
||||||
|
del schema["definitions"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
class APIListSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: Optional[List[Any]]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apidoc(cls):
|
||||||
|
"""
|
||||||
|
Helper to inline references from the generated schema
|
||||||
|
"""
|
||||||
|
schema = cls.schema()
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = schema["properties"]["data"]["items"]["$ref"]
|
||||||
|
ref = key.split("/").pop()
|
||||||
|
definition = schema["definitions"][ref]
|
||||||
|
schema["properties"]["data"]["items"] = definition
|
||||||
|
del schema["definitions"][ref]
|
||||||
|
if bool(schema["definitions"]) is False:
|
||||||
|
del schema["definitions"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
class APISimpleErrorResponse(BaseModel):
|
||||||
|
success: bool = False
|
||||||
|
errors: Optional[List[str]]
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.request import validate_args
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.models import Submissions, db
|
from CTFd.models import Submissions, db
|
||||||
from CTFd.schemas.submissions import SubmissionSchema
|
from CTFd.schemas.submissions import SubmissionSchema
|
||||||
@@ -10,10 +15,40 @@ submissions_namespace = Namespace(
|
|||||||
"submissions", description="Endpoint to retrieve Submission"
|
"submissions", description="Endpoint to retrieve Submission"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SubmissionModel = sqlalchemy_to_pydantic(Submissions)
|
||||||
|
TransientSubmissionModel = sqlalchemy_to_pydantic(Submissions, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: SubmissionModel
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[SubmissionModel]
|
||||||
|
|
||||||
|
|
||||||
|
submissions_namespace.schema_model(
|
||||||
|
"SubmissionDetailedSuccessResponse", SubmissionDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
submissions_namespace.schema_model(
|
||||||
|
"SubmissionListSuccessResponse", SubmissionListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@submissions_namespace.route("")
|
@submissions_namespace.route("")
|
||||||
class SubmissionsList(Resource):
|
class SubmissionsList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@submissions_namespace.doc(
|
||||||
|
description="Endpoint to get submission objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "SubmissionListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
args = request.args.to_dict()
|
args = request.args.to_dict()
|
||||||
schema = SubmissionSchema(many=True)
|
schema = SubmissionSchema(many=True)
|
||||||
@@ -30,8 +65,19 @@ class SubmissionsList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
@submissions_namespace.doc(
|
||||||
req = request.get_json()
|
description="Endpoint to create a submission object. Users should interact with the attempt endpoint to submit flags.",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "SubmissionListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@validate_args(TransientSubmissionModel, location="json")
|
||||||
|
def post(self, json_args):
|
||||||
|
req = json_args
|
||||||
Model = Submissions.get_child(type=req.get("type"))
|
Model = Submissions.get_child(type=req.get("type"))
|
||||||
schema = SubmissionSchema(instance=Model())
|
schema = SubmissionSchema(instance=Model())
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
@@ -54,6 +100,16 @@ class SubmissionsList(Resource):
|
|||||||
@submissions_namespace.param("submission_id", "A Submission ID")
|
@submissions_namespace.param("submission_id", "A Submission ID")
|
||||||
class Submission(Resource):
|
class Submission(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@submissions_namespace.doc(
|
||||||
|
description="Endpoint to get submission objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "SubmissionDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, submission_id):
|
def get(self, submission_id):
|
||||||
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
|
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
|
||||||
schema = SubmissionSchema()
|
schema = SubmissionSchema()
|
||||||
@@ -65,6 +121,16 @@ class Submission(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@submissions_namespace.doc(
|
||||||
|
description="Endpoint to get submission objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "APISimpleSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def delete(self, submission_id):
|
def delete(self, submission_id):
|
||||||
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
|
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
|
||||||
db.session.delete(submission)
|
db.session.delete(submission)
|
||||||
|
|||||||
@@ -1,16 +1,47 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.models import Tags, db
|
from CTFd.models import Tags, db
|
||||||
from CTFd.schemas.tags import TagSchema
|
from CTFd.schemas.tags import TagSchema
|
||||||
from CTFd.utils.decorators import admins_only
|
from CTFd.utils.decorators import admins_only
|
||||||
|
|
||||||
tags_namespace = Namespace("tags", description="Endpoint to retrieve Tags")
|
tags_namespace = Namespace("tags", description="Endpoint to retrieve Tags")
|
||||||
|
|
||||||
|
TagModel = sqlalchemy_to_pydantic(Tags)
|
||||||
|
|
||||||
|
|
||||||
|
class TagDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: TagModel
|
||||||
|
|
||||||
|
|
||||||
|
class TagListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[TagModel]
|
||||||
|
|
||||||
|
|
||||||
|
tags_namespace.schema_model(
|
||||||
|
"TagDetailedSuccessResponse", TagDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
tags_namespace.schema_model("TagListSuccessResponse", TagListSuccessResponse.apidoc())
|
||||||
|
|
||||||
|
|
||||||
@tags_namespace.route("")
|
@tags_namespace.route("")
|
||||||
class TagList(Resource):
|
class TagList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@tags_namespace.doc(
|
||||||
|
description="Endpoint to list Tag objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TagListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
# TODO: Filter by challenge_id
|
# TODO: Filter by challenge_id
|
||||||
tags = Tags.query.all()
|
tags = Tags.query.all()
|
||||||
@@ -23,6 +54,16 @@ class TagList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@tags_namespace.doc(
|
||||||
|
description="Endpoint to create a Tag object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TagDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = TagSchema()
|
schema = TagSchema()
|
||||||
@@ -44,6 +85,16 @@ class TagList(Resource):
|
|||||||
@tags_namespace.param("tag_id", "A Tag ID")
|
@tags_namespace.param("tag_id", "A Tag ID")
|
||||||
class Tag(Resource):
|
class Tag(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@tags_namespace.doc(
|
||||||
|
description="Endpoint to get a specific Tag object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TagDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, tag_id):
|
def get(self, tag_id):
|
||||||
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
||||||
|
|
||||||
@@ -55,6 +106,16 @@ class Tag(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@tags_namespace.doc(
|
||||||
|
description="Endpoint to edit a specific Tag object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TagDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self, tag_id):
|
def patch(self, tag_id):
|
||||||
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
||||||
schema = TagSchema()
|
schema = TagSchema()
|
||||||
@@ -72,6 +133,10 @@ class Tag(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@tags_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific Tag object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, tag_id):
|
def delete(self, tag_id):
|
||||||
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
||||||
db.session.delete(tag)
|
db.session.delete(tag)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import copy
|
import copy
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import abort, request, session
|
from flask import abort, request, session
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_standings, clear_team_session, clear_user_session
|
from CTFd.cache import clear_standings, clear_team_session, clear_user_session
|
||||||
from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db
|
from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db
|
||||||
from CTFd.schemas.awards import AwardSchema
|
from CTFd.schemas.awards import AwardSchema
|
||||||
@@ -17,10 +20,40 @@ from CTFd.utils.user import get_current_team, get_current_user_type, is_admin
|
|||||||
|
|
||||||
teams_namespace = Namespace("teams", description="Endpoint to retrieve Teams")
|
teams_namespace = Namespace("teams", description="Endpoint to retrieve Teams")
|
||||||
|
|
||||||
|
TeamModel = sqlalchemy_to_pydantic(Teams)
|
||||||
|
TransientTeamModel = sqlalchemy_to_pydantic(Teams, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class TeamDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: TeamModel
|
||||||
|
|
||||||
|
|
||||||
|
class TeamListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[TeamModel]
|
||||||
|
|
||||||
|
|
||||||
|
teams_namespace.schema_model(
|
||||||
|
"TeamDetailedSuccessResponse", TeamDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
teams_namespace.schema_model(
|
||||||
|
"TeamListSuccessResponse", TeamListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route("")
|
@teams_namespace.route("")
|
||||||
class TeamList(Resource):
|
class TeamList(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to get Team objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TeamListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
if is_admin() and request.args.get("view") == "admin":
|
if is_admin() and request.args.get("view") == "admin":
|
||||||
teams = Teams.query.filter_by()
|
teams = Teams.query.filter_by()
|
||||||
@@ -38,6 +71,16 @@ class TeamList(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to create a Team object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TeamDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
user_type = get_current_user_type()
|
user_type = get_current_user_type()
|
||||||
@@ -63,6 +106,16 @@ class TeamList(Resource):
|
|||||||
@teams_namespace.param("team_id", "Team ID")
|
@teams_namespace.param("team_id", "Team ID")
|
||||||
class TeamPublic(Resource):
|
class TeamPublic(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to get a specific Team object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TeamDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, team_id):
|
def get(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
|
|
||||||
@@ -82,6 +135,16 @@ class TeamPublic(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to edit a specific Team object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TeamDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self, team_id):
|
def patch(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -104,6 +167,10 @@ class TeamPublic(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific Team object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, team_id):
|
def delete(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
team_id = team.id
|
team_id = team.id
|
||||||
@@ -128,6 +195,16 @@ class TeamPublic(Resource):
|
|||||||
class TeamPrivate(Resource):
|
class TeamPrivate(Resource):
|
||||||
@authed_only
|
@authed_only
|
||||||
@require_team
|
@require_team
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to get the current user's Team object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TeamDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
response = TeamSchema(view="self").dump(team)
|
response = TeamSchema(view="self").dump(team)
|
||||||
@@ -141,6 +218,16 @@ class TeamPrivate(Resource):
|
|||||||
|
|
||||||
@authed_only
|
@authed_only
|
||||||
@require_team
|
@require_team
|
||||||
|
@teams_namespace.doc(
|
||||||
|
description="Endpoint to edit the current user's Team object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TeamDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self):
|
def patch(self):
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
if team.captain_id != session["id"]:
|
if team.captain_id != session["id"]:
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request, session
|
from flask import request, session
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.models import Tokens, db
|
from CTFd.models import Tokens, db
|
||||||
from CTFd.schemas.tokens import TokenSchema
|
from CTFd.schemas.tokens import TokenSchema
|
||||||
from CTFd.utils.decorators import authed_only, require_verified_emails
|
from CTFd.utils.decorators import authed_only, require_verified_emails
|
||||||
@@ -11,11 +14,50 @@ from CTFd.utils.user import get_current_user, get_current_user_type, is_admin
|
|||||||
|
|
||||||
tokens_namespace = Namespace("tokens", description="Endpoint to retrieve Tokens")
|
tokens_namespace = Namespace("tokens", description="Endpoint to retrieve Tokens")
|
||||||
|
|
||||||
|
TokenModel = sqlalchemy_to_pydantic(Tokens)
|
||||||
|
ValuelessTokenModel = sqlalchemy_to_pydantic(Tokens, exclude=["value"])
|
||||||
|
|
||||||
|
|
||||||
|
class TokenDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: TokenModel
|
||||||
|
|
||||||
|
|
||||||
|
class ValuelessTokenDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: ValuelessTokenModel
|
||||||
|
|
||||||
|
|
||||||
|
class TokenListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[TokenModel]
|
||||||
|
|
||||||
|
|
||||||
|
tokens_namespace.schema_model(
|
||||||
|
"TokenDetailedSuccessResponse", TokenDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
tokens_namespace.schema_model(
|
||||||
|
"ValuelessTokenDetailedSuccessResponse",
|
||||||
|
ValuelessTokenDetailedSuccessResponse.apidoc(),
|
||||||
|
)
|
||||||
|
|
||||||
|
tokens_namespace.schema_model(
|
||||||
|
"TokenListSuccessResponse", TokenListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@tokens_namespace.route("")
|
@tokens_namespace.route("")
|
||||||
class TokenList(Resource):
|
class TokenList(Resource):
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@tokens_namespace.doc(
|
||||||
|
description="Endpoint to get token objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TokenListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
tokens = Tokens.query.filter_by(user_id=user.id)
|
tokens = Tokens.query.filter_by(user_id=user.id)
|
||||||
@@ -30,6 +72,16 @@ class TokenList(Resource):
|
|||||||
|
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@tokens_namespace.doc(
|
||||||
|
description="Endpoint to create a token object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "TokenDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
expiration = req.get("expiration")
|
expiration = req.get("expiration")
|
||||||
@@ -54,6 +106,16 @@ class TokenList(Resource):
|
|||||||
class TokenDetail(Resource):
|
class TokenDetail(Resource):
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@tokens_namespace.doc(
|
||||||
|
description="Endpoint to get an existing token object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "ValuelessTokenDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, token_id):
|
def get(self, token_id):
|
||||||
if is_admin():
|
if is_admin():
|
||||||
token = Tokens.query.filter_by(id=token_id).first_or_404()
|
token = Tokens.query.filter_by(id=token_id).first_or_404()
|
||||||
@@ -73,6 +135,10 @@ class TokenDetail(Resource):
|
|||||||
|
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@tokens_namespace.doc(
|
||||||
|
description="Endpoint to delete an existing token object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, token_id):
|
def delete(self, token_id):
|
||||||
if is_admin():
|
if is_admin():
|
||||||
token = Tokens.query.filter_by(id=token_id).first_or_404()
|
token = Tokens.query.filter_by(id=token_id).first_or_404()
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.models import Unlocks, db, get_class_by_tablename
|
from CTFd.models import Unlocks, db, get_class_by_tablename
|
||||||
from CTFd.schemas.awards import AwardSchema
|
from CTFd.schemas.awards import AwardSchema
|
||||||
@@ -15,10 +19,40 @@ from CTFd.utils.user import get_current_user
|
|||||||
|
|
||||||
unlocks_namespace = Namespace("unlocks", description="Endpoint to retrieve Unlocks")
|
unlocks_namespace = Namespace("unlocks", description="Endpoint to retrieve Unlocks")
|
||||||
|
|
||||||
|
UnlockModel = sqlalchemy_to_pydantic(Unlocks)
|
||||||
|
TransientUnlockModel = sqlalchemy_to_pydantic(Unlocks, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class UnlockDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: UnlockModel
|
||||||
|
|
||||||
|
|
||||||
|
class UnlockListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[UnlockModel]
|
||||||
|
|
||||||
|
|
||||||
|
unlocks_namespace.schema_model(
|
||||||
|
"UnlockDetailedSuccessResponse", UnlockDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
unlocks_namespace.schema_model(
|
||||||
|
"UnlockListSuccessResponse", UnlockListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@unlocks_namespace.route("")
|
@unlocks_namespace.route("")
|
||||||
class UnlockList(Resource):
|
class UnlockList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@unlocks_namespace.doc(
|
||||||
|
description="Endpoint to get unlock objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UnlockListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
hints = Unlocks.query.all()
|
hints = Unlocks.query.all()
|
||||||
schema = UnlockSchema()
|
schema = UnlockSchema()
|
||||||
@@ -32,6 +66,16 @@ class UnlockList(Resource):
|
|||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@unlocks_namespace.doc(
|
||||||
|
description="Endpoint to create an unlock object. Used to unlock hints.",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UnlockDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from flask import abort, request
|
from flask import abort, request
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
from CTFd.cache import clear_standings, clear_user_session
|
from CTFd.cache import clear_standings, clear_user_session
|
||||||
from CTFd.models import (
|
from CTFd.models import (
|
||||||
Awards,
|
Awards,
|
||||||
@@ -28,9 +32,40 @@ from CTFd.utils.user import get_current_user, get_current_user_type, is_admin
|
|||||||
users_namespace = Namespace("users", description="Endpoint to retrieve Users")
|
users_namespace = Namespace("users", description="Endpoint to retrieve Users")
|
||||||
|
|
||||||
|
|
||||||
|
UserModel = sqlalchemy_to_pydantic(Users)
|
||||||
|
TransientUserModel = sqlalchemy_to_pydantic(Users, exclude=["id"])
|
||||||
|
|
||||||
|
|
||||||
|
class UserDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: UserModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[UserModel]
|
||||||
|
|
||||||
|
|
||||||
|
users_namespace.schema_model(
|
||||||
|
"UserDetailedSuccessResponse", UserDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
users_namespace.schema_model(
|
||||||
|
"UserListSuccessResponse", UserListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route("")
|
@users_namespace.route("")
|
||||||
class UserList(Resource):
|
class UserList(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to get User objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UserListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
if is_admin() and request.args.get("view") == "admin":
|
if is_admin() and request.args.get("view") == "admin":
|
||||||
users = Users.query.filter_by()
|
users = Users.query.filter_by()
|
||||||
@@ -44,12 +79,21 @@ class UserList(Resource):
|
|||||||
|
|
||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
|
@users_namespace.doc()
|
||||||
|
@admins_only
|
||||||
@users_namespace.doc(
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to create a User object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UserDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
params={
|
params={
|
||||||
"notify": "Whether to send the created user an email with their credentials"
|
"notify": "Whether to send the created user an email with their credentials"
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
@admins_only
|
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = UserSchema("admin")
|
schema = UserSchema("admin")
|
||||||
@@ -79,6 +123,16 @@ class UserList(Resource):
|
|||||||
@users_namespace.param("user_id", "User ID")
|
@users_namespace.param("user_id", "User ID")
|
||||||
class UserPublic(Resource):
|
class UserPublic(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to get a specific User object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UserDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, user_id):
|
def get(self, user_id):
|
||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
|
|
||||||
@@ -97,6 +151,16 @@ class UserPublic(Resource):
|
|||||||
return {"success": True, "data": response.data}
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to edit a specific User object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UserDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self, user_id):
|
def patch(self, user_id):
|
||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -118,6 +182,10 @@ class UserPublic(Resource):
|
|||||||
return {"success": True, "data": response}
|
return {"success": True, "data": response}
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific User object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
def delete(self, user_id):
|
def delete(self, user_id):
|
||||||
Notifications.query.filter_by(user_id=user_id).delete()
|
Notifications.query.filter_by(user_id=user_id).delete()
|
||||||
Awards.query.filter_by(user_id=user_id).delete()
|
Awards.query.filter_by(user_id=user_id).delete()
|
||||||
@@ -138,6 +206,16 @@ class UserPublic(Resource):
|
|||||||
@users_namespace.route("/me")
|
@users_namespace.route("/me")
|
||||||
class UserPrivate(Resource):
|
class UserPrivate(Resource):
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to get the User object for the current user",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UserDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
response = UserSchema("self").dump(user).data
|
response = UserSchema("self").dump(user).data
|
||||||
@@ -146,6 +224,16 @@ class UserPrivate(Resource):
|
|||||||
return {"success": True, "data": response}
|
return {"success": True, "data": response}
|
||||||
|
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to edit the User object for the current user",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "UserDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
def patch(self):
|
def patch(self):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -294,6 +382,10 @@ class UserPublicAwards(Resource):
|
|||||||
@users_namespace.param("user_id", "User ID")
|
@users_namespace.param("user_id", "User ID")
|
||||||
class UserEmails(Resource):
|
class UserEmails(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
|
@users_namespace.doc(
|
||||||
|
description="Endpoint to email a User object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
@ratelimit(method="POST", limit=10, interval=60)
|
@ratelimit(method="POST", limit=10, interval=60)
|
||||||
def post(self, user_id):
|
def post(self, user_id):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
@@ -314,4 +406,4 @@ class UserEmails(Resource):
|
|||||||
|
|
||||||
result, response = sendmail(addr=user.email, text=text)
|
result, response = sendmail(addr=user.email, text=text)
|
||||||
|
|
||||||
return {"success": result, "data": {}}
|
return {"success": result}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ flask-marshmallow==0.10.1
|
|||||||
marshmallow-sqlalchemy==0.17.0
|
marshmallow-sqlalchemy==0.17.0
|
||||||
boto3==1.13.9
|
boto3==1.13.9
|
||||||
marshmallow==2.20.2
|
marshmallow==2.20.2
|
||||||
|
pydantic==1.5.1
|
||||||
lxml==4.5.1
|
lxml==4.5.1
|
||||||
html5lib==1.0.1
|
html5lib==1.0.1
|
||||||
WTForms==2.3.1
|
WTForms==2.3.1
|
||||||
|
|||||||
Reference in New Issue
Block a user