Merge branch 'master' of github.com:IceCTF/new-platform
commit
b4dbc062d2
|
@ -102,5 +102,6 @@ tags
|
||||||
dev.db
|
dev.db
|
||||||
dump.rdb
|
dump.rdb
|
||||||
/problems
|
/problems
|
||||||
|
/problem_static
|
||||||
/secrets
|
/secrets
|
||||||
/database
|
/database
|
||||||
|
|
5
app.py
5
app.py
|
@ -12,7 +12,7 @@ from data.database import db
|
||||||
import data
|
import data
|
||||||
|
|
||||||
# Blueprints
|
# Blueprints
|
||||||
from routes import api, admin, teams, users, challenges, tickets, scoreboard
|
from routes import api, admin, teams, users, challenges, tickets, scoreboard, shell
|
||||||
|
|
||||||
if config.production:
|
if config.production:
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
@ -30,6 +30,7 @@ app.register_blueprint(users.users)
|
||||||
app.register_blueprint(challenges.challenges)
|
app.register_blueprint(challenges.challenges)
|
||||||
app.register_blueprint(tickets.tickets)
|
app.register_blueprint(tickets.tickets)
|
||||||
app.register_blueprint(scoreboard.scoreboard)
|
app.register_blueprint(scoreboard.scoreboard)
|
||||||
|
app.register_blueprint(shell.shell)
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
|
@ -68,7 +69,7 @@ def scoreboard_variables():
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def root():
|
def root():
|
||||||
if g.logged_in:
|
if g.logged_in:
|
||||||
return redirect(url_for('team.dashboard'))
|
return redirect(url_for('teams.dashboard'))
|
||||||
return redirect(url_for('users.register'))
|
return redirect(url_for('users.register'))
|
||||||
|
|
||||||
|
|
||||||
|
|
12
config.py
12
config.py
|
@ -7,7 +7,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
ctf_name = "IceCTF"
|
ctf_name = "IceCTF"
|
||||||
eligibility = "In order to be eligible for prizes, all members of your team must be Icelandic residents, and you must not have more than three team members."
|
eligibility = "In order to be eligible for prizes, all members of your team must be Icelandic residents, and you must not have more than three team members."
|
||||||
tagline = "The Icelandic Hacking Competition"
|
tagline = "The Icelandic Hacking Competition"
|
||||||
#IRC Channel
|
# IRC Channel
|
||||||
ctf_chat_channel = "#IceCTF"
|
ctf_chat_channel = "#IceCTF"
|
||||||
ctf_home_url = "https://icec.tf"
|
ctf_home_url = "https://icec.tf"
|
||||||
|
|
||||||
|
@ -32,8 +32,8 @@ immediate_scoreboard = False
|
||||||
disallowed_domain = "icec.tf"
|
disallowed_domain = "icec.tf"
|
||||||
|
|
||||||
# Where the static stuff is stored
|
# Where the static stuff is stored
|
||||||
static_prefix = "http://127.0.0.1/static/"
|
static_prefix = "/problem-static/"
|
||||||
static_dir = "{}/static/".format(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
static_dir = "{}/problem_static/".format(os.path.dirname(os.path.abspath(__file__)))
|
||||||
custom_stylesheet = "css/main.css"
|
custom_stylesheet = "css/main.css"
|
||||||
|
|
||||||
# Shell accounts?
|
# Shell accounts?
|
||||||
|
@ -41,14 +41,16 @@ custom_stylesheet = "css/main.css"
|
||||||
enable_shell = True
|
enable_shell = True
|
||||||
|
|
||||||
shell_port = 22
|
shell_port = 22
|
||||||
|
shell_host = "shell.icec.tf"
|
||||||
|
|
||||||
shell_user_prefixes = ["ctf-"]
|
shell_user_prefixes = ["ctf-"]
|
||||||
shell_password_length = 6
|
shell_password_length = 8
|
||||||
shell_free_acounts = 10
|
shell_free_acounts = 10
|
||||||
shell_max_accounts = 99999
|
shell_max_accounts = 99999
|
||||||
|
|
||||||
shell_user_creation = "sudo useradd -m {username} -p {password} -g ctf -b /home_users"
|
shell_user_creation = "sudo useradd -m {username} -p {password} -g ctf -b /home_users"
|
||||||
|
|
||||||
|
|
||||||
# when the competition begins
|
# when the competition begins
|
||||||
competition_begin = datetime(1970, 1, 1, 0, 0)
|
competition_begin = datetime(1970, 1, 1, 0, 0)
|
||||||
competition_end = datetime(2018, 1, 1, 0, 0)
|
competition_end = datetime(2018, 1, 1, 0, 0)
|
||||||
|
|
107
ctftool
107
ctftool
|
@ -17,7 +17,7 @@ import yaml
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
tables = [Team, User, UserAccess, Challenge, ChallengeSolve, ChallengeFailure, NewsItem, TroubleTicket, TicketComment, Notification, ScoreAdjustment, AdminUser, SshAccount]
|
tables = [Team, User, UserAccess, Stage, Challenge, ChallengeSolve, ChallengeFailure, NewsItem, TroubleTicket, TicketComment, Notification, ScoreAdjustment, AdminUser, SshAccount]
|
||||||
|
|
||||||
def create_tables(args):
|
def create_tables(args):
|
||||||
check = True
|
check = True
|
||||||
|
@ -66,58 +66,73 @@ def add_admin(args):
|
||||||
secret = "".join([r.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") for i in range(16)])
|
secret = "".join([r.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") for i in range(16)])
|
||||||
AdminUser.create(username=username, password=pwhash, secret=secret)
|
AdminUser.create(username=username, password=pwhash, secret=secret)
|
||||||
print("AdminUser created; Enter the following key into your favorite TOTP application (Google Authenticator Recommended): {}".format(secret))
|
print("AdminUser created; Enter the following key into your favorite TOTP application (Google Authenticator Recommended): {}".format(secret))
|
||||||
|
def scan_challenges_problem(d, files):
|
||||||
|
staticpaths = {}
|
||||||
|
if "static.yml" in files:
|
||||||
|
with open(os.path.join(d, "static.yml")) as f:
|
||||||
|
statics = yaml.load(f)
|
||||||
|
for static in statics:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(os.path.join(d, static), "rb") as staticfile:
|
||||||
|
while True:
|
||||||
|
buf = staticfile.read(4096)
|
||||||
|
h.update(buf)
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
|
||||||
|
if "." in static:
|
||||||
|
name, ext = static.split(".", maxsplit=1)
|
||||||
|
fn = "{}_{}.{}".format(name, h.hexdigest(), ext)
|
||||||
|
else:
|
||||||
|
fn = "{}_{}".format(static, h.hexdigest())
|
||||||
|
staticpaths[static] = fn
|
||||||
|
shutil.copy(os.path.join(d, static), os.path.join(config.static_dir, fn))
|
||||||
|
print(fn)
|
||||||
|
|
||||||
|
with open(os.path.join(d, "problem.yml")) as f:
|
||||||
|
data = yaml.load(f)
|
||||||
|
print("Inserting problem in directory %s" % (d))
|
||||||
|
for i in staticpaths:
|
||||||
|
print("looking for |{}|".format(i))
|
||||||
|
data["description"] = data["description"].replace("|{}|".format(i), "{}{}".format(config.static_prefix, staticpaths[i]))
|
||||||
|
|
||||||
|
data["stage"] = Stage.get(Stage.alias == data["stage"])
|
||||||
|
query = Challenge.select().where(Challenge.alias == data["alias"])
|
||||||
|
if query.exists():
|
||||||
|
print("Updating " + str(data["name"]) + "...")
|
||||||
|
q = Challenge.update(**data).where(Challenge.alias == data["alias"])
|
||||||
|
q.execute()
|
||||||
|
else:
|
||||||
|
Challenge.create(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_challenges_stage(d, files):
|
||||||
|
|
||||||
|
with open(os.path.join(d, "stage.yml")) as f:
|
||||||
|
data = yaml.load(f)
|
||||||
|
query = Stage.select().where(Stage.alias == data["alias"])
|
||||||
|
if query.exists():
|
||||||
|
print("Updating %s..." % (data["name"]))
|
||||||
|
q = Challenge.update(**data).where(Stage.alias == data["alias"])
|
||||||
|
else:
|
||||||
|
Stage.create(**data)
|
||||||
|
|
||||||
|
|
||||||
def scan_challenges(args):
|
def scan_challenges(args):
|
||||||
path = args.path
|
path = args.path
|
||||||
dirs = [j for j in [os.path.join(path, i) for i in os.listdir(path)] if os.path.isdir(j)]
|
|
||||||
# recurse to 2 levels
|
|
||||||
# TODO: do this better
|
|
||||||
dirs = [j for j in [os.path.join(k, i) for k in dirs for i in os.listdir(k)] if os.path.isdir(j)]
|
|
||||||
print(dirs)
|
|
||||||
n = 0
|
n = 0
|
||||||
|
|
||||||
for d in dirs:
|
os.makedirs(config.static_dir, exist_ok=True)
|
||||||
staticpaths = {}
|
for root, dirs, files in os.walk(path):
|
||||||
if os.path.exists(os.path.join(d, "static.yml")):
|
if "problem.yml" in files:
|
||||||
with open(os.path.join(d, "static.yml")) as f:
|
n += 1
|
||||||
statics = yaml.load(f)
|
scan_challenges_problem(root, files)
|
||||||
for static in statics:
|
if "stage.yml" in files:
|
||||||
h = hashlib.sha256()
|
scan_challenges_stage(root, files)
|
||||||
with open(os.path.join(d, static), "rb") as staticfile:
|
|
||||||
while True:
|
|
||||||
buf = staticfile.read(4096)
|
|
||||||
h.update(buf)
|
|
||||||
if not buf:
|
|
||||||
break
|
|
||||||
|
|
||||||
if "." in static:
|
|
||||||
name, ext = static.split(".", maxsplit=1)
|
|
||||||
fn = "{}_{}.{}".format(name, h.hexdigest(), ext)
|
|
||||||
else:
|
|
||||||
fn = "{}_{}".format(static, h.hexdigest())
|
|
||||||
staticpaths[static] = fn
|
|
||||||
shutil.copy(os.path.join(d, static), os.path.join(config.static_dir, fn))
|
|
||||||
print(fn)
|
|
||||||
|
|
||||||
if os.path.exists(os.path.join(d, "problem.yml")):
|
|
||||||
with open(os.path.join(d, "problem.yml")) as f:
|
|
||||||
n += 1
|
|
||||||
data = yaml.load(f)
|
|
||||||
print("Inserting problem in directory %s" % (d))
|
|
||||||
for i in staticpaths:
|
|
||||||
print("looking for |{}|".format(i))
|
|
||||||
data["description"] = data["description"].replace("|{}|".format(i), "{}{}".format(config.static_prefix, staticpaths[i]))
|
|
||||||
|
|
||||||
query = Challenge.select().where(Challenge.name == data["name"])
|
|
||||||
if query.exists():
|
|
||||||
print("Updating " + str(data["name"]) + "...")
|
|
||||||
q = Challenge.update(**data).where(Challenge.name == data["name"])
|
|
||||||
q.execute()
|
|
||||||
else:
|
|
||||||
Challenge.create(**data)
|
|
||||||
|
|
||||||
print(n, "challenges loaded")
|
print(n, "challenges loaded")
|
||||||
|
|
||||||
|
|
||||||
def add_challenge(args):
|
def add_challenge(args):
|
||||||
challengefile = args.file
|
challengefile = args.file
|
||||||
with open(challengefile) as f:
|
with open(challengefile) as f:
|
||||||
|
@ -152,7 +167,7 @@ def clear_challenges(args):
|
||||||
|
|
||||||
|
|
||||||
def recache_solves(args):
|
def recache_solves(args):
|
||||||
r = redis.StrictRedis()
|
r = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db)
|
||||||
for chal in Challenge.select():
|
for chal in Challenge.select():
|
||||||
r.hset("solves", chal.id, chal.solves.count())
|
r.hset("solves", chal.id, chal.solves.count())
|
||||||
print(r.hvals("solves"))
|
print(r.hvals("solves"))
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import redis
|
import redis
|
||||||
import data.scoreboard
|
from data import scoreboard
|
||||||
import json
|
import json
|
||||||
|
import config
|
||||||
r = redis.StrictRedis()
|
from data.database import db
|
||||||
|
|
||||||
def set_complex(key, val):
|
|
||||||
r.set(key, json.dumps(val))
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
data = data.scoreboard.calculate_scores()
|
r = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db)
|
||||||
graphdata = utils.scoreboard.calculate_graph(data)
|
db.connect()
|
||||||
|
|
||||||
|
def set_complex(key, val):
|
||||||
|
r.set(key, json.dumps(val))
|
||||||
|
data = scoreboard.calculate_scores()
|
||||||
|
graphdata = scoreboard.calculate_graph(data)
|
||||||
set_complex("scoreboard", data)
|
set_complex("scoreboard", data)
|
||||||
set_complex("graph", graphdata)
|
set_complex("graph", graphdata)
|
||||||
|
db.close()
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import api
|
|
||||||
import spur
|
import spur
|
||||||
import pwd
|
|
||||||
import random
|
import random
|
||||||
import config
|
import config
|
||||||
from data.database import db
|
from data.database import db
|
||||||
from data import ssh
|
from data import ssh
|
||||||
|
import utils.misc
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
|
|
||||||
db.connect()
|
db.connect()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shell = spur.SshShell(
|
shell = spur.SshShell(
|
||||||
hostname=config.secrets.shell_host,
|
hostname=config.shell_host,
|
||||||
username=config.secrets.shell_username,
|
username=config.secret.shell_username,
|
||||||
password=config.secrets.shell_password,
|
private_key_file=config.secret.shell_privkey,
|
||||||
port=config.shell_port,
|
port=config.shell_port,
|
||||||
missing_host_key=spur.ssh.MissingHostKey.accept
|
missing_host_key=spur.ssh.MissingHostKey.accept
|
||||||
)
|
)
|
||||||
|
@ -43,10 +42,10 @@ def run():
|
||||||
|
|
||||||
accounts = []
|
accounts = []
|
||||||
while new_accounts > 0:
|
while new_accounts > 0:
|
||||||
username = random.choice(api.config.shell_user_prefixes) + \
|
username = random.choice(config.shell_user_prefixes) + \
|
||||||
str(random.randint(0, api.config.shell_max_accounts))
|
str(random.randint(0, config.shell_max_accounts))
|
||||||
|
|
||||||
plaintext_password = api.common.token()[:api.config.shell_password_length]
|
plaintext_password = utils.misc.generate_random_string(config.shell_password_length, chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
|
||||||
|
|
||||||
hashed_password = shell.run(["bash", "-c", "echo '{}' | openssl passwd -1 -stdin".format(plaintext_password)])
|
hashed_password = shell.run(["bash", "-c", "echo '{}' | openssl passwd -1 -stdin".format(plaintext_password)])
|
||||||
hashed_password = hashed_password.output.decode("utf-8").strip()
|
hashed_password = hashed_password.output.decode("utf-8").strip()
|
||||||
|
@ -75,7 +74,7 @@ def run():
|
||||||
new_accounts -= 1
|
new_accounts -= 1
|
||||||
|
|
||||||
if len(accounts) > 0:
|
if len(accounts) > 0:
|
||||||
ssh.insert_accounts(accounts)
|
ssh.create_accounts(accounts)
|
||||||
print("Successfully imported accounts.")
|
print("Successfully imported accounts.")
|
||||||
|
|
||||||
for team in teams:
|
for team in teams:
|
||||||
|
|
|
@ -1,17 +1,58 @@
|
||||||
from data.database import Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, Team
|
from data.database import Stage, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, Team
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from exceptions import ValidationError
|
from exceptions import ValidationError
|
||||||
from flask import g
|
from flask import g
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
def get_stages():
|
||||||
|
return list(Stage.select().order_by(Stage.name))
|
||||||
|
|
||||||
|
|
||||||
|
def get_stage_challenges(stage_id):
|
||||||
|
print(stage_id)
|
||||||
|
return list(Challenge.select(Challenge.alias).where(Challenge.stage == stage_id))
|
||||||
|
|
||||||
|
|
||||||
|
def get_categories():
|
||||||
|
return [q.category for q in Challenge.select(Challenge.category).distinct().order_by(Challenge.category)]
|
||||||
|
|
||||||
|
|
||||||
def get_challenges():
|
def get_challenges():
|
||||||
return Challenge.select().order_by(Challenge.points, Challenge.name)
|
challenges = Challenge.select().where(Challenge.enabled == True).order_by(Challenge.stage, Challenge.points, Challenge.name)
|
||||||
|
d = dict()
|
||||||
|
for chall in challenges:
|
||||||
|
if chall.stage_id in d:
|
||||||
|
d[chall.stage_id].append(chall)
|
||||||
|
else:
|
||||||
|
d[chall.stage_id] = [chall]
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
def get_challenge(id):
|
def get_solve_counts():
|
||||||
|
# TODO: optimize
|
||||||
|
d = dict()
|
||||||
|
for k in Challenge.select(Challenge.id):
|
||||||
|
d[k.id] = get_solve_count(k.id)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def get_solve_count(chall_id):
|
||||||
|
s = g.redis.hget("solves", chall_id)
|
||||||
|
if s is not None:
|
||||||
|
return int(s.decode())
|
||||||
|
else:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def get_challenge(id=None, alias=None):
|
||||||
try:
|
try:
|
||||||
return Challenge.get(Challenge.id == id)
|
if id is not None:
|
||||||
|
return Challenge.get(Challenge.id == id, Challenge.enabled == True)
|
||||||
|
elif alias is not None:
|
||||||
|
return Challenge.get(Challenge.alias == alias, Challenge.enabled == True)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid argument")
|
||||||
except Challenge.DoesNotExist:
|
except Challenge.DoesNotExist:
|
||||||
raise ValidationError("Challenge does not exist!")
|
raise ValidationError("Challenge does not exist!")
|
||||||
|
|
||||||
|
@ -20,6 +61,10 @@ def get_solved(team):
|
||||||
return Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team)
|
return Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team)
|
||||||
|
|
||||||
|
|
||||||
|
def get_solves(team):
|
||||||
|
return ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).where(ChallengeSolve.team == g.team)
|
||||||
|
|
||||||
|
|
||||||
def get_adjustments(team):
|
def get_adjustments(team):
|
||||||
return ScoreAdjustment.select().where(ScoreAdjustment.team == team)
|
return ScoreAdjustment.select().where(ScoreAdjustment.team == team)
|
||||||
|
|
||||||
|
@ -35,7 +80,7 @@ def submit_flag(chall, user, team, flag):
|
||||||
raise ValidationError("This challenge is disabled.")
|
raise ValidationError("This challenge is disabled.")
|
||||||
elif flag.strip().lower() != chall.flag.strip().lower():
|
elif flag.strip().lower() != chall.flag.strip().lower():
|
||||||
ChallengeFailure.create(user=user, team=team, challenge=chall, attempt=flag, time=datetime.now())
|
ChallengeFailure.create(user=user, team=team, challenge=chall, attempt=flag, time=datetime.now())
|
||||||
return "Incorrect flag"
|
raise ValidationError("Incorrect flag")
|
||||||
else:
|
else:
|
||||||
ChallengeSolve.create(user=user, team=team, challenge=chall, time=datetime.now())
|
ChallengeSolve.create(user=user, team=team, challenge=chall, time=datetime.now())
|
||||||
g.redis.hincrby("solves", chall.id, 1)
|
g.redis.hincrby("solves", chall.id, 1)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
from peewee import *
|
from peewee import PostgresqlDatabase, SqliteDatabase
|
||||||
|
from peewee import Model, CharField, TextField, IntegerField, BooleanField, DateTimeField, ForeignKeyField, CompositeKey
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import config
|
import config
|
||||||
|
|
||||||
if config.production:
|
if config.production:
|
||||||
db = PostgresqlDatabase(config.database.database, user=config.database.user, password=config.database.password)
|
db = PostgresqlDatabase(config.database.database, user=config.database.user, password=config.database.password)
|
||||||
else:
|
else:
|
||||||
|
@ -11,16 +14,20 @@ class BaseModel(Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
|
||||||
|
|
||||||
class Team(BaseModel):
|
class Team(BaseModel):
|
||||||
name = CharField(unique=True)
|
name = CharField(unique=True)
|
||||||
affiliation = CharField(null = True)
|
affiliation = CharField(null=True)
|
||||||
restricts = TextField(default="")
|
restricts = TextField(default="")
|
||||||
key = CharField(unique=True, index=True)
|
key = CharField(unique=True, index=True)
|
||||||
|
eligibility = BooleanField(null=True)
|
||||||
|
|
||||||
def solved(self, challenge):
|
def solved(self, challenge):
|
||||||
return ChallengeSolve.select().where(ChallengeSolve.team == self, ChallengeSolve.challenge == challenge).count()
|
return ChallengeSolve.select().where(ChallengeSolve.team == self, ChallengeSolve.challenge == challenge).count()
|
||||||
|
|
||||||
def eligible(self):
|
def eligible(self):
|
||||||
|
if self.eligibility is not None:
|
||||||
|
return self.eligibility
|
||||||
return all([member.eligible() for member in self.members]) and self.members.count() <= 3
|
return all([member.eligible() for member in self.members]) and self.members.count() <= 3
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -29,22 +36,23 @@ class Team(BaseModel):
|
||||||
adjust_points = sum([i.value for i in self.adjustments])
|
adjust_points = sum([i.value for i in self.adjustments])
|
||||||
return challenge_points + adjust_points
|
return challenge_points + adjust_points
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
username = CharField(unique=True, index=True)
|
username = CharField(unique=True, index=True)
|
||||||
email = CharField(index=True)
|
email = CharField(index=True)
|
||||||
email_confirmed = BooleanField(default=False)
|
email_confirmed = BooleanField(default=False)
|
||||||
email_confirmation_key = CharField()
|
email_confirmation_key = CharField()
|
||||||
password = CharField(null = True)
|
password = CharField(null=True)
|
||||||
background = CharField()
|
background = CharField()
|
||||||
country = CharField()
|
country = CharField()
|
||||||
tshirt_size = CharField(null = True)
|
tshirt_size = CharField(null=True)
|
||||||
gender = CharField(null = True)
|
gender = CharField(null=True)
|
||||||
first_login = BooleanField(default=True)
|
first_login = BooleanField(default=True)
|
||||||
restricts = TextField(default="")
|
restricts = TextField(default="")
|
||||||
team = ForeignKeyField(Team, related_name="members")
|
team = ForeignKeyField(Team, related_name="members")
|
||||||
banned = BooleanField(default=False)
|
banned = BooleanField(default=False)
|
||||||
password_reset_token = CharField(null = True)
|
password_reset_token = CharField(null=True)
|
||||||
password_reset_expired = DateTimeField(null = True)
|
password_reset_expired = DateTimeField(null=True)
|
||||||
|
|
||||||
def set_password(self, pw):
|
def set_password(self, pw):
|
||||||
self.password = bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt())
|
self.password = bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
@ -54,15 +62,24 @@ class User(BaseModel):
|
||||||
return bcrypt.checkpw(pw.encode("utf-8"), self.password.encode("utf-8"))
|
return bcrypt.checkpw(pw.encode("utf-8"), self.password.encode("utf-8"))
|
||||||
|
|
||||||
def eligible(self):
|
def eligible(self):
|
||||||
return self.country == "ISL"
|
return self.country == "ISL" and not self.banned
|
||||||
|
|
||||||
|
|
||||||
class UserAccess(BaseModel):
|
class UserAccess(BaseModel):
|
||||||
user = ForeignKeyField(User, related_name='accesses')
|
user = ForeignKeyField(User, related_name='accesses')
|
||||||
ip = CharField()
|
ip = CharField()
|
||||||
time = DateTimeField()
|
time = DateTimeField()
|
||||||
|
|
||||||
|
|
||||||
|
class Stage(BaseModel):
|
||||||
|
name = CharField()
|
||||||
|
alias = CharField(unique=True, index=True)
|
||||||
|
description = CharField(null=True)
|
||||||
|
|
||||||
|
|
||||||
class Challenge(BaseModel):
|
class Challenge(BaseModel):
|
||||||
name = CharField()
|
name = CharField()
|
||||||
|
alias = CharField(unique=True, index=True)
|
||||||
category = CharField()
|
category = CharField()
|
||||||
author = CharField()
|
author = CharField()
|
||||||
description = TextField()
|
description = TextField()
|
||||||
|
@ -70,6 +87,8 @@ class Challenge(BaseModel):
|
||||||
breakthrough_bonus = IntegerField(default=0)
|
breakthrough_bonus = IntegerField(default=0)
|
||||||
enabled = BooleanField(default=True)
|
enabled = BooleanField(default=True)
|
||||||
flag = TextField()
|
flag = TextField()
|
||||||
|
stage = ForeignKeyField(Stage, related_name='challenges')
|
||||||
|
|
||||||
|
|
||||||
class ChallengeSolve(BaseModel):
|
class ChallengeSolve(BaseModel):
|
||||||
user = ForeignKeyField(User, related_name='solves')
|
user = ForeignKeyField(User, related_name='solves')
|
||||||
|
@ -80,6 +99,7 @@ class ChallengeSolve(BaseModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
primary_key = CompositeKey('team', 'challenge')
|
primary_key = CompositeKey('team', 'challenge')
|
||||||
|
|
||||||
|
|
||||||
class ChallengeFailure(BaseModel):
|
class ChallengeFailure(BaseModel):
|
||||||
user = ForeignKeyField(User, related_name='failures')
|
user = ForeignKeyField(User, related_name='failures')
|
||||||
team = ForeignKeyField(Team, related_name='failures')
|
team = ForeignKeyField(Team, related_name='failures')
|
||||||
|
@ -87,10 +107,12 @@ class ChallengeFailure(BaseModel):
|
||||||
attempt = CharField()
|
attempt = CharField()
|
||||||
time = DateTimeField()
|
time = DateTimeField()
|
||||||
|
|
||||||
|
|
||||||
class NewsItem(BaseModel):
|
class NewsItem(BaseModel):
|
||||||
summary = CharField()
|
summary = CharField()
|
||||||
description = TextField()
|
description = TextField()
|
||||||
|
|
||||||
|
|
||||||
class TroubleTicket(BaseModel):
|
class TroubleTicket(BaseModel):
|
||||||
team = ForeignKeyField(Team, related_name='tickets')
|
team = ForeignKeyField(Team, related_name='tickets')
|
||||||
summary = CharField()
|
summary = CharField()
|
||||||
|
@ -98,26 +120,31 @@ class TroubleTicket(BaseModel):
|
||||||
active = BooleanField(default=True)
|
active = BooleanField(default=True)
|
||||||
opened_at = DateTimeField()
|
opened_at = DateTimeField()
|
||||||
|
|
||||||
|
|
||||||
class TicketComment(BaseModel):
|
class TicketComment(BaseModel):
|
||||||
ticket = ForeignKeyField(TroubleTicket, related_name='comments')
|
ticket = ForeignKeyField(TroubleTicket, related_name='comments')
|
||||||
comment_by = CharField()
|
comment_by = CharField()
|
||||||
comment = TextField()
|
comment = TextField()
|
||||||
time = DateTimeField()
|
time = DateTimeField()
|
||||||
|
|
||||||
|
|
||||||
class Notification(BaseModel):
|
class Notification(BaseModel):
|
||||||
team = ForeignKeyField(Team, related_name='notifications')
|
team = ForeignKeyField(Team, related_name='notifications')
|
||||||
notification = TextField()
|
notification = TextField()
|
||||||
|
|
||||||
|
|
||||||
class ScoreAdjustment(BaseModel):
|
class ScoreAdjustment(BaseModel):
|
||||||
team = ForeignKeyField(Team, related_name='adjustments')
|
team = ForeignKeyField(Team, related_name='adjustments')
|
||||||
value = IntegerField()
|
value = IntegerField()
|
||||||
reason = TextField()
|
reason = TextField()
|
||||||
|
|
||||||
|
|
||||||
class AdminUser(BaseModel):
|
class AdminUser(BaseModel):
|
||||||
username = CharField()
|
username = CharField()
|
||||||
password = CharField()
|
password = CharField()
|
||||||
secret = CharField()
|
secret = CharField()
|
||||||
|
|
||||||
|
|
||||||
class SshAccount(BaseModel):
|
class SshAccount(BaseModel):
|
||||||
team = ForeignKeyField(Team, null=True, related_name='ssh_account')
|
team = ForeignKeyField(Team, null=True, related_name='ssh_account')
|
||||||
username = CharField()
|
username = CharField()
|
||||||
|
|
|
@ -20,3 +20,9 @@ def assign_shell_account(team):
|
||||||
acct = SshAccount.select().order_by(fn.Random()).get()
|
acct = SshAccount.select().order_by(fn.Random()).get()
|
||||||
acct.team = team
|
acct.team = team
|
||||||
acct.save()
|
acct.save()
|
||||||
|
|
||||||
|
def get_team_account(team):
|
||||||
|
try:
|
||||||
|
return team.ssh_account.get()
|
||||||
|
except SshAccount.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
|
@ -33,7 +33,7 @@ def create_team(name, affiliation):
|
||||||
team_key = misc.generate_team_key()
|
team_key = misc.generate_team_key()
|
||||||
team = Team.create(name=name, affiliation=affiliation, key=team_key)
|
team = Team.create(name=name, affiliation=affiliation, key=team_key)
|
||||||
|
|
||||||
return True, team
|
return team
|
||||||
|
|
||||||
|
|
||||||
def update_team(current_team, name, affiliation):
|
def update_team(current_team, name, affiliation):
|
||||||
|
|
|
@ -8,7 +8,7 @@ def get_tickets(team):
|
||||||
|
|
||||||
def get_ticket(team, id):
|
def get_ticket(team, id):
|
||||||
try:
|
try:
|
||||||
return TroubleTicket.get(TroubleTicket.id == ticket and TroubleTicket.team == team)
|
return TroubleTicket.get(TroubleTicket.id == id, TroubleTicket.team == team)
|
||||||
except TroubleTicket.DoesNotExist:
|
except TroubleTicket.DoesNotExist:
|
||||||
raise ValidationError("Ticket not found!")
|
raise ValidationError("Ticket not found!")
|
||||||
|
|
||||||
|
@ -16,10 +16,10 @@ def get_comments(ticket):
|
||||||
return TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time)
|
return TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time)
|
||||||
|
|
||||||
def create_ticket(team, summary, description):
|
def create_ticket(team, summary, description):
|
||||||
return TroubleTicket.create(team=g.team, summary=summary, description=description, opened_at=datetime.now())
|
return TroubleTicket.create(team=team, summary=summary, description=description, opened_at=datetime.now())
|
||||||
|
|
||||||
def create_comment(ticket, user, comment):
|
def create_comment(ticket, user, comment):
|
||||||
TicketComment.create(ticket=ticket, comment_by=user.username, comment=request.form["comment"], time=datetime.now())
|
TicketComment.create(ticket=ticket, comment_by=user.username, comment=comment, time=datetime.now())
|
||||||
|
|
||||||
def open_ticket(ticket):
|
def open_ticket(ticket):
|
||||||
ticket.active = True
|
ticket.active = True
|
||||||
|
|
|
@ -82,7 +82,7 @@ def create_user(username, email, password, background, country, team, tshirt_siz
|
||||||
def confirm_email(current_user, confirmation_key):
|
def confirm_email(current_user, confirmation_key):
|
||||||
if current_user.email_confirmed:
|
if current_user.email_confirmed:
|
||||||
raise ValidationError("Email already confirmed")
|
raise ValidationError("Email already confirmed")
|
||||||
if current_user.confirmation_key == confirmation_key:
|
if current_user.email_confirmation_key == confirmation_key:
|
||||||
current_user.email_confirmed = True
|
current_user.email_confirmed = True
|
||||||
current_user.save()
|
current_user.save()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -7,3 +7,4 @@ pyyaml
|
||||||
oath
|
oath
|
||||||
pycountry
|
pycountry
|
||||||
psycopg2
|
psycopg2
|
||||||
|
spur
|
||||||
|
|
|
@ -53,7 +53,7 @@ def admin_dashboard():
|
||||||
adjustments = ScoreAdjustment.select()
|
adjustments = ScoreAdjustment.select()
|
||||||
scoredata = scoreboard.get_all_scores(teams, solves, adjustments)
|
scoredata = scoreboard.get_all_scores(teams, solves, adjustments)
|
||||||
lastsolvedata = scoreboard.get_last_solves(teams, solves)
|
lastsolvedata = scoreboard.get_last_solves(teams, solves)
|
||||||
tickets = list(TroubleTicket.select().where(TroubleTicket.active==True))
|
tickets = list(TroubleTicket.select().where(TroubleTicket.active == True))
|
||||||
return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets)
|
return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets)
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,19 +78,19 @@ def admin_ticket_comment(ticket):
|
||||||
ticket = TroubleTicket.get(TroubleTicket.id == ticket)
|
ticket = TroubleTicket.get(TroubleTicket.id == ticket)
|
||||||
if request.form["comment"]:
|
if request.form["comment"]:
|
||||||
TicketComment.create(ticket=ticket, comment_by=session["admin"], comment=request.form["comment"], time=datetime.now())
|
TicketComment.create(ticket=ticket, comment_by=session["admin"], comment=request.form["comment"], time=datetime.now())
|
||||||
Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id))))
|
Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id))))
|
||||||
flash("Comment added.")
|
flash("Comment added.")
|
||||||
|
|
||||||
if ticket.active and "resolved" in request.form:
|
if ticket.active and "resolved" in request.form:
|
||||||
ticket.active = False
|
ticket.active = False
|
||||||
ticket.save()
|
ticket.save()
|
||||||
Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id))))
|
Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id))))
|
||||||
flash("Ticket closed.")
|
flash("Ticket closed.")
|
||||||
|
|
||||||
elif not ticket.active and "resolved" not in request.form:
|
elif not ticket.active and "resolved" not in request.form:
|
||||||
ticket.active = True
|
ticket.active = True
|
||||||
ticket.save()
|
ticket.save()
|
||||||
Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id))))
|
Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id))))
|
||||||
flash("Ticket reopened.")
|
flash("Ticket reopened.")
|
||||||
|
|
||||||
return redirect(url_for(".admin_ticket_detail", ticket=ticket.id))
|
return redirect(url_for(".admin_ticket_detail", ticket=ticket.id))
|
||||||
|
@ -116,23 +116,15 @@ def admin_impersonate_team(tid):
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_toggle_eligibility(tid):
|
def admin_toggle_eligibility(tid):
|
||||||
team = Team.get(Team.id == tid)
|
team = Team.get(Team.id == tid)
|
||||||
team.eligible = not team.eligible
|
if team.eligibility is None:
|
||||||
|
team.eligibility = False
|
||||||
|
else:
|
||||||
|
team.eligibility = not team.eligibility
|
||||||
team.save()
|
team.save()
|
||||||
flash("Eligibility set to {}".format(team.eligible))
|
flash("Eligibility set to {}".format(team.eligible))
|
||||||
return redirect(url_for(".admin_show_team", tid=tid))
|
return redirect(url_for(".admin_show_team", tid=tid))
|
||||||
|
|
||||||
|
|
||||||
@admin.route("/team/<int:tid>/<csrf>/toggle_eligibility_lock/")
|
|
||||||
@csrf_check
|
|
||||||
@admin_required
|
|
||||||
def admin_toggle_eligibility_lock(tid):
|
|
||||||
team = Team.get(Team.id == tid)
|
|
||||||
team.eligibility_locked = not team.eligibility_locked
|
|
||||||
team.save()
|
|
||||||
flash("Eligibility lock set to {}".format(team.eligibility_locked))
|
|
||||||
return redirect(url_for(".admin_show_team", tid=tid))
|
|
||||||
|
|
||||||
|
|
||||||
@admin.route("/team/<int:tid>/adjust_score/", methods=["POST"])
|
@admin.route("/team/<int:tid>/adjust_score/", methods=["POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_score_adjust(tid):
|
def admin_score_adjust(tid):
|
||||||
|
|
|
@ -8,7 +8,7 @@ import config
|
||||||
api = Blueprint("api", __name__, url_prefix="/api")
|
api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
@api.route("/submit/<int:challenge_id>.json", methods=["POST"])
|
@api.route("/submit/<challenge_id>.json", methods=["POST"])
|
||||||
@decorators.must_be_allowed_to("solve challenges")
|
@decorators.must_be_allowed_to("solve challenges")
|
||||||
@decorators.must_be_allowed_to("view challenges")
|
@decorators.must_be_allowed_to("view challenges")
|
||||||
@decorators.competition_running_required
|
@decorators.competition_running_required
|
||||||
|
@ -16,14 +16,15 @@ api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
@ratelimit.ratelimit(limit=10, per=120, over_limit=ratelimit.on_over_api_limit)
|
@ratelimit.ratelimit(limit=10, per=120, over_limit=ratelimit.on_over_api_limit)
|
||||||
def submit_api(challenge_id):
|
def submit_api(challenge_id):
|
||||||
try:
|
try:
|
||||||
chall = challenge.get_challenge(challenge_id)
|
chall = challenge.get_challenge(alias=challenge_id)
|
||||||
except exceptions.ValidationError as e:
|
except exceptions.ValidationError as e:
|
||||||
return jsonify(dict(code=1001, message=str(e)))
|
return jsonify(dict(code=1001, message=str(e)))
|
||||||
flag = request.form["flag"]
|
flag = request.form["flag"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
challenge.submit_flag(chall, g.user, g.team, flag)
|
challenge.submit_flag(chall, g.user, g.team, flag)
|
||||||
return jsonify(dict(code=0, message="Success!"))
|
solves = challenge.get_solve_count(chall.id)
|
||||||
|
return jsonify(dict(code=0, message="Success!", solves=solves))
|
||||||
except exceptions.ValidationError as e:
|
except exceptions.ValidationError as e:
|
||||||
return jsonify(dict(code=1001, message=str(e)))
|
return jsonify(dict(code=1001, message=str(e)))
|
||||||
|
|
||||||
|
|
|
@ -13,21 +13,23 @@ challenges = Blueprint("challenges", __name__, template_folder="../templates/cha
|
||||||
@decorators.competition_running_required
|
@decorators.competition_running_required
|
||||||
@decorators.confirmed_email_required
|
@decorators.confirmed_email_required
|
||||||
def index():
|
def index():
|
||||||
chals = challenge.get_challenges()
|
stages = challenge.get_stages()
|
||||||
|
challs = challenge.get_challenges()
|
||||||
solved = challenge.get_solved(g.team)
|
solved = challenge.get_solved(g.team)
|
||||||
solves = {i: int(g.redis.hget("solves", i).decode()) for i in [k.id for k in chals]}
|
solves = challenge.get_solve_counts()
|
||||||
categories = sorted(list({chal.category for chal in chals}))
|
categories = challenge.get_categories()
|
||||||
return render_template("challenges.html", challenges=chals, solved=solved, categories=categories, solves=solves)
|
first_stage = {chall.alias: True for chall in challs[stages[0].id]} if stages else None
|
||||||
|
return render_template("challenges.html", stages=stages, first_stage=first_stage, challenges=challs, solved=solved, categories=categories, solves=solves)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/challenges/<int:challenge_id>/solves/')
|
@challenges.route('/challenges/<challenge_id>/solves/')
|
||||||
@decorators.must_be_allowed_to("view challenge solves")
|
@decorators.must_be_allowed_to("view challenge solves")
|
||||||
@decorators.must_be_allowed_to("view challenges")
|
@decorators.must_be_allowed_to("view challenges")
|
||||||
@decorators.competition_running_required
|
@decorators.competition_running_required
|
||||||
@decorators.confirmed_email_required
|
@decorators.confirmed_email_required
|
||||||
def show_solves(challenge_id):
|
def show_solves(challenge_id):
|
||||||
try:
|
try:
|
||||||
chall = challenge.get_challenge(challenge_id)
|
chall = challenge.get_challenge(alias=challenge_id)
|
||||||
except exceptions.ValidationError as e:
|
except exceptions.ValidationError as e:
|
||||||
flash(str(e))
|
flash(str(e))
|
||||||
return redirect(url_for(".index"))
|
return redirect(url_for(".index"))
|
||||||
|
@ -35,7 +37,7 @@ def show_solves(challenge_id):
|
||||||
return render_template("challenge_solves.html", challenge=chall, solves=solves)
|
return render_template("challenge_solves.html", challenge=chall, solves=solves)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/submit/<int:challenge_id>/', methods=["POST"])
|
@challenges.route('/submit/<challenge_id>/', methods=["POST"])
|
||||||
@decorators.must_be_allowed_to("solve challenges")
|
@decorators.must_be_allowed_to("solve challenges")
|
||||||
@decorators.must_be_allowed_to("view challenges")
|
@decorators.must_be_allowed_to("view challenges")
|
||||||
@decorators.competition_running_required
|
@decorators.competition_running_required
|
||||||
|
|
|
@ -19,6 +19,6 @@ def index():
|
||||||
data.scoreboard.set_complex("scoreboard", data, 120)
|
data.scoreboard.set_complex("scoreboard", data, 120)
|
||||||
data.scoreboard.set_complex("graph", graphdata, 120)
|
data.scoreboard.set_complex("graph", graphdata, 120)
|
||||||
else:
|
else:
|
||||||
return "No scoreboard data available. Please contact an organizer."
|
return "CTF hasn't started!"
|
||||||
|
|
||||||
return render_template("scoreboard.html", data=scoreboard_data, graphdata=graphdata)
|
return render_template("scoreboard.html", data=scoreboard_data, graphdata=graphdata)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
from flask import Blueprint, g, render_template
|
||||||
|
|
||||||
|
from data import ssh
|
||||||
|
from utils import decorators
|
||||||
|
|
||||||
|
|
||||||
|
shell = Blueprint("shell", __name__, template_folder="../templates/shell")
|
||||||
|
|
||||||
|
|
||||||
|
@shell.route('/shell/')
|
||||||
|
@decorators.must_be_allowed_to("access shell")
|
||||||
|
@decorators.competition_running_required
|
||||||
|
@decorators.confirmed_email_required
|
||||||
|
def index():
|
||||||
|
account = ssh.get_team_account(g.team)
|
||||||
|
return render_template("shell.html", account=account)
|
|
@ -17,7 +17,7 @@ teams = Blueprint("teams", __name__, template_folder="../templates/teams")
|
||||||
@ratelimit.ratelimit(limit=6, per=120)
|
@ratelimit.ratelimit(limit=6, per=120)
|
||||||
def dashboard():
|
def dashboard():
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
team_solves = challenge.get_solved(g.team)
|
team_solves = challenge.get_solves(g.team)
|
||||||
team_adjustments = challenge.get_adjustments(g.team)
|
team_adjustments = challenge.get_adjustments(g.team)
|
||||||
team_score = sum([i.challenge.points for i in team_solves] + [i.value for i in team_adjustments])
|
team_score = sum([i.challenge.points for i in team_solves] + [i.value for i in team_adjustments])
|
||||||
return render_template("team.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score)
|
return render_template("team.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score)
|
||||||
|
|
|
@ -30,44 +30,44 @@ def open_ticket():
|
||||||
description = request.form["description"]
|
description = request.form["description"]
|
||||||
t = ticket.create_ticket(g.team, summary, description)
|
t = ticket.create_ticket(g.team, summary, description)
|
||||||
flash("Ticket #{} opened.".format(t.id))
|
flash("Ticket #{} opened.".format(t.id))
|
||||||
return redirect(url_for(".detail", ticket=t.id))
|
return redirect(url_for(".detail", ticket_id=t.id))
|
||||||
|
|
||||||
|
|
||||||
@tickets.route('/tickets/<int:ticket>/')
|
@tickets.route('/tickets/<int:ticket_id>/')
|
||||||
@decorators.must_be_allowed_to("view tickets")
|
@decorators.must_be_allowed_to("view tickets")
|
||||||
@decorators.login_required
|
@decorators.login_required
|
||||||
def detail(ticket):
|
def detail(ticket_id):
|
||||||
try:
|
try:
|
||||||
ticket = ticket.get_ticket(g.team, ticket)
|
t = ticket.get_ticket(g.team, ticket_id)
|
||||||
except exceptions.ValidationError as e:
|
except exceptions.ValidationError as e:
|
||||||
flash(str(e))
|
flash(str(e))
|
||||||
return redirect(url_for(".index"))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
comments = ticket.get_comments(ticket)
|
comments = ticket.get_comments(t)
|
||||||
return render_template("ticket_detail.html", ticket=ticket, comments=comments)
|
return render_template("ticket_detail.html", ticket=t, comments=comments)
|
||||||
|
|
||||||
|
|
||||||
@tickets.route('/tickets/<int:ticket>/comment/', methods=["POST"])
|
@tickets.route('/tickets/<int:ticket_id>/comment/', methods=["POST"])
|
||||||
@decorators.must_be_allowed_to("comment on tickets")
|
@decorators.must_be_allowed_to("comment on tickets")
|
||||||
@decorators.must_be_allowed_to("view tickets")
|
@decorators.must_be_allowed_to("view tickets")
|
||||||
@ratelimit.ratelimit(limit=1, per=120)
|
@ratelimit.ratelimit(limit=1, per=120)
|
||||||
def comment(ticket):
|
def comment(ticket_id):
|
||||||
try:
|
try:
|
||||||
ticket = ticket.get_ticket(g.team, ticket)
|
t = ticket.get_ticket(g.team, ticket_id)
|
||||||
except exceptions.ValidationError as e:
|
except exceptions.ValidationError as e:
|
||||||
flash(str(e))
|
flash(str(e))
|
||||||
return redirect(url_for(".index"))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
if request.form["comment"]:
|
if request.form["comment"]:
|
||||||
ticket.create_comment(ticket, g.user, request.form["comment"])
|
ticket.create_comment(t, g.user, request.form["comment"])
|
||||||
flash("Comment added.")
|
flash("Comment added.")
|
||||||
|
|
||||||
if ticket.active and "resolved" in request.form:
|
if t.active and "resolved" in request.form:
|
||||||
ticket.close_ticket(ticket)
|
ticket.close_ticket(t)
|
||||||
flash("Ticket closed.")
|
flash("Ticket closed.")
|
||||||
|
|
||||||
elif not ticket.active and "resolved" not in request.form:
|
elif not t.active and "resolved" not in request.form:
|
||||||
ticket.open_ticket(ticket)
|
ticket.open_ticket(t)
|
||||||
flash("Ticket re-opened.")
|
flash("Ticket re-opened.")
|
||||||
|
|
||||||
return redirect(url_for(".detail", ticket=ticket.id))
|
return redirect(url_for(".detail", ticket_id=t.id))
|
||||||
|
|
|
@ -68,7 +68,7 @@ def register():
|
||||||
if join_team:
|
if join_team:
|
||||||
team_key = request.form["team_key"].strip()
|
team_key = request.form["team_key"].strip()
|
||||||
t = team.get_team(key=team_key)
|
t = team.get_team(key=team_key)
|
||||||
if not team:
|
if not t:
|
||||||
flash("This team could not be found, check your team key.")
|
flash("This team could not be found, check your team key.")
|
||||||
return redirect(url_for('.register'))
|
return redirect(url_for('.register'))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -170,6 +170,25 @@ body {
|
||||||
margin-left: -40px;
|
margin-left: -40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code, kbd, pre, samp {
|
||||||
|
font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge code {
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 90%;
|
||||||
|
color: #c7254e;
|
||||||
|
background-color: #f9f2f4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-frame {
|
||||||
|
border-width: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* MEDIUM */
|
/* MEDIUM */
|
||||||
@media only screen and (max-width : 992px) {
|
@media only screen and (max-width : 992px) {
|
||||||
.cont {
|
.cont {
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<a href="{{ url_for('admin.admin_show_team', tid=team.id) }}">{{ team.name }}</a>
|
<a href="{{ url_for('admin.admin_show_team', tid=team.id) }}">{{ team.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ team.affiliation }}</td>
|
<td>{{ team.affiliation }}</td>
|
||||||
<td>{{ "Eligible" if team.eligible else "Ineligible" }}</td>
|
<td>{{ "Eligible" if team.eligible() else "Ineligible" }}</td>
|
||||||
<td><abbr class="time" title="{{ lastsolvedata[team.id] }}">{{ lastsolvedata[team.id] }}</abbr></td>
|
<td><abbr class="time" title="{{ lastsolvedata[team.id] }}">{{ lastsolvedata[team.id] }}</abbr></td>
|
||||||
<td>{{ scoredata[team.id] }}</td>
|
<td>{{ scoredata[team.id] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
<h2>{{ team.name }}</h2>
|
<h2>{{ team.name }}</h2>
|
||||||
<a href="{{ url_for('admin.admin_impersonate_team', tid=team.id, csrf=csrf_token()) }}">Impersonate team</a><br />
|
<a href="{{ url_for('admin.admin_impersonate_team', tid=team.id, csrf=csrf_token()) }}">Impersonate team</a><br />
|
||||||
<p>
|
<p>
|
||||||
This team is <strong>{{ "eligible" if team.eligible else "not eligible" }}</strong> (<a href="{{ url_for('admin.admin_toggle_eligibility', tid=team.id, csrf=csrf_token()) }}">toggle</a>).
|
This team is <strong>{{ "eligible" if team.eligible() else "not eligible" }}</strong> (<a href="{{ url_for('admin.admin_toggle_eligibility', tid=team.id, csrf=csrf_token()) }}">toggle</a>).
|
||||||
Eligibility is <strong>{{ "locked" if team.eligibility_locked else "unlocked" }}</strong> (<a href="{{ url_for('admin.admin_toggle_eligibility_lock', tid=team.id, csrf=csrf_token()) }}">toggle</a>).
|
|
||||||
</p>
|
</p>
|
||||||
<p>This team's affiliation is <strong>{{ team.affiliation }}</strong></p>
|
<p>This team's affiliation is <strong>{{ team.affiliation }}</strong></p>
|
||||||
<h3>Email</h3>
|
<h3>Email</h3>
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
<ul id="nav-mobile" class="side-nav fixed">
|
<ul id="nav-mobile" class="side-nav fixed">
|
||||||
|
|
||||||
{% if config.competition_is_running() %}
|
{% if config.competition_is_running() or session.admin %}
|
||||||
{% if logged_in %}
|
{% if logged_in %}
|
||||||
<li><a href="{{ url_for('challenges.index') }}">
|
<li><a href="{{ url_for('challenges.index') }}">
|
||||||
<div class="side-icon"><i class="material-icons blue-text">flag</i></div>
|
<div class="side-icon"><i class="material-icons blue-text">flag</i></div>
|
||||||
|
@ -59,9 +59,9 @@
|
||||||
<div class="side-icon"><i class="material-icons red-text">timeline</i></div>
|
<div class="side-icon"><i class="material-icons red-text">timeline</i></div>
|
||||||
<div class="side-text">Scoreboard</div>
|
<div class="side-text">Scoreboard</div>
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href="{{ url_for('chat') }}">
|
<li><a href="{{ url_for('shell.index') }}">
|
||||||
<div class="side-icon"><i class="material-icons purple-text">build</i></div>
|
<div class="side-icon"><i class="material-icons purple-text">computer</i></div>
|
||||||
<div class="side-text">Status</div>
|
<div class="side-text">Shell</div>
|
||||||
</a></li>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if logged_in %}
|
{% if logged_in %}
|
||||||
|
|
|
@ -1,60 +1,59 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Challenges{% endblock %}
|
{% block title %}Challenges{% endblock %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script>
|
|
||||||
var state = !{{ solved.count() }};
|
|
||||||
function openAll() {
|
|
||||||
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") || $(x).click(); });
|
|
||||||
$("#toggleState").html("Collapse all challenges.");
|
|
||||||
}
|
|
||||||
function closeAll() {
|
|
||||||
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") && $(x).click(); });
|
|
||||||
$("#toggleState").html("Expand all challenges.");
|
|
||||||
}
|
|
||||||
function toggle() {
|
|
||||||
if(state) closeAll();
|
|
||||||
else openAll();
|
|
||||||
state = !state;
|
|
||||||
}
|
|
||||||
function filterCategories(t) {
|
|
||||||
var v = t.options[t.selectedIndex].value;
|
|
||||||
if(v == "*")
|
|
||||||
$(".challenge").show();
|
|
||||||
else {
|
|
||||||
$(".challenge[data-category=" + v + "]").show();
|
|
||||||
$(".challenge[data-category!=" + v + "]").hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<section>
|
||||||
<select onchange="filterCategories(this);">
|
<div class="row">
|
||||||
<option value="*">Show all</option>
|
<div class="col s12">
|
||||||
{% for category in categories %}
|
<div class="card purple darken-1">
|
||||||
<option>{{ category }}</option>
|
<div class="card-content white-text">
|
||||||
{% endfor %}
|
Due to way more users than expected, the platform and some problems may not be stable. Please report all errors encountered in a ticket or on IRC, and be patient :).
|
||||||
</select>
|
</div>
|
||||||
<span class="left"><a href="javascript:toggle()" id="toggleState">{% if solved.count() %}Expand all challenges.{% else %}Collapse all challenges.{% endif %}</a></span>
|
</div>
|
||||||
<br />
|
</div>
|
||||||
|
</div>
|
||||||
<ul class="collapsible popout" data-collapsible="accordion">
|
</section>
|
||||||
{% for challenge in challenges %}
|
<div class="row">
|
||||||
<li class="challenge" data-category="{{ challenge.category }}">
|
<div class="col s6">
|
||||||
<div id="header{{ challenge.id }}" class="collapsible-header">
|
<div class="left">
|
||||||
|
<select onchange="filterCategories(this);">
|
||||||
|
<option value="*">Show all</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option>{{ category }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col s6">
|
||||||
|
<span class="right"><a class="btn" href="#" id="toggleState">Collapse all challenges</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for stage in stages %}
|
||||||
|
<hr />
|
||||||
|
{% if stage.description %}
|
||||||
|
<p>{{stage.description}}</p>
|
||||||
|
{% endif %}
|
||||||
|
<span class="left"><h4>{{ stage.name }}</h4></span>
|
||||||
|
<span class="right"><a class="stage-collapse" data-collapse="{{ stage.alias }}" href="#">Toggle</a></span>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
<ul class="collapsible popout" data-collapsible="expandable">
|
||||||
|
{% for challenge in challenges[stage.id] %}
|
||||||
|
<li class="challenge" data-category="{{ challenge.category }}" data-stage="{{stage.alias}}">
|
||||||
|
<div id="header-{{ challenge.alias }}" data-challenge="{{ challenge.alias }}" class="collapsible-header">
|
||||||
<div class="status-dot green circle tooltipped" data-position="right" data-tooltip="This challenge is Online"></div>
|
<div class="status-dot green circle tooltipped" data-position="right" data-tooltip="This challenge is Online"></div>
|
||||||
<div class="challenge-text truncate">{{ challenge.name }}</div>
|
<div class="challenge-text truncate">{{ challenge.name }}</div>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
<span>{{ challenge.author }}</span>
|
<span>{{ challenge.author }}</span>
|
||||||
<b>·</b>
|
<b>·</b>
|
||||||
<span id="solves{{ challenge.id }}">{{ solves[challenge.id] }}</span> {% if solves[challenge.id] == 1 %}solve{% else %}solves{% endif %}
|
<span id="solves-{{ challenge.alias }}">{{ solves[challenge.id] }}</span> {% if solves[challenge.id] == 1 %}solve{% else %}solves{% endif %}
|
||||||
<b>·</b>
|
<b>·</b>
|
||||||
{{ challenge.category }}
|
{{ challenge.category }}
|
||||||
<b>·</b>
|
<b>·</b>
|
||||||
{{ challenge.points }} pt
|
{{ challenge.points }} pt
|
||||||
|
|
||||||
{% if challenge in solved %}
|
{% if challenge in solved %}
|
||||||
<i id="check{{ challenge.id }}" class="material-icons green-text right check-icon">done</i>
|
<i id="check-{{ challenge.alias }}" class="material-icons green-text right check-icon">done</i>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="check-pad"></div>
|
<div class="check-pad"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -63,19 +62,19 @@
|
||||||
<div class="collapsible-body">
|
<div class="collapsible-body">
|
||||||
<p>{{ challenge.description | safe }}
|
<p>{{ challenge.description | safe }}
|
||||||
{% if challenge in solved %}
|
{% if challenge in solved %}
|
||||||
<br /><br /><strong>You've solved this challenge!</strong><br />
|
<br /><br /><strong>Your team has solved this challenge!</strong><br />
|
||||||
<a href="{{ url_for('.show_solves', challenge_id=challenge.id) }}">View solves</a>
|
<a href="{{ url_for('.show_solves', challenge_id=challenge.alias) }}">View solves</a>
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<a href="{{ url_for('.show_solves', challenge_id=challenge.id) }}">View solves</a>
|
<a href="{{ url_for('.show_solves', challenge_id=challenge.alias) }}">View solves</a>
|
||||||
</p>
|
</p>
|
||||||
<form class="flag-form" action="{{ url_for('.submit', challenge_id=challenge.id) }}" data-challengeid="{{ challenge.id }}" method="POST">
|
<form class="flag-form" action="{{ url_for('.submit', challenge_id=challenge.alias) }}" data-challenge="{{ challenge.alias }}" method="POST">
|
||||||
<div class="row no-bot">
|
<div class="row no-bot">
|
||||||
<div class="col s12 m10">
|
<div class="col s12 m10">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<input required id="flag{{ challenge.id }}" name="flag" type="text" />
|
<input required id="flag-{{ challenge.alias }}" name="flag" type="text" />
|
||||||
<label for="flag{{ challenge.id }}">Flag</label>
|
<label for="flag-{{ challenge.alias }}">Flag</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m2">
|
<div class="col s12 m2">
|
||||||
|
@ -88,6 +87,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block postscript %}
|
{% block postscript %}
|
||||||
<script>
|
<script>
|
||||||
|
@ -97,17 +97,77 @@
|
||||||
</script>
|
</script>
|
||||||
{% if config.apisubmit %}
|
{% if config.apisubmit %}
|
||||||
<script>
|
<script>
|
||||||
|
var challenges_open = JSON.parse(localStorage.getItem("challenges-open")) || {{ first_stage | tojson }};
|
||||||
|
Object.keys(challenges_open).forEach(function(el){
|
||||||
|
$(".collapsible-header#header-" + el).addClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".collapsible-header").on("click", function(){
|
||||||
|
var chall = $(this).data('challenge');
|
||||||
|
if(challenges_open.hasOwnProperty(chall)) {
|
||||||
|
delete challenges_open[chall];
|
||||||
|
} else {
|
||||||
|
challenges_open[chall] = true;
|
||||||
|
}
|
||||||
|
localStorage.setItem("challenges-open", JSON.stringify(challenges_open));
|
||||||
|
|
||||||
|
});
|
||||||
|
var state = $(".collapsible-header.active").length == 0;
|
||||||
|
function updateButton(){
|
||||||
|
if(state){
|
||||||
|
$("#toggleState").html("Expand all challenges");
|
||||||
|
} else {
|
||||||
|
$("#toggleState").html("Collapse all challenges");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
updateButton(state);
|
||||||
|
function toggle() {
|
||||||
|
if(state) {
|
||||||
|
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") || $(x).click(); });
|
||||||
|
} else {
|
||||||
|
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") && $(x).click(); });
|
||||||
|
}
|
||||||
|
state = !state;
|
||||||
|
updateButton(state);
|
||||||
|
}
|
||||||
|
function filterCategories(t) {
|
||||||
|
var v = t.options[t.selectedIndex].value;
|
||||||
|
if(v == "*")
|
||||||
|
$(".challenge").show();
|
||||||
|
else {
|
||||||
|
$(".challenge[data-category=" + v + "]").show();
|
||||||
|
$(".challenge[data-category!=" + v + "]").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$("#toggleState").on("click", function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
$(".stage-collapse").on("click", function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
var stage = $(this).data('collapse');
|
||||||
|
var header_selector = ".challenge[data-stage=" + stage + "] .collapsible-header";
|
||||||
|
var state = $(header_selector + ".active").length != 0;
|
||||||
|
if(state)
|
||||||
|
$(header_selector).each(function(i, x){ $(x).hasClass("active") && $(x).click(); });
|
||||||
|
else
|
||||||
|
$(header_selector).each(function(i, x){ $(x).hasClass("active") || $(x).click(); });
|
||||||
|
});
|
||||||
$("form").submit(function(e) {
|
$("form").submit(function(e) {
|
||||||
var id = $(this).attr("data-challengeid");
|
var id = $(this).data("challenge");
|
||||||
api.makeCall("/submit/" + id + ".json", {flag: $("#flag" + id).val(), _csrf_token: "{{ csrf_token() }}"}, function(data) {
|
var flag = $("#flag-" + id).val();
|
||||||
|
console.log(id);
|
||||||
|
console.log(flag);
|
||||||
|
api.makeCall("/submit/" + id + ".json", {flag: flag, _csrf_token: "{{ csrf_token() }}"}, function(data) {
|
||||||
if(data.code) {
|
if(data.code) {
|
||||||
Materialize.toast(data.message, 4000);
|
Materialize.toast(data.message, 4000);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Materialize.toast("Flag accepted!", 4000);
|
Materialize.toast("Flag accepted!", 4000);
|
||||||
$("#check" + id).show();
|
$("#check-" + id).show();
|
||||||
$("#header" + id).click();
|
$("#header-" + id).click();
|
||||||
$("#solves" + id).html(parseInt($("#solves" + id).html()) + 1);
|
$("#solves-" + id).html(data.solves);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
{% if account %}
|
||||||
|
<div class="card orange darken-2">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<p class="card-title">Username: {{account.username}} Password: {{account.password}}</p>
|
||||||
|
<code> ssh {{account.username}}@{{account.hostname}} -p {{account.port}}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center" style="margin-top: 50px;">
|
||||||
|
<iframe class="shell-frame" src="https://shell.icec.tf/wetty" frameBorder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card red darken-2">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<p class="card-title">You don't have an account yet! Don't worry, one will be created shortly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -177,6 +177,4 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block postscript %}
|
{% block postscript %}
|
||||||
<script>$('.modal-trigger').leanModal();</script>
|
<script>$('.modal-trigger').leanModal();</script>
|
||||||
{% endif %}
|
|
||||||
<script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<small><abbr class="time" title="{{ comment.time }}">{{ comment.time }}</abbr> · {{ comment.comment_by }}</small>
|
<small><abbr class="time" title="{{ comment.time }}">{{ comment.time }}</abbr> · {{ comment.comment_by }}</small>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<br />
|
<br />
|
||||||
<form action="{{ url_for('.comment', ticket=ticket.id) }}" method="POST">
|
<form action="{{ url_for('.comment', ticket_id=ticket.id) }}" method="POST">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<textarea id="comment" name="comment" class="materialize-textarea"></textarea>
|
<textarea id="comment" name="comment" class="materialize-textarea"></textarea>
|
||||||
<label for="comment">Comment</label>
|
<label for="comment">Comment</label>
|
||||||
|
|
|
@ -7,7 +7,7 @@ You have the following open tickets. If you're having an issue, you can <a href=
|
||||||
<div class="collection">
|
<div class="collection">
|
||||||
{% for ticket in tickets %}
|
{% for ticket in tickets %}
|
||||||
{% if ticket.active %}
|
{% if ticket.active %}
|
||||||
<a class="collection-item" href="{{ url_for('.detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
|
<a class="collection-item" href="{{ url_for('.detail', ticket_id=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@ You have the following closed tickets:
|
||||||
<div class="collection">
|
<div class="collection">
|
||||||
{% for ticket in tickets %}
|
{% for ticket in tickets %}
|
||||||
{% if not ticket.active %}
|
{% if not ticket.active %}
|
||||||
<a class="collection-item" href="{{ url_for('.detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
|
<a class="collection-item" href="{{ url_for('.detail', ticket_id=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ def login_required(f):
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
flash("You need to be logged in")
|
flash("You need to be logged in")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('users.login'))
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
def must_be_allowed_to(thing):
|
def must_be_allowed_to(thing):
|
||||||
|
@ -17,7 +17,7 @@ def must_be_allowed_to(thing):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if getattr(g, 'user_restricts', None) is None:
|
if getattr(g, 'user_restricts', None) is None:
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('users.login'))
|
||||||
if g.user_restricts and thing in g.user_restricts:
|
if g.user_restricts and thing in g.user_restricts:
|
||||||
return "You are restricted from performing the {} action. Contact an organizer.".format(thing)
|
return "You are restricted from performing the {} action. Contact an organizer.".format(thing)
|
||||||
|
|
||||||
|
@ -31,20 +31,20 @@ def confirmed_email_required(f):
|
||||||
if "user_id" in session and session["user_id"]:
|
if "user_id" in session and session["user_id"]:
|
||||||
if not g.user.email_confirmed:
|
if not g.user.email_confirmed:
|
||||||
flash("Please confirm your email")
|
flash("Please confirm your email")
|
||||||
return redirect(url_for('user_dashboard'))
|
return redirect(url_for('users.dashboard'))
|
||||||
else:
|
else:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
flash("You need to be logged in to access that page.")
|
flash("You need to be logged in to access that page.")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('users.login'))
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
def competition_running_required(f):
|
def competition_running_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if not config.competition_is_running():
|
if not config.competition_is_running() and not ("admin" in session and session["admin"]):
|
||||||
flash("The competition hasn't started")
|
flash("The competition hasn't started")
|
||||||
return redirect(url_for('scoreboard'))
|
return redirect(url_for('scoreboard.index'))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class RateLimit(object):
|
||||||
self.current = min(p.execute()[0], limit)
|
self.current = min(p.execute()[0], limit)
|
||||||
|
|
||||||
remaining = property(lambda x: x.limit - x.current)
|
remaining = property(lambda x: x.limit - x.current)
|
||||||
over_limit = property(lambda x: x.current >= x.limit)
|
over_limit = property(lambda x: x.current > x.limit)
|
||||||
|
|
||||||
|
|
||||||
def get_view_rate_limit():
|
def get_view_rate_limit():
|
||||||
|
|
Loading…
Reference in New Issue