mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
Merge remote-tracking branch 'origin/3.0.0-dev' into 1318-api-pagination
This commit is contained in:
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,9 +6,9 @@ If this is a feature request please describe the behavior that you'd like to see
|
|||||||
|
|
||||||
**Environment**:
|
**Environment**:
|
||||||
|
|
||||||
- CTFd Version/Commit:
|
- CTFd Version/Commit:
|
||||||
- Operating System:
|
- Operating System:
|
||||||
- Web Browser and Version:
|
- Web Browser and Version:
|
||||||
|
|
||||||
**What happened?**
|
**What happened?**
|
||||||
|
|
||||||
@@ -17,4 +17,3 @@ If this is a feature request please describe the behavior that you'd like to see
|
|||||||
**How to reproduce your issue**
|
**How to reproduce your issue**
|
||||||
|
|
||||||
**Any associated stack traces or error logs**
|
**Any associated stack traces or error logs**
|
||||||
|
|
||||||
|
|||||||
1460
CHANGELOG.md
1460
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
#### **Did you find a bug?**
|
#### **Did you find a bug?**
|
||||||
|
|
||||||
* **Do not open up a GitHub issue if the bug is a security vulnerability in CTFd**. Instead [email the details to us at support@ctfd.io](mailto:support@ctfd.io).
|
- **Do not open up a GitHub issue if the bug is a security vulnerability in CTFd**. Instead [email the details to us at support@ctfd.io](mailto:support@ctfd.io).
|
||||||
|
|
||||||
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/CTFd/CTFd/issues).
|
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/CTFd/CTFd/issues).
|
||||||
|
|
||||||
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/CTFd/CTFd/issues/new). Be sure to fill out the issue template with a **title and clear description**, and as much relevant information as possible (e.g. deployment setup, browser version, etc).
|
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/CTFd/CTFd/issues/new). Be sure to fill out the issue template with a **title and clear description**, and as much relevant information as possible (e.g. deployment setup, browser version, etc).
|
||||||
|
|
||||||
#### **Did you write a patch that fixes a bug or implements a new feature?**
|
#### **Did you write a patch that fixes a bug or implements a new feature?**
|
||||||
|
|
||||||
* Open a new pull request with the patch.
|
- Open a new pull request with the patch.
|
||||||
|
|
||||||
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
||||||
|
|
||||||
* Ensure all status checks pass. PR's with test failures will not be merged. PR's with insufficient coverage may be merged depending on the situation.
|
- Ensure all status checks pass. PR's with test failures will not be merged. PR's with insufficient coverage may be merged depending on the situation.
|
||||||
|
|
||||||
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
|
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def pages_preview():
|
|||||||
data = request.form.to_dict()
|
data = request.form.to_dict()
|
||||||
schema = PageSchema()
|
schema = PageSchema()
|
||||||
page = schema.load(data)
|
page = schema.load(data)
|
||||||
return render_template("page.html", content=build_html(page.data["content"]))
|
return render_template("page.html", content=build_html(page.data.content))
|
||||||
|
|
||||||
|
|
||||||
@admin.route("/admin/pages/<int:page_id>")
|
@admin.route("/admin/pages/<int:page_id>")
|
||||||
|
|||||||
@@ -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,12 +1,25 @@
|
|||||||
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 Challenges, Fails, Flags, Hints, HintUnlocks, Solves, Tags, db
|
from CTFd.models import (
|
||||||
|
Challenges,
|
||||||
|
Fails,
|
||||||
|
Flags,
|
||||||
|
Hints,
|
||||||
|
HintUnlocks,
|
||||||
|
Solves,
|
||||||
|
Submissions,
|
||||||
|
Tags,
|
||||||
|
db,
|
||||||
|
)
|
||||||
from CTFd.plugins.challenges import CHALLENGE_CLASSES, get_chal_class
|
from CTFd.plugins.challenges import CHALLENGE_CLASSES, get_chal_class
|
||||||
from CTFd.schemas.flags import FlagSchema
|
from CTFd.schemas.flags import FlagSchema
|
||||||
from CTFd.schemas.hints import HintSchema
|
from CTFd.schemas.hints import HintSchema
|
||||||
@@ -37,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()
|
||||||
@@ -138,6 +181,16 @@ class ChallengeList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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"]
|
||||||
@@ -168,11 +221,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()
|
||||||
@@ -289,6 +351,15 @@ class Challenge(Resource):
|
|||||||
else:
|
else:
|
||||||
response["solves"] = None
|
response["solves"] = None
|
||||||
|
|
||||||
|
if authed():
|
||||||
|
# Get current attempts for the user
|
||||||
|
attempts = Submissions.query.filter_by(
|
||||||
|
account_id=user.account_id, challenge_id=challenge_id
|
||||||
|
).count()
|
||||||
|
else:
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
response["attempts"] = attempts
|
||||||
response["files"] = files
|
response["files"] = files
|
||||||
response["tags"] = tags
|
response["tags"] = tags
|
||||||
response["hints"] = hints
|
response["hints"] = hints
|
||||||
@@ -299,6 +370,8 @@ class Challenge(Resource):
|
|||||||
files=files,
|
files=files,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
hints=[Hints(**h) for h in hints],
|
hints=[Hints(**h) for h in hints],
|
||||||
|
max_attempts=chal.max_attempts,
|
||||||
|
attempts=attempts,
|
||||||
challenge=chal,
|
challenge=chal,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -306,6 +379,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)
|
||||||
@@ -314,6 +397,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)
|
||||||
@@ -524,7 +611,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
|
||||||
@@ -572,7 +658,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):
|
||||||
@@ -588,7 +673,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):
|
||||||
@@ -604,7 +688,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):
|
||||||
@@ -619,7 +702,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).paginate(max_per_page=100)
|
files = Files.query.filter_by(type=file_type).paginate(max_per_page=100)
|
||||||
@@ -37,6 +70,16 @@ class FilesList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
@@ -60,6 +103,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()
|
||||||
@@ -71,6 +124,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.paginate(max_per_page=100)
|
flags = Flags.query.paginate(max_per_page=100)
|
||||||
schema = FlagSchema(many=True)
|
schema = FlagSchema(many=True)
|
||||||
@@ -35,6 +68,16 @@ class FlagList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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()
|
||||||
@@ -75,6 +118,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()
|
||||||
@@ -88,6 +141,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()
|
||||||
|
|
||||||
@@ -98,6 +155,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.paginate(max_per_page=100)
|
hints = Hints.query.paginate(max_per_page=100)
|
||||||
response = HintSchema(many=True).dump(hints.items)
|
response = HintSchema(many=True).dump(hints.items)
|
||||||
@@ -35,6 +68,16 @@ class HintList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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")
|
||||||
@@ -55,6 +98,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()
|
||||||
@@ -80,6 +133,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()
|
||||||
@@ -98,6 +161,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.paginate(max_per_page=100)
|
notifications = Notifications.query.paginate(max_per_page=100)
|
||||||
schema = NotificationSchema(many=True)
|
schema = NotificationSchema(many=True)
|
||||||
@@ -34,6 +68,16 @@ class NotificantionList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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()
|
||||||
|
|
||||||
@@ -62,6 +106,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()
|
||||||
@@ -72,6 +126,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.paginate(max_per_page=100)
|
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).paginate(max_per_page=100)
|
||||||
schema = PageSchema(exclude=["content"], many=True)
|
schema = PageSchema(exclude=["content"], many=True)
|
||||||
response = schema.dump(pages.items)
|
response = schema.dump(pages.items)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
@@ -35,8 +85,19 @@ class PageList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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)
|
||||||
|
|
||||||
@@ -55,8 +116,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()
|
||||||
@@ -68,6 +140,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()
|
||||||
@@ -88,6 +161,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)
|
||||||
|
|||||||
105
CTFd/api/v1/schemas/__init__.py
Normal file
105
CTFd/api/v1/schemas/__init__.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from typing import Any, Dict, 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 PaginatedAPIListSuccessResponse(APIListSuccessResponse):
|
||||||
|
meta: Dict[str, Any]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apidoc(cls):
|
||||||
|
"""
|
||||||
|
Helper to inline references from the generated schema
|
||||||
|
"""
|
||||||
|
schema = cls.schema()
|
||||||
|
|
||||||
|
schema["properties"]["meta"] = {
|
||||||
|
"title": "Meta",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pagination": {
|
||||||
|
"title": "Pagination",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"page": {"title": "Page", "type": "integer"},
|
||||||
|
"next": {"title": "Next", "type": "integer"},
|
||||||
|
"prev": {"title": "Prev", "type": "integer"},
|
||||||
|
"pages": {"title": "Pages", "type": "integer"},
|
||||||
|
"per_page": {"title": "Per Page", "type": "integer"},
|
||||||
|
"total": {"title": "Total", "type": "integer"},
|
||||||
|
},
|
||||||
|
"required": ["page", "next", "prev", "pages", "per_page", "total"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["pagination"],
|
||||||
|
}
|
||||||
|
|
||||||
|
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,14 @@
|
|||||||
|
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,
|
||||||
|
PaginatedAPIListSuccessResponse,
|
||||||
|
)
|
||||||
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 +18,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(PaginatedAPIListSuccessResponse):
|
||||||
|
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)
|
||||||
@@ -51,8 +89,19 @@ class SubmissionsList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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)
|
||||||
@@ -75,6 +124,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()
|
||||||
@@ -86,6 +145,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.paginate(max_per_page=100)
|
tags = Tags.query.paginate(max_per_page=100)
|
||||||
@@ -36,6 +67,16 @@ class TagList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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()
|
||||||
@@ -57,6 +98,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()
|
||||||
|
|
||||||
@@ -68,6 +119,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()
|
||||||
@@ -85,6 +146,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,14 @@
|
|||||||
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,
|
||||||
|
PaginatedAPIListSuccessResponse,
|
||||||
|
)
|
||||||
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 +23,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(PaginatedAPIListSuccessResponse):
|
||||||
|
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().paginate(per_page=50, max_per_page=100)
|
teams = Teams.query.filter_by().paginate(per_page=50, max_per_page=100)
|
||||||
@@ -53,6 +89,16 @@ class TeamList(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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()
|
||||||
@@ -78,6 +124,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()
|
||||||
|
|
||||||
@@ -97,6 +153,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()
|
||||||
@@ -119,6 +185,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
|
||||||
@@ -143,6 +213,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)
|
||||||
@@ -156,6 +236,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).paginate(max_per_page=100)
|
tokens = Tokens.query.filter_by(user_id=user.id).paginate(max_per_page=100)
|
||||||
@@ -43,6 +85,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")
|
||||||
@@ -67,6 +119,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()
|
||||||
@@ -86,6 +148,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):
|
||||||
unlocks = Unlocks.query.paginate(max_per_page=100)
|
unlocks = Unlocks.query.paginate(max_per_page=100)
|
||||||
schema = UnlockSchema()
|
schema = UnlockSchema()
|
||||||
@@ -45,6 +79,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,13 @@
|
|||||||
|
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,
|
||||||
|
PaginatedAPIListSuccessResponse,
|
||||||
|
)
|
||||||
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,15 +35,46 @@ 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(PaginatedAPIListSuccessResponse):
|
||||||
|
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().paginate(max_per_page=100)
|
users = Users.query.filter_by().paginate(per_page=50, max_per_page=100)
|
||||||
else:
|
else:
|
||||||
users = Users.query.filter_by(banned=False, hidden=False).paginate(
|
users = Users.query.filter_by(banned=False, hidden=False).paginate(
|
||||||
max_per_page=100
|
per_page=50, max_per_page=100
|
||||||
)
|
)
|
||||||
|
|
||||||
response = UserSchema(view="user", many=True).dump(users.items)
|
response = UserSchema(view="user", many=True).dump(users.items)
|
||||||
@@ -59,12 +97,21 @@ class UserList(Resource):
|
|||||||
"data": response.data,
|
"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")
|
||||||
@@ -94,6 +141,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()
|
||||||
|
|
||||||
@@ -112,6 +169,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()
|
||||||
@@ -133,6 +200,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()
|
||||||
@@ -153,6 +224,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
|
||||||
@@ -161,6 +242,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()
|
||||||
@@ -309,6 +400,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()
|
||||||
@@ -329,4 +424,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}
|
||||||
|
|||||||
@@ -18,3 +18,32 @@ TeamAttrs = namedtuple(
|
|||||||
"created",
|
"created",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _TeamAttrsWrapper:
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
from CTFd.utils.user import get_current_team_attrs
|
||||||
|
|
||||||
|
attrs = get_current_team_attrs()
|
||||||
|
return getattr(attrs, attr, None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def place(self):
|
||||||
|
from CTFd.utils.user import get_current_team
|
||||||
|
|
||||||
|
team = get_current_team()
|
||||||
|
if team:
|
||||||
|
return team.place
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def score(self):
|
||||||
|
from CTFd.utils.user import get_current_team
|
||||||
|
|
||||||
|
team = get_current_team()
|
||||||
|
if team:
|
||||||
|
return team.score
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
Team = _TeamAttrsWrapper()
|
||||||
|
|||||||
@@ -20,3 +20,32 @@ UserAttrs = namedtuple(
|
|||||||
"created",
|
"created",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _UserAttrsWrapper:
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
from CTFd.utils.user import get_current_user_attrs
|
||||||
|
|
||||||
|
attrs = get_current_user_attrs()
|
||||||
|
return getattr(attrs, attr, None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def place(self):
|
||||||
|
from CTFd.utils.user import get_current_user
|
||||||
|
|
||||||
|
user = get_current_user()
|
||||||
|
if user:
|
||||||
|
return user.place
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def score(self):
|
||||||
|
from CTFd.utils.user import get_current_user
|
||||||
|
|
||||||
|
user = get_current_user()
|
||||||
|
if user:
|
||||||
|
return user.score
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
User = _UserAttrsWrapper()
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ Within CTFd you are free to mix and match regular and dynamic challenges.
|
|||||||
|
|
||||||
The current implementation requires the challenge to keep track of three values:
|
The current implementation requires the challenge to keep track of three values:
|
||||||
|
|
||||||
* Initial - The original point valuation
|
- Initial - The original point valuation
|
||||||
* Decay - The amount of solves before the challenge will be at the minimum
|
- Decay - The amount of solves before the challenge will be at the minimum
|
||||||
* Minimum - The lowest possible point valuation
|
- Minimum - The lowest possible point valuation
|
||||||
|
|
||||||
The value decay logic is implemented with the following math:
|
The value decay logic is implemented with the following math:
|
||||||
|
|
||||||
@@ -50,5 +50,5 @@ so that higher valued challenges have a slower drop from their initial value.
|
|||||||
**REQUIRES: CTFd >= v1.2.0**
|
**REQUIRES: CTFd >= v1.2.0**
|
||||||
|
|
||||||
1. Clone this repository to `CTFd/plugins`. It is important that the folder is
|
1. Clone this repository to `CTFd/plugins`. It is important that the folder is
|
||||||
named `DynamicValueChallenge` so CTFd can serve the files in the `assets`
|
named `DynamicValueChallenge` so CTFd can serve the files in the `assets`
|
||||||
directory.
|
directory.
|
||||||
|
|||||||
332
CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue
Normal file
332
CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
<template>
|
||||||
|
<div id="media-modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h3 class="text-center">Media Library</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close"
|
||||||
|
data-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mh-100">
|
||||||
|
<div class="col-md-6" id="media-library-list">
|
||||||
|
<div
|
||||||
|
class="media-item-wrapper"
|
||||||
|
v-for="file in files"
|
||||||
|
:key="file.id"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="
|
||||||
|
selectFile(file);
|
||||||
|
return false;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-bind:class="getIconClass(file.location)"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<small class="media-item-title">{{
|
||||||
|
file.location.split("/").pop()
|
||||||
|
}}</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6" id="media-library-details">
|
||||||
|
<h4 class="text-center">Media Details</h4>
|
||||||
|
<div id="media-item">
|
||||||
|
<div class="text-center" id="media-icon">
|
||||||
|
<div v-if="this.selectedFile">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
getIconClass(this.selectedFile.location) ===
|
||||||
|
'far fa-file-image'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-bind:src="buildSelectedFileUrl()"
|
||||||
|
style="max-width: 100%; max-height: 100%; object-fit: contain;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<i
|
||||||
|
v-bind:class="
|
||||||
|
`${getIconClass(
|
||||||
|
this.selectedFile.location
|
||||||
|
)} fa-4x`
|
||||||
|
"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
class="text-center"
|
||||||
|
id="media-filename"
|
||||||
|
v-if="this.selectedFile"
|
||||||
|
>
|
||||||
|
<a v-bind:href="buildSelectedFileUrl()" target="_blank">
|
||||||
|
{{ this.selectedFile.location.split("/").pop() }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div v-if="this.selectedFile">
|
||||||
|
Link:
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
id="media-link"
|
||||||
|
v-bind:value="buildSelectedFileUrl()"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
Link:
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
id="media-link"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group text-center">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button
|
||||||
|
@click="insertSelectedFile"
|
||||||
|
class="btn btn-success w-100"
|
||||||
|
id="media-insert"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
title="Insert link into editor"
|
||||||
|
>
|
||||||
|
Insert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button
|
||||||
|
@click="downloadSelectedFile"
|
||||||
|
class="btn btn-primary w-100"
|
||||||
|
id="media-download"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
title="Download file"
|
||||||
|
>
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button
|
||||||
|
@click="deleteSelectedFile"
|
||||||
|
class="btn btn-danger w-100"
|
||||||
|
id="media-delete"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
title="Delete file"
|
||||||
|
>
|
||||||
|
<i class="far fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="media-library-upload" enctype="multipart/form-data">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="media-files">
|
||||||
|
Upload Files
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
id="media-files"
|
||||||
|
class="form-control-file"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
<sub class="help-block">
|
||||||
|
Attach multiple files using Control+Click or Cmd+Click.
|
||||||
|
</sub>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" value="page" name="type" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="float-right">
|
||||||
|
<button
|
||||||
|
@click="uploadChosenFiles"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary media-upload-button"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CTFd from "core/CTFd";
|
||||||
|
import { ezQuery, ezToast } from "core/ezq";
|
||||||
|
import { default as helpers } from "core/helpers";
|
||||||
|
|
||||||
|
function get_page_files() {
|
||||||
|
return CTFd.fetch("/api/v1/files?type=page", {
|
||||||
|
credentials: "same-origin"
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
editor: Object
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
files: [],
|
||||||
|
selectedFile: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getPageFiles: function() {
|
||||||
|
get_page_files().then(response => {
|
||||||
|
this.files = response.data;
|
||||||
|
return this.files;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
uploadChosenFiles: function() {
|
||||||
|
// TODO: We should reduce the need to interact with the DOM directly.
|
||||||
|
// This looks jank and we should be able to remove it.
|
||||||
|
let form = document.querySelector("#media-library-upload");
|
||||||
|
helpers.files.upload(form, {}, _data => {
|
||||||
|
this.getPageFiles();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectFile: function(file) {
|
||||||
|
this.selectedFile = file;
|
||||||
|
return this.selectedFile;
|
||||||
|
},
|
||||||
|
buildSelectedFileUrl: function() {
|
||||||
|
return CTFd.config.urlRoot + "/files/" + this.selectedFile.location;
|
||||||
|
},
|
||||||
|
deleteSelectedFile: function() {
|
||||||
|
var file_id = this.selectedFile.id;
|
||||||
|
|
||||||
|
if (confirm("Are you sure you want to delete this file?")) {
|
||||||
|
CTFd.fetch("/api/v1/files/" + file_id, {
|
||||||
|
method: "DELETE"
|
||||||
|
}).then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
response.json().then(object => {
|
||||||
|
if (object.success) {
|
||||||
|
this.getPageFiles();
|
||||||
|
this.selectedFile = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertSelectedFile: function() {
|
||||||
|
let editor = this.$props.editor;
|
||||||
|
if (editor.hasOwnProperty("codemirror")) {
|
||||||
|
editor = editor.codemirror;
|
||||||
|
}
|
||||||
|
let doc = editor.getDoc();
|
||||||
|
let cursor = doc.getCursor();
|
||||||
|
|
||||||
|
let url = this.buildSelectedFileUrl();
|
||||||
|
let img =
|
||||||
|
this.getIconClass(this.selectedFile.location) === "far fa-file-image";
|
||||||
|
let filename = url.split("/").pop();
|
||||||
|
link = "[{0}]({1})".format(filename, url);
|
||||||
|
if (img) {
|
||||||
|
link = "!" + link;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.replaceRange(link, cursor);
|
||||||
|
},
|
||||||
|
downloadSelectedFile: function() {
|
||||||
|
var link = this.buildSelectedFileUrl();
|
||||||
|
window.open(link, "_blank");
|
||||||
|
},
|
||||||
|
getIconClass: function(filename) {
|
||||||
|
var mapping = {
|
||||||
|
// Image Files
|
||||||
|
png: "far fa-file-image",
|
||||||
|
jpg: "far fa-file-image",
|
||||||
|
jpeg: "far fa-file-image",
|
||||||
|
gif: "far fa-file-image",
|
||||||
|
bmp: "far fa-file-image",
|
||||||
|
svg: "far fa-file-image",
|
||||||
|
|
||||||
|
// Text Files
|
||||||
|
txt: "far fa-file-alt",
|
||||||
|
|
||||||
|
// Video Files
|
||||||
|
mov: "far fa-file-video",
|
||||||
|
mp4: "far fa-file-video",
|
||||||
|
wmv: "far fa-file-video",
|
||||||
|
flv: "far fa-file-video",
|
||||||
|
mkv: "far fa-file-video",
|
||||||
|
avi: "far fa-file-video",
|
||||||
|
|
||||||
|
// PDF Files
|
||||||
|
pdf: "far fa-file-pdf",
|
||||||
|
|
||||||
|
// Audio Files
|
||||||
|
mp3: "far fa-file-sound",
|
||||||
|
wav: "far fa-file-sound",
|
||||||
|
aac: "far fa-file-sound",
|
||||||
|
|
||||||
|
// Archive Files
|
||||||
|
zip: "far fa-file-archive",
|
||||||
|
gz: "far fa-file-archive",
|
||||||
|
tar: "far fa-file-archive",
|
||||||
|
"7z": "far fa-file-archive",
|
||||||
|
rar: "far fa-file-archive",
|
||||||
|
|
||||||
|
// Code Files
|
||||||
|
py: "far fa-file-code",
|
||||||
|
c: "far fa-file-code",
|
||||||
|
cpp: "far fa-file-code",
|
||||||
|
html: "far fa-file-code",
|
||||||
|
js: "far fa-file-code",
|
||||||
|
rb: "far fa-file-code",
|
||||||
|
go: "far fa-file-code"
|
||||||
|
};
|
||||||
|
|
||||||
|
var ext = filename.split(".").pop();
|
||||||
|
return mapping[ext] || "far fa-file";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
return this.getPageFiles();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,155 +1,11 @@
|
|||||||
import "./main";
|
import "./main";
|
||||||
|
import { showMediaLibrary } from "../styles";
|
||||||
import "core/utils";
|
import "core/utils";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import CTFd from "core/CTFd";
|
import CTFd from "core/CTFd";
|
||||||
import { default as helpers } from "core/helpers";
|
|
||||||
import CodeMirror from "codemirror";
|
import CodeMirror from "codemirror";
|
||||||
import "codemirror/mode/htmlmixed/htmlmixed.js";
|
import "codemirror/mode/htmlmixed/htmlmixed.js";
|
||||||
import { ezQuery, ezToast } from "core/ezq";
|
import { ezToast } from "core/ezq";
|
||||||
|
|
||||||
function get_filetype_icon_class(filename) {
|
|
||||||
var mapping = {
|
|
||||||
// Image Files
|
|
||||||
png: "fa-file-image",
|
|
||||||
jpg: "fa-file-image",
|
|
||||||
jpeg: "fa-file-image",
|
|
||||||
gif: "fa-file-image",
|
|
||||||
bmp: "fa-file-image",
|
|
||||||
svg: "fa-file-image",
|
|
||||||
|
|
||||||
// Text Files
|
|
||||||
txt: "fa-file-alt",
|
|
||||||
|
|
||||||
// Video Files
|
|
||||||
mov: "fa-file-video",
|
|
||||||
mp4: "fa-file-video",
|
|
||||||
wmv: "fa-file-video",
|
|
||||||
flv: "fa-file-video",
|
|
||||||
mkv: "fa-file-video",
|
|
||||||
avi: "fa-file-video",
|
|
||||||
|
|
||||||
// PDF Files
|
|
||||||
pdf: "fa-file-pdf",
|
|
||||||
|
|
||||||
// Audio Files
|
|
||||||
mp3: "fa-file-sound",
|
|
||||||
wav: "fa-file-sound",
|
|
||||||
aac: "fa-file-sound",
|
|
||||||
|
|
||||||
// Archive Files
|
|
||||||
zip: "fa-file-archive",
|
|
||||||
gz: "fa-file-archive",
|
|
||||||
tar: "fa-file-archive",
|
|
||||||
"7z": "fa-file-archive",
|
|
||||||
rar: "fa-file-archive",
|
|
||||||
|
|
||||||
// Code Files
|
|
||||||
py: "fa-file-code",
|
|
||||||
c: "fa-file-code",
|
|
||||||
cpp: "fa-file-code",
|
|
||||||
html: "fa-file-code",
|
|
||||||
js: "fa-file-code",
|
|
||||||
rb: "fa-file-code",
|
|
||||||
go: "fa-file-code"
|
|
||||||
};
|
|
||||||
|
|
||||||
var ext = filename.split(".").pop();
|
|
||||||
return mapping[ext];
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_page_files() {
|
|
||||||
return CTFd.fetch("/api/v1/files?type=page", {
|
|
||||||
credentials: "same-origin"
|
|
||||||
}).then(function(response) {
|
|
||||||
return response.json();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_files(data) {
|
|
||||||
var list = $("#media-library-list");
|
|
||||||
list.empty();
|
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
var f = data[i];
|
|
||||||
var fname = f.location.split("/").pop();
|
|
||||||
var ext = get_filetype_icon_class(f.location);
|
|
||||||
|
|
||||||
var wrapper = $("<div>").attr("class", "media-item-wrapper");
|
|
||||||
|
|
||||||
var link = $("<a>");
|
|
||||||
link.attr("href", "##");
|
|
||||||
|
|
||||||
if (ext === undefined) {
|
|
||||||
link.append(
|
|
||||||
'<i class="far fa-file" aria-hidden="true"></i> '.format(ext)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
link.append('<i class="far {0}" aria-hidden="true"></i> '.format(ext));
|
|
||||||
}
|
|
||||||
|
|
||||||
link.append(
|
|
||||||
$("<small>")
|
|
||||||
.attr("class", "media-item-title")
|
|
||||||
.text(fname)
|
|
||||||
);
|
|
||||||
|
|
||||||
link.click(function(_e) {
|
|
||||||
var media_div = $(this).parent();
|
|
||||||
var icon = $(this).find("i")[0];
|
|
||||||
var f_loc = media_div.attr("data-location");
|
|
||||||
var fname = media_div.attr("data-filename");
|
|
||||||
var f_id = media_div.attr("data-id");
|
|
||||||
$("#media-delete").attr("data-id", f_id);
|
|
||||||
$("#media-link").val(f_loc);
|
|
||||||
$("#media-filename").html(
|
|
||||||
$("<a>")
|
|
||||||
.attr("href", f_loc)
|
|
||||||
.attr("target", "_blank")
|
|
||||||
.text(fname)
|
|
||||||
);
|
|
||||||
|
|
||||||
$("#media-icon").empty();
|
|
||||||
if ($(icon).hasClass("fa-file-image")) {
|
|
||||||
$("#media-icon").append(
|
|
||||||
$("<img>")
|
|
||||||
.attr("src", f_loc)
|
|
||||||
.css({
|
|
||||||
"max-width": "100%",
|
|
||||||
"max-height": "100%",
|
|
||||||
"object-fit": "contain"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// icon is empty so we need to pull outerHTML
|
|
||||||
var copy_icon = $(icon).clone();
|
|
||||||
$(copy_icon).addClass("fa-4x");
|
|
||||||
$("#media-icon").append(copy_icon);
|
|
||||||
}
|
|
||||||
$("#media-item").show();
|
|
||||||
});
|
|
||||||
wrapper.append(link);
|
|
||||||
wrapper.attr("data-location", CTFd.config.urlRoot + "/files/" + f.location);
|
|
||||||
wrapper.attr("data-id", f.id);
|
|
||||||
wrapper.attr("data-filename", fname);
|
|
||||||
list.append(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh_files(cb) {
|
|
||||||
get_page_files().then(function(response) {
|
|
||||||
var data = response.data;
|
|
||||||
show_files(data);
|
|
||||||
if (cb) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function insert_at_cursor(editor, text) {
|
|
||||||
var doc = editor.getDoc();
|
|
||||||
var cursor = doc.getCursor();
|
|
||||||
doc.replaceRange(text, cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit_form() {
|
function submit_form() {
|
||||||
// Save the CodeMirror data to the Textarea
|
// Save the CodeMirror data to the Textarea
|
||||||
@@ -196,12 +52,6 @@ function preview_page() {
|
|||||||
$("#page-edit").submit();
|
$("#page-edit").submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function upload_media() {
|
|
||||||
helpers.files.upload($("#media-library-upload"), {}, function(_data) {
|
|
||||||
refresh_files();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
window.editor = CodeMirror.fromTextArea(
|
window.editor = CodeMirror.fromTextArea(
|
||||||
document.getElementById("admin-pages-editor"),
|
document.getElementById("admin-pages-editor"),
|
||||||
@@ -213,55 +63,8 @@ $(() => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$("#media-insert").click(function(_e) {
|
$("#media-button").click(function(_e) {
|
||||||
var tag = "";
|
showMediaLibrary(window.editor);
|
||||||
try {
|
|
||||||
tag = $("#media-icon")
|
|
||||||
.children()[0]
|
|
||||||
.nodeName.toLowerCase();
|
|
||||||
} catch (err) {
|
|
||||||
tag = "";
|
|
||||||
}
|
|
||||||
var link = $("#media-link").val();
|
|
||||||
var fname = $("#media-filename").text();
|
|
||||||
var entry = null;
|
|
||||||
if (tag === "img") {
|
|
||||||
entry = "".format(fname, link);
|
|
||||||
} else {
|
|
||||||
entry = "[{0}]({1})".format(fname, link);
|
|
||||||
}
|
|
||||||
insert_at_cursor(window.editor, entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#media-download").click(function(_e) {
|
|
||||||
var link = $("#media-link").val();
|
|
||||||
window.open(link, "_blank");
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#media-delete").click(function(_e) {
|
|
||||||
var file_id = $(this).attr("data-id");
|
|
||||||
ezQuery({
|
|
||||||
title: "Delete File?",
|
|
||||||
body: "Are you sure you want to delete this file?",
|
|
||||||
success: function() {
|
|
||||||
CTFd.fetch("/api/v1/files/" + file_id, {
|
|
||||||
method: "DELETE",
|
|
||||||
credentials: "same-origin",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}).then(function(response) {
|
|
||||||
if (response.status === 200) {
|
|
||||||
response.json().then(function(object) {
|
|
||||||
if (object.success) {
|
|
||||||
refresh_files();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#save-page").click(function(e) {
|
$("#save-page").click(function(e) {
|
||||||
@@ -269,17 +72,6 @@ $(() => {
|
|||||||
submit_form();
|
submit_form();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#media-button").click(function() {
|
|
||||||
$("#media-library-list").empty();
|
|
||||||
refresh_files(function() {
|
|
||||||
$("#media-modal").modal();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".media-upload-button").click(function() {
|
|
||||||
upload_media();
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".preview-page").click(function() {
|
$(".preview-page").click(function() {
|
||||||
preview_page();
|
preview_page();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,33 @@ import "bootstrap/dist/js/bootstrap.bundle";
|
|||||||
import { makeSortableTables } from "core/utils";
|
import { makeSortableTables } from "core/utils";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import EasyMDE from "easymde";
|
import EasyMDE from "easymde";
|
||||||
|
import Vue from "vue/dist/vue.esm.browser";
|
||||||
|
import MediaLibrary from "./components/files/MediaLibrary.vue";
|
||||||
|
|
||||||
|
export function showMediaLibrary(editor) {
|
||||||
|
const mediaModal = Vue.extend(MediaLibrary);
|
||||||
|
|
||||||
|
// Create an empty div and append it to our <main>
|
||||||
|
let vueContainer = document.createElement("div");
|
||||||
|
document.querySelector("main").appendChild(vueContainer);
|
||||||
|
|
||||||
|
// Create MediaLibrary component and pass it our editor
|
||||||
|
let m = new mediaModal({
|
||||||
|
propsData: {
|
||||||
|
editor: editor
|
||||||
|
}
|
||||||
|
// Mount to the empty div
|
||||||
|
}).$mount(vueContainer);
|
||||||
|
|
||||||
|
// Destroy the Vue instance and the media modal when closed
|
||||||
|
$("#media-modal").on("hidden.bs.modal", function(_e) {
|
||||||
|
m.$destroy();
|
||||||
|
$("#media-modal").remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pop the Component modal
|
||||||
|
$("#media-modal").modal();
|
||||||
|
}
|
||||||
|
|
||||||
export function bindMarkdownEditors() {
|
export function bindMarkdownEditors() {
|
||||||
$("textarea.markdown").each(function(_i, e) {
|
$("textarea.markdown").each(function(_i, e) {
|
||||||
@@ -19,6 +46,14 @@ export function bindMarkdownEditors() {
|
|||||||
"|",
|
"|",
|
||||||
"link",
|
"link",
|
||||||
"image",
|
"image",
|
||||||
|
{
|
||||||
|
name: "media",
|
||||||
|
action: editor => {
|
||||||
|
showMediaLibrary(editor);
|
||||||
|
},
|
||||||
|
className: "fas fa-file-upload",
|
||||||
|
title: "Media Library"
|
||||||
|
},
|
||||||
"|",
|
"|",
|
||||||
"preview",
|
"preview",
|
||||||
"guide"
|
"guide"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
35
CTFd/themes/admin/static/js/vendor.bundle.min.js
vendored
35
CTFd/themes/admin/static/js/vendor.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -4,94 +4,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="media-modal" class="modal fade" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<h3 class="text-center">Media Library</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row mh-100">
|
|
||||||
<div class="col-md-6" id="media-library-list">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6" id="media-library-details">
|
|
||||||
<h4 class="text-center">Media Details</h4>
|
|
||||||
<div id="media-item">
|
|
||||||
<div class="text-center" id="media-icon">
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="text-center" id="media-filename">
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="form-group">
|
|
||||||
Link: <input class="form-control" type="text" id="media-link" readonly>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group text-center">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<button class="btn btn-success w-100" id="media-insert"
|
|
||||||
data-toggle="tooltip" data-placement="top"
|
|
||||||
title="Insert link into editor">
|
|
||||||
Insert
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<button class="btn btn-primary w-100" id="media-download"
|
|
||||||
data-toggle="tooltip" data-placement="top"
|
|
||||||
title="Download file">
|
|
||||||
<i class="fas fa-download"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<button class="btn btn-danger w-100" id="media-delete"
|
|
||||||
data-toggle="tooltip" data-placement="top"
|
|
||||||
title="Delete file">
|
|
||||||
<i class="far fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% with form = Forms.pages.PageFilesUploadForm() %}
|
|
||||||
<form id="media-library-upload" enctype="multipart/form-data">
|
|
||||||
<div class="form-group">
|
|
||||||
<b>{{ form.file.label }}</b>
|
|
||||||
{{ form.file(id="media-files", class="form-control-file") }}
|
|
||||||
<sub class="help-block">
|
|
||||||
{{ form.file.description }}
|
|
||||||
</sub>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" value="page" name="type">
|
|
||||||
</form>
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="float-right">
|
|
||||||
<button type="submit" class="btn btn-primary media-upload-button">Upload</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row pt-5">
|
<div class="row pt-5">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
|||||||
@@ -83,6 +83,16 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if max_attempts > 1 %}
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<p>
|
||||||
|
{{ attempts }}/{{ max_attempts }} attempt{{ attempts|pluralize(attempts) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row submit-row">
|
<div class="row submit-row">
|
||||||
<div class="col-md-9 form-group">
|
<div class="col-md-9 form-group">
|
||||||
{% block input %}
|
{% block input %}
|
||||||
|
|||||||
5
CTFd/utils/humanize/words.py
Normal file
5
CTFd/utils/humanize/words.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
def pluralize(number, singular="", plural="s"):
|
||||||
|
if number == 1:
|
||||||
|
return singular
|
||||||
|
else:
|
||||||
|
return plural
|
||||||
@@ -23,6 +23,7 @@ from CTFd.utils.config.pages import get_pages
|
|||||||
from CTFd.utils.countries import get_countries, lookup_country_code
|
from CTFd.utils.countries import get_countries, lookup_country_code
|
||||||
from CTFd.utils.dates import isoformat, unix_time, unix_time_millis
|
from CTFd.utils.dates import isoformat, unix_time, unix_time_millis
|
||||||
from CTFd.utils.events import EventManager, RedisEventManager
|
from CTFd.utils.events import EventManager, RedisEventManager
|
||||||
|
from CTFd.utils.humanize.words import pluralize
|
||||||
from CTFd.utils.modes import generate_account_url, get_mode_as_word
|
from CTFd.utils.modes import generate_account_url, get_mode_as_word
|
||||||
from CTFd.utils.plugins import (
|
from CTFd.utils.plugins import (
|
||||||
get_configurable_plugins,
|
get_configurable_plugins,
|
||||||
@@ -48,12 +49,15 @@ def init_template_filters(app):
|
|||||||
app.jinja_env.filters["unix_time"] = unix_time
|
app.jinja_env.filters["unix_time"] = unix_time
|
||||||
app.jinja_env.filters["unix_time_millis"] = unix_time_millis
|
app.jinja_env.filters["unix_time_millis"] = unix_time_millis
|
||||||
app.jinja_env.filters["isoformat"] = isoformat
|
app.jinja_env.filters["isoformat"] = isoformat
|
||||||
|
app.jinja_env.filters["pluralize"] = pluralize
|
||||||
|
|
||||||
|
|
||||||
def init_template_globals(app):
|
def init_template_globals(app):
|
||||||
from CTFd.constants.config import Configs
|
from CTFd.constants.config import Configs
|
||||||
from CTFd.constants.plugins import Plugins
|
from CTFd.constants.plugins import Plugins
|
||||||
from CTFd.constants.sessions import Session
|
from CTFd.constants.sessions import Session
|
||||||
|
from CTFd.constants.users import User
|
||||||
|
from CTFd.constants.teams import Team
|
||||||
from CTFd.forms import Forms
|
from CTFd.forms import Forms
|
||||||
from CTFd.utils.config.visibility import (
|
from CTFd.utils.config.visibility import (
|
||||||
accounts_visible,
|
accounts_visible,
|
||||||
@@ -96,6 +100,8 @@ def init_template_globals(app):
|
|||||||
app.jinja_env.globals.update(Plugins=Plugins)
|
app.jinja_env.globals.update(Plugins=Plugins)
|
||||||
app.jinja_env.globals.update(Session=Session)
|
app.jinja_env.globals.update(Session=Session)
|
||||||
app.jinja_env.globals.update(Forms=Forms)
|
app.jinja_env.globals.update(Forms=Forms)
|
||||||
|
app.jinja_env.globals.update(User=User)
|
||||||
|
app.jinja_env.globals.update(Team=Team)
|
||||||
|
|
||||||
|
|
||||||
def init_logs(app):
|
def init_logs(app):
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -3,11 +3,13 @@ lint:
|
|||||||
yarn lint
|
yarn lint
|
||||||
black --check --exclude=CTFd/uploads --exclude=node_modules .
|
black --check --exclude=CTFd/uploads --exclude=node_modules .
|
||||||
prettier --check 'CTFd/themes/**/assets/**/*'
|
prettier --check 'CTFd/themes/**/assets/**/*'
|
||||||
|
prettier --check '**/*.md'
|
||||||
|
|
||||||
format:
|
format:
|
||||||
isort --skip=CTFd/uploads -rc CTFd/ tests/
|
isort --skip=CTFd/uploads -rc CTFd/ tests/
|
||||||
black --exclude=CTFd/uploads --exclude=node_modules .
|
black --exclude=CTFd/uploads --exclude=node_modules .
|
||||||
prettier --write 'CTFd/themes/**/assets/**/*'
|
prettier --write 'CTFd/themes/**/assets/**/*'
|
||||||
|
prettier --write '**/*.md'
|
||||||
|
|
||||||
test:
|
test:
|
||||||
pytest -rf --cov=CTFd --cov-context=test --ignore=node_modules/ \
|
pytest -rf --cov=CTFd --cov-context=test --ignore=node_modules/ \
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -1,47 +1,49 @@
|
|||||||

|
# 
|
||||||
====
|
|
||||||
|
|
||||||
[](https://travis-ci.org/CTFd/CTFd)
|
[](https://travis-ci.org/CTFd/CTFd)
|
||||||
[](https://community.majorleaguecyber.org/)
|
[](https://community.majorleaguecyber.org/)
|
||||||
[](https://docs.ctfd.io/en/latest/?badge=latest)
|
[](https://docs.ctfd.io/en/latest/?badge=latest)
|
||||||
|
|
||||||
## What is CTFd?
|
## What is CTFd?
|
||||||
|
|
||||||
CTFd is a Capture The Flag framework focusing on ease of use and customizability. It comes with everything you need to run a CTF and it's easy to customize with plugins and themes.
|
CTFd is a Capture The Flag framework focusing on ease of use and customizability. It comes with everything you need to run a CTF and it's easy to customize with plugins and themes.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Create your own challenges, categories, hints, and flags from the Admin Interface
|
|
||||||
* Dynamic Scoring Challenges
|
- Create your own challenges, categories, hints, and flags from the Admin Interface
|
||||||
* Unlockable challenge support
|
- Dynamic Scoring Challenges
|
||||||
* Challenge plugin architecture to create your own custom challenges
|
- Unlockable challenge support
|
||||||
* Static & Regex based flags
|
- Challenge plugin architecture to create your own custom challenges
|
||||||
* Custom flag plugins
|
- Static & Regex based flags
|
||||||
* Unlockable hints
|
- Custom flag plugins
|
||||||
* File uploads to the server or an Amazon S3-compatible backend
|
- Unlockable hints
|
||||||
* Limit challenge attempts & hide challenges
|
- File uploads to the server or an Amazon S3-compatible backend
|
||||||
* Automatic bruteforce protection
|
- Limit challenge attempts & hide challenges
|
||||||
* Individual and Team based competitions
|
- Automatic bruteforce protection
|
||||||
* Have users play on their own or form teams to play together
|
- Individual and Team based competitions
|
||||||
* Scoreboard with automatic tie resolution
|
- Have users play on their own or form teams to play together
|
||||||
* Hide Scores from the public
|
- Scoreboard with automatic tie resolution
|
||||||
* Freeze Scores at a specific time
|
- Hide Scores from the public
|
||||||
* Scoregraphs comparing the top 10 teams and team progress graphs
|
- Freeze Scores at a specific time
|
||||||
* Markdown content management system
|
- Scoregraphs comparing the top 10 teams and team progress graphs
|
||||||
* SMTP + Mailgun email support
|
- Markdown content management system
|
||||||
* Email confirmation support
|
- SMTP + Mailgun email support
|
||||||
* Forgot password support
|
- Email confirmation support
|
||||||
* Automatic competition starting and ending
|
- Forgot password support
|
||||||
* Team management, hiding, and banning
|
- Automatic competition starting and ending
|
||||||
* Customize everything using the [plugin](https://github.com/CTFd/CTFd/wiki/Plugins) and [theme](https://github.com/CTFd/CTFd/tree/master/CTFd/themes) interfaces
|
- Team management, hiding, and banning
|
||||||
* Importing and Exporting of CTF data for archival
|
- Customize everything using the [plugin](https://github.com/CTFd/CTFd/wiki/Plugins) and [theme](https://github.com/CTFd/CTFd/tree/master/CTFd/themes) interfaces
|
||||||
* And a lot more...
|
- Importing and Exporting of CTF data for archival
|
||||||
|
- And a lot more...
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
1. Install dependencies: `pip install -r requirements.txt`
|
|
||||||
|
1. Install dependencies: `pip install -r requirements.txt`
|
||||||
1. You can also use the `prepare.sh` script to install system dependencies using apt.
|
1. You can also use the `prepare.sh` script to install system dependencies using apt.
|
||||||
2. Modify [CTFd/config.py](https://github.com/CTFd/CTFd/blob/master/CTFd/config.py) to your liking.
|
2. Modify [CTFd/config.py](https://github.com/CTFd/CTFd/blob/master/CTFd/config.py) to your liking.
|
||||||
3. Use `flask run` in a terminal to drop into debug mode.
|
3. Use `flask run` in a terminal to drop into debug mode.
|
||||||
|
|
||||||
You can use the auto-generated Docker images with the following command:
|
You can use the auto-generated Docker images with the following command:
|
||||||
|
|
||||||
@@ -54,17 +56,21 @@ Or you can use Docker Compose with the following command from the source reposit
|
|||||||
Check out the [wiki](https://github.com/CTFd/CTFd/wiki) for [deployment options](https://github.com/CTFd/CTFd/wiki/Basic-Deployment) and the [Getting Started](https://github.com/CTFd/CTFd/wiki/Getting-Started) guide
|
Check out the [wiki](https://github.com/CTFd/CTFd/wiki) for [deployment options](https://github.com/CTFd/CTFd/wiki/Basic-Deployment) and the [Getting Started](https://github.com/CTFd/CTFd/wiki/Getting-Started) guide
|
||||||
|
|
||||||
## Live Demo
|
## Live Demo
|
||||||
|
|
||||||
https://demo.ctfd.io/
|
https://demo.ctfd.io/
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
To get basic support, you can join the [MajorLeagueCyber Community](https://community.majorleaguecyber.org/): [](https://community.majorleaguecyber.org/)
|
To get basic support, you can join the [MajorLeagueCyber Community](https://community.majorleaguecyber.org/): [](https://community.majorleaguecyber.org/)
|
||||||
|
|
||||||
If you prefer commercial support or have a special project, feel free to [contact us](https://ctfd.io/contact/).
|
If you prefer commercial support or have a special project, feel free to [contact us](https://ctfd.io/contact/).
|
||||||
|
|
||||||
## Managed Hosting
|
## Managed Hosting
|
||||||
|
|
||||||
Looking to use CTFd but don't want to deal with managing infrastructure? Check out [the CTFd website](https://ctfd.io/) for managed CTFd deployments.
|
Looking to use CTFd but don't want to deal with managing infrastructure? Check out [the CTFd website](https://ctfd.io/) for managed CTFd deployments.
|
||||||
|
|
||||||
## MajorLeagueCyber
|
## MajorLeagueCyber
|
||||||
|
|
||||||
CTFd is heavily integrated with [MajorLeagueCyber](https://majorleaguecyber.org/). MajorLeagueCyber (MLC) is a CTF stats tracker that provides event scheduling, team tracking, and single sign on for events.
|
CTFd is heavily integrated with [MajorLeagueCyber](https://majorleaguecyber.org/). MajorLeagueCyber (MLC) is a CTF stats tracker that provides event scheduling, team tracking, and single sign on for events.
|
||||||
|
|
||||||
By registering your CTF event with MajorLeagueCyber users can automatically login, track their individual and team scores, submit writeups, and get notifications of important events.
|
By registering your CTF event with MajorLeagueCyber users can automatically login, track their individual and team scores, submit writeups, and get notifications of important events.
|
||||||
@@ -77,6 +83,7 @@ OAUTH_CLIENT_SECRET = None
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
* Logo by [Laura Barbera](http://www.laurabb.com/)
|
|
||||||
* Theme by [Christopher Thompson](https://github.com/breadchris)
|
- Logo by [Laura Barbera](http://www.laurabb.com/)
|
||||||
* Notification Sound by [Terrence Martin](https://soundcloud.com/tj-martin-composer)
|
- Theme by [Christopher Thompson](https://github.com/breadchris)
|
||||||
|
- Notification Sound by [Terrence Martin](https://soundcloud.com/tj-martin-composer)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"bootstrap": "~4.3.1",
|
"bootstrap": "~4.3.1",
|
||||||
"bootstrap-multimodal": "~1.0.4",
|
"bootstrap-multimodal": "~1.0.4",
|
||||||
"codemirror": "~5.42.2",
|
"codemirror": "~5.42.2",
|
||||||
"css-loader": "~2.1.0",
|
"css-loader": "^3.6.0",
|
||||||
"easymde": "^2.10.1",
|
"easymde": "^2.10.1",
|
||||||
"echarts": "^4.8.0",
|
"echarts": "^4.8.0",
|
||||||
"eslint": "~5.12.0",
|
"eslint": "~5.12.0",
|
||||||
@@ -54,6 +54,10 @@
|
|||||||
"typeface-lato": "~0.0.54",
|
"typeface-lato": "~0.0.54",
|
||||||
"typeface-raleway": "~0.0.54",
|
"typeface-raleway": "~0.0.54",
|
||||||
"uglifyjs-webpack-plugin": "~2.1.1",
|
"uglifyjs-webpack-plugin": "~2.1.1",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-loader": "15.9.3",
|
||||||
|
"vue-style-loader": "^4.1.2",
|
||||||
|
"vue-template-compiler": "^2.6.11",
|
||||||
"webpack": "~4.28.1",
|
"webpack": "~4.28.1",
|
||||||
"webpack-cli": "~3.2.1",
|
"webpack-cli": "~3.2.1",
|
||||||
"webpack-fix-style-only-entries": "~0.3.0",
|
"webpack-fix-style-only-entries": "~0.3.0",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
|
|||||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||||
const RemoveStrictPlugin = require('remove-strict-webpack-plugin')
|
const RemoveStrictPlugin = require('remove-strict-webpack-plugin')
|
||||||
const WebpackShellPlugin = require('webpack-shell-plugin');
|
const WebpackShellPlugin = require('webpack-shell-plugin');
|
||||||
|
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||||
|
|
||||||
const roots = {
|
const roots = {
|
||||||
'themes/core': {
|
'themes/core': {
|
||||||
@@ -132,9 +133,25 @@ function getJSConfig(root, type, entries, mode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
options: {
|
||||||
|
loaders: {
|
||||||
|
css: ['vue-style-loader', {
|
||||||
|
loader: 'css-loader',
|
||||||
|
}],
|
||||||
|
js: [
|
||||||
|
'babel-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cacheBusting: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new VueLoaderPlugin(),
|
||||||
new webpack.NamedModulesPlugin(),
|
new webpack.NamedModulesPlugin(),
|
||||||
new RemoveStrictPlugin(),
|
new RemoveStrictPlugin(),
|
||||||
// Identify files that are generated in development but not in production and create stubs to avoid a 404
|
// Identify files that are generated in development but not in production and create stubs to avoid a 404
|
||||||
@@ -191,6 +208,13 @@ function getCSSConfig(root, type, entries, mode) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// test: /\.css$/,
|
||||||
|
// use: [
|
||||||
|
// 'vue-style-loader',
|
||||||
|
// 'css-loader'
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
test: /\.(s?)css$/,
|
test: /\.(s?)css$/,
|
||||||
use: [
|
use: [
|
||||||
|
|||||||
206
yarn.lock
206
yarn.lock
@@ -599,11 +599,32 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz#8644bc25b19475779a7b7c1fc104bc0a794f4465"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz#8644bc25b19475779a7b7c1fc104bc0a794f4465"
|
||||||
integrity sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ==
|
integrity sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ==
|
||||||
|
|
||||||
|
"@types/json-schema@^7.0.4":
|
||||||
|
version "7.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
|
||||||
|
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
|
||||||
|
|
||||||
"@types/q@^1.5.1":
|
"@types/q@^1.5.1":
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||||
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
|
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
|
||||||
|
|
||||||
|
"@vue/component-compiler-utils@^3.1.0":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz#8213a5ff3202f9f2137fe55370f9e8b9656081c3"
|
||||||
|
integrity sha512-QLq9z8m79mCinpaEeSURhnNCN6djxpHw0lpP/bodMlt5kALfONpryMthvnrQOlTcIKoF+VoPi+lPHUYeDFPXug==
|
||||||
|
dependencies:
|
||||||
|
consolidate "^0.15.1"
|
||||||
|
hash-sum "^1.0.2"
|
||||||
|
lru-cache "^4.1.2"
|
||||||
|
merge-source-map "^1.1.0"
|
||||||
|
postcss "^7.0.14"
|
||||||
|
postcss-selector-parser "^6.0.2"
|
||||||
|
source-map "~0.6.1"
|
||||||
|
vue-template-es2015-compiler "^1.9.0"
|
||||||
|
optionalDependencies:
|
||||||
|
prettier "^1.18.2"
|
||||||
|
|
||||||
"@webassemblyjs/ast@1.7.11":
|
"@webassemblyjs/ast@1.7.11":
|
||||||
version "1.7.11"
|
version "1.7.11"
|
||||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
|
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
|
||||||
@@ -799,6 +820,11 @@ ajv-keywords@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d"
|
||||||
integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==
|
integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==
|
||||||
|
|
||||||
|
ajv-keywords@^3.4.1:
|
||||||
|
version "3.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773"
|
||||||
|
integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==
|
||||||
|
|
||||||
ajv@^6.1.0, ajv@^6.5.3, ajv@^6.9.1:
|
ajv@^6.1.0, ajv@^6.5.3, ajv@^6.9.1:
|
||||||
version "6.10.0"
|
version "6.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
|
||||||
@@ -809,7 +835,7 @@ ajv@^6.1.0, ajv@^6.5.3, ajv@^6.9.1:
|
|||||||
json-schema-traverse "^0.4.1"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ajv@^6.5.5:
|
ajv@^6.12.2, ajv@^6.5.5:
|
||||||
version "6.12.2"
|
version "6.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
||||||
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
|
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
|
||||||
@@ -1064,6 +1090,11 @@ block-stream@*:
|
|||||||
dependencies:
|
dependencies:
|
||||||
inherits "~2.0.0"
|
inherits "~2.0.0"
|
||||||
|
|
||||||
|
bluebird@^3.1.1:
|
||||||
|
version "3.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
bluebird@^3.5.5:
|
bluebird@^3.5.5:
|
||||||
version "3.5.5"
|
version "3.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
||||||
@@ -1300,7 +1331,7 @@ camelcase@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
|
||||||
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
|
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
|
||||||
|
|
||||||
camelcase@^5.0.0, camelcase@^5.2.0:
|
camelcase@^5.0.0, camelcase@^5.3.1:
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
@@ -1616,6 +1647,13 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||||
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
|
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
|
||||||
|
|
||||||
|
consolidate@^0.15.1:
|
||||||
|
version "0.15.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
|
||||||
|
integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==
|
||||||
|
dependencies:
|
||||||
|
bluebird "^3.1.1"
|
||||||
|
|
||||||
constants-browserify@^1.0.0:
|
constants-browserify@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||||
@@ -1766,22 +1804,24 @@ css-declaration-sorter@^4.0.1:
|
|||||||
postcss "^7.0.1"
|
postcss "^7.0.1"
|
||||||
timsort "^0.3.0"
|
timsort "^0.3.0"
|
||||||
|
|
||||||
css-loader@~2.1.0:
|
css-loader@^3.6.0:
|
||||||
version "2.1.1"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
|
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
|
||||||
integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==
|
integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase "^5.2.0"
|
camelcase "^5.3.1"
|
||||||
icss-utils "^4.1.0"
|
cssesc "^3.0.0"
|
||||||
|
icss-utils "^4.1.1"
|
||||||
loader-utils "^1.2.3"
|
loader-utils "^1.2.3"
|
||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
postcss "^7.0.14"
|
postcss "^7.0.32"
|
||||||
postcss-modules-extract-imports "^2.0.0"
|
postcss-modules-extract-imports "^2.0.0"
|
||||||
postcss-modules-local-by-default "^2.0.6"
|
postcss-modules-local-by-default "^3.0.2"
|
||||||
postcss-modules-scope "^2.1.0"
|
postcss-modules-scope "^2.2.0"
|
||||||
postcss-modules-values "^2.0.0"
|
postcss-modules-values "^3.0.0"
|
||||||
postcss-value-parser "^3.3.0"
|
postcss-value-parser "^4.1.0"
|
||||||
schema-utils "^1.0.0"
|
schema-utils "^2.7.0"
|
||||||
|
semver "^6.3.0"
|
||||||
|
|
||||||
css-select-base-adapter@^0.1.1:
|
css-select-base-adapter@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
@@ -1938,6 +1978,11 @@ date-now@^0.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||||
integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
|
integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
|
||||||
|
|
||||||
|
de-indent@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||||
|
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
|
||||||
|
|
||||||
debug@^2.2.0, debug@^2.3.3:
|
debug@^2.2.0, debug@^2.3.3:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
@@ -2906,6 +2951,11 @@ hash-base@^3.0.0:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
hash-sum@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
|
||||||
|
integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
|
||||||
|
|
||||||
hash.js@^1.0.0, hash.js@^1.0.3:
|
hash.js@^1.0.0, hash.js@^1.0.3:
|
||||||
version "1.1.7"
|
version "1.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
||||||
@@ -2914,6 +2964,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
|||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
minimalistic-assert "^1.0.1"
|
minimalistic-assert "^1.0.1"
|
||||||
|
|
||||||
|
he@^1.1.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
|
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||||
|
|
||||||
hex-color-regex@^1.1.0:
|
hex-color-regex@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
@@ -2992,12 +3047,7 @@ iconv-lite@^0.4.24, iconv-lite@^0.4.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
icss-replace-symbols@^1.1.0:
|
icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
|
||||||
integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
|
|
||||||
|
|
||||||
icss-utils@^4.1.0:
|
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
|
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
|
||||||
integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
|
integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
|
||||||
@@ -3711,7 +3761,7 @@ lowercase-keys@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||||
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
|
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
|
||||||
|
|
||||||
lru-cache@^4.0.1, lru-cache@^4.1.5:
|
lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.5:
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
||||||
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
|
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
|
||||||
@@ -3833,6 +3883,13 @@ meow@^3.7.0:
|
|||||||
redent "^1.0.0"
|
redent "^1.0.0"
|
||||||
trim-newlines "^1.0.0"
|
trim-newlines "^1.0.0"
|
||||||
|
|
||||||
|
merge-source-map@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
|
||||||
|
integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==
|
||||||
|
dependencies:
|
||||||
|
source-map "^0.6.1"
|
||||||
|
|
||||||
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
|
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
|
||||||
version "3.1.10"
|
version "3.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
||||||
@@ -4753,29 +4810,30 @@ postcss-modules-extract-imports@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.5"
|
postcss "^7.0.5"
|
||||||
|
|
||||||
postcss-modules-local-by-default@^2.0.6:
|
postcss-modules-local-by-default@^3.0.2:
|
||||||
version "2.0.6"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63"
|
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
|
||||||
integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==
|
integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.6"
|
icss-utils "^4.1.1"
|
||||||
postcss-selector-parser "^6.0.0"
|
postcss "^7.0.16"
|
||||||
postcss-value-parser "^3.3.1"
|
postcss-selector-parser "^6.0.2"
|
||||||
|
postcss-value-parser "^4.0.0"
|
||||||
|
|
||||||
postcss-modules-scope@^2.1.0:
|
postcss-modules-scope@^2.2.0:
|
||||||
version "2.1.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb"
|
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
|
||||||
integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==
|
integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.6"
|
postcss "^7.0.6"
|
||||||
postcss-selector-parser "^6.0.0"
|
postcss-selector-parser "^6.0.0"
|
||||||
|
|
||||||
postcss-modules-values@^2.0.0:
|
postcss-modules-values@^3.0.0:
|
||||||
version "2.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64"
|
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
|
||||||
integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==
|
integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
|
||||||
dependencies:
|
dependencies:
|
||||||
icss-replace-symbols "^1.1.0"
|
icss-utils "^4.0.0"
|
||||||
postcss "^7.0.6"
|
postcss "^7.0.6"
|
||||||
|
|
||||||
postcss-normalize-charset@^4.0.1:
|
postcss-normalize-charset@^4.0.1:
|
||||||
@@ -4906,7 +4964,7 @@ postcss-selector-parser@^5.0.0-rc.4:
|
|||||||
indexes-of "^1.0.1"
|
indexes-of "^1.0.1"
|
||||||
uniq "^1.0.1"
|
uniq "^1.0.1"
|
||||||
|
|
||||||
postcss-selector-parser@^6.0.0:
|
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
||||||
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
|
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
|
||||||
@@ -4934,11 +4992,16 @@ postcss-unique-selectors@^4.0.1:
|
|||||||
postcss "^7.0.0"
|
postcss "^7.0.0"
|
||||||
uniqs "^2.0.0"
|
uniqs "^2.0.0"
|
||||||
|
|
||||||
postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1:
|
postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.1:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||||
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
||||||
|
|
||||||
|
postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||||
|
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||||
|
|
||||||
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.5, postcss@^7.0.6:
|
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.5, postcss@^7.0.6:
|
||||||
version "7.0.17"
|
version "7.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f"
|
||||||
@@ -4948,6 +5011,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.5, postcss@^7.0.6:
|
|||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
supports-color "^6.1.0"
|
supports-color "^6.1.0"
|
||||||
|
|
||||||
|
postcss@^7.0.16, postcss@^7.0.32:
|
||||||
|
version "7.0.32"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
|
||||||
|
integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
|
||||||
|
dependencies:
|
||||||
|
chalk "^2.4.2"
|
||||||
|
source-map "^0.6.1"
|
||||||
|
supports-color "^6.1.0"
|
||||||
|
|
||||||
prelude-ls@~1.1.2:
|
prelude-ls@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
@@ -4958,6 +5030,11 @@ prepend-http@^1.0.0, prepend-http@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||||
|
|
||||||
|
prettier@^1.18.2:
|
||||||
|
version "1.19.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||||
|
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||||
|
|
||||||
private@^0.1.6:
|
private@^0.1.6:
|
||||||
version "0.1.8"
|
version "0.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
|
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
|
||||||
@@ -5505,6 +5582,15 @@ schema-utils@^2.1.0:
|
|||||||
ajv "^6.1.0"
|
ajv "^6.1.0"
|
||||||
ajv-keywords "^3.1.0"
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
|
schema-utils@^2.7.0:
|
||||||
|
version "2.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
|
||||||
|
integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
|
||||||
|
dependencies:
|
||||||
|
"@types/json-schema" "^7.0.4"
|
||||||
|
ajv "^6.12.2"
|
||||||
|
ajv-keywords "^3.4.1"
|
||||||
|
|
||||||
scss-tokenizer@^0.2.3:
|
scss-tokenizer@^0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
|
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
|
||||||
@@ -6446,6 +6532,48 @@ vm-browserify@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
|
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
|
||||||
integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
|
integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
|
||||||
|
|
||||||
|
vue-hot-reload-api@^2.3.0:
|
||||||
|
version "2.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
||||||
|
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
|
||||||
|
|
||||||
|
vue-loader@15.9.3:
|
||||||
|
version "15.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.3.tgz#0de35d9e555d3ed53969516cac5ce25531299dda"
|
||||||
|
integrity sha512-Y67VnGGgVLH5Voostx8JBZgPQTlDQeOVBLOEsjc2cXbCYBKexSKEpOA56x0YZofoDOTszrLnIShyOX1p9uCEHA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/component-compiler-utils" "^3.1.0"
|
||||||
|
hash-sum "^1.0.2"
|
||||||
|
loader-utils "^1.1.0"
|
||||||
|
vue-hot-reload-api "^2.3.0"
|
||||||
|
vue-style-loader "^4.1.0"
|
||||||
|
|
||||||
|
vue-style-loader@^4.1.0, vue-style-loader@^4.1.2:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"
|
||||||
|
integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ==
|
||||||
|
dependencies:
|
||||||
|
hash-sum "^1.0.2"
|
||||||
|
loader-utils "^1.0.2"
|
||||||
|
|
||||||
|
vue-template-compiler@^2.6.11:
|
||||||
|
version "2.6.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
|
||||||
|
integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
|
||||||
|
dependencies:
|
||||||
|
de-indent "^1.0.2"
|
||||||
|
he "^1.1.0"
|
||||||
|
|
||||||
|
vue-template-es2015-compiler@^1.9.0:
|
||||||
|
version "1.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||||
|
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||||
|
|
||||||
|
vue@^2.6.11:
|
||||||
|
version "2.6.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
||||||
|
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
||||||
|
|
||||||
watchpack@^1.5.0:
|
watchpack@^1.5.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
||||||
|
|||||||
Reference in New Issue
Block a user