Admin Team List - New Team (#470)

* Allow admins to create teams manually
* Test an admin creating a team
selenium-screenshot-testing
Jared Deckard 2017-11-18 20:30:09 -06:00 committed by Kevin Chung
parent 0b0305f969
commit e10c8b103b
3 changed files with 230 additions and 29 deletions

View File

@ -6,6 +6,8 @@ from sqlalchemy.sql import not_
from CTFd import utils from CTFd import utils
import re
admin_teams = Blueprint('admin_teams', __name__) admin_teams = Blueprint('admin_teams', __name__)
@ -45,6 +47,54 @@ def admin_teams_view(page):
return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page) return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
@admin_teams.route('/admin/team/new', methods=['POST'])
@admins_only
def admin_create_team():
name = request.form.get('name', None)
password = request.form.get('password', None)
email = request.form.get('email', None)
website = request.form.get('website', None)
affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None)
errors = []
if not name:
errors.append('The team requires a name')
elif Teams.query.filter(Teams.name == name).first():
errors.append('That name is taken')
if not email:
errors.append('The team requires an email')
elif Teams.query.filter(Teams.email == email).first():
errors.append('That email is taken')
if email:
valid_email = re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email)
if not valid_email:
errors.append("That email address is invalid")
if not password:
errors.append('The team requires a password')
if website and (website.startswith('http://') or website.startswith('https://')) is False:
errors.append('Websites must start with http:// or https://')
if errors:
db.session.close()
return jsonify({'data': errors})
team = Teams(name, email, password)
team.website = website
team.affiliation = affiliation
team.country = country
db.session.add(team)
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
@admin_teams.route('/admin/team/<int:teamid>', methods=['GET', 'POST']) @admin_teams.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_team(teamid): def admin_team(teamid):
@ -93,6 +143,11 @@ def admin_team(teamid):
errors = [] errors = []
if email:
valid_email = re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email)
if not valid_email:
errors.append("That email address is invalid")
name_used = Teams.query.filter(Teams.name == name).first() name_used = Teams.query.filter(Teams.name == name).first()
if name_used and int(name_used.id) != int(teamid): if name_used and int(name_used.id) != int(teamid):
errors.append('That name is taken') errors.append('That name is taken')
@ -101,6 +156,9 @@ def admin_team(teamid):
if email_used and int(email_used.id) != int(teamid): if email_used and int(email_used.id) != int(teamid):
errors.append('That email is taken') errors.append('That email is taken')
if website and (website.startswith('http://') or website.startswith('https://')) is False:
errors.append('Websites must start with http:// or https://')
if errors: if errors:
db.session.close() db.session.close()
return jsonify({'data': errors}) return jsonify({'data': errors})

View File

