mirror of https://github.com/JohnHammond/CTFd.git
Admin Team List - New Team (#470)
* Allow admins to create teams manually * Test an admin creating a teamselenium-screenshot-testing
parent
0b0305f969
commit
e10c8b103b
|
@ -6,6 +6,8 @@ from sqlalchemy.sql import not_
|
|||
|
||||
from CTFd import utils
|
||||
|
||||
import re
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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'])
|
||||
@admins_only
|
||||
def admin_team(teamid):
|
||||
|
@ -93,6 +143,11 @@ def admin_team(teamid):
|
|||
|
||||
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()
|
||||
if name_used and int(name_used.id) != int(teamid):
|
||||
errors.append('That name is taken')
|
||||
|
@ -101,6 +156,9 @@ def admin_team(teamid):
|
|||
if email_used and int(email_used.id) != int(teamid):
|
||||
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:
|
||||
db.session.close()
|
||||
return jsonify({'data': errors})
|
||||
|
|
|
@ -10,7 +10,10 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
|
||||
{% block content %}
|
||||
<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 class="modal-dialog">
|
||||
<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-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h2 class="text-center">Email User</h2>
|
||||
<h2 class="text-center">Email Team</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST">
|
||||
|
@ -61,9 +64,9 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h2 class="text-center">Edit User</h2>
|
||||
<h2 class="text-center modal-action">Edit Team</h2>
|
||||
</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/">
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<input type="hidden" name="id">
|
||||
|
@ -94,7 +97,7 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
<div id="results">
|
||||
|
||||
</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>
|
||||
</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-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
|
||||
</td>
|
||||
<td class="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-email" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
|
||||
<td class="team-website">{% if team.website %}<a href="{{ team.website }}">{{ team.website | truncate(32) }}</a>{% endif %}
|
||||
</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>
|
||||
|
@ -207,13 +210,22 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
function load_update_modal(id, name, email, website, affiliation, country){
|
||||
var modal_form = $('#user form');
|
||||
|
||||
modal_form.find('input[name=name]').val(name)
|
||||
modal_form.find('input[name=id]').val(id)
|
||||
modal_form.find('input[name=email]').val(email)
|
||||
modal_form.find('input[name=website]').val(website)
|
||||
modal_form.find('input[name=affiliation]').val(affiliation)
|
||||
modal_form.find('input[name=country]').val(country)
|
||||
$('#user form').attr('action', '{{ request.script_root }}/admin/team/'+id)
|
||||
modal_form.find('input[name=name]').val(name);
|
||||
modal_form.find('input[name=id]').val(id);
|
||||
modal_form.find('input[name=email]').val(email);
|
||||
modal_form.find('input[name=website]').val(website);
|
||||
modal_form.find('input[name=affiliation]').val(affiliation);
|
||||
modal_form.find('input[name=country]').val(country);
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -221,28 +233,20 @@ $('#update-user').click(function(e){
|
|||
e.preventDefault();
|
||||
var id = $('#user input[name="id"]').val()
|
||||
var user_data = $('#user form').serializeArray()
|
||||
$('#results').empty();
|
||||
$.post($('#user form').attr('action'), $('#user form').serialize(), function(data){
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
for (var i = 0; i < data['data'].length; i++) {
|
||||
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');
|
||||
location.reload();
|
||||
}
|
||||
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">×</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);
|
||||
});
|
||||
|
||||
$('.create-team').click(function(){
|
||||
load_update_modal('new', '', '', '', '', '');
|
||||
});
|
||||
|
||||
function load_confirm_modal(id, name){
|
||||
var modal = $('#confirm')
|
||||
modal.find('input[name=id]').val(id)
|
||||
|
|
|
@ -235,3 +235,138 @@ def test_admin_chal_detail_returns_proper_data():
|
|||
assert data == response
|
||||
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue