We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Journalist API returns error 500 when JSON in malformed or absent
## Description

## Steps to Reproduce

1. `make dev`
2. `curl -X POST 127.0.0.1:8081/api/v1/token`

## Expected Behavior

The server should return an error 400 stating that the request is invalid.

## Actual Behavior

The server returns an error 500 stating that there is a server error.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/journalist_app/api.py`
Content:
```
1 from datetime import datetime, timedelta
2 from functools import wraps
3 import hashlib
4 import json
5 from werkzeug.exceptions import default_exceptions  # type: ignore
6 
7 from flask import abort, Blueprint, current_app, jsonify, request, send_file
8 
9 from db import db
10 from journalist_app import utils
11 from models import (Journalist, Reply, Source, Submission,
12                     LoginThrottledException, InvalidUsernameException,
13                     BadTokenException, WrongPasswordException)
14 from store import NotEncrypted
15 
16 
17 TOKEN_EXPIRATION_MINS = 60 * 8
18 
19 
20 def get_user_object(request):
21     """Helper function to use in token_required views that need a user
22     object
23     """
24     auth_token = request.headers.get('Authorization').split(" ")[1]
25     user = Journalist.validate_api_token_and_get_user(auth_token)
26     return user
27 
28 
29 def token_required(f):
30     @wraps(f)
31     def decorated_function(*args, **kwargs):
32         try:
33             auth_header = request.headers['Authorization']
34         except KeyError:
35             return abort(403, 'API token not found in Authorization header.')
36 
37         if auth_header:
38             auth_token = auth_header.split(" ")[1]
39         else:
40             auth_token = ''
41         if not Journalist.validate_api_token_and_get_user(auth_token):
42             return abort(403, 'API token is invalid or expired.')
43         return f(*args, **kwargs)
44     return decorated_function
45 
46 
47 def get_or_404(model, object_id, column=''):
48     if column:
49         result = model.query.filter(column == object_id).one_or_none()
50     else:
51         result = model.query.get(object_id)
52     if result is None:
53         abort(404)
54     return result
55 
56 
57 def make_blueprint(config):
58     api = Blueprint('api', __name__)
59 
60     @api.route('/')
61     def get_endpoints():
62         endpoints = {'sources_url': '/api/v1/sources',
63                      'current_user_url': '/api/v1/user',
64                      'submissions_url': '/api/v1/submissions',
65                      'auth_token_url': '/api/v1/token'}
66         return jsonify(endpoints), 200
67 
68     @api.route('/token', methods=['POST'])
69     def get_token():
70         creds = json.loads(request.data)
71 
72         username = creds.get('username', None)
73         passphrase = creds.get('passphrase', None)
74         one_time_code = creds.get('one_time_code', None)
75 
76         if username is None:
77             return abort(400, 'username field is missing')
78         if passphrase is None:
79             return abort(400, 'passphrase field is missing')
80         if one_time_code is None:
81             return abort(400, 'one_time_code field is missing')
82 
83         try:
84             journalist = Journalist.login(username, passphrase, one_time_code)
85             token_expiry = datetime.utcnow() + timedelta(
86                 seconds=TOKEN_EXPIRATION_MINS * 60)
87             response = jsonify({'token': journalist.generate_api_token(
88                  expiration=TOKEN_EXPIRATION_MINS * 60),
89                  'expiration': token_expiry.isoformat() + 'Z'})
90 
91             # Update access metadata
92             journalist.last_access = datetime.utcnow()
93             db.session.add(journalist)
94             db.session.commit()
95 
96             return response, 200
97         except (LoginThrottledException, InvalidUsernameException,
98                 BadTokenException, WrongPasswordException):
99             return abort(403, 'Token authentication failed.')
100 
101     @api.route('/sources', methods=['GET'])
102     @token_required
103     def get_all_sources():
104         sources = Source.query.filter_by(pending=False).all()
105         return jsonify(
106             {'sources': [source.to_json() for source in sources]}), 200
107 
108     @api.route('/sources/<source_uuid>', methods=['GET', 'DELETE'])
109     @token_required
110     def single_source(source_uuid):
111         if request.method == 'GET':
112             source = get_or_404(Source, source_uuid, column=Source.uuid)
113             return jsonify(source.to_json()), 200
114         elif request.method == 'DELETE':
115             source = get_or_404(Source, source_uuid, column=Source.uuid)
116             utils.delete_collection(source.filesystem_id)
117             return jsonify({'message': 'Source and submissions deleted'}), 200
118 
119     @api.route('/sources/<source_uuid>/add_star', methods=['POST'])
120     @token_required
121     def add_star(source_uuid):
122         source = get_or_404(Source, source_uuid, column=Source.uuid)
123         utils.make_star_true(source.filesystem_id)
124         db.session.commit()
125         return jsonify({'message': 'Star added'}), 201
126 
127     @api.route('/sources/<source_uuid>/remove_star', methods=['DELETE'])
128     @token_required
129     def remove_star(source_uuid):
130         source = get_or_404(Source, source_uuid, column=Source.uuid)
131         utils.make_star_false(source.filesystem_id)
132         db.session.commit()
133         return jsonify({'message': 'Star removed'}), 200
134 
135     @api.route('/sources/<source_uuid>/flag', methods=['POST'])
136     @token_required
137     def flag(source_uuid):
138         source = get_or_404(Source, source_uuid,
139                             column=Source.uuid)
140         source.flagged = True
141         db.session.commit()
142         return jsonify({'message': 'Source flagged for reply'}), 200
143 
144     @api.route('/sources/<source_uuid>/submissions', methods=['GET'])
145     @token_required
146     def all_source_submissions(source_uuid):
147         source = get_or_404(Source, source_uuid, column=Source.uuid)
148         return jsonify(
149             {'submissions': [submission.to_json() for
150                              submission in source.submissions]}), 200
151 
152     @api.route('/sources/<source_uuid>/submissions/<submission_uuid>/download',  # noqa
153                methods=['GET'])
154     @token_required
155     def download_submission(source_uuid, submission_uuid):
156         source = get_or_404(Source, source_uuid, column=Source.uuid)
157         submission = get_or_404(Submission, submission_uuid,
158                                 column=Submission.uuid)
159 
160         # Mark as downloaded
161         submission.downloaded = True
162         db.session.commit()
163 
164         response = send_file(current_app.storage.path(source.filesystem_id,
165                                                       submission.filename),
166                              mimetype="application/pgp-encrypted",
167                              as_attachment=True,
168                              add_etags=False)  # Disable Flask default ETag
169 
170         response.direct_passthrough = False
171         response.headers['Etag'] = '"sha256:{}"'.format(
172             hashlib.sha256(response.get_data()).hexdigest())
173         return response
174 
175     @api.route('/sources/<source_uuid>/submissions/<submission_uuid>',
176                methods=['GET', 'DELETE'])
177     @token_required
178     def single_submission(source_uuid, submission_uuid):
179         if request.method == 'GET':
180             submission = get_or_404(Submission, submission_uuid,
181                                     column=Submission.uuid)
182             return jsonify(submission.to_json()), 200
183         elif request.method == 'DELETE':
184             submission = get_or_404(Submission, submission_uuid,
185                                     column=Submission.uuid)
186             source = get_or_404(Source, source_uuid, column=Source.uuid)
187             utils.delete_file(source.filesystem_id, submission.filename,
188                               submission)
189             return jsonify({'message': 'Submission deleted'}), 200
190 
191     @api.route('/sources/<source_uuid>/reply', methods=['POST'])
192     @token_required
193     def post_reply(source_uuid):
194         source = get_or_404(Source, source_uuid,
195                             column=Source.uuid)
196         if request.json is None:
197             abort(400, 'please send requests in valid JSON')
198 
199         if 'reply' not in request.json:
200             abort(400, 'reply not found in request body')
201 
202         user = get_user_object(request)
203 
204         data = json.loads(request.data)
205         if not data['reply']:
206             abort(400, 'reply should not be empty')
207 
208         source.interaction_count += 1
209         try:
210             filename = current_app.storage.save_pre_encrypted_reply(
211                 source.filesystem_id,
212                 source.interaction_count,
213                 source.journalist_filename,
214                 data['reply'])
215         except NotEncrypted:
216             return jsonify(
217                 {'message': 'You must encrypt replies client side'}), 400
218 
219         reply = Reply(user, source,
220                       current_app.storage.path(source.filesystem_id, filename))
221         db.session.add(reply)
222         db.session.add(source)
223         db.session.commit()
224         return jsonify({'message': 'Your reply has been stored'}), 201
225 
226     @api.route('/submissions', methods=['GET'])
227     @token_required
228     def get_all_submissions():
229         submissions = Submission.query.all()
230         return jsonify({'submissions': [submission.to_json() for
231                                         submission in submissions]}), 200
232 
233     @api.route('/user', methods=['GET'])
234     @token_required
235     def get_current_user():
236         user = get_user_object(request)
237         return jsonify(user.to_json()), 200
238 
239     def _handle_http_exception(error):
240         # Workaround for no blueprint-level 404/5 error handlers, see:
241         # https://github.com/pallets/flask/issues/503#issuecomment-71383286
242         response = jsonify({'error': error.name,
243                            'message': error.description})
244 
245         return response, error.code
246 
247     for code in default_exceptions:
248         api.errorhandler(code)(_handle_http_exception)
249 
250     return api
251
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/journalist_app/api.py b/securedrop/journalist_app/api.py
--- a/securedrop/journalist_app/api.py
+++ b/securedrop/journalist_app/api.py
@@ -65,6 +65,21 @@
                      'auth_token_url': '/api/v1/token'}
         return jsonify(endpoints), 200
 
