{"id":19297158,"url":"https://github.com/tempehs/flask_pwa_api_extension_task_source","last_synced_at":"2025-04-12T05:36:22.830Z","repository":{"id":260577806,"uuid":"881656140","full_name":"TempeHS/Flask_PWA_API_Extension_Task_Source","owner":"TempeHS","description":"Completed source code for a guided development task for students to build their first API and integrate it with a PWA application. Using Flask, SQLite3 for the backend and Bootstrap for the frontend. This is an extension of the Flask PWA Programming For The Web Task: https://github.com/TempeHS/Flask_PWA_Programming_For_The_Web_Task_Template.","archived":false,"fork":false,"pushed_at":"2025-03-18T10:35:07.000Z","size":12536,"stargazers_count":0,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-26T01:01:50.358Z","etag":null,"topics":["bootstrap","flask","flask-api","learning","learning-by-doing","pwa","python","sqlite3"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TempeHS.png","metadata":{"files":{"readme":"docs/README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-11-01T01:26:29.000Z","updated_at":"2025-03-18T10:35:10.000Z","dependencies_parsed_at":"2024-11-01T06:22:36.655Z","dependency_job_id":"add5b107-5fe5-4159-b7ea-0fc23d4bd9c3","html_url":"https://github.com/TempeHS/Flask_PWA_API_Extension_Task_Source","commit_stats":{"total_commits":39,"total_committers":2,"mean_commits":19.5,"dds":0.3589743589743589,"last_synced_commit":"beed8887f72b82068e414407919065541487e95a"},"previous_names":["tempehs/flask_pwa_api_extension_task_source"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TempeHS%2FFlask_PWA_API_Extension_Task_Source","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TempeHS%2FFlask_PWA_API_Extension_Task_Source/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TempeHS%2FFlask_PWA_API_Extension_Task_Source/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TempeHS%2FFlask_PWA_API_Extension_Task_Source/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TempeHS","download_url":"https://codeload.github.com/TempeHS/Flask_PWA_API_Extension_Task_Source/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248524849,"owners_count":21118615,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bootstrap","flask","flask-api","learning","learning-by-doing","pwa","python","sqlite3"],"created_at":"2024-11-09T23:01:21.332Z","updated_at":"2025-04-12T05:36:22.801Z","avatar_url":"https://github.com/TempeHS.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Flask PWA - API Extension Task\n\nThis task is to build a safe [RESTful API](https://blog.hubspot.com/website/what-is-rest-api) that extends the [Flask PWA - Programming for the Web Task](https://github.com/TempeHS/Flask_PWA_Programming_For_The_Web_Task_Template). From the parent task, students will abstract the database and management to an REST API with key authentication. The PWA will then be retooled to GET request the data from the REST API and POST request data to the REST API. The PWA UI for the API will be rapidly prototyped using the [Bootstrap](https://getbootstrap.com/) frontend framework.\n\nThe API instructions focus on modelling how to build and test an API incrementally. The PWA instructions focus on using the [Bootstrap](https://getbootstrap.com/) frontend framework to prototype an enhanced UI/UX frontend rapidly using [Bootstrap](https://getbootstrap.com/) components and classes.\n\n\u003e [!note]\n\u003e The template for this project has been pre-populated with assets from the Flask PWA task, including the logo, icons and database. Students can migrate their own assets if they wish.\n\n## Dependencies\n\n## Requirements\n\n1. [VSCode](https://code.visualstudio.com/download) or [GitHub Codespaces](https://github.com/features/codespaces)\n2. [Python 3.x](https://www.python.org/downloads/)\n3. [GIT 2.x.x +](https://git-scm.com/downloads)\n4. [SQLite3 Editor](https://marketplace.visualstudio.com/items?itemName=yy0931.vscode-sqlite3-editor)\n5. [Start git-bash](https://marketplace.visualstudio.com/items?itemName=McCarter.start-git-bash) 6.[Thunder Client](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client)\n6. pip/pip3 installs\n\n```bash\n    pip install Flask\n    pip install SQLite3\n    pip install flask_wtf\n    pip install flask_csp\n    pip install jsonschema\n    pip install requests\n```\n\n\u003e [!Important]\n\u003e MacOS and Linux users may have a `pip3` soft link instead of `pip`, run the below commands to see what path your system is configured with and use that command through the project. If neither command returns a version, then likely [Python 3.x](https://www.python.org/downloads/) needs to be installed.\n\u003e\n\u003e ```bash\n\u003e pip show pip\n\u003e pip3 show pip\n\u003e ```\n\n## Instructions for building the API\n\n\u003e [!Warning]\n\u003e These instructions are less verbose than the parent task because students are expected to be now familiar with Bash, Flask \u0026 SQLite3.\n\n### Step 1: Learn the basics of implementing an API in Flask\n\nWatch: [Build a Flask API in 12 Minutes](https://www.youtube.com/watch?v=zsYIw6RXjfM)\n\n\u003e [!Note]\n\u003e The video uses [Postman](https://www.postman.com/), this tutorial uses [Thunder Client](https://www.thunderclient.com/) a VS Code extension that has similar functionality.\n\n### Step 2: Create the Directory Structure\n\nStudents can create files as they are needed. This structure defines the correct directory structure for all files. As students `touch` each file, they should refer to this structure to ensure the file path is correct.\n\n```text\n├── database\n│   └─── data_source.db\n├── static\n│   ├── css\n│   │   ├──bootstrap.min.css\n│   │   └──style.css\n│   ├── icons\n│   │   ├──desktop_screenshot.png\n│   │   ├──icon-128x128.png\n│   │   ├──icon-192x192.png\n│   │   ├──icon-384x384.png\n│   │   ├──icon-512x512.png\n│   │   └──mobile_screenshot.png\n│   ├── images\n│   │   ├──favicon.png\n│   │   └──logo.png\n│   ├─── js\n│   │   ├──app.js\n│   │   ├──bootstrap.bundle.min.js\n│   │   └──serviceWorker.js\n│   └── manifest.json\n├── templates\n│   ├── partials\n│   │   ├──footer.html\n│   │   └──menu.html\n│   ├──index.html\n│   ├──layout.html\n│   └──privacy.html\n├── api.py\n├── database_manager.py\n├── LICENSE\n└── main.py\n```\n\n### Step 3: Setup a basic API in api.py\n\nThis Python implementation in 'api.py':\n\n1. Imports all the required dependencies for the whole project.\n2. Configure the 'Cross Origin Request' policy.\n3. Configure the rate limiter.\n4. Configure a route for the root `/` with a GET method to return stub data and a 200 response.\n5. Configure a route to /add_extension with a POST method to return stub data and a 201 response.\n\n```python\nfrom flask import Flask\nfrom flask import request\nfrom flask_cors import CORS\nfrom flask_limiter import Limiter\nfrom flask_limiter.util import get_remote_address\nimport logging\n\nimport database_manager as dbHandler\n\n\napi = Flask(__name__)\ncors = CORS(api)\napi.config[\"CORS_HEADERS\"] = \"Content-Type\"\nlimiter = Limiter(\n    get_remote_address,\n    app=api,\n    default_limits=[\"200 per day\", \"50 per hour\"],\n    storage_uri=\"memory://\",\n)\n\n\n@api.route(\"/\", methods=[\"GET\"])\n@limiter.limit(\"3/second\", override_defaults=False)\ndef get():\n    return (\"API Works\"), 200\n\n\n@api.route(\"/add_extension\", methods=[\"POST\"])\n@limiter.limit(\"1/second\", override_defaults=False)\ndef post():\n    data = request.get_json()\n    return data, 201\n\n\nif __name__ == \"__main__\":\n    api.run(debug=True, host=\"0.0.0.0\", port=3000)\n```\n\n### Step 3: Test your basic API with Thunder Client\n\n![Screen recording testing an API with Thunder Client](/docs/README_resources/test_basic_API.gif \"Follow these steps to test your basic API\")\n\n### Step 4: Build a basic GET response\n\nExtend the `get():` method in `api.py` to get data from the database via the `dbHandler` and return it to the request with a status `200`.\n\n```python\ndef get():\n    content = dbHandler.extension_get(\"%\")\n    return (content), 200\n```\n\nThis Python implementation in 'database_manager.py'\n\n1. Imports all the required dependencies for the project\n2. Connects to the SQLite3 database\n3. Executes a query\n4. Converts the query data to a JSON structure\n5. Returns the JSON data\n\n```python\nfrom flask import jsonify\nimport sqlite3 as sql\nfrom jsonschema import validate\nfrom flask import current_app\n\n\ndef extension_get(lang):\n    con = sql.connect(\"database/data_source.db\")\n    cur = con.cursor()\n    cur.execute(\"SELECT * FROM extension\")\n    migrate_data = [\n        dict(\n            extID=row[0],\n            name=row[1],\n            hyperlink=row[2],\n            about=row[3],\n            image=row[4],\n            language=row[5],\n        )\n        for row in cur.fetchall()\n    ]\n    return jsonify(migrate_data)\n```\n\n### Step 5: Test your basic GET Response\n\n![Screen recording testing a API GET with Thunder Client](/docs/README_resources/test_basic_GET_API.gif \"Follow these steps to test your basic GET API\")\n\n### Step 6 Add a GET request argument to filter extensions by language\n\nExtend the `get():` method in `api.py` to either get all data or data that matches a language parameter from the database by\n\n1. Validating the argument is \"lang\" and that the \"lang\" is only alpha characters for security.\n2. Passing the language request to the dbHandler.\n3. If no language is specified, the wildcard `%` will be passed.\n4. Return the data from dbHandler to the request.\n5. Return a status `200`.\n\n```python\ndef get():\n    # For security data is validated on entry\n    if request.args.get(\"lang\") and request.args.get(\"lang\").isalpha():\n        lang = request.args.get(\"lang\")\n        lang = lang.upper()*\n        content = dbHandler.extension_get(lang)\n    else:\n        content = dbHandler.extension_get(\"%\")\n    return (content), 200\n```\n\nExtend the database query in the `extension_get():` method in the `database_manager.py` to filter the SQL query based on the argument parameter and return it as JSON data where:\n\n1. If no valid parameter is passed, the function will return the entire database in a JSON format because of the `%` wildcard.\n2. If a valid parameter is passed, the database will be queried with a `WHERE language LIKE' SQL query, and all matching languages (if any) will be returned in JSON format.\n\n```python\ndef extension_get(lang):\n    con = sql.connect(\"database/data_source.db\")\n    cur = con.cursor()\n    cur.execute(\"SELECT * FROM extension WHERE language LIKE ?;\", [lang])\n    migrate_data = [\n        dict(\n            extID=row[0],\n            name=row[1],\n            hyperlink=row[2],\n            about=row[3],\n            image=row[4],\n            language=row[5],\n        )\n        for row in cur.fetchall()\n    ]\n    return jsonify(migrate_data)\n```\n\n### Step 7: Test your GET Response\n\n![Screen recording testing a API GET with Thunder Client](/docs/README_resources/test_GET_API.gif \"Follow these steps to test your GET API\")\n\n### Step 8: Setup your basic POST response\n\nExtend the `/add_extension` route in api.py to pass the POST data to the 'dbHandler' and set up a driver to return the response with a 201 status code.\n\n```python\ndef post():\n    data = request.get_json()\n    response = dbHandler.extension_add(data)\n    return response\n```\n\nExtend the `extension_add():` method in the `database_manager.py` to be a driver that returns the received data to the POST request.\n\n```python\ndef extension_add(response):\n    data = response\n    return data, 200\n```\n\n### Step 9: Test your basic POST response\n\n![Screen recording testing a API basic POST with Thunder Client](/docs/README_resources/test_basic_POST_API.gif \"Follow these steps to test your basic POST API\")\n\n### Step 10: Extend the dbHandler to validate the JSON\n\nUpdate the `extension_add():` method in `database_manager.py` to validate the JSON and return a message and response code. The schema provided validates the JSON with the following rules:\n\n1. All 5 properties are required.\n2. No extra properties are allowed.\n3. The data type for all 5 properties is string.\n4. The hyperlink pattern enforces the URL to start with `https://marketplace.visualstudio.com/items?itemName=`, and the characters `\u003c` and `\u003e` are not allowed to prevent XXS attacks.\n5. The image pattern requires https:// but `\u003c` and `\u003e` are not allowed to prevent XXS attacks.\n6. Languages must be enumerated with the list of languages.\n\n\u003e [!Important]\n\u003e You can use [https://regex101.com/](https://regex101.com/) to design and test patterns for your database design. Regular expressions in Python require a raw string (with the r prefix) due to the way characters need to be escaped.\n\n```python\n    if validate_json(data):\n        return {\"message\": \"Extension added successfully\"}, 201\n    else:\n        return {\"error\": \"Invalid JSON\"}, 400\n\n\nschema = {\n    \"type\": \"object\",\n    \"validationLevel\": \"strict\",\n    \"required\": [\n        \"name\",\n        \"hyperlink\",\n        \"about\",\n        \"image\",\n        \"language\",\n    ],\n    \"properties\": {\n        \"name\": {\"type\": \"string\"},\n                    \"pattern\": r\"^https:\\/\\/marketplace\\.visualstudio\\.com\\/items\\?itemName=(?!.*[\u003c\u003e])[a-zA-Z0-9\\-._~:\\/?#\\[\\]@!$\u0026'()*+,;=]*$\",\n        },\n        \"about\": {\"type\": \"string\"},\n        \"image\": {\n            \"type\": \"string\",\n            \"pattern\": r\"^https:\\/\\/(?!.*[\u003c\u003e])[a-zA-Z0-9\\-._~:\\/?#\\[\\]@!$\u0026'()*+,;=]*$\",\n        },\n        \"language\": {\n            \"type\": \"string\",\n            \"enum\": [\"PYTHON\", \"CPP\", \"BASH\", \"SQL\", \"HTML\", \"CSS\", \"JAVASCRIPT\"],\n        },\n    },\n    \"additionalProperties\": False,\n}\n\ndef validate_json(json_data):\n    try:\n        validate(instance=json_data, schema=schema)\n        return True\n    except:\n        return False\n```\n\nSample JSON data for you to test the API:\n\n```text\n{\"name\": \"test\", \"hyperlink\": \"https://marketplace.visualstudio.com/items?itemName=123.html\", \"about\": \"This is a test\", \"image\": \"https://test.jpg\", \"language\": \"BASH\"}\n```\n\n### Step 10: Test your validation POST response\n\n![Screen recording testing a API basic POST with Thunder Client](/docs/README_resources/test_POST_API_Auth.gif \"Follow these steps to test your basic POST API\")\n\n### Step 11: Insert the POST data into the database\n\nUpdate the `extension_add():` method in database_manager.py`to INSERT the JSON data into the database. The`extID` is not required as it has been configured to auto increment in the database table.\n\n```python\ndef extension_add(data):\n    if validate_json(data):\n        con = sql.connect(\"database/data_source.db\")\n        cur = con.cursor()\n        cur.execute(\n            \"INSERT INTO extension (name, hyperlink, about, image, language) VALUES (?, ?, ?, ?, ?);\",\n            [\n                data[\"name\"],\n                data[\"hyperlink\"],\n                data[\"about\"],\n                data[\"image\"],\n                data[\"language\"],\n            ],\n        )\n        con.commit()\n        con.close()\n        return {\"message\": \"Extension added successfully\"}, 201\n    else:\n        return {\"error\": \"Invalid JSON\"}, 400\n```\n\n### Step 12: Implement POST Authorisation\n\n[API Key Authorisation](https://cloud.google.com/endpoints/docs/openapi/when-why-api-key) is a common method for authorising an application, site, or project. In this scenario, the API is not authorising a specific user. This is a very simple implementation of API Key Authorisation.\n\nExtend the `api.py` to store the key as a variable. Students will need to generate a unique basic 16 secret key with [https://acte.ltd/utils/randomkeygen](https://acte.ltd/utils/randomkeygen).\n\n```python\nauth_key = \"4L50v92nOgcDCYUM\"\n```\n\nExtend the `def post():` method in `api.py` to request the `authorisation` attribute from the post head, compare it to the `auth_key,` and process the appropriate response.\n\n```python\ndef post():\n    if request.headers.get(\"Authorisation\") == auth_key:\n        data = request.get_json()\n        response = dbHandler.extension_add(data)\n        return response\n    else:\n        return {\"error\": \"Unauthorised\"}, 401\n```\n\n### Step 13: Test your authorisation for a POST response\n\n![Screen recording testing a API basic POST with Thunder Client](/docs/README_resources/test_POST_API_Auth.gif \"Follow these steps to test your POST API Authorisation\")\n\n### 14: Configure the logger to log to api_security_log.log\n\nExtend the `api.py` with the implementation below, which should be inserted directly below the `imports`. This will configure the logger to log to a file for security analysis.\n\n```python\napi_log = logging.getLogger(__name__)\nlogging.basicConfig(\n    filename=\"api_security_log.log\",\n    encoding=\"utf-8\",\n    level=logging.DEBUG,\n    format=\"%(asctime)s %(message)s\",\n)\n```\n\n## Instructions for building the PWA user interface to the API.\n\n\u003e [!Note]\n\u003e This implementation uses the [Bootstrap](https://getbootstrap.com/) frontend CSS \u0026 JS design framework. Version 5.3.3 has been included in the static files.\n\n### Step 1: Setup the Jinga2 template engine file structure\n\n```text\n├── templates\n│   ├── partials\n│   │   ├──footer.html\n│   │   └──menu.html\n│   ├──index.html\n│   ├──layout.html\n│   └──privacy.html\n```\n\n### Step 2: Setup the Jinga2 template\n\nThis Jinga2/HTML implementation in layout.html:\n\n1. Security features are defined in the head.\n2. The menu and footer are defined in a partial for easy maintenance.\n3. The body will be defined by the block content when the `layout.html` is inherited.\n4. Bootstrap components (CSS \u0026 JavaScript) are linked.\n5. JS Components, including the PWA service worker, are linked.\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta\n      http-equiv=\"Content-Security-Policy\"\n      content=\"base-uri 'self'; default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' *; media-src 'self'; font-src 'self'; connect-src 'self'; object-src 'self'; worker-src 'self'; frame-src 'none'; form-action 'self'; manifest-src 'self'\"\n    /\u003e\n    \u003cmeta charset=\"utf-8\" /\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /\u003e\n    \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"static/css/style.css\" /\u003e\n    \u003ctitle\u003eVS Code Extensions for Software Engineering\u003c/title\u003e\n    \u003clink rel=\"manifest\" href=\"static/manifest.json\" /\u003e\n    \u003clink rel=\"icon\" type=\"image/x-icon\" href=\"static/images/favicon.png\" /\u003e\n    \u003cmeta name=\"theme-color\" content=\"\" /\u003e\n    \u003clink href=\"static/css/bootstrap.min.css\" rel=\"stylesheet\" /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    {% include \"partials/menu.html\" %}\n    \u003cmain\u003e{% block content %}{% endblock %}\u003c/main\u003e\n    {% include \"partials/footer.html\" %}\n    \u003cscript src=\"static/js/bootstrap.bundle.min.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"static/js/serviceWorker.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"static/js/app.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Step 3: Setup the footer.html\n\nThis HTML implementation provides a full-width horizontal rule and a Bootstrap column containing a link to the privacy page.\n\n```html\n\u003cdiv class=\"container-fluid\"\u003e\n  \u003chr /\u003e\n\u003c/div\u003e\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003cdiv class=\"col-12\"\u003e\n      \u003ca href=\"privacy.html\"\u003ePrivacy Policy\u003c/a\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\n### Step 4: Setup the menu.html and add some UX/accessibility advanced features using JS.\n\nThis HTML implementation is an adaption of the basic [Bootstrap Navbar](https://getbootstrap.com/docs/5.3/components/navbar/).\n\n```html\n\u003cnav class=\"navbar navbar-expand-lg bg-body-tertiary\"\u003e\n  \u003cdiv class=\"container-fluid\"\u003e\n    \u003ca class=\"navbar-brand\" href=\"/\"\u003e\n      \u003cimg src=\"static/images/logo.png\" alt=\"logo\" height=\"80\" /\u003e\n    \u003c/a\u003e\n    \u003cbutton\n      class=\"navbar-toggler\"\n      type=\"button\"\n      data-bs-toggle=\"collapse\"\n      data-bs-target=\"#navbarSupportedContent\"\n      aria-controls=\"navbarSupportedContent\"\n      aria-expanded=\"false\"\n      aria-label=\"Toggle navigation\"\n    \u003e\n      \u003cspan class=\"navbar-toggler-icon\"\u003e\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cdiv class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\"\u003e\n      \u003cul class=\"navbar-nav me-auto mb-2 mb-lg-0\"\u003e\n        \u003cli class=\"nav-item\"\u003e\n          \u003ca class=\"nav-link active\" href=\"/\" aria-current=\"page\"\u003eHome\u003c/a\u003e\n        \u003c/li\u003e\n        \u003cli class=\"nav-item\"\u003e\n          \u003ca class=\"nav-link\" href=\"/add.html\"\u003eAdd Extension\u003c/a\u003e\n        \u003c/li\u003e\n        \u003cli class=\"nav-item\"\u003e\n          \u003ca class=\"nav-link\" href=\"/privacy.html\"\u003ePrivacy\u003c/a\u003e\n        \u003c/li\u003e\n      \u003c/ul\u003e\n      \u003cform class=\"d-flex\" role=\"search\" id=\"search-form\"\u003e\n        \u003cinput\n          class=\"form-control me-2\"\n          type=\"search\"\n          placeholder=\"Search\"\n          aria-label=\"Search\"\n          id=\"search-input\"\n        /\u003e\n        \u003cbutton class=\"btn btn-outline-success\" type=\"submit\"\u003eSearch\u003c/button\u003e\n      \u003c/form\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/nav\u003e\n```\n\nExtend the `app.js` with this script that toggles the active class and the `aria-current=\"page\"` attribute for the current page menu item. The `active` class improves UX by styling the current page in the menu differently and adding the [`aria-current` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) to the current page which improves the context understanding of screen readers for enhanced accessibility.\n\n```js\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n  const navLinks = document.querySelectorAll(\".nav-link\");\n  const currentUrl = window.location.pathname;\n\n  navLinks.forEach((link) =\u003e {\n    const linkUrl = link.getAttribute(\"href\");\n    if (linkUrl === currentUrl) {\n      link.classList.add(\"active\");\n      link.setAttribute(\"aria-current\", \"page\");\n    } else {\n      link.classList.remove(\"active\");\n      link.removeAttribute(\"aria-current\");\n    }\n  });\n});\n```\n\nExtend the `app.js` with this script that adds basic search functionality to the search button in the menu by searching the current page and highlighting matching words.\n\n```js\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n  const form = document.getElementById(\"search-form\");\n  const input = document.getElementById(\"search-input\");\n\n  form.addEventListener(\"submit\", function (event) {\n    event.preventDefault();\n    const searchTerm = input.value.trim().toLowerCase();\n    if (searchTerm) {\n      highlightText(searchTerm);\n    }\n  });\n\n  function highlightText(searchTerm) {\n    const mainContent = document.querySelector(\"main\");\n    removeHighlights(mainContent);\n    highlightTextNodes(mainContent, searchTerm);\n  }\n\n  function removeHighlights(element) {\n    const highlightedElements = element.querySelectorAll(\"span.highlight\");\n    highlightedElements.forEach((el) =\u003e {\n      el.replaceWith(el.textContent);\n    });\n  }\n\n  function highlightTextNodes(element, searchTerm) {\n    const regex = new RegExp(`(${searchTerm})`, \"gi\");\n    const walker = document.createTreeWalker(\n      element,\n      NodeFilter.SHOW_TEXT,\n      null,\n      false\n    );\n    let node;\n    while ((node = walker.nextNode())) {\n      const parent = node.parentNode;\n      if (\n        parent \u0026\u0026\n        parent.nodeName !== \"SCRIPT\" \u0026\u0026\n        parent.nodeName !== \"STYLE\"\n      ) {\n        const text = node.nodeValue;\n        const highlightedText = text.replace(\n          regex,\n          '\u003cspan class=\"highlight\"\u003e$1\u003c/span\u003e'\n        );\n        if (highlightedText !== text) {\n          const tempDiv = document.createElement(\"div\");\n          tempDiv.innerHTML = highlightedText;\n          while (tempDiv.firstChild) {\n            parent.insertBefore(tempDiv.firstChild, node);\n          }\n          parent.removeChild(node);\n        }\n      }\n    }\n  }\n});\n```\n\nExtend style.css to add the class required by the search script.\n\n```css\n.highlight {\n  background-color: yellow;\n  border-radius: 20px;\n  border: 1px yellow solid;\n}\n```\n\n### Step 5: Inherit the layout to the /index.html and set up the app route.\n\nInsert the basic HTML into index.html.\n\n```html\n{% extends 'layout.html' %} {% block content %}\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"row\"\u003e\u003c/div\u003e\n  \u003cdiv class=\"row\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n{% endblock %}\n```\n\nThis Python Flask implementation in `main.py`\n\n1. Imports all dependencies required for the whole project.\n2. Set up CSRFProtect to provide asynchronous keys that protect the app from a CSRF attack. Students will need to generate a unique basic 16 secret key with [https://acte.ltd/utils/randomkeygen](https://acte.ltd/utils/randomkeygen).\n3. Defines the head attribute for authorising a POST request to the API.\n4. Define a secure Content Secure Policy (CSP) head.\n5. Configures the Flask app.\n6. Redirect /index.html to the domain root for a consistent user experience.\n7. Renders the index.html for a GET app route.\n8. Provide an endpoint to log CSP violations for security analysis.\n\n```python\nfrom flask import Flask\nfrom flask import redirect\nfrom flask import render_template\nfrom flask import request\nimport requests\nfrom flask_wtf import CSRFProtect\nfrom flask_csp.csp import csp_header\nimport logging\n\n\n# Generate a unique basic 16 key: https://acte.ltd/utils/randomkeygen\napp = Flask(__name__)\ncsrf = CSRFProtect(app)\napp.secret_key = b\"6HlQfWhu03PttohW;apl\"\n\napp_header = {\"Authorisation\": \"4L50v92nOgcDCYUM\"}\n\n\n@app.route(\"/index.html\", methods=[\"GET\"])\ndef root():\n    return redirect(\"/\", 302)\n\n\n@app.route(\"/\", methods=[\"GET\"])\n@csp_header(\n        {\n        \"base-uri\": \"self\",\n        \"default-src\": \"'self'\",\n        \"style-src\": \"'self'\",\n        \"script-src\": \"'self'\",\n        \"img-src\": \"*\",\n        \"media-src\": \"'self'\",\n        \"font-src\": \"self\",\n        \"object-src\": \"'self'\",\n        \"child-src\": \"'self'\",\n        \"connect-src\": \"'self'\",\n        \"worker-src\": \"'self'\",\n        \"report-uri\": \"/csp_report\",\n        \"frame-ancestors\": \"'none'\",\n        \"form-action\": \"'self'\",\n        \"frame-src\": \"'none'\",\n        }\n)\ndef index():\n    return render_template(\"/index.html\")\n\n\n@app.route(\"/csp_report\", methods=[\"POST\"])\n@csrf.exempt\ndef csp_report():\n    app.logger.critical(request.data.decode())\n    return \"done\"\n\n\nif __name__ == \"__main__\":\n    app.run(debug=True, host=\"0.0.0.0\", port=5000)\n\n```\n\n### Step 7: Test the basic index.html\n\n```bash\npython main.py\n```\n\n![A render of the basic index.html](README_resources\\basic_index.png \"The basic index.html should load like this\")\n\n### Step 8: Setup the Privacy Policy\n\n```html\n{% extends 'layout.html' %} {% block content %}\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003ch1 class=\"display-1\"\u003ePrivacy Policy\u003c/h1\u003e\n    \u003cp\u003ePolicy here...\u003c/p\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"row\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n{% endblock %}\n```\n\nExtend `main.py` to include an app route to privacy.html\n\n```python\n@app.route(\"/privacy.html\", methods=[\"GET\"])\ndef privacy():\n    return render_template(\"/privacy.html\")\n```\n\n### Step 9: Test privacy.html and search functionality.\n\n![A render of the privacy.html](README_resources\\privacy_search.png \"The privacy index.html should load like this\")\n\nEnsure your page renders correctly with the test cases:\n\n1. The page renders correctly\n2. The privacy menu item is darker than the other menu items\n3. A search for \"priv\" highlights the correct letters in the main body.\n\n### Step 10: Setup the cards and request the data from the API\n\nExtend the `index():` method in `main.py` so it requests data from the API and handles the exception that the API did not respond with an error message.\n\n```python\ndef index():\n    url = \"http://127.0.0.1:3000\"\n    try:\n        response = requests.get(url)\n        response.raise_for_status()  # Raise an exception for HTTP errors\n        data = response.json()\n    except requests.exceptions.RequestException as e:\n        data = {\"error\": \"Failed to retrieve data from the API\"}\n    return render_template(\"index.html\", data=data)\n```\n\nReplace the test html in 'index.html` template that:\n\n1. Implements a [Bootstrap jumbotron heading](https://getbootstrap.com/docs/5.3/examples/jumbotron/).\n2. Implements a [Bootstrap button group](https://getbootstrap.com/docs/5.3/components/button-group/) that will later allow users to filter the extensions by language.\n3. Implements the database items as [Bootstrap cards](https://getbootstrap.com/docs/5.3/components/card/) in a responsive[Bootstrap Column Layout](https://getbootstrap.com/docs/5.3/layout/columns/).\n4. Provides API error feedback to the user that is styled by the [Bootstrap color utility](https://getbootstrap.com/docs/5.3/utilities/colors/).\n5. Apply [Bootstrap sizing](https://getbootstrap.com/docs/5.3/utilities/sizing/) and [Bootstrap spacing](https://getbootstrap.com/docs/5.3/utilities/spacing/) utilities to layout the cards.\n\n```html\n{% extends 'layout.html' %} {% block content %}\n\u003cdiv class=\"container py-4\"\"\u003e\n  \u003cdiv class=\"p-4 bg-body-tertiary rounded-3\"\u003e\n    \u003cdiv class=\"container-fluid py-2\"\u003e\n      \u003ch1 class=\"display-4\"\u003eVS Code Extensions for Software Engineering\u003c/h1\u003e\n      \u003cp class=\"lead\"\u003e\n        This is a collection of Visual Studio Code extensions that are useful\n        for software engineering.\n      \u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003cdiv class=\"btn-group\" role=\"group\" aria-label=\"Filter by language\"\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"all\"\u003eAll\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"python\"\u003ePython\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"c++\"\u003eC++\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"bash\"\u003eBASH\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"sql\"\u003eSQL\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"html\"\u003eHTML\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"css\"\u003eCSS\u003c/button\u003e\n      \u003cbutton type=\"button\" class=\"btn btn-primary\" id=\"js\"\u003eJAVASCRIPT\u003c/button\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"container pt-4\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003cdiv class=\"error\"\u003e\u003ch2 class=\"text-danger\"\u003e{{ data.error }}\u003c/h2\u003e\u003c/div\u003e\n    {% if data.error is not defined %}\n      {% for row in data %}\n      \u003cdiv class=\"col-sm-12 col-lg-4 mb-4\"\u003e\n        \u003cdiv class=\"card h-100\" style=\"width: 18rem\"\u003e\n          \u003cimg\n            src=\"{{ row.image }}\"\n            class=\"card-img-top\"\n            alt=\"Product image for the {{ row.name }} VSCode extension.\"\n          /\u003e\n          \u003cdiv class=\"card-body\"\u003e\n            \u003ch5 class=\"card-title\"\u003e{{ row.name }}\u003c/h5\u003e\n            \u003cp class=\"card-text\"\u003e{{ row.about }}\u003c/p\u003e\n            \u003ca href=\"{{ row.hyperlink }}\" class=\"btn btn-primary\"\u003eRead More\u003c/a\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      {% endfor %}\n    {% endif %}\n  \u003c/div\u003e\n\u003c/div\u003e\n{% endblock %}\n```\n\n### Step 11: Test the index.html and API integration\n\n![Screen recording testing a the index and the API integration](/docs/README_resources/test_index_and_API.gif \"Follow these steps to test your index.html and API\")\n\n### Step 12: Implement a form with attribute controls to POST a new extension to API.\n\nThe HTML Implementation in `add.html`\n\n1. Provides error and message feedback to the user that is styled by the [Bootstrap color utility](https://getbootstrap.com/docs/5.3/utilities/colors/).\n2. Uses [Bootstrap Forms](https://getbootstrap.com/docs/5.3/forms/form-control/) to layout a data entry form.\n3. Uses Form attributes type, place holder \u0026 pattern to improve user experience in entering the correct data.\n\n```html\n{% extends 'layout.html' %} {% block content %}\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003ch1\u003eAdd an Extension\u003c/h1\u003e\n    \u003cdiv class=\"error\"\u003e\n      \u003ch2\u003e\n        \u003cspan class=\"text-danger\"\u003e{{ data.error }}\u003c/span\n        \u003e\u003cspan class=\"text-success\"\u003e{{ data.message }}\u003c/span\u003e\n      \u003c/h2\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"container\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003cform action=\"/add.html\" method=\"POST\" class=\"box\"\u003e\n      \u003cdiv class=\"col-auto\"\u003e\n        \u003clabel for=\"name\" class=\"form-label\"\u003eExtension name\u003c/label\u003e\n        \u003ctextarea\n          id=\"name\"\n          name=\"name\"\n          class=\"form-control\"\n          rows=\"1\"\n          autocomplete=\"off\"\n        \u003e\u003c/textarea\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"col-auto\"\u003e\n        \u003clabel for=\"hyperlink\" name=\"hyperlink\" class=\"form-label\"\n          \u003eHyperlink to extension\u003c/label\n        \u003e\n        \u003cinput\n          id=\"hyperlink\"\n          name=\"hyperlink\"\n          type=\"url\"\n          class=\"form-control\"\n          placeholder=\"https://marketplace.visualstudio.com/items?itemName=\"\n          pattern=\"^https:\\/\\/marketplace\\.visualstudio\\.com\\/items\\?itemName=(?!.*[\u003c\u003e])[a-zA-Z0-9\\-._~:\\/?#\\[\\]@!$\u0026'()*+,;=]*$\"\n        /\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"col-auto\"\u003e\n        \u003clabel for=\"about\" class=\"form-label\"\u003eAbout\u003c/label\u003e\n        \u003ctextarea\n          id=\"about\"\n          name=\"about\"\n          class=\"form-control\"\n          rows=\"3\"\n          placeholder=\"A brief description of the extension\"\n        \u003e\u003c/textarea\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"col-auto\"\u003e\n        \u003clabel for=\"name\" name=\"image\" class=\"form-label\"\u003eURL to Icon\u003c/label\u003e\n        \u003cinput\n          id=\"image\"\n          name=\"image\"\n          type=\"url\"\n          class=\"form-control\"\n          pattern=\"^https:\\/\\/(?!.*[\u003c\u003e])[a-zA-Z0-9\\-._~:\\/?#\\[\\]@!$\u0026'()*+,;=]*$\"\n          placeholder=\"https://\"\n        /\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"col-auto\"\u003e\n        \u003clabel for=\"language\" name=\"language\" class=\"form-label\"\n          \u003eProgramming language\u003c/label\n        \u003e\n        \u003cselect\n          id=\"language\"\n          name=\"language\"\n          class=\"form-select\"\n          aria-label=\"Default select language\"\n        \u003e\n          \u003coption selected\u003eSelect a language from this menu\u003c/option\u003e\n          \u003coption value=\"PYTHON\"\u003ePYTHON\u003c/option\u003e\n          \u003coption value=\"CPP\"\u003eCPP\u003c/option\u003e\n          \u003coption value=\"BASH\"\u003eBASH\u003c/option\u003e\n          \u003coption value=\"SQL\"\u003eSQL\u003c/option\u003e\n          \u003coption value=\"HTML\"\u003eHTML\u003c/option\u003e\n          \u003coption value=\"CSS\"\u003eCSS\u003c/option\u003e\n          \u003coption value=\"JAVASCRIPT\"\u003eJAVASCRIPT\u003c/option\u003e\n        \u003c/select\u003e\n      \u003c/div\u003e\n      \u003cbr /\u003e\n      \u003cdiv class=\"col-auto\"\u003e\n        \u003cbutton type=\"submit\" class=\"btn btn-primary mb-3\"\u003eSubmit\u003c/button\u003e\n      \u003c/div\u003e\n      \u003cinput type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\" /\u003e\n    \u003c/form\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n{% endblock %}\n```\n\nExtend `main.py' to provide a route with POST and GET methods for `add.html` that\n\n1. Renders `add.html` on GET requests.\n2. On a POST method, read the form in add.html and construct a JSON.\n3. Then, POST the JSON with the header that includes the Authentication key to the API.\n4. Render `add.html` with any errors or messages from the API.\n\n```python\n@app.route(\"/add.html\", methods=[\"POST\", \"GET\"])\ndef form():\n    if request.method == \"POST\":\n        name = request.form[\"name\"]\n        hyperlink = request.form[\"hyperlink\"]\n        about = request.form[\"about\"]\n        image = request.form[\"image\"]\n        language = request.form[\"language\"]\n        data = {\n            \"name\": name,\n            \"hyperlink\": hyperlink,\n            \"about\": about,\n            \"image\": image,\n            \"language\": language,\n        }\n        app.logger.critical(data)\n        try:\n            response = requests.post(\n                \"http://127.0.0.1:3000/add_extension\",\n                json=data,\n                headers=app_header,\n            )\n            data = response.json()\n        except requests.exceptions.RequestException as e:\n            data = {\"error\": \"Failed to retrieve data from the API\"}\n        return render_template(\"/add.html\", data=data)\n    else:\n        return render_template(\"/add.html\", data={})\n```\n\n### Step 13: Add event listeners to the `/` page for the buttons to filter the extensions by language.\n\nExtend `app.js` with a script to provide functionality to the home page buttons.\n\n```js\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n  if (window.location.pathname === \"/\") {\n    const buttons = [\n      { id: \"all\", url: \"/\" },\n      { id: \"python\", url: \"?lang=python\" },\n      { id: \"cpp\", url: \"?lang=cpp\" },\n      { id: \"bash\", url: \"?lang=bash\" },\n      { id: \"sql\", url: \"?lang=sql\" },\n      { id: \"html\", url: \"?lang=html\" },\n      { id: \"css\", url: \"?lang=css\" },\n      { id: \"js\", url: \"?lang=javascript\" },\n    ];\n\n    buttons.forEach((button) =\u003e {\n      const element = document.getElementById(button.id);\n      if (element) {\n        element.addEventListener(\"click\", function () {\n          window.location.href = button.url;\n        });\n      }\n    });\n  }\n});\n```\n\n### Step 14: Forward the GET request argument to the API to filter extensions by language.\n\n```python\n    url = \"http://127.0.0.1:3000\"\n    if request.args.get(\"lang\") and request.args.get(\"lang\").isalpha():\n        lang = request.args.get(\"lang\")\n        url += f\"?lang={lang}\"\n```\n\n### 15: Configure the logger to log to main_security_log.log\n\nExtend the `main.py` with the implementation below, which should be inserted directly below the `imports`. This will configure the logger to log to a file for security analysis.\n\n```python\napp_log = logging.getLogger(__name__)\nlogging.basicConfig(\n    filename=\"main_security_log.log\",\n    encoding=\"utf-8\",\n    level=logging.DEBUG,\n    format=\"%(asctime)s %(message)s\",\n)\n```\n\n## Extension activities to improve the API and PWA.\n\n1. Create a get_languages method that returns all the languages in the database.\n2. Improve exception handling of the `add_extension` endpoint to give more detailed feedback to the user.\n3. Use the new get-languages method to define the content that renders in the PWA.\n4. Implement a sort extension by function\n\n\u003cp xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:dct=\"http://purl.org/dc/terms/\"\u003e\u003ca property=\"dct:title\" rel=\"cc:attributionURL\" href=\"https://github.com/TempeHS/Flask_PWA_API_Extension_Task_Source\"\u003eFlask PWA API Extension Task Source\u003c/a\u003e and \u003ca property=\"dct:title\" rel=\"cc:attributionURL\" href=\"https://github.com/TempeHS/Flask_PWA_API_Extension_Task_Template\"\u003eFlask PWA API Extension Task Template\u003c/a\u003e by \u003ca rel=\"cc:attributionURL dct:creator\" property=\"cc:attributionName\" href=\"https://github.com/benpaddlejones\"\u003eBen Jones\u003c/a\u003e is licensed under \u003ca href=\"https://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1\" target=\"_blank\" rel=\"license noopener noreferrer\" style=\"display:inline-block; \"\u003eCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International\u003cimg style=\"height:22px!important; margin-left:3px; vertical-align:text-bottom; \" src=\"https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1\" alt=\"\"\u003e\u003cimg style=\"height:22px!important; margin-left:3px; vertical-align:text-bottom; \" src=\"https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1\" alt=\"\"\u003e\u003cimg style=\"height:22px!important; margin-left:3px; vertical-align:text-bottom; \" src=\"https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1\" alt=\"\"\u003e\u003cimg style=\"height:22px!important; margin-left:3px; vertical-align:text-bottom; \" src=\"https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1\" alt=\"\"\u003e\u003c/a\u003e\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftempehs%2Fflask_pwa_api_extension_task_source","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftempehs%2Fflask_pwa_api_extension_task_source","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftempehs%2Fflask_pwa_api_extension_task_source/lists"}