Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Staging #36

Open
wants to merge 51 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b095bc1
[WIP] Connect with OpenCTF
Aug 26, 2016
0ad271b
Use functools.partial for functions as SQLAlchemy Column defaults.
chaosagent Aug 30, 2016
aba5bb8
Use url_for instead of hardcoding URLs in layout template.
chaosagent Aug 30, 2016
855bee6
Paginate lists.
chaosagent Aug 30, 2016
a32515a
Move Event start time isoformatting to a @property
chaosagent Aug 30, 2016
7657e05
Implement upcoming and past events
chaosagent Aug 30, 2016
6eadae3
Add page numbers + use .all()
Aug 31, 2016
69c229f
Disable 'next' button on last page.
chaosagent Aug 31, 2016
4968553
Add 'page/' URL component to all paginated things.
chaosagent Aug 31, 2016
2739305
Use functools.partial for functions as SQLAlchemy Column defaults.
chaosagent Aug 30, 2016
a849b43
Fix event manage.
chaosagent Sep 1, 2016
846e91a
Add (unix time) message.
Sep 1, 2016
b0a5b88
Create .travis.yml
Sep 1, 2016
a0d7034
Get rid of python3
Sep 1, 2016
4adf155
Add sanity test.
Sep 1, 2016
728d0d2
Remove .coverage and .cache
Sep 1, 2016
8deba85
ayy committing blank lines?
Sep 1, 2016
786477b
Request sudo
Sep 1, 2016
3e26c38
Add forgot password page.
Sep 4, 2016
bc45fab
Merge branch 'master' of github.com:easyctf/ctf-calendar
Sep 4, 2016
dcaba16
Merge branch 'master' into feature/forgot-password
Sep 4, 2016
d5c3aa2
Password reset + send email
Sep 5, 2016
e2af844
Finished password reset
Sep 5, 2016
d771c55
Merge pull request #31 from EasyCTF/master
chaosagent Sep 5, 2016
1f60757
Update from david-kun's comments
Sep 5, 2016
bfae171
Clean up code
chaosagent Sep 5, 2016
7498791
Clean up some code, attach (password reset) tokens to user/email pair…
chaosagent Sep 5, 2016
23d7245
Reformat code
chaosagent Sep 5, 2016
1f6f508
Edit revision.
Sep 5, 2016
7f0b14d
Fix migration for passwordresettoken/user.id FK
chaosagent Sep 5, 2016
21d7fbf
Merge branch 'feature/forgot-password' of github.com:EasyCTF/ctf-cale…
chaosagent Sep 5, 2016
dc085b0
Merge pull request #30 from EasyCTF/feature/forgot-password
chaosagent Sep 6, 2016
8394942
Add tests.
Sep 7, 2016
e90945b
Enable testing.
Sep 8, 2016
dcefa2d
Add configuration.
Sep 8, 2016
6cb1957
Get rid of drop_everything
Sep 8, 2016
ce864fc
Remove redundant config set
chaosagent Sep 9, 2016
0391330
Drop tables before and after each db fixture
chaosagent Sep 9, 2016
5faa4ef
Merge pull request #35 from EasyCTF/feature/tests
chaosagent Sep 9, 2016
fe7a93c
More tests!
Oct 18, 2016
8677fbb
Merge branch 'feature/tests' of github.com:easyctf/ctf-calendar into …
Oct 18, 2016
3633911
Covered filters.
Oct 18, 2016
4182653
Fix timezone :/
Oct 19, 2016
4c788eb
More tests.
Nov 22, 2016
85ae46c
ayy
Nov 22, 2016
f135a86
Merge branch 'feature/oauth2-provider'
Nov 22, 2016
0d7d06f
Merge branch 'staging' of github.com:easyctf/ctf-calendar into staging
Nov 22, 2016
ce804ef
Merge branch 'feature/tests' into staging
Nov 22, 2016
b434ff1
hi
Nov 22, 2016
1c59dd5
Some stuff.
Nov 22, 2016
3c09f66
Add TODO marking the invalidated redirect.
chaosagent Feb 1, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[report]
omit =
tests/*
config.py
manage.py
models.py
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
.env
.secret_key
npm-debug.log

.coverage
.cache
coverage.xml
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
web: python manage.py db upgrade && gunicorn cal:app --log-file -
web: python manage.py db upgrade && gunicorn -w 4 cal:app --log-file -
dev: python manage.py db upgrade && python manage.py runserver
1 change: 1 addition & 0 deletions cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
login_manager.init_app(app)
oauth.init_app(app)

app.register_blueprint(views.api.blueprint, url_prefix='/api')
app.register_blueprint(views.base.blueprint)
app.register_blueprint(views.events.blueprint, url_prefix='/events')
app.register_blueprint(views.oauth.blueprint, url_prefix='/oauth')
Expand Down
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def __init__(self, app_root=None, testing=False):
self.SQLALCHEMY_TRACK_MODIFICATIONS = False
self.TEMPLATES_AUTO_RELOAD = True

self.MAILGUN_DOMAIN = os.getenv('MAILGUN_DOMAIN', '')
self.MAILGUN_API_KEY = os.getenv('MAILGUN_API_KEY', '')

if testing:
self.TESTING = True
self.WTF_CSRF_ENABLED = False
Expand Down
Empty file removed conftest.py
Empty file.
88 changes: 66 additions & 22 deletions forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,103 @@
from wtforms.fields import *
from wtforms.validators import *
from wtforms.widgets import TextArea
from wtforms_components import read_only

import util
from models import User


class LoginForm(Form):
identifier = StringField('Username', validators=[InputRequired()])
password = PasswordField('Password', validators=[InputRequired()])
identifier = StringField("Username", validators=[InputRequired()])
password = PasswordField("Password", validators=[InputRequired()])

def get_user(self, identifier=None):
return User.get_by_identifier(identifier or self.identifier.data)

def validate_identifier(self, field):
if self.get_user(field.data) is None:
raise ValidationError('Invalid identifier')
raise ValidationError("Invalid identifier")

def validate_password(self, field):
user = self.get_user(self.identifier.data)
if not user:
return
if not user.check_password(field.data):
raise ValidationError('Invalid password')
raise ValidationError("Invalid password")


class RegisterForm(Form):
email = StringField('Email', validators=[InputRequired()])
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=16,
message='Username must be between 4 and 16 characters long.')])
password = PasswordField('Password', validators=[InputRequired(), Length(min=8, max=56,
message='Password must be between 8 and 56 characters long.')])
email = StringField("Email", validators=[InputRequired()])
username = StringField("Username", validators=[InputRequired(), Length(min=4, max=16, message="Username must be between 4 and 16 characters long.")])
password = PasswordField("Password", validators=[InputRequired(), Length(min=8, max=56, message="Password must be between 8 and 56 characters long.")])


def validate_email(self, field):
if not util.validate_email_format(field.data):
raise ValidationError("Invalid email")
if User.query.filter(func.lower(User.email) == func.lower(field.data)).count():
raise ValidationError("Email taken!")


def validate_username(self, field):
if not util.validate_username_format(field.data):
raise ValidationError("Invalid username")
if User.query.filter(func.lower(User.username) == func.lower(field.data)).count():
raise ValidationError("Username taken!")


class PasswordForgotForm(Form):
email = StringField("Email", validators=[InputRequired()])

def __init__(self):
super(PasswordForgotForm, self).__init__()
self._user = None
self._user_cached = False

@property
def user(self):
if not self._user_cached:
self._user = User.query.filter(func.lower(User.email) == self.email.data.lower()).first()
self._user_cached = True
return self._user

def validate_email(self, field):
if not util.validate_email_format(field.data):
raise ValidationError('Invalid email')
if User.query.filter(func.lower(User.email) == func.lower(field.data)).count():
raise ValidationError('Email taken!')
raise ValidationError("Invalid email")

def validate_username(self, field):
if not util.validate_username_format(field.data):
raise ValidationError('Invalid username')
if User.query.filter(func.lower(User.username) == func.lower(field.data)).count():
raise ValidationError('Username taken!')

class PasswordResetForm(Form):
password = PasswordField('New Password', validators=[InputRequired(), Length(min=8, max=56, message='Password must be between 8 and 56 characters long.')])
password_confirm = PasswordField('Confirm Password', validators=[InputRequired(), EqualTo('password', message='Passwords must match.')])

class EventForm(Form):

class EventCreateForm(Form):
title = StringField('Title', validators=[InputRequired(), Length(max=256)])
start_time = IntegerField('Start Time (UNIX Time)', validators=[InputRequired(), NumberRange(min=0, max=2147483647,
message='Start time must be between 0 and 2147483647!')])
duration = FloatField('Duration (Hours)', validators=[InputRequired(), NumberRange(min=0, max=2147483647,
message='Duration must be between 0 and 2147483647!')])
start_time = IntegerField('Start Time', validators=[InputRequired(), NumberRange(min=0, max=2147483647, message='Start time must be between 0 and 2147483647!')])
duration = FloatField('Duration (hours)', validators=[InputRequired(), NumberRange(min=0, max=2147483647, message='Duration must be between 0 and 2147483647!')])
description = StringField('Description', widget=TextArea(), validators=[InputRequired(), Length(max=1024)])
link = StringField('Link', validators=[InputRequired(), Length(max=256)])

def validate_link(self, field):
if not any(field.data.startswith(prefix) for prefix in [u'http://', u'https://']):
raise ValidationError('Invalid link')


class EventManageForm(Form):
title = StringField('Title', validators=[InputRequired(), Length(max=256)])
start_time = IntegerField('Start Time', validators=[InputRequired(), NumberRange(min=0, max=2147483647, message='Start time must be between 0 and 2147483647!')])
duration = FloatField('Duration (hours)', validators=[InputRequired(), NumberRange(min=0, max=2147483647, message='Duration must be between 0 and 2147483647!')])
description = StringField('Description', widget=TextArea(), validators=[InputRequired(), Length(max=1024)])
link = StringField('Link', validators=[InputRequired(), Length(max=256)])
client_id = StringField('Client ID')
client_secret = StringField('Client Secret')
redirect_uris = StringField('Redirect URIs', widget=TextArea(), validators=[])

def __init__(self, *args, **kwargs):
super(EventManageForm, self).__init__(*args, **kwargs)
read_only(self.client_id)
read_only(self.client_secret)

def validate_link(self, field):
if not any(field.data.startswith(prefix) for prefix in [u'http://', u'https://']):
raise ValidationError('Invalid link')
33 changes: 33 additions & 0 deletions migrations/versions/7905cec750a2_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Add password reset fields.

Revision ID: 7905cec750a2
Revises: d25be15cb75d
Create Date: 2016-09-04 00:53:58.969972

"""

# revision identifiers, used by Alembic.
revision = '7905cec750a2'
down_revision = 'd25be15cb75d'

import sqlalchemy as sa
from alembic import op


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('password_reset_tokens',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('token', sa.String(length=16), nullable=True),
sa.Column('email', sa.Unicode(length=128), nullable=True),
sa.Column('expire', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('password_reset_tokens')
### end Alembic commands ###
28 changes: 28 additions & 0 deletions migrations/versions/bf3dc65dd471_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Add relationship to user to password reset token.

Revision ID: bf3dc65dd471
Revises: 7905cec750a2
Create Date: 2016-09-05 12:38:07.126105

"""

# revision identifiers, used by Alembic.
revision = 'bf3dc65dd471'
down_revision = '7905cec750a2'

import sqlalchemy as sa
from alembic import op


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('password_reset_tokens', sa.Column('user_id', sa.Integer(), nullable=True))
op.create_foreign_key('pwd_reset_token_user_id_fk', 'password_reset_tokens', 'users', ['user_id'], ['id'])
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('pwd_reset_token_user_id_fk', 'password_reset_tokens', type_='foreignkey')
op.drop_column('password_reset_tokens', 'user_id')
### end Alembic commands ###
39 changes: 33 additions & 6 deletions models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import time
from datetime import datetime, timedelta
from functools import partial

from flask_login import current_user, LoginManager
from flask_oauthlib.provider import OAuth2Provider
Expand All @@ -22,7 +22,7 @@ class User(db.Model):
email = db.Column(db.Unicode(length=128), unique=True)
_password = db.Column('password', db.String(length=60)) # password hash
admin = db.Column(db.Boolean, default=False)
joined = db.Column(db.DateTime, default=datetime.utcnow)
_joined = db.Column('joined', db.DateTime, default=datetime.utcnow)

def __eq__(self, other):
if isinstance(other, User):
Expand All @@ -40,6 +40,10 @@ def __ne__(self, other):
def __repr__(self):
return '<User %r>' % self.username

@hybrid_property
def joined(self):
return int(time.mktime(self._joined.timetuple()))

@property
def is_active(self):
return True
Expand Down Expand Up @@ -99,11 +103,11 @@ class Event(db.Model):
removed = db.Column(db.Boolean, default=False)

# OAuth2 stuff
client_id = db.Column(db.String(40), unique=True, default=partial(util.generate_string, 16))
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False, default=partial(util.generate_string, 32))
client_id = db.Column(db.String(40), unique=True, default=lambda: util.generate_string(16))
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False, default=lambda: util.generate_string(32))
is_confidential = db.Column(db.Boolean, default=True)
_redirect_uris = db.Column(db.Text)
_default_scopes = db.Column(db.Text)
_default_scopes = db.Column(db.Text, default='profile')

@property
def formatted_start_time(self):
Expand All @@ -125,16 +129,24 @@ def client_type(self):

@property
def redirect_uris(self):
'getting'
if self._redirect_uris:
return self._redirect_uris.split()
return self._redirect_uris # .split()
return []

@redirect_uris.setter
def redirect_uris(self, value):
'setting'
self._redirect_uris = value
db.session.commit()

@property
def default_redirect_uri(self):
return self.redirect_uris[0]

@property
def default_scopes(self):
return ["user"]
if self._default_scopes:
return self._default_scopes.split()
return []
Expand Down Expand Up @@ -210,6 +222,21 @@ def scopes(self):
return []


class PasswordResetToken(db.Model):
__tablename__ = 'password_reset_tokens'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id', name='pwd_reset_token_user_id_fk'))
user = db.relationship('User', backref=db.backref('password_reset_tokens', lazy='dynamic'), lazy='joined')
active = db.Column(db.Boolean)
token = db.Column(db.String(length=16), default=util.generate_string_of(16))
email = db.Column(db.Unicode(length=128))
expire = db.Column(db.DateTime)

@property
def expired(self):
return datetime.now() >= self.expire


def get_current_user():
if current_user:
return current_user
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ Flask-SQLAlchemy
Flask-WTF
gunicorn
jinja2
git+git://github.com/failedxyz/oauthlib.git@master#egg=oauthlib
passlib
pathlib
psycopg2
pymysql
pytest
python-dotenv
requests
sqlalchemy
wtforms
wtforms_components
3 changes: 3 additions & 0 deletions static/css/calendar.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
html, body {
font-family: "Product Sans", Arial, sans-serif;
}

#timeline #ctf_schedule {
/* min-height: 400px; */
Expand Down
6 changes: 0 additions & 6 deletions templates/base/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ <h2>{{ event.title }}</h2>
</div>
</div>

