mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
# 3.1.0 / 2020-09-08 **General** - Loosen team password confirmation in team settings to also accept the team captain's password to make it easier to change the team password - Adds the ability to add custom user and team fields for registration/profile settings. - Improve Notifications pubsub events system to use a subscriber per server instead of a subscriber per browser. This should improve the reliability of CTFd at higher load and make it easier to deploy the Notifications system **Admin Panel** - Add a comments functionality for admins to discuss challenges, users, teams, pages - Adds a legal section in Configs where users can add a terms of service and privacy policy - Add a Custom Fields section in Configs where admins can add/edit custom user/team fields - Move user graphs into a modal for Admin Panel **API** - Add `/api/v1/comments` to manipulate and create comments **Themes** - Make scoreboard caching only cache the score table instead of the entire page. This is done by caching the specific template section. Refer to #1586, specifically the changes in `scoreboard.html`. - Add rel=noopener to external links to prevent tab napping attacks - Change the registration page to reference links to Terms of Service and Privacy Policy if specified in configuration **Miscellaneous** - Make team settings modal larger in the core theme - Update tests in Github Actions to properly test under MySQL and Postgres - Make gevent default in serve.py and add a `--disable-gevent` switch in serve.py - Add `tenacity` library for retrying logic - Add `pytest-sugar` for slightly prettier pytest output - Add a `listen()` method to `CTFd.utils.events.EventManager` and `CTFd.utils.events.RedisEventManager`. - This method should implement subscription for a CTFd worker to whatever underlying notification system there is. This should be implemented with gevent or a background thread. - The `subscribe()` method (which used to implement the functionality of the new `listen()` function) now only handles passing notifications from CTFd to the browser. This should also be implemented with gevent or a background thread.
523 lines
18 KiB
HTML
523 lines
18 KiB
HTML
{% extends "admin/base.html" %}
|
|
|
|
{% block stylesheets %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="team-info-edit-modal" class="modal fade">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-action text-center w-100">Edit Team</h2>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body clearfix">
|
|
{% include "admin/modals/teams/edit.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="team-statistics-modal" class="modal fade">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-action text-center w-100">Team Statistics</h2>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body clearfix">
|
|
{% include "admin/modals/teams/statistics.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="team-captain-modal" class="modal fade">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-action text-center w-100">Choose Captain</h2>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body clearfix">
|
|
{% include "admin/modals/teams/captain.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="team-award-modal" class="modal fade">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-action text-center w-100">Award Team Member</h2>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body clearfix">
|
|
<div class="form-group">
|
|
<label for="award-member-input">Member</label>
|
|
<select class="form-control custom-select" id="award-member-input">
|
|
<option value=""> -- </option>
|
|
{% for member in members %}
|
|
<option value="{{ member.id }}">{{ member.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% include "admin/modals/awards/create.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template id="team-member-select">
|
|
<select class="form-control custom-select">
|
|
<option value=""> -- </option>
|
|
{% for member in members %}
|
|
<option value="{{ member.id }}">{{ member.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</template>
|
|
|
|
<div id="team-addresses-modal" class="modal fade">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-action text-center w-100">IP Addresses</h2>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body clearfix">
|
|
{% include "admin/modals/teams/addresses.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="jumbotron">
|
|
<div class="container">
|
|
<h1 id="team-id" class="text-center">{{ team.name }}</h1>
|
|
<div class="mb-2">
|
|
{% if team.verified %}
|
|
<span class="badge badge-success">verified</span>
|
|
{% endif %}
|
|
{% if team.hidden %}
|
|
<span class="badge badge-danger">hidden</span>
|
|
{% endif %}
|
|
{% if team.banned %}
|
|
<span class="badge badge-danger">banned</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if team.oauth_id %}
|
|
<a href="https://majorleaguecyber.org/t/{{ team.name }}">
|
|
<h3><span class="badge badge-primary">Official</span></h3>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if team.affiliation %}
|
|
<h3 class="d-inline-block">
|
|
<span class="badge badge-primary">{{ team.affiliation }}</span>
|
|
</h3>
|
|
{% endif %}
|
|
{% if team.country %}
|
|
<h3 class="d-inline-block">
|
|
<span class="badge badge-primary">
|
|
<i class="flag-{{ team.country.lower() }}"></i>
|
|
{{ lookup_country_code(team.country) }}
|
|
</span>
|
|
</h3>
|
|
{% endif %}
|
|
|
|
{% for field in team.get_fields(admin=true) %}
|
|
<h3 class="d-block">
|
|
{{ field.name }}: {{ field.value }}
|
|
</h3>
|
|
{% endfor %}
|
|
|
|
<h2 class="text-center">{{ members | length }} members</h2>
|
|
<h3 id="team-place" class="text-center">
|
|
{% if place %}
|
|
{{ place }}
|
|
<small>place</small>
|
|
{% endif %}
|
|
</h3>
|
|
<h3 id="team-score" class="text-center">
|
|
{% if score %}
|
|
{{ score }}
|
|
<small>points</small>
|
|
{% endif %}
|
|
</h3>
|
|
<hr class="w-50">
|
|
<div class="pt-3">
|
|
<a class="edit-team text-dark">
|
|
<i class="btn-fa fas fa-pencil-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
|
title="Edit Team"></i>
|
|
</a>
|
|
<a class="statistics-team text-dark">
|
|
<i class="btn-fa fas fa-chart-pie fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
|
title="Team Statistics"></i>
|
|
</a>
|
|
<a class="edit-captain text-dark">
|
|
<i class="btn-fa fas fa-user-tag fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
|
title="Choose Captain"></i>
|
|
</a>
|
|
<a class="award-team text-dark">
|
|
<i class="btn-fa fas fa-trophy fa-2x px-2" data-toggle="tooltip" data-placement="top" title="Award Team Member"></i>
|
|
</a>
|
|
<a class="delete-team text-dark">
|
|
<i class="btn-fa fas fa-trash-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
|
title="Delete Team"></i>
|
|
</a>
|
|
</div>
|
|
<div class="pt-3">
|
|
<a class="addresses-team text-dark">
|
|
<i class="btn-fa fas fa-network-wired fa-2x px-2" data-toggle="tooltip" data-placement="top" title="IP Addresses"></i>
|
|
</a>
|
|
{% if team.website %}
|
|
<a href="{{ team.website }}" target="_blank" class="text-dark" rel="noopener">
|
|
<i class="btn-fa fas fa-external-link-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
|
title="{{ team.website }}" aria-hidden="true"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="row min-vh-25 pt-5 pb-5">
|
|
<div class="col-md-12">
|
|
<table class="table table-striped">
|
|
<h3 class="text-center">Team Members</h3>
|
|
<thead>
|
|
<tr>
|
|
<td class="text-center"></td>
|
|
<td class="text-center"><b>User Name</b></td>
|
|
<td class="text-center"><b>E-Mail</b></td>
|
|
<td class="text-center"></td>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for member in members %}
|
|
<tr>
|
|
<td class="text-center" data-href="{{ url_for('admin.users_detail', user_id=member.id) }}">
|
|
{% if team.captain_id == member.id %}
|
|
<span class="badge badge-primary">Captain</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-left" data-href="{{ url_for('admin.users_detail', user_id=member.id) }}">
|
|
<a href="{{ url_for('admin.users_detail', user_id=member.id) }}">
|
|
{{ member.name }}
|
|
</a>
|
|
</td>
|
|
<td class="text-center">
|
|
<a href="mailto:{{ member.email }}" target="_blank">
|
|
{{ member.email }}
|
|
</a>
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="delete-member cursor-pointer" member-id="{{ member.id }}"
|
|
member-name="{{ member.name }}" data-toggle="tooltip"
|
|
data-placement="top" title="Remove {{ member.name }}">
|
|
<i class="fas fa-times"></i>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="nav nav-tabs nav-fill pt-5" id="myTab" role="tablist">
|
|
<a class="nav-item nav-link active" id="nav-solves-tab" data-toggle="tab" href="#nav-solves" role="tab"
|
|
aria-controls="nav-solves" aria-selected="true">Solves</a>
|
|
|
|
<a class="nav-item nav-link" id="nav-wrong-tab" data-toggle="tab" href="#nav-wrong" role="tab"
|
|
aria-controls="nav-wrong" aria-selected="false">Fails</a>
|
|
|
|
<a class="nav-item nav-link" id="nav-awards-tab" data-toggle="tab" href="#nav-awards" role="tab"
|
|
aria-controls="nav-awards" aria-selected="false">Awards</a>
|
|
|
|
<a class="nav-item nav-link" id="nav-missing-tab" data-toggle="tab" href="#nav-missing" role="tab"
|
|
aria-controls="nav-missing" aria-selected="false">Missing</a>
|
|
</nav>
|
|
|
|
<div class="tab-content min-vh-50 pb-5" id="nav-tabContent">
|
|
<div class="tab-pane fade show active" id="nav-solves" role="tabpanel" aria-labelledby="nav-solves-tab">
|
|
<h3 class="text-center pt-5 d-block">Solves</h3>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="float-right pb-3">
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-outline-danger" id="solves-delete-button">
|
|
<i class="btn-fa fas fa-trash-alt"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<table class="table table-striped border">
|
|
<thead>
|
|
<tr>
|
|
<th class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" data-checkbox-all>
|
|
</div>
|
|
</th>
|
|
<th class="sort-col text-center"><b>Challenge</b></th>
|
|
<th class="sort-col text-center"><b>User</b></th>
|
|
<th class="sort-col text-center"><b>Submitted</b></th>
|
|
<th class="sort-col text-center"><b>Category</b></th>
|
|
<th class="sort-col text-center"><b>Value</b></th>
|
|
<th class="sort-col text-center"><b>Time</b></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for solve in solves %}
|
|
<tr class="chal-solve" data-href="{{ url_for("admin.challenges_detail", challenge_id=solve.challenge_id) }}">
|
|
<td class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" value="{{ solve.id }}" data-submission-id="{{ solve.id }}"
|
|
data-submission-type="{{ solve.type }}"
|
|
data-submission-challenge="{{ solve.challenge.name }}">
|
|
</div>
|
|
</td>
|
|
<td class="text-center chal" id="{{ solve.challenge_id }}">
|
|
<a href="{{ url_for("admin.challenges_detail", challenge_id=solve.challenge_id) }}">
|
|
{{ solve.challenge.name }}
|
|
</a>
|
|
</td>
|
|
<td class="text-center">
|
|
<a href="{{ url_for("admin.users_detail", user_id=solve.user_id) }}">
|
|
{{ solve.user.name }}
|
|
</a>
|
|
</td>
|
|
<td class="flag" id="{{ solve.id }}"><pre>{{ solve.provided }}</pre></td>
|
|
<td class="text-center">{{ solve.challenge.category }}</td>
|
|
<td class="text-center">{{ solve.challenge.value }}</td>
|
|
<td class="text-center solve-time">
|
|
<span data-time="{{ solve.date | isoformat }}"></span>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="nav-wrong" role="tabpanel" aria-labelledby="nav-wrong-tab">
|
|
<h3 class="text-center pt-5 d-block">Fails</h3>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="float-right pb-3">
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-outline-danger" id="fails-delete-button">
|
|
<i class="btn-fa fas fa-trash-alt"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<table class="table table-striped border">
|
|
<thead>
|
|
<tr>
|
|
<th class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" data-checkbox-all>
|
|
</div>
|
|
</th>
|
|
<th class="sort-col text-center"><b>Challenge</b></th>
|
|
<th class="sort-col text-center"><b>User</b></th>
|
|
<th class="sort-col text-center"><b>Submitted</b></th>
|
|
<th class="sort-col text-center"><b>Time</b></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for fail in fails %}
|
|
<tr class="chal-wrong" data-href="{{ url_for("admin.challenges_detail", challenge_id=fail.challenge_id) }}">
|
|
<td class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" value="{{ fail.id }}" data-submission-id="{{ fail.id }}"
|
|
data-submission-type="{{ fail.type }}"
|
|
data-submission-challenge="{{ fail.challenge.name }}">
|
|
</div>
|
|
</td>
|
|
<td class="text-center chal" id="{{ fail.challenge_id }}">
|
|
<a href="{{ url_for("admin.challenges_detail", challenge_id=fail.challenge_id) }}">
|
|
{{ fail.challenge.name }}
|
|
</a>
|
|
</td>
|
|
<td class="text-center">
|
|
<a href="{{ url_for("admin.users_detail", user_id=fail.user_id) }}">
|
|
{{ fail.user.name }}
|
|
</a>
|
|
</td>
|
|
<td class="flag" id="{{ fail.id }}"><pre>{{ fail.provided }}</pre></td>
|
|
<td class="text-center solve-time">
|
|
<span data-time="{{ fail.date | isoformat }}"></span>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="nav-awards" role="tabpanel" aria-labelledby="nav-awards-tab">
|
|
<h3 class="text-center pt-5 d-block">Awards</h3>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="float-right pb-3">
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-outline-danger" id="awards-delete-button">
|
|
<i class="btn-fa fas fa-trash-alt"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<table class="table table-striped border">
|
|
<thead>
|
|
<tr>
|
|
<th class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" data-checkbox-all>
|
|
</div>
|
|
</th>
|
|
<th class="sort-col text-center"><b>Name</b></th>
|
|
<th class="sort-col text-center"><b>User</b></th>
|
|
<th class="sort-col text-center"><b>Description</b></th>
|
|
<th class="sort-col text-center"><b>Date</b></th>
|
|
<th class="sort-col text-center"><b>Value</b></th>
|
|
<th class="sort-col text-center"><b>Category</b></th>
|
|
<th class="sort-col text-center"><b>Icon</b></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="awards-body">
|
|
{% for award in awards %}
|
|
<tr class="award-row">
|
|
<td class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" value="{{ award.id }}" data-award-id="{{ award.id }}" data-award-name="{{ award.name }}">
|
|
</div>
|
|
</td>
|
|
<td class="text-center chal" id="{{ award.id }}">{{ award.name }}</td>
|
|
<td class="text-center">
|
|
<a href="{{ url_for("admin.users_detail", user_id=award.user_id) }}">
|
|
{{ award.user.name }}
|
|
</a>
|
|
</td>
|
|
<td class=""><pre>{{ award.description }}</pre></td>
|
|
<td class="text-center solve-time">
|
|
<span data-time="{{ award.date | isoformat }}"></span>
|
|
</td>
|
|
<td class="text-center">{{ award.value }}</td>
|
|
<td class="text-center">{{ award.category }}</td>
|
|
<td class="text-center"><i class="award-icon award-{{ award.icon }}"></i> {{ award.icon }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="nav-missing" role="tabpanel" aria-labelledby="nav-missing-tab">
|
|
<h3 class="text-center pt-5 d-block">Missing</h3>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="float-right pb-3">
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-outline-success" id="missing-solve-button">
|
|
<i class="btn-fa fas fa-check"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<table class="table table-striped border">
|
|
<thead>
|
|
<tr>
|
|
<th class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" data-checkbox-all>
|
|
</div>
|
|
</th>
|
|
<th class="sort-col text-center"><b>Challenge</b></th>
|
|
<th class="sort-col text-center"><b>Category</b></th>
|
|
<th class="sort-col text-center"><b>Value</b></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for challenge in missing %}
|
|
<tr class="chal-solve" data-href="{{ url_for("admin.challenges_detail", challenge_id=challenge.id) }}">
|
|
<td class="border-right" data-checkbox>
|
|
<div class="form-check text-center">
|
|
<input type="checkbox" class="form-check-input" value="{{ challenge.id }}" data-missing-challenge-id="{{ challenge.id }}"
|
|
data-missing-challenge-name="{{ challenge.name }}">
|
|
</div>
|
|
</td>
|
|
<td class="text-center chal" id="{{ challenge.id }}">
|
|
<a href="{{ url_for("admin.challenges_detail", challenge_id=challenge.id) }}">
|
|
{{ challenge.name }}
|
|
</a>
|
|
</td>
|
|
<td class="text-center">{{ challenge.category }}</td>
|
|
<td class="text-center">{{ challenge.value }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row min-vh-25 pt-5">
|
|
<div class="col-md-10 offset-md-1">
|
|
<div id="comment-box">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
var TEAM_ID = {{ team.id }};
|
|
var TEAM_NAME = {{ team.name | tojson }};
|
|
var stats_data = {{ {
|
|
'type': 'team',
|
|
'id': team.id,
|
|
'name': team.name,
|
|
'account_id': team.id,
|
|
} | tojson }};
|
|
</script>
|
|
<script defer src="{{ url_for('views.themes', theme='admin', path='js/echarts.bundle.js') }}"></script>
|
|
<script defer src="{{ url_for('views.themes', theme='admin', path='js/graphs.js') }}"></script>
|
|
{% endblock %}
|
|
|
|
{% block entrypoint %}
|
|
<script defer src="{{ url_for('views.themes', theme='admin', path='js/pages/team.js') }}"></script>
|
|
{% endblock %}
|