Files
CTFd/CTFd/themes/admin/templates/teams/team.html
Kevin Chung 9264e96428 Mark 3.1.0 (#1634)
# 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.
2020-09-08 00:08:35 -04:00

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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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>&nbsp;
</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 }}">&nbsp;
</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>&nbsp;
</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 }}">&nbsp;
</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>&nbsp;
</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 }}">&nbsp;
</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>&nbsp;
</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 }}">&nbsp;
</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 %}