aboutsummaryrefslogtreecommitdiff
path: root/ihatemoney/api
diff options
context:
space:
mode:
Diffstat (limited to 'ihatemoney/api')
-rw-r--r--ihatemoney/api/__init__.py0
-rw-r--r--ihatemoney/api/common.py191
-rw-r--r--ihatemoney/api/v1/__init__.py5
-rw-r--r--ihatemoney/api/v1/resources.py34
4 files changed, 230 insertions, 0 deletions
diff --git a/ihatemoney/api/__init__.py b/ihatemoney/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ihatemoney/api/__init__.py
diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py
new file mode 100644
index 0000000..728d2a8
--- /dev/null
+++ b/ihatemoney/api/common.py
@@ -0,0 +1,191 @@
+# coding: utf8
+from flask import request, current_app
+from flask_restful import Resource, abort
+from wtforms.fields.core import BooleanField
+
+from ihatemoney.models import db, Project, Person, Bill
+from ihatemoney.forms import ProjectForm, EditProjectForm, MemberForm, get_billform_for
+from werkzeug.security import check_password_hash
+from functools import wraps
+
+
+def need_auth(f):
+ """Check the request for basic authentication for a given project.
+
+ Return the project if the authorization is good, abort the request with a 401 otherwise
+ """
+
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ auth = request.authorization
+ project_id = kwargs.get("project_id")
+
+ # Use Basic Auth
+ if auth and project_id and auth.username == project_id:
+ project = Project.query.get(auth.username)
+ if project and check_password_hash(project.password, auth.password):
+ # The whole project object will be passed instead of project_id
+ kwargs.pop("project_id")
+ return f(*args, project=project, **kwargs)
+ else:
+ # Use Bearer token Auth
+ auth_header = request.headers.get("Authorization", "")
+ auth_token = ""
+ try:
+ auth_token = auth_header.split(" ")[1]
+ except IndexError:
+ abort(401)
+ project_id = Project.verify_token(auth_token, token_type="non_timed_token")
+ if auth_token and project_id:
+ project = Project.query.get(project_id)
+ if project:
+ kwargs.pop("project_id")
+ return f(*args, project=project, **kwargs)
+ abort(401)
+
+ return wrapper
+
+
+class ProjectsHandler(Resource):
+ def post(self):
+ form = ProjectForm(meta={"csrf": False})
+ if form.validate() and current_app.config.get("ALLOW_PUBLIC_PROJECT_CREATION"):
+ project = form.save()
+ db.session.add(project)
+ db.session.commit()
+ return project.id, 201
+ return form.errors, 400
+
+
+class ProjectHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project):
+ return project
+
+ def delete(self, project):
+ db.session.delete(project)
+ db.session.commit()
+ return "DELETED"
+
+ def put(self, project):
+ form = EditProjectForm(meta={"csrf": False})
+ if form.validate() and current_app.config.get("ALLOW_PUBLIC_PROJECT_CREATION"):
+ form.update(project)
+ db.session.commit()
+ return "UPDATED"
+ return form.errors, 400
+
+
+class ProjectStatsHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project):
+ return project.members_stats
+
+
+class APIMemberForm(MemberForm):
+ """ Member is not disablable via a Form.
+
+ But we want Member.enabled to be togglable via the API.
+ """
+
+ activated = BooleanField(false_values=("false", "", "False"))
+
+ def save(self, project, person):
+ person.activated = self.activated.data
+ return super(APIMemberForm, self).save(project, person)
+
+
+class MembersHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project):
+ return project.members
+
+ def post(self, project):
+ form = MemberForm(project, meta={"csrf": False})
+ if form.validate():
+ member = Person()
+ form.save(project, member)
+ db.session.commit()
+ return member.id, 201
+ return form.errors, 400
+
+
+class MemberHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project, member_id):
+ member = Person.query.get(member_id, project)
+ if not member or member.project != project:
+ return "Not Found", 404
+ return member
+
+ def put(self, project, member_id):
+ form = APIMemberForm(project, meta={"csrf": False}, edit=True)
+ if form.validate():
+ member = Person.query.get(member_id, project)
+ form.save(project, member)
+ db.session.commit()
+ return member
+ return form.errors, 400
+
+ def delete(self, project, member_id):
+ if project.remove_member(member_id):
+ return "OK"
+ return "Not Found", 404
+
+
+class BillsHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project):
+ return project.get_bills().all()
+
+ def post(self, project):
+ form = get_billform_for(project, True, meta={"csrf": False})
+ if form.validate():
+ bill = Bill()
+ form.save(bill, project)
+ db.session.add(bill)
+ db.session.commit()
+ return bill.id, 201
+ return form.errors, 400
+
+
+class BillHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project, bill_id):
+ bill = Bill.query.get(project, bill_id)
+ if not bill:
+ return "Not Found", 404
+ return bill, 200
+
+ def put(self, project, bill_id):
+ form = get_billform_for(project, True, meta={"csrf": False})
+ if form.validate():
+ bill = Bill.query.get(project, bill_id)
+ form.save(bill, project)
+ db.session.commit()
+ return bill.id, 200
+ return form.errors, 400
+
+ def delete(self, project, bill_id):
+ bill = Bill.query.delete(project, bill_id)
+ db.session.commit()
+ if not bill:
+ return "Not Found", 404
+ return "OK", 200
+
+
+class TokenHandler(Resource):
+ method_decorators = [need_auth]
+
+ def get(self, project):
+ if not project:
+ return "Not Found", 404
+
+ token = project.generate_token()
+ return {"token": token}, 200
diff --git a/ihatemoney/api/v1/__init__.py b/ihatemoney/api/v1/__init__.py
new file mode 100644
index 0000000..11afff7
--- /dev/null
+++ b/ihatemoney/api/v1/__init__.py
@@ -0,0 +1,5 @@
+from .resources import api
+
+__all__ = [
+ "api",
+]
diff --git a/ihatemoney/api/v1/resources.py b/ihatemoney/api/v1/resources.py
new file mode 100644
index 0000000..821ba2b
--- /dev/null
+++ b/ihatemoney/api/v1/resources.py
@@ -0,0 +1,34 @@
+# coding: utf8
+from flask import Blueprint
+from flask_restful import Api
+from flask_cors import CORS
+
+from ihatemoney.api.common import (
+ ProjectsHandler,
+ ProjectHandler,
+ TokenHandler,
+ MemberHandler,
+ ProjectStatsHandler,
+ MembersHandler,
+ BillHandler,
+ BillsHandler,
+)
+
+api = Blueprint("api", __name__, url_prefix="/api")
+CORS(api)
+restful_api = Api(api)
+
+restful_api.add_resource(ProjectsHandler, "/projects")
+restful_api.add_resource(ProjectHandler, "/projects/<string:project_id>")
+restful_api.add_resource(TokenHandler, "/projects/<string:project_id>/token")
+restful_api.add_resource(MembersHandler, "/projects/<string:project_id>/members")
+restful_api.add_resource(
+ ProjectStatsHandler, "/projects/<string:project_id>/statistics"
+)
+restful_api.add_resource(
+ MemberHandler, "/projects/<string:project_id>/members/<int:member_id>"
+)
+restful_api.add_resource(BillsHandler, "/projects/<string:project_id>/bills")
+restful_api.add_resource(
+ BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>"
+)