@ -10,7 +10,10 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
{% block content %} {% block content %}
<div class="row"> <div class="row">
<h1>Teams</h1> <h1>
Teams
<i class="fa fa-plus-circle create-team" role="button" data-toggle="tooltip" title="Create Team"></i>
</h1>
<div id="confirm" class="modal fade" tabindex="-1"> <div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -41,7 +44,7 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h2 class="text-center">Email User</h2> <h2 class="text-center">Email Team</h2>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="POST"> <form method="POST">
@ -61,9 +64,9 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h2 class="text-center">Edit User</h2> <h2 class="text-center modal-action">Edit Team</h2>
</div> </div>
<div class="modal-body" style="padding:20px; height:525px;"> <div class="modal-body clearfix" style="padding:20px;">
<form method="POST" action="{{ request.script_root }}/admin/teams/"> <form method="POST" action="{{ request.script_root }}/admin/teams/">
<input type="hidden" name="nonce" value="{{ nonce }}"> <input type="hidden" name="nonce" value="{{ nonce }}">
<input type="hidden" name="id"> <input type="hidden" name="id">
@ -94,7 +97,7 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<div id="results"> <div id="results">
</div> </div>
<button id="update-user" type="submit" class="btn btn-theme btn-outlined pull-right">Update</button> <button id="update-user" type="submit" class="btn btn-theme btn-outlined pull-right modal-action">Update</button>
</form> </form>
</div> </div>
</div> </div>
@ -158,8 +161,8 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<td class="team-id" value="{{ team.id }}">{{ team.id }}</td> <td class="team-id" value="{{ team.id }}">{{ team.id }}</td>
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a> <td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
</td> </td>
<td class="team-email">{{ team.email | truncate(32) }}</td> <td class="team-email" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
<td class="team-website">{% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %}<a href="{{ team.website }}">{{ team.website | truncate(32) }}</a>{% endif %} <td class="team-website">{% if team.website %}<a href="{{ team.website }}">{{ team.website | truncate(32) }}</a>{% endif %}
</td> </td>
<td class="team-affiliation" value="{{ team.affiliation if team.affiliation is not none }}"><span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span> <td class="team-affiliation" value="{{ team.affiliation if team.affiliation is not none }}"><span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
</td> </td>
@ -207,13 +210,22 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
function load_update_modal(id, name, email, website, affiliation, country){ function load_update_modal(id, name, email, website, affiliation, country){
var modal_form = $('#user form'); var modal_form = $('#user form');
modal_form.find('input[name=name]').val(name) modal_form.find('input[name=name]').val(name);
modal_form.find('input[name=id]').val(id) modal_form.find('input[name=id]').val(id);
modal_form.find('input[name=email]').val(email) modal_form.find('input[name=email]').val(email);
modal_form.find('input[name=website]').val(website) modal_form.find('input[name=website]').val(website);
modal_form.find('input[name=affiliation]').val(affiliation) modal_form.find('input[name=affiliation]').val(affiliation);
modal_form.find('input[name=country]').val(country) modal_form.find('input[name=country]').val(country);
$('#user form').attr('action', '{{ request.script_root }}/admin/team/'+id) modal_form.find('input[name=password]').val('');
if (id == 'new'){
$('#user .modal-action').text('Create Team');
} else {
$('#user .modal-action').text('Edit Team');
}
$('#results').empty();
$('#user form').attr('action', '{{ request.script_root }}/admin/team/'+id);
$('#user').modal("show"); $('#user').modal("show");
} }
@ -221,28 +233,20 @@ $('#update-user').click(function(e){
e.preventDefault(); e.preventDefault();
var id = $('#user input[name="id"]').val() var id = $('#user input[name="id"]').val()
var user_data = $('#user form').serializeArray() var user_data = $('#user form').serializeArray()
$('#results').empty();
$.post($('#user form').attr('action'), $('#user form').serialize(), function(data){ $.post($('#user form').attr('action'), $('#user form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data)) var data = $.parseJSON(JSON.stringify(data))
for (var i = 0; i < data['data'].length; i++) { for (var i = 0; i < data['data'].length; i++) {
if (data['data'][i] == 'success'){ if (data['data'][i] == 'success'){
var row = $('tr[name='+id+']')
console.log($.grep(user_data, function(e){ return e.name == 'name'; })[0]['value'])
console.log(row.find('.team-name > a'))
row.find('.team-name > a').text( $.grep(user_data, function(e){ return e.name == 'name'; })[0]['value'] );
var new_email = $.grep(user_data, function(e){ return e.name == 'email'; })[0]['value'];
if (new_email){
row.find('.team-email').text( new_email );
}
row.find('.team-website > a').empty()
var website = $.grep(user_data, function(e){ return e.name == 'website'; })[0]['value']
row.find('.team-website').append($('<a>').attr('href', website).text(website));
row.find('.team-affiliation').text( $.grep(user_data, function(e){ return e.name == 'affiliation'; })[0]['value'] );
row.find('.team-country').text( $.grep(user_data, function(e){ return e.name == 'country'; })[0]['value'] );
$('#user').modal('hide'); $('#user').modal('hide');
location.reload();
} }
else{ else{
$('#results').append($('p').text( data['data'][i] )) var error = $('<div class="alert alert-danger alert-dismissable">\n' +
' <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>\n' +
' {0}\n'.format(data['data'][i]) +
'</div>');
$('#results').append(error);
} }
}; };
}) })
@ -306,6 +310,10 @@ $('.fa-pencil-square-o').click(function(){
load_update_modal(id, name, email, website, affiliation, country); load_update_modal(id, name, email, website, affiliation, country);
}); });
$('.create-team').click(function(){
load_update_modal('new', '', '', '', '', '');
});
function load_confirm_modal(id, name){ function load_confirm_modal(id, name){
var modal = $('#confirm') var modal = $('#confirm')
modal.find('input[name=id]').val(id) modal.find('input[name=id]').val(id)

View File

@ -235,3 +235,138 @@ def test_admin_chal_detail_returns_proper_data():
assert data == response assert data == response
destroy_ctfd(app) destroy_ctfd(app)
def test_admins_can_create_teams():
'''Test that admins can create new teams'''
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
with client.session_transaction() as sess:
data = {
'name': 'TunnelBunnies',
'password': 'fUnn3lJuNK135',
'email': 'scary.hares@trace.us',
'website': 'https://scary-hares.trace.us/',
'affiliation': 'Energizer',
'country': 'USA',
'nonce': sess.get('nonce'),
}
r = client.post('/admin/team/new', data=data)
assert r.status_code == 200
team = Teams.query.filter_by(id=2).first()
assert team
assert team.name == 'TunnelBunnies'
assert team.email == 'scary.hares@trace.us'
assert team.website == 'https://scary-hares.trace.us/'
assert team.affiliation == 'Energizer'
assert team.country == 'USA'
destroy_ctfd(app)
def test_admin_create_team_without_required_fields():
'''Test that an admin can't create a new team without the required fields'''
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
with client.session_transaction() as sess:
data = {
'name': '',
'password': '',
'email': '',
'website': '',
'affiliation': '',
'country': '',
'nonce': sess.get('nonce'),
}
r = client.post('/admin/team/new', data=data)
assert r.status_code == 200
response = json.loads(r.get_data(as_text=True))
assert 'data' in response
assert len(response['data']) == 3
assert 'The team requires a name' in response['data']
assert 'The team requires an email' in response['data']
assert 'The team requires a password' in response['data']
destroy_ctfd(app)
def test_admin_create_team_with_existing_name():
'''Test that an admin can't create a new team with an existing name'''
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
with client.session_transaction() as sess:
data = {
'name': 'admin',
'password': 'fUnn3lJuNK135',
'email': 'scary.hares@trace.us',
'website': 'https://scary-hares.trace.us/',
'affiliation': 'Energizer',
'country': 'USA',
'nonce': sess.get('nonce'),
}
r = client.post('/admin/team/new', data=data)
assert r.status_code == 200
response = json.loads(r.get_data(as_text=True))
assert 'data' in response
assert len(response['data']) == 1
assert 'That name is taken' in response['data']
destroy_ctfd(app)
def test_admin_create_team_with_existing_email():
'''Test that an admin can't create a new team with an existing email'''
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
with client.session_transaction() as sess:
data = {
'name': 'TunnelBunnies',
'password': 'fUnn3lJuNK135',
'email': 'admin@ctfd.io',
'website': 'https://scary-hares.trace.us/',
'affiliation': 'Energizer',
'country': 'USA',
'nonce': sess.get('nonce'),
}
r = client.post('/admin/team/new', data=data)
assert r.status_code == 200
response = json.loads(r.get_data(as_text=True))
assert 'data' in response
assert len(response['data']) == 1
assert 'That email is taken' in response['data']
destroy_ctfd(app)
def test_admin_create_team_with_invalid_website():
'''Test that an admin can't create a new team with an invalid website'''
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
with client.session_transaction() as sess:
data = {
'name': 'TunnelBunnies',
'password': 'fUnn3lJuNK135',
'email': 'scary.hares@trace.us',
'website': 'ftp://scary-hares.trace.us/',
'affiliation': 'Energizer',
'country': 'USA',
'nonce': sess.get('nonce'),
}
r = client.post('/admin/team/new', data=data)
assert r.status_code == 200
response = json.loads(r.get_data(as_text=True))
assert 'data' in response
assert len(response['data']) == 1
assert 'Websites must start with http:// or https://' in response['data']
destroy_ctfd(app)