<script src="http://momentjs.com/downloads/moment.min.js"
integrity="sha384-ohw6o2vy/chIavW0iVsUoWaIPmAnF9VKjEMSusADB79PrE3CdeJhvR84BjcynjVl"
crossorigin="anonymous"></script>
<script src="https://cdn.rawgit.com/mattbradley/livestampjs/1.1.2/livestamp.min.js"
integrity="sha384-3w5S8eBXk/lb0z05Hu52Nal4FV5PNERXScY2BXGqwiaMURz8I5urTaOU6JMYodVi"
crossorigin="anonymous"></script>
<script type="text/javascript" src="{{ url_for('static', filename='/js/dragscroll.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='/js/jquery.infinitescroll.min.js') }}"></script>
{% endblock %}
Expand Down
4 changes: 2 additions & 2 deletions templates/events/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@
</table>
</div>

<p>
<center>
{% if page_number == 1 %}Prev{% else %}
<a href="{{ url_for('events.events_%s' % tab, page_number=page_number - 1) }}">Prev</a>{% endif %}
/ Page <b>{{ page_number }}</b> /
{% if last_page %}Next{% else %}
<a href="{{ url_for('events.events_%s' % tab, page_number=page_number + 1) }}">Next</a>{% endif %}
</p>
</center>

<script type="text/javascript">
$(document).ready(function () {
Expand Down
Loading