diff --git a/__init__.py b/__init__.py index 88b77f8..4a2007a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,11 +1,23 @@ +from __future__ import absolute_import + from flask import Blueprint from flask import jsonify +from flask import url_for + +from .auth import requires_auth +from . import v1 api = Blueprint('api', __name__) @api.route('/') +@requires_auth def index(): - return jsonify( - "description"="REST API for CT", - "version"="1.0" - ) + return jsonify({ + "description": "REST API for CT", + "links": [{ + "rel": "api-version-1.0", + "href": url_for(".v1_0.index") + }] + }) + +v1.add_routes(api) diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..2db6a02 --- /dev/null +++ b/auth.py @@ -0,0 +1,77 @@ +from __future__ import absolute_import + +from functools import wraps +from hashlib import sha256 +from flask import request, Response +from flask import session, g + +from ct.core.apis import BaseAPI + + +def get_ct_object(username, password): + key = get_session_key(username, password) + if key in session and session[key].valid_session(): + return session[key] + + if do_ct_login(username, password): + return get_ct_object(username, password) + + return None + + +def do_ct_login(username, password): + ct = BaseAPI("https://currenttime.bouvet.no") + logged_in = ct.login(username, password) + if logged_in: + key = get_session_key(username, password) + session[key] = ct + + return logged_in + + +def get_session_key(username, password): + return sha256("%s:%s" % (username, password)).hexdigest() + + +def check_auth(username, password): + """This function is called to check if a username / + password combination is valid. + """ + return get_ct_object(username, password) is not None + + +def get_auth_headers(): + """Returns the WWW-Authenticate headers. We use Basic unless the + clients has set the UseXBasic header. In that case we use XBasic + instead. This is because most web browsers insist on showing the + Basic auth dialogue even if the request is done using XHR.""" + + auth_type = "Basic" + if request.headers.get('UseXBasic'): + auth_type = "XBasic" + + return { + 'WWW-Authenticate': '%s realm="Login Required"' % auth_type + } + + +def authenticate(): + """Sends a 401 response that enables basic auth""" + + return Response( + 'Could not verify your access level for that URL.\n' + 'You have to login with proper credentials', 401, + get_auth_headers()) + + +def requires_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if not auth or not check_auth(auth.username, auth.password): + return authenticate() + + g.ct = get_ct_object(auth.username, auth.password) + + return f(*args, **kwargs) + return decorated diff --git a/v1.py b/v1.py new file mode 100644 index 0000000..208519e --- /dev/null +++ b/v1.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import + +from .auth import requires_auth +from flask import jsonify +from flask import url_for +from flask import g + + +@requires_auth +def index(): + return jsonify({ + "description": "REST API for CT version 1.0", + "links": [{ + "rel": "available-projects", + "href": url_for('.projects') + }, { + "rel": "activities-by-week", + "href": url_for('.week', year="", week="") + }] + }) + + +def serialize_projects(projects): + result = [] + for p in projects: + result.append({ + 'id': p.id, + 'name': p.name, + 'project_name': p.project_name, + 'task_name': p.task_name, + 'subtask_name': p.subtask_name, + 'activity_name': p.activity_name, + }) + return result + + +def serialize_activities(activities): + result = [] + for activity in activities: + if activity.duration <= 0: + continue + + date = activity.day.strftime("%Y-%m-%d") + result.append({ + 'id': activity.project_id, + 'comment': activity.comment, + 'duration': str(activity.duration), + 'day': date + }) + return result + + +@requires_auth +def get_projects(): + projects = serialize_projects(g.ct.get_projects()) + + return jsonify({ + "projects": projects + }) + + +@requires_auth +def get_week(year, week): + activities = serialize_activities(g.ct.get_week(year, week)) + + return jsonify({ + "activities": activities + }) + + +def add_routes(api): + root = 'v1_0' + api.add_url_rule('/v1/', + root + '.index', index) + api.add_url_rule('/v1/projects/', + root + '.projects', get_projects) + api.add_url_rule('/v1/week//', + root + '.week', get_week)