We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Posting a Reply should return a UUID
## Description

The API should not return 204, but should return 200 with an object containing the UUID of the reply. Without doing that, there is no way for a client to sanely sync its local copy of a conversation with the server's copy.

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

{"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@@ -244,7 +244,8 @@\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+            return jsonify({'message': 'Your reply has been stored',\n+                            'uuid': reply.uuid}), 201\n \n     @api.route('/sources/<source_uuid>/replies/<reply_uuid>',\n                methods=['GET', 'DELETE'])\n", "issue": "Posting a Reply should return a UUID\n## Description\r\n\r\nThe API should not return 204, but should return 200 with an object containing the UUID of the reply. Without doing that, there is no way for a client to sanely sync its local copy of a conversation with the server's copy.\n", "before_files": [{"content": "from datetime import datetime, timedelta\nfrom functools import wraps\nimport json\nfrom werkzeug.exceptions import default_exceptions  # type: ignore\n\nfrom flask import abort, Blueprint, current_app, jsonify, request\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                     'replies_url': '/api/v1/replies',\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        return utils.serve_file_with_etag(source, submission.filename)\n\n    @api.route('/sources/<source_uuid>/replies/<reply_uuid>/download',\n               methods=['GET'])\n    @token_required\n    def download_reply(source_uuid, reply_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        reply = get_or_404(Reply, reply_uuid, column=Reply.uuid)\n\n        return utils.serve_file_with_etag(source, reply.filename)\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            source = get_or_404(Source, source_uuid, column=Source.uuid)\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>/replies', methods=['GET', 'POST'])\n    @token_required\n    def all_source_replies(source_uuid):\n        if request.method == 'GET':\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            return jsonify(\n                {'replies': [reply.to_json() for\n                             reply in source.replies]}), 200\n        elif request.method == 'POST':\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,\n                                                   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('/sources/<source_uuid>/replies/<reply_uuid>',\n               methods=['GET', 'DELETE'])\n    @token_required\n    def single_reply(source_uuid, reply_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        reply = get_or_404(Reply, reply_uuid, column=Reply.uuid)\n        if request.method == 'GET':\n            return jsonify(reply.to_json()), 200\n        elif request.method == 'DELETE':\n            utils.delete_file(source.filesystem_id, reply.filename,\n                              reply)\n            return jsonify({'message': 'Reply deleted'}), 200\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('/replies', methods=['GET'])\n    @token_required\n    def get_all_replies():\n        replies = Reply.query.all()\n        return jsonify(\n            {'replies': [reply.to_json() for reply in replies]}), 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 json\nfrom werkzeug.exceptions import default_exceptions  # type: ignore\n\nfrom flask import abort, Blueprint, current_app, jsonify, request\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                     'replies_url': '/api/v1/replies',\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        return utils.serve_file_with_etag(source, submission.filename)\n\n    @api.route('/sources/<source_uuid>/replies/<reply_uuid>/download',\n               methods=['GET'])\n    @token_required\n    def download_reply(source_uuid, reply_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        reply = get_or_404(Reply, reply_uuid, column=Reply.uuid)\n\n        return utils.serve_file_with_etag(source, reply.filename)\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            source = get_or_404(Source, source_uuid, column=Source.uuid)\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>/replies', methods=['GET', 'POST'])\n    @token_required\n    def all_source_replies(source_uuid):\n        if request.method == 'GET':\n            source = get_or_404(Source, source_uuid, column=Source.uuid)\n            return jsonify(\n                {'replies': [reply.to_json() for\n                             reply in source.replies]}), 200\n        elif request.method == 'POST':\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,\n                                                   filename))\n            db.session.add(reply)\n            db.session.add(source)\n            db.session.commit()\n            return jsonify({'message': 'Your reply has been stored',\n                            'uuid': reply.uuid}), 201\n\n    @api.route('/sources/<source_uuid>/replies/<reply_uuid>',\n               methods=['GET', 'DELETE'])\n    @token_required\n    def single_reply(source_uuid, reply_uuid):\n        source = get_or_404(Source, source_uuid, column=Source.uuid)\n        reply = get_or_404(Reply, reply_uuid, column=Reply.uuid)\n        if request.method == 'GET':\n            return jsonify(reply.to_json()), 200\n        elif request.method == 'DELETE':\n            utils.delete_file(source.filesystem_id, reply.filename,\n                              reply)\n            return jsonify({'message': 'Reply deleted'}), 200\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('/replies', methods=['GET'])\n    @token_required\n    def get_all_replies():\n        replies = Reply.query.all()\n        return jsonify(\n            {'replies': [reply.to_json() for reply in replies]}), 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"}]}