+    # Before every post, we validate the payload before processing the request
+    @api.before_request
+    def validate_data():
+        if request.method == 'POST':
+            # flag and star can have empty payloads
+            if not request.data:
+                if ('flag' not in request.path and 'star' not in request.path):
+                    return abort(400, 'malformed request')
+            # other requests must have valid JSON payload
+            else:
+                try:
+                    json.loads(request.data)
+                except (ValueError):
+                    return abort(400, 'malformed request')
+
     @api.route('/token', methods=['POST'])
     def get_token():
         creds = json.loads(request.data)

{"golden_diff": "diff --git a/securedrop/journalist_app/api.py b/securedrop/journalist_app/api.py\n--- a/securedrop/journalist_app/api.py\n+++ b/securedrop/journalist_app/api.py\n@@ -65,6 +65,21 @@\n                      'auth_token_url': '/api/v1/token'}\n         return jsonify(endpoints), 200\n \n+    # Before every post, we validate the payload before processing the request\n+    @api.before_request\n+    def validate_data():\n+        if request.method == 'POST':\n+            # flag and star can have empty payloads\n+            if not request.data:\n+                if ('flag' not in request.path and 'star' not in request.path):\n+                    return abort(400, 'malformed request')\n+            # other requests must have valid JSON payload\n+            else:\n+                try:\n+                    json.loads(request.data)\n+                except (ValueError):\n+                    return abort(400, 'malformed request')\n+\n     @api.route('/token', methods=['POST'])\n     def get_token():\n         creds = json.loads(request.data)\n", "issue": "Journalist API returns error 500 when JSON in malformed or absent\n## Description\r\n\r\n## Steps to Reproduce\r\n\r\n1. `make dev`\r\n2. `curl -X POST 127.0.0.1:8081/api/v1/token`\r\n\r\n## Expected Behavior\r\n\r\nThe server should return an error 400 stating that the request is invalid.\r\n\r\n## Actual Behavior\r\n\r\nThe server returns an error 500 stating that there is a server error.\r\n\n", "before_files": [{"content": "from datetime import datetime, timedelta\nfrom functools import wraps\nimport hashlib\nimport json\nfrom werkzeug.exceptions import default_exceptions  # type: ignore\n\nfrom flask import abort, Blueprint, current_app, jsonify, request, send_file\n\nfrom db import db\nfrom journalist_app import utils\nfrom models import (Journalist, Reply, Source, Submission,\n                    LoginThrottledException, InvalidUsernameException,\n                    BadTokenException, WrongPasswordException)\nfrom store import NotEncrypted\n\n\nTOKEN_EXPIRATION_MINS = 60 * 8\n\n\ndef get_user_object(request):\n    \"\"\"Helper function to use in token_required views that need a user\n    object\n    \"\"\"\n    auth_token = request.headers.get('Authorization').split(\" \")[1]\n    user = Journalist.validate_api_token_and_get_user(auth_token)\n    return user\n\n\ndef token_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        try:\n            auth_header = request.headers['Authorization']\n        except KeyError:\n            return abort(403, 'API token not found in Authorization header.')\n\n        if auth_header:\n            auth_token = auth_header.split(\" \")[1]\n        else:\n            auth_token = ''\n        if not Journalist.validate_api_token_and_get_user(auth_token):\n            return abort(403, 'API token is invalid or expired.')\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef get_or_404(model, object_id, column=''):\n    if column:\n        result = model.query.filter(column == object_id).one_or_none()\n    else:\n        result = model.query.get(object_id)\n    if result is None:\n        abort(404)\n    return result\n\n\ndef make_blueprint(config):\n    api = Blueprint('api', __name__)\n\n    @api.route('/')\n    def get_endpoints():\n        endpoints = {'sources_url': '/api/v1/sources',\n                     'current_user_url': '/api/v1/user',\n                     'submissions_url': '/api/v1/submissions',\n                     'auth_token_url': '/api/v1/token'}\n        return jsonify(endpoints), 200\n\n    @api.route('/token', methods=['POST'])\n    def get_token():\n        creds = json.loads(request.data)\n\n        username = creds.get('username', None)\n        passphrase = creds.get('passphrase', None)\n        one_time_code = creds.get('one_time_code', None)\n\n        if username is None:\n            return abort(400, 'username field is missing')\n        if passphrase is None:\n            return abort(400, 'passphrase field is missing')\n        if one_time_code is None:\n            return abort(400, 'one_time_code field is missing')\n\n        try:\n            journalist = Journalist.login(username, passphrase, one_time_code)\n            token_expiry = datetime.utcnow() + timedelta(\n                seconds=TOKEN_EXPIRATION_MINS * 60)\n            response = jsonify({'token': journalist.generate_api_token(\n                 expiration=TOKEN_EXPIRATION_MINS * 60),\n                 'expiration': token_expiry.isoformat() + 'Z'})\n\n            # Update access metadata\n            journalist.last_access = datetime.utcnow()\n            db.session.add(journalist)\n            db.session.commit()\n\n            return response, 200\n        except (LoginThrottledException, InvalidUsernameException,\n                BadTokenException, WrongPasswordException):\n            return abort(403, 'Token authentication failed.')\n\n    @api.route('/sources', methods=['GET'])\n    @token_required\n    def get_all_sources():\n        sources = Source.query.filter_by(pending=False).all()\n        return jsonify(\n            {'sources': [source.to_json() for source in sources]}), 200\n\n    @api.route('/sources/<source_uuid>', methods=['GET', 'DELETE'])\n    @token_required\n    def single_source(source_uuid):\n        if request.method == 'GET':\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            return jsonify(source.to_json()), 200\n        elif request.method == 'DELETE':\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            utils.delete_collection(source.filesystem_id)\n            return jsonify({'message': 'Source and submissions deleted'}), 200\n\n    @api.route('/sources/<source_uuid>/add_star', methods=['POST'])\n    @token_required\n    def add_star(source_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        utils.make_star_true(source.filesystem_id)\n        db.session.commit()\n        return jsonify({'message': 'Star added'}), 201\n\n    @api.route('/sources/<source_uuid>/remove_star', methods=['DELETE'])\n    @token_required\n    def remove_star(source_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        utils.make_star_false(source.filesystem_id)\n        db.session.commit()\n        return jsonify({'message': 'Star removed'}), 200\n\n    @api.route('/sources/<source_uuid>/flag', methods=['POST'])\n    @token_required\n    def flag(source_uuid):\n        source = get_or_404(Source, source_uuid,\n                            column=Source.uuid)\n        source.flagged = True\n        db.session.commit()\n        return jsonify({'message': 'Source flagged for reply'}), 200\n\n    @api.route('/sources/<source_uuid>/submissions', methods=['GET'])\n    @token_required\n    def all_source_submissions(source_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        return jsonify(\n            {'submissions': [submission.to_json() for\n                             submission in source.submissions]}), 200\n\n    @api.route('/sources/<source_uuid>/submissions/<submission_uuid>/download',  # noqa\n               methods=['GET'])\n    @token_required\n    def download_submission(source_uuid, submission_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        submission = get_or_404(Submission, submission_uuid,\n                                column=Submission.uuid)\n\n        # Mark as downloaded\n        submission.downloaded = True\n        db.session.commit()\n\n        response = send_file(current_app.storage.path(source.filesystem_id,\n                                                      submission.filename),\n                             mimetype=\"application/pgp-encrypted\",\n                             as_attachment=True,\n                             add_etags=False)  # Disable Flask default ETag\n\n        response.direct_passthrough = False\n        response.headers['Etag'] = '\"sha256:{}\"'.format(\n            hashlib.sha256(response.get_data()).hexdigest())\n        return response\n\n    @api.route('/sources/<source_uuid>/submissions/<submission_uuid>',\n               methods=['GET', 'DELETE'])\n    @token_required\n    def single_submission(source_uuid, submission_uuid):\n        if request.method == 'GET':\n            submission = get_or_404(Submission, submission_uuid,\n                                    column=Submission.uuid)\n            return jsonify(submission.to_json()), 200\n        elif request.method == 'DELETE':\n            submission = get_or_404(Submission, submission_uuid,\n                                    column=Submission.uuid)\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            utils.delete_file(source.filesystem_id, submission.filename,\n                              submission)\n            return jsonify({'message': 'Submission deleted'}), 200\n\n    @api.route('/sources/<source_uuid>/reply', methods=['POST'])\n    @token_required\n    def post_reply(source_uuid):\n        source = get_or_404(Source, source_uuid,\n                            column=Source.uuid)\n        if request.json is None:\n            abort(400, 'please send requests in valid JSON')\n\n        if 'reply' not in request.json:\n            abort(400, 'reply not found in request body')\n\n        user = get_user_object(request)\n\n        data = json.loads(request.data)\n        if not data['reply']:\n            abort(400, 'reply should not be empty')\n\n        source.interaction_count += 1\n        try:\n            filename = current_app.storage.save_pre_encrypted_reply(\n                source.filesystem_id,\n                source.interaction_count,\n                source.journalist_filename,\n                data['reply'])\n        except NotEncrypted:\n            return jsonify(\n                {'message': 'You must encrypt replies client side'}), 400\n\n        reply = Reply(user, source,\n                      current_app.storage.path(source.filesystem_id, filename))\n        db.session.add(reply)\n        db.session.add(source)\n        db.session.commit()\n        return jsonify({'message': 'Your reply has been stored'}), 201\n\n    @api.route('/submissions', methods=['GET'])\n    @token_required\n    def get_all_submissions():\n        submissions = Submission.query.all()\n        return jsonify({'submissions': [submission.to_json() for\n                                        submission in submissions]}), 200\n\n    @api.route('/user', methods=['GET'])\n    @token_required\n    def get_current_user():\n        user = get_user_object(request)\n        return jsonify(user.to_json()), 200\n\n    def _handle_http_exception(error):\n        # Workaround for no blueprint-level 404/5 error handlers, see:\n        # https://github.com/pallets/flask/issues/503#issuecomment-71383286\n        response = jsonify({'error': error.name,\n                           'message': error.description})\n\n        return response, error.code\n\n    for code in default_exceptions:\n        api.errorhandler(code)(_handle_http_exception)\n\n    return api\n", "path": "securedrop/journalist_app/api.py"}], "after_files": [{"content": "from datetime import datetime, timedelta\nfrom functools import wraps\nimport hashlib\nimport json\nfrom werkzeug.exceptions import default_exceptions  # type: ignore\n\nfrom flask import abort, Blueprint, current_app, jsonify, request, send_file\n\nfrom db import db\nfrom journalist_app import utils\nfrom models import (Journalist, Reply, Source, Submission,\n                    LoginThrottledException, InvalidUsernameException,\n                    BadTokenException, WrongPasswordException)\nfrom store import NotEncrypted\n\n\nTOKEN_EXPIRATION_MINS = 60 * 8\n\n\ndef get_user_object(request):\n    \"\"\"Helper function to use in token_required views that need a user\n    object\n    \"\"\"\n    auth_token = request.headers.get('Authorization').split(\" \")[1]\n    user = Journalist.validate_api_token_and_get_user(auth_token)\n    return user\n\n\ndef token_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        try:\n            auth_header = request.headers['Authorization']\n        except KeyError:\n            return abort(403, 'API token not found in Authorization header.')\n\n        if auth_header:\n            auth_token = auth_header.split(\" \")[1]\n        else:\n            auth_token = ''\n        if not Journalist.validate_api_token_and_get_user(auth_token):\n            return abort(403, 'API token is invalid or expired.')\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef get_or_404(model, object_id, column=''):\n    if column:\n        result = model.query.filter(column == object_id).one_or_none()\n    else:\n        result = model.query.get(object_id)\n    if result is None:\n        abort(404)\n    return result\n\n\ndef make_blueprint(config):\n    api = Blueprint('api', __name__)\n\n    @api.route('/')\n    def get_endpoints():\n        endpoints = {'sources_url': '/api/v1/sources',\n                     'current_user_url': '/api/v1/user',\n                     'submissions_url': '/api/v1/submissions',\n                     'auth_token_url': '/api/v1/token'}\n        return jsonify(endpoints), 200\n\n    # Before every post, we validate the payload before processing the request\n    @api.before_request\n    def validate_data():\n        if request.method == 'POST':\n            # flag and star can have empty payloads\n            if not request.data:\n                if ('flag' not in request.path and 'star' not in request.path):\n                    return abort(400, 'malformed request')\n            # other requests must have valid JSON payload\n            else:\n                try:\n                    json.loads(request.data)\n                except (ValueError):\n                    return abort(400, 'malformed request')\n\n    @api.route('/token', methods=['POST'])\n    def get_token():\n        creds = json.loads(request.data)\n\n        username = creds.get('username', None)\n        passphrase = creds.get('passphrase', None)\n        one_time_code = creds.get('one_time_code', None)\n\n        if username is None:\n            return abort(400, 'username field is missing')\n        if passphrase is None:\n            return abort(400, 'passphrase field is missing')\n        if one_time_code is None:\n            return abort(400, 'one_time_code field is missing')\n\n        try:\n            journalist = Journalist.login(username, passphrase, one_time_code)\n            token_expiry = datetime.utcnow() + timedelta(\n                seconds=TOKEN_EXPIRATION_MINS * 60)\n            response = jsonify({'token': journalist.generate_api_token(\n                 expiration=TOKEN_EXPIRATION_MINS * 60),\n                 'expiration': token_expiry.isoformat() + 'Z'})\n\n            # Update access metadata\n            journalist.last_access = datetime.utcnow()\n            db.session.add(journalist)\n            db.session.commit()\n\n            return response, 200\n        except (LoginThrottledException, InvalidUsernameException,\n                BadTokenException, WrongPasswordException):\n            return abort(403, 'Token authentication failed.')\n\n    @api.route('/sources', methods=['GET'])\n    @token_required\n    def get_all_sources():\n        sources = Source.query.filter_by(pending=False).all()\n        return jsonify(\n            {'sources': [source.to_json() for source in sources]}), 200\n\n    @api.route('/sources/<source_uuid>', methods=['GET', 'DELETE'])\n    @token_required\n    def single_source(source_uuid):\n        if request.method == 'GET':\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            return jsonify(source.to_json()), 200\n        elif request.method == 'DELETE':\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            utils.delete_collection(source.filesystem_id)\n            return jsonify({'message': 'Source and submissions deleted'}), 200\n\n    @api.route('/sources/<source_uuid>/add_star', methods=['POST'])\n    @token_required\n    def add_star(source_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        utils.make_star_true(source.filesystem_id)\n        db.session.commit()\n        return jsonify({'message': 'Star added'}), 201\n\n    @api.route('/sources/<source_uuid>/remove_star', methods=['DELETE'])\n    @token_required\n    def remove_star(source_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        utils.make_star_false(source.filesystem_id)\n        db.session.commit()\n        return jsonify({'message': 'Star removed'}), 200\n\n    @api.route('/sources/<source_uuid>/flag', methods=['POST'])\n    @token_required\n    def flag(source_uuid):\n        source = get_or_404(Source, source_uuid,\n                            column=Source.uuid)\n        source.flagged = True\n        db.session.commit()\n        return jsonify({'message': 'Source flagged for reply'}), 200\n\n    @api.route('/sources/<source_uuid>/submissions', methods=['GET'])\n    @token_required\n    def all_source_submissions(source_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        return jsonify(\n            {'submissions': [submission.to_json() for\n                             submission in source.submissions]}), 200\n\n    @api.route('/sources/<source_uuid>/submissions/<submission_uuid>/download',  # noqa\n               methods=['GET'])\n    @token_required\n    def download_submission(source_uuid, submission_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        submission = get_or_404(Submission, submission_uuid,\n                                column=Submission.uuid)\n\n        # Mark as downloaded\n        submission.downloaded = True\n        db.session.commit()\n\n        response = send_file(current_app.storage.path(source.filesystem_id,\n                                                      submission.filename),\n                             mimetype=\"application/pgp-encrypted\",\n                             as_attachment=True,\n                             add_etags=False)  # Disable Flask default ETag\n\n        response.direct_passthrough = False\n        response.headers['Etag'] = '\"sha256:{}\"'.format(\n            hashlib.sha256(response.get_data()).hexdigest())\n        return response\n\n    @api.route('/sources/<source_uuid>/submissions/<submission_uuid>',\n               methods=['GET', 'DELETE'])\n    @token_required\n    def single_submission(source_uuid, submission_uuid):\n        if request.method == 'GET':\n            submission = get_or_404(Submission, submission_uuid,\n                                    column=Submission.uuid)\n            return jsonify(submission.to_json()), 200\n        elif request.method == 'DELETE':\n            submission = get_or_404(Submission, submission_uuid,\n                                    column=Submission.uuid)\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            utils.delete_file(source.filesystem_id, submission.filename,\n                              submission)\n            return jsonify({'message': 'Submission deleted'}), 200\n\n    @api.route('/sources/<source_uuid>/reply', methods=['POST'])\n    @token_required\n    def post_reply(source_uuid):\n        source = get_or_404(Source, source_uuid,\n                            column=Source.uuid)\n        if request.json is None:\n            abort(400, 'please send requests in valid JSON')\n\n        if 'reply' not in request.json:\n            abort(400, 'reply not found in request body')\n\n        user = get_user_object(request)\n\n        data = json.loads(request.data)\n        if not data['reply']:\n            abort(400, 'reply should not be empty')\n\n        source.interaction_count += 1\n        try:\n            filename = current_app.storage.save_pre_encrypted_reply(\n                source.filesystem_id,\n                source.interaction_count,\n                source.journalist_filename,\n                data['reply'])\n        except NotEncrypted:\n            return jsonify(\n                {'message': 'You must encrypt replies client side'}), 400\n\n        reply = Reply(user, source,\n                      current_app.storage.path(source.filesystem_id, filename))\n        db.session.add(reply)\n        db.session.add(source)\n        db.session.commit()\n        return jsonify({'message': 'Your reply has been stored'}), 201\n\n    @api.route('/submissions', methods=['GET'])\n    @token_required\n    def get_all_submissions():\n        submissions = Submission.query.all()\n        return jsonify({'submissions': [submission.to_json() for\n                                        submission in submissions]}), 200\n\n    @api.route('/user', methods=['GET'])\n    @token_required\n    def get_current_user():\n        user = get_user_object(request)\n        return jsonify(user.to_json()), 200\n\n    def _handle_http_exception(error):\n        # Workaround for no blueprint-level 404/5 error handlers, see:\n        # https://github.com/pallets/flask/issues/503#issuecomment-71383286\n        response = jsonify({'error': error.name,\n                           'message': error.description})\n\n        return response, error.code\n\n    for code in default_exceptions:\n        api.errorhandler(code)(_handle_http_exception)\n\n    return api\n", "path": "securedrop/journalist_app/api.py"}]}