Skip to content

Commit

Permalink
complete backend
Browse files Browse the repository at this point in the history
  • Loading branch information
lecafard committed Sep 13, 2020
1 parent b5add9a commit dcad034
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 99 deletions.
143 changes: 105 additions & 38 deletions backend/blueprints/guest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
from flask_inputs.validators import JsonSchema
import itertools
import bcrypt
import base64
import math
from bitarray import bitarray

from models import db, Meeting, Entry
from models import db, Meeting
import schemas

blueprint = Blueprint('guest', __name__)

def create_user(name, password, schedule=[], notes=""):
def create_user(meeting_id, name, password, schedule=[], notes=""):
digest = ""
if password:
# hash the digest
Expand All @@ -21,17 +24,39 @@ def create_user(name, password, schedule=[], notes=""):

# create new entry
entry = Entry(
meeting_id=res.id,
meeting_id=meeting_id,
name=name,
password=digest,
schedule=[],
availability=b"",
notes=""
)
db.session.add(entry)
db.session.commit()
return entry


def login(meeting_id, name, password):
"""
Attempts to log in a user with the name and password, returns an entry object if the user
exists else return None.
"""
entry = Entry.query.filter_by(
meeting_id=meeting_id,
name=name
).first()

if entry and entry.password == "" and password == "":
# no password
return entry
elif entry and entry.password == "" and password != "":
abort(401)
elif entry and entry.password != "":
# with password, verify first
if bcrypt.checkpw(password.encode("ascii"), entry.password.encode("ascii")):
return entry

return None

@blueprint.route('/<_id>/auth', methods=['POST'])
def login(_id):
def post_login(_id):
"""
Attempts to perform a login with the specified credentials, and return the user's current
schedule. If an unknown username is specified and allow_registration is true, a new user
Expand All @@ -48,24 +73,19 @@ class Validator(Inputs):
if res == None:
abort(404)

entry = Entry.query.filter_by(
meeting_id=res.id,
name=request.json["auth"]["name"]
).first()

if entry and entry.password == "" and request.json["auth"]["password"] == "":
# no password
return jsonify({"schedule": entry.schedule, "notes": entry.notes})
elif entry and entry.password != "":
# with password, verify first
if bcrypt.checkpw(
request.json["auth"]["password"].encode("ascii"),
entry.password.encode("ascii")):

return jsonify({"availability": entry.availability, "notes": entry.notes})
entry = login(res.id, request.json["auth"]["name"], request.json["auth"]["password"])

if entry:
return jsonify(
success=True,
data={
"schedule": base64.b64encode(entry.availability).decode("ascii"),
"notes": entry.notes
}
)
elif not entry and res.allow_registration:
# return an empty schedule and notes, but don't create the schedule just yet
return jsonify({"availability": "", "notes": ""})
return jsonify(success=True, data={"schedule": "", "notes": ""})

# credentials are invalid and/or allow_registration is set to false
abort(401)
Expand All @@ -85,44 +105,91 @@ def get_meeting(_id):

if not res.private:
entries = Entry.query.filter_by(meeting_id=res.id).all()
schedules = { i.name: i.schedule for i in entries }
schedules = { i.name: base64.b64encode(i.availability).decode("ascii") for i in entries }
notes = { i.name: i.notes for i in entries }
else:
schedules = {}
notes = {}

return jsonify({
"name": res.name,
"location": res.location,
"private": res.private,
"allow_registration": res.allow_registration,
"options": res.options,
"schedules": schedules,
"notes": notes
})
return jsonify(
success=True,
data={
"name": res.name,
"location": res.location,
"private": res.private,
"allow_registration": res.allow_registration,
"options": res.options,
"schedules": schedules,
"notes": notes
}
)

@blueprint.route('/<_id>/schedule', methods=['POST'])
def update_schedule():
def update_schedule(_id):
"""
Updates the current user's schedule.
"""

class Validator(Inputs):
json = [JsonSchema(schema=schemas.guest_entry)]
inputs = Validator(request)
if not inputs.validate():
return jsonify(success=False, errors=inputs.errors)

res = Meeting.query.filter_by(guest_key=_id).first()
if res == None:
abort(404)

# TODO: return updated schedule and also use create_user() if user does not exist and opts allow
return jsonify({"message": "updated schedule"})
# calculate number of slots
if res.options["type"] == "day":
n_slots = (res.options["maxTime"] - res.options["minTime"]) * res.options["days"].count("1")
elif res.options["type"] == "date":
n_slots = (res.options["maxTime"] - res.options["minTime"]) * len(res.options["dates"])
else:
abort(500)

entry = login(res.id, request.json["auth"]["name"], request.json["auth"]["password"])
if not entry:
entry = create_user(res.id, request.json["auth"]["name"], request.json["auth"]["password"])
db.session.add(entry)

data = request.json["entry"]
if data["from"] > data["to"] or data["to"] > n_slots:
return jsonify(success=False, error="invalid entry"), 400

if entry.availability == b"":
slots = bitarray("0" * n_slots, endian="big")
else:
slots = bitarray(endian="big")
slots.frombytes(entry.availability)

slots[data["from"]:data["to"]] = data["state"]
entry.availability = slots.tobytes()
db.session.commit()

return jsonify(success=True)

@blueprint.route('/<_id>/notes', methods=['POST'])
def update_notes():
def update_notes(_id):
"""
Updates the current user's notes.
"""

class Validator(Inputs):
json = [JsonSchema(schema=schemas.guest_notes)]
inputs = Validator(request)
if not inputs.validate():
return jsonify(success=False, errors=inputs.errors)

res = Meeting.query.filter_by(guest_key=_id).first()
if res == None:
abort(404)

# TODO
return jsonify({"status": True})
entry = login(res.id, request.json["auth"]["name"], request.json["auth"]["password"])
if not entry:
entry = create_user(res.id, request.json["auth"]["name"], request.json["auth"]["password"])
db.session.add(entry)

entry.notes = request.json["notes"]
db.session.commit()
return jsonify(success=True)
75 changes: 53 additions & 22 deletions backend/blueprints/owner.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from flask import jsonify, Blueprint, request
from flask import jsonify, Blueprint, request, abort
from flask_inputs import Inputs
from flask_inputs.validators import JsonSchema
import secrets
import json
import base64

from models import db, Meeting
from models import db, Meeting, Entry
import schemas


Expand All @@ -28,17 +29,20 @@ class Validator(Inputs):
return jsonify(success=False, error=inputs.errors)

# additional validation for the options
if "options" not in request.json:
return json(success=False, error="options is a required property"), 400

options = request.json["options"]
if options["minTime"] >= options["maxTime"]:
return jsonify(success=False, error="minTime is greater than maxTime")
return jsonify(success=False, error="minTime is greater than maxTime"), 400
if options["type"] == "dates":
try:
options["dates"] = list(set(options["dates"]))
options["dates"].sort()
if len(options["dates"]) * (options["maxTime"] - options["minTime"]) > MAX_SLOTS:
return jsonify(success=False, error="too many timeslots")
return jsonify(success=False, error="too many timeslots"), 400
except ValueError:
return jsonify(success=False, error="invalid dates provided")
return jsonify(success=False, error="invalid dates provided"), 400


owner_key = secrets.token_urlsafe(12)
Expand All @@ -50,7 +54,7 @@ class Validator(Inputs):
location=request.json["location"],
private=request.json["private"],
allow_registration=request.json["allow_registration"],
options={})
options=options)

db.session.add(meeting)
db.session.commit()
Expand All @@ -73,31 +77,58 @@ def create_users(_id):
"""
Allows an admin to pre-create users (not MVP).
"""
return jsonify({"message": "not implemented"}), 501
return jsonify(success=False, error="not implemented"), 501

@blueprint.route('/<_id>/schedule', methods=['GET'])
def get_schedule(_id):
@blueprint.route('/<_id>', methods=['GET'])
def get_meeting(_id):
"""
Gets the overlaid schedules.
Retrieves the schedule metadata + schedule. Only return certain options based on the privacy
setting.
"""

res = Meeting.query.filter_by(owner_key=_id).first()
if res == None:
abort(404)

entries = Entry.query.filter_by(meeting_id=res.id).all()
schedules = { i.name: base64.b64encode(i.availability).decode("ascii") for i in entries }
notes = { i.name: i.notes for i in entries }


return jsonify(
success=True,
data={
"stub1": "010001",
"stub2": "100001"
"name": res.name,
"location": res.location,
"private": res.private,
"allow_registration": res.allow_registration,
"guest_key": res.guest_key,
"options": res.options,
"schedules": schedules,
"notes": notes
}
)

@blueprint.route('/<_id>/options', methods=['GET'])
def get_options(_id):
"""
Retrieves the schedule metadata.
"""
return jsonify({"message": "current schedule"})

@blueprint.route('/<_id>/options', methods=['POST'])
@blueprint.route('/<_id>', methods=['POST'])
def update_options(_id):
"""
Updates the schedule metadata, only certain fields such as privacy and name can be updated.
Updates the schedule metadata, ignores updates to options.
"""
return jsonify({"message": "current schedule"})

res = Meeting.query.filter_by(owner_key=_id).first()
if res == None:
abort(404)

class Validator(Inputs):
json = [JsonSchema(schema=schemas.meeting)]
inputs = Validator(request)

if not inputs.validate():
return jsonify(success=False, error=inputs.errors), 400

res.name = request.json["name"]
res.location = request.json["location"]
res.allow_registration = request.json["allow_registration"]
res.private = request.json["private"]
db.session.commit()
return jsonify(success=True)
2 changes: 1 addition & 1 deletion backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Entry(db.Model):
meeting_id = db.Column(db.Integer, db.ForeignKey('meetings.id'), nullable=False)
name = db.Column(db.String(32), nullable=False)
password = db.Column(db.String(64))
availability = db.Column(db.Blob(250))
availability = db.Column(db.LargeBinary(250))
notes = db.Column(db.Text)

db.UniqueConstraint("meeting_id", "name", name="meeting_id_name_key")
5 changes: 4 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
attrs==20.2.0
bcrypt==3.2.0
bitarray==1.5.3
cffi==1.14.2
click==7.1.2
Flask==1.1.2
Flask-Inputs==0.3.0
flask-jsonschema-validator==0.0.4
Flask-SQLAlchemy==2.4.4
future==0.18.2
itsdangerous==1.1.0
Jinja2==2.11.2
jsonschema==3.2.0
MarkupSafe==1.1.1
psycopg2-binary==2.8.5
psycopg2==2.8.6
pycparser==2.20
pyrsistent==0.16.0
six==1.15.0
Expand Down
Loading

0 comments on commit dcad034

Please sign in to comment.