Skip to content

06. Admin Rights

Update Profile Picture

This feature is not specific to any particular role, so we will add it to the DashCompo so that it will be available to all of the three dashboards.

Note Dealing with images requires understanding how SQLite stores images. It stores media data as BLOB, which is a binary large object. Note that storing images in the database can slow down queries, increase the size of the database file, and make backups more difficult.

Let's update the code.

js filename=static/components/DashCompo.js

const DashCompo = {
  name: "DashCompo",
  template: `
    <div>
      ...
      <main>
        <div class="sidebar bg-black">
          <div class="sidebar-icon mb-5">
            <!-- Add your icon or content here -->
            <span
              style="color: #343a40; font-size: 24px; display: inline-block; overflow: hidden; width: 100px; height: 100px; border-radius: 50%;">
              <img v-if="this.$store.state.authenticatedUser.image"
                :src="'data:image/jpeg;base64,' + this.$store.state.authenticatedUser.image" alt="Profile"
                style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">
              <i v-else class="fas fa-user" style="font-size: 100%;"></i>
            </span>
            <a class="pointer-on-hover" @click="change">change</a>
            <form v-if="picUpdate" @submit.prevent="updatePic" enctype="multipart/form-data">
              <label class="form-label" for="image">upload</label>
              <input class="form-control" type="file" id="image" @change="handleFileUpload" accept="image/*" required>
              <br>
              <input type="submit" class="btn btn-outline-secondary" value="update">
            </form>
          </div>
          <!-- Add any sidebar content here -->
        </div>
        <router-view></router-view>
      </main>
      ...
    </div>
      `,
  data() {
    return {
      picUpdate: false,
      profilePic: null
    };
  },
  methods: {
    change() {
      if (this.picUpdate == false) {
        this.picUpdate = true;
      }
    },
    async updatePic() {
      const formData = new FormData();
      formData.append("image", this.profilePic);
      try {
        const response = await fetch(
          "http://127.0.0.1:5000/update/profile/" +
            this.$store.state.authenticatedUser.id,
          {
            method: "PUT",
            headers: {
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
            body: formData,
          }
        );
        if (response.status === 201) {
          const data = await response.json();
          console.log(data.resource);
          this.$store.commit("setAuthenticatedUser", data.resource);
          this.picUpdate = false;
        } else {
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    }
  },
  mounted() {
    ...
  },
};
export default DashCompo;

The feature is now available for all the dashboards.

Note The code above uses the fetch API to send a request to the server to update the profile picture. If you are not familiar with the fetch API, you can learn about it here developer.mozilla.org/en-US/docs/Web/API/Fetch_API. Note that we cannot have an HTML form with direct submission when using the fetch API. Instead, we need to manually create a FormData object and send it in the request.

The list of things that you should know are here:

  • The handleFileUpload function will take the image file and store it in the profilePic variable. When the change button is clicked, the picUpdate variable will be set to true, which will display the form for updating the profile picture.

  • The current user information is stored in the Vuex store, allowing us to access it via this.$store.state.authenticatedUser. To update the user's information, such as after changing a profile picture, we can use this.$store.commit("setAuthenticatedUser", data.resource). This action will modify the store's state, triggering an automatic UI update to reflect the changes.

We need to store the user data and authentication token in the Vuex store. Let's update the code in the index.js file of the static/store folder to include the user state and a mutation to set it.

js filename=static/store/index.js

const store = new Vuex.Store({
    state: {
        authenticatedUser: "", // [tl! add]
    },
    getters: {
        // write your code here
    },
    mutations: {
        // write your code here
        setAuthenticatedUser: (state, user) => {
            // [tl! add:start]
            state.authenticatedUser = user;
        }, // [tl! add:end]
    },
    actions: {
        // write your code here
    },
});
export default store;

Note Vuex is a state management library for Vue.js. It helps you manage the state of your application in a predictable and maintainable way. The way Vuex organizes the code is a bit different from the way we use VueX in our Vue app. All the methods related to add, update, and delete are defined in the mutations section. For more information about Vuex, you can refer to the official documentationvuex.vuejs.org/guide/.

Backend Implementation

Now that the frontend implementation is complete, we can focus on implementing the backend logic.

python filename=app.py

...
# ---------------------- All operational endpoints --------------#
main = Blueprint('main', __name__)  # [tl! add:start]
@main.route('/update/profile/<int:id>', methods=['PUT'])
@jwt_required()
def update_profile(id):
    if isinstance(id, int):
        if request.method == 'PUT':
            user = User.query.filter_by(id=id).first()
            if user:
                # Handle file upload
                user.image = request.files['image'].read()
                db.session.commit()
                user_data = {
                    'id': user.id,
                    'role': user.role,
                    'email': user.email,
                    # Assuming image is stored as a base64-encoded string
                    'image': base64.b64encode(user.image).decode('utf-8')
                }
                return jsonify({'message': "User profile updated successfully\
                                in the database",
                                'resource': user_data}), 201
            else:
                return jsonify({'message': "Not found"}), 404
    else:
        return '', 400 # [tl! add:end]
...
app.register_blueprint(main)
...

Add Category

Now we're going to add the feature where the admin can add categories through the application. To add any new features, we'll need to update the following components of the application:

    1. Update the database in the database.py file.
    1. Create a new component or modify an existing component in the Vue application.
    1. Update Vue Router and/or Vuex if necessary.
    1. Create a new route in the app.py file.

Database Update

For storing the categories we will create a new table in the database. The table will have the following structure:

python filename=database.py

...
class Category(db.Model): # [tl! add:start]
    __tablename__ = 'category'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String, nullable=False) # [tl! add:end]

Create Category Component

This Create Category Component will allow the admin to submit the database from frontend to backend for adding a new category.

js filename=static/components/CreateCatCompo.js

const CreateCatCompo = {
  name: "CreateCatCompo",
  template: `
    <div class="row justify-content-center m-3 text-color-light">
      <div class="card bg-light" style="width: 18rem;">
        <div class="card-body">
        <div class="d-flex justify-content-end">
          <!-- Cross button to close the card -->
          <button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
        </div>
          <h5 class="card-title">Add Category</h5>
          <form @submit.prevent="addcategory">
            <div class="mb-3">
            <label for="name" class="form-label">Category Name</label>
            <input type="text" class="form-control" v-model="name" required>
              <div v-if="message" class="alert alert-warning">
                {{message}}
              </div>
            </div>
            <button type="submit" class="btn btn-outline-primary">Add</button>
          </form>
        </div>
      </div>
    </div>
  `,
  data() {
    return {
      name: "",
      message: "",
    };
  },
  methods: {
    closeCard() {
      if (this.$store.state.authenticatedUser.role === "admin") {
        if (this.$route.path != "/app/admin") {
          this.$router.push("/app/admin");
        }
      } else {
        if (this.$route.path != "/app/manager") {
          this.$router.push("/app/manager");
        }
      }
    },
    async addcategory() {
      try {
        const response = await fetch("http://127.0.0.1:5000/add/cat", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
          body: JSON.stringify({
            name: this.name,
          }),
        });
        if (response.status === 201) {
          const data = await response.json();
          console.log(data.resource);
          if (this.$store.state.authenticatedUser.role === "admin") {
            this.$store.commit("addCat", data.resource);
          } else {
            this.$store.commit("addNoti", data.resource);
          }
          this.closeCard();
        } else {
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    },
  },
};
export default CreateCatCompo;

We need to store the categories data in the Vuex store to make our application reactive. Now, let's create a list in Vuex's state to hold the categories data.

js filename=static/store/index.js

const store = new Vuex.Store({
  state: {
    authenticatedUser: "",
    categories: [] // [tl! add]
  },
  getters: {
    // write your code here
  },
  mutations: {
    // write your code here
    ...
    setCategories: (state, categories) => { // [tl! add:start]
      state.categories = categories;
    } // [tl! add:end]
  },
  actions: {
    // write your code here
  },
});
export default store;

We will now add the "Add Category" feature to both the admin and manager dashboards. Even though this feature is common to both users, its backend implementation will be slightly different. All CRUD operations initiated by a manager will require approval from an admin.

js filename=static/views/AdminApp.js

const AdminApp = {
  ...
  ...
    <nav class="navbar navbar-expand-lg navbar-dark bg-success">
      ...
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item">
                        <a class="nav-link pointer-on-hover" @click="home">Home</a>
                    </li>
                    <li class="nav-item"> <!-- [tl! add:start] -->
                        <a class="nav-link pointer-on-hover" @click="createCat">Add category</a>
                    </li>  <!-- [tl! add:end] -->
                </ul>
      ...
    </nav>
    <main>
    <div class="sidebar bg-black">
      ...
      <ul class="list-group">
          <li class="list-group-item" v-for="category in this.$store.state.categories" :key="category.id">
              <input @click="searchByCat(category.name, category.id)" class="form-check-input me-1 pointer-on-hover" type="radio" :name="category.name" :value="category.id" :id="category.id" :checked="checkedValue === category.id">
              <label class="form-check-label" :for="category.id"> {{ category.name }} </label>
              <a class="pointer-on-hover" @click="editCat(category.id)">edit</a>
          </li>
      </ul>
    </div>
    </main>
    ...
  methods: {
    ...
    createCat() { // [tl! add:start]
      if (this.$route.path != "/app/admin/cat/create") {
        this.$router.push("/app/admin/cat/create");
      }
    } // [tl! add:end]
  },
  mounted() { // [tl! add:start]
    this.$store.dispatch("fetchCategories");
  } // [tl! add:end]
}
export default AdminApp;

Note We use mounted() hook to fetch the categories from the backend. What happens during the mounted phase? During the mounted() phase, the following things have already happened:

    1. The component's template has been compiled and rendered.
    1. The component's data has been initialized.
    1. The component's watchers have been set up.
    1. The component's DOM element has been created and inserted into the DOM.

Backend Implementation

python filename=app.py

...
from role_auth import role_required # [tl! add]
...
@main.route('/add/cat', methods=['POST']) # [tl! add:start]
@jwt_required()
@role_required(['admin'])
def create():
    data = request.get_json()
    if data:
        if not Category.query.filter_by(name=data['name']).first():
              category = Category(name=data['name'])
              db.session.add(category)
              db.session.commit()
              return jsonify({
                  'message': f"Category {data['name']} created successfully",
                  'resource': {
                      'id': category.id,
                      'name': category.name
                  }
              }), 201
        abort(409, message="Resource already exists")
    else:
        abort(404, message="Not found") # [tl! add:end]
...

We are also implementing authorization by protecting this route with role verification. This ensures that only authorized users can access this route.

Update Category

Updating a category involves two operations: editing and deleting. Now that we have already created the table in the database, we can start by creating the edit component.

Edit Category Component

What we want to do is that whenever we click on the edit button, it should display a form to edit the category and all the fields should show the current data of the category.

js filename=static/components/EditCatCompo.js

const EditCatCompo = {
  name: "EditCatCompo",
  template: `
    <div class="row justify-content-center m-3 text-color-light">
      <div class="card bg-light" style="width: 18rem;">
        <div class="card-body">
        <div class="d-flex justify-content-end">
          <!-- Cross button to close the card -->
          <button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
        </div>
          <h5 class="card-title">Add Category</h5>
          <form @submit.prevent="updatecategory">
            <div class="mb-3">
            <label for="name" class="form-label">Category Name</label>
            <input type="text" class="form-control" v-model="name" required>
              <div v-if="message" class="alert alert-warning">
                {{message}}
              </div>
            </div>
            <div class="d-flex">
            <button type="submit" class="btn btn-outline-primary me-5">Update</button>
            </div>
            </form>
        </div>
      </div>
    </div>
  `,
  data() {
    return {
      name: "",
      message: "",
    };
  },
  methods: {
    closeCard() {
      if (this.$route.path != "/app/admin") {
        this.$router.push("/app/admin");
      }
    },
    async fetchcategory() {
      try {
        const response = await fetch(
          "http://127.0.0.1:5000/update/" + this.$route.params.id,
          {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          }
        );
        if (response.status === 200) {
          const data = await response.json();
          this.name = data.name;
        } else if (response.status === 404) {
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    },
    async updatecategory() {
      if (confirm("Are you sure?")) {
        try {
          const response = await fetch(
            "http://127.0.0.1:5000/update/" + this.$route.params.id,
            {
              method: "PUT",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${localStorage.getItem("token")}`,
              },
              body: JSON.stringify({
                name: this.name,
              }),
            }
          );
          if (response.status === 201) {
            const data = await response.json();
            console.log(data, "printed data");
            if (this.$store.state.authenticatedUser.role === "admin") {
              this.$store.commit("updateCategory", data.resource);
            } else {
              this.$store.commit("addNoti", data.resource);
            }
            this.closeCard();
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    },
  },
  mounted() {
    this.fetchcategory();
  },
};
export default EditCatCompo;

Note It is always a good practice to ask for user confirmation before performing any destructive actions such as updating or deleting data. This is what we are implementing using the confirm function. The confirm() function is part of the Window interface in the browser's Document Object Model (DOM). It shows a modal dialog with an optional message and two buttons: "OK" and "Cancel". We will use this function to ask the user for confirmation before updating or deleting any data. Additionally, from now on, we will have to submit the authorization token in the header whenever we make a request to the server.

Since we are storing all the data in the VueX store, so we will have to write mutation for updating the category in the static/store/index.js file.

js filename=static/store/index.js

...
mutations: {
  ...
  updateCategory: (state, updatedCategory) => {
    const index = state.categories.findIndex(
      (p) => p.id === updatedCategory.id
    );
    if (index !== -1) {
      // Replace the existing product with the updated one
      state.categories.splice(index, 1, updatedCategory);
    }
  }
...
}

Backend Implementation

python filename=app.py

...
@main.route('/update/<int:cat_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update(cat_id):
    if isinstance(cat_id, int):
        if request.method == 'GET':
            category = Category.query.filter_by(id=cat_id).first()
            if category:
                cat = {
                    'id': category.id,
                    'name': category.name,
                }
                return jsonify(cat)
            else:
                abort(404, message="Not found")
        if request.method == 'PUT':
            data = request.get_json()
            category = Category.query.filter_by(id=cat_id).first()
            category.name = data['name']
            db.session.commit()
            return jsonify({
                'message': f"Category {data['name']} update successfully\
                    in database",
                'resource': {
                    'id': category.id, 'name': category.name}}), 201
    else:
        return '', 400
...
  • Things to note here are that we are using @jwt_required() middleware to check for the authentication, and @role_required(['admin']) middleware to check for the authorization of the user. There are following things you should know:

    1. In route for such operations we require to pass the primary id (in this case, the id of the category) for fetching the correct row from the table. <int: cat_id>
    1. By default the route allow GET method if you want to allow other methods then you need to allow explicitly. methods=['GET', 'PUT', 'DELETE']
    1. The data which you submit from the frontend can access by the request object from flask. For example like this request.get_json().

Delete Category

Deleting is just to add delete button on the EditCatCompo itself.

Deleting UI

We will add the delete button on the EditCatCompo itself that's it.

js filename=static/components/EditCatCompo.js

const EditCatCompo = {
...
        <div class="d-flex">
        <button type="submit" class="btn btn-outline-primary me-5">Update</button>
        <a class="btn btn-outline-danger" @click="deletecategory">Delete</a> // [tl! add]
        </div>
...
  methods: {
    ...
    async deletecategory() { // [tl! add:start]
      if (confirm("Are you sure?")) {
        try {
          const response = await fetch(
            "http://127.0.0.1:5000/update/" + this.$route.params.id,
            {
              method: "DELETE",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${localStorage.getItem("token")}`,
              },
            }
          );
          if (response.status === 201) {
            const data = await response.json();
            console.log(data, "printed data");
            if (this.$store.state.authenticatedUser.role === "admin") {
              this.$store.commit("deleteCategory", data.resource.id);
            } else {
              this.$store.commit("addNoti", data.resource);
            }
            this.closeCard();
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    } // [tl! add:end]
  },
  mounted() {
    this.fetchcategory();
  },
};
export default EditCatCompo;

So on clicking this delete button make a delete request to the backend.

Backend Implementation

python filename=app.py

...
@main.route('/update/<int:cat_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update(cat_id):
    if isinstance(cat_id, int):
        ...
        if request.method == 'DELETE':
            category = Category.query.filter_by(id=cat_id).first()
            if category:
                products = Product.query.filter_by(
                    category_id=int(cat_id)).all()
                for product in products:
                    carts = Cart.query.filter_by(
                        product_id=product.id).all()
                    for cart in carts:
                        db.session.delete(cart)
                        db.session.commit()
                    db.session.delete(product)
                    db.session.commit()
                db.session.delete(category)
                db.session.commit()
                return jsonify(
                    {
                        'message': f"Category { category.name } Deleted\
                            successfully from database",
                        'resource': {
                            'id': category.id,
                            'name': category.name
                        }
                    }), 201
            return jsonify({'message': "Not found"}), 404
    else:
        return '', 400
...

Now admin can delete the category from the database.

Create Product

Now that we have implemented the CRUD operations for categories, we can follow a similar approach to implement the CRUD operations for products, orders, and other resources in the database.

Database Update

We will create a product table in the database to store the products. Each product belongs to a category, so we will create a one-to-many relationship between the product table and the category table. This means that one category can have multiple products.

python filename=database.py
...
class Category(db.Model):
    __tablename__ = 'category'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String, nullable=False)
    products = db.relationship('Product', backref='category', lazy=True) # [tl! add]


class Product(db.Model):
    __tablename__ = 'product'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    quantity = db.Column(db.Integer, nullable=False)
    avg_rate = db.Column(db.Integer)
    name = db.Column(db.String, nullable=False)
    description = db.Column(db.String(1000))
    manufacture = db.Column(db.DateTime, nullable=False)
    expiry = db.Column(db.DateTime, nullable=False)
    rpu = db.Column(db.Float, nullable=False)
    unit = db.Column(db.String, nullable=False)
    image = db.Column(db.BLOB, nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))

Create Product Component

Just as we created the category component, we will similarly create the product component.

js filename=static/components/CreateProCompo.js

const CreateProCompo = {
    template: `
  <div class="row justify-content-center m-3 text-color-light">
  <div class="card bg-light" style="width: 36rem;">
    <div class="card-body">
    <div class="d-flex justify-content-end">
      <button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
      </div>
        <h5 class="card-title">Add Product</h5>
        <form @submit.prevent="addProduct" enctype="multipart/form-data">
        <label class="form-label" for="name">Product Name:</label>
        <input class="form-control" v-model="product.name" type="text" id="name" name="name" required>
        <br>

        <label class="form-label" for="quantity">Quantity:</label>
        <input class="form-control" v-model="product.quantity" type="number" id="quantity" name="quantity" required>
        <br>

        <label class="form-label" for="manufacture">Manufacture Date:</label>
        <input class="form-control" v-model="product.manufacture" type="date" id="manufacture" name="manufacture" required>
        <br>

        <label class="form-label" for="expiry">Expiry Date:</label>
        <input class="form-control" v-model="product.expiry" type="date" id="expiry" name="expiry" required>
        <br>

        <label class="form-label" for="rpu">Rate Per Unit:</label>
        <input class="form-control" v-model="product.rpu" type="number" id="rpu" name="rpu" step="0.01" required>
        <br>

        <label class="form-label" for="description">description:</label>
        <textarea class="form-control" v-model="product.description" type="text" id="description" name="description" required></textarea>
        <br>
        <label class="form-label" for="Select Category">Select Category:</label>
        <select class="form-select" name="Select Category" id="Select Category"
            v-model="product.category_id" required>
            <option v-for="category in this.$store.state.categories" :key="category.id" :value="category.id">{{category.name}}</option>
        </select>
        <label class="form-label" for="unit">Unit:</label>
        <select class="form-select" name="Select Unit"
        v-model="product.unit" required>
            <option  value="l">l</option>
            <option  value="ml">ml</option>
            <option  value="g">g</option>
            <option  value="kg">kg</option>
            <option  value="m">m</option>
            <option  value="cm">cm</option>
            <option  value="inch">inch</option>
            <option  value="piece">piece</option>
            <option  value="dozen">dozen</option>
            </select>
        <br>

        <label class="form-label" for="image">Image:</label>
        <input class="form-control" type="file" id="image" @change="handleFileUpload" accept="image/*" required>
        <br>
        <input type="submit" class="btn btn-outline-primary" value="Add Product">
      </form>
    </div>
  </div>
</div>
    `,
    data() {
        return {
            product: {
                name: "",
                quantity: 0,
                manufacture: "",
                expiry: "",
                rpu: 0,
                unit: "",
                description: "",
                image: null,
                category_id: "",
            },
        };
    },
    methods: {
        closeCard() {
            if (this.$store.state.authenticatedUser.role === "admin") {
                if (this.$route.path != "/app/admin") {
                    this.$router.push("/app/admin");
                }
            } else {
                if (this.$route.path != "/app/manager") {
                    this.$router.push("/app/manager");
                }
            }
        },
        handleFileUpload(event) {
            this.product.image = event.target.files[0];
        },
        async addProduct() {
            const formData = new FormData();
            formData.append("name", this.product.name);
            formData.append("quantity", this.product.quantity);
            formData.append("manufacture", this.product.manufacture);
            formData.append("expiry", this.product.expiry);
            formData.append("rpu", this.product.rpu);
            formData.append("unit", this.product.unit);
            formData.append("description", this.product.description);
            formData.append("image", this.product.image);
            formData.append("category_id", this.product.category_id);
            try {
                const response = await fetch(
                    "http://127.0.0.1:5000/add/product",
                    {
                        method: "POST",
                        headers: {
                            "Content-type": "application/json",
                            Authorization: `Bearer ${localStorage.getItem(
                                "token"
                            )}`,
                        },
                        body: formData,
                    }
                );
                if (response.status === 201) {
                    const data = await response.json();
                    console.log(data.resource);
                    this.$store.commit("addProduct", data.resource);
                    this.closeCard();
                } else if (response.status === 409) {
                    const data = await response.json();
                    alert(data.message);
                } else {
                    alert(data.message);
                }
            } catch (error) {
                console.error(error);
            }
        },
    },
};
export default CreateProCompo;

Let's add the product list to store list of products in the vue store.


js filename=static/store/index.js
const store = new Vuex.Store({
  state: {
    authenticatedUser: "",
    categories: [],
    products: []// [tl! add]
  },
  getters: {
    // write your code here
  },
  mutations: {
    // write your code here
    ...
    setCategories: (state, categories) => {
      state.categories = categories;
    },
    addProduct: (state, newProduct) => {// [tl! add:start]
      state.products.push(newProduct);
    } // [tl! add:end]
  },
  actions: {
    // write your code here
  }
});
export default store;

Now, let's add the UI of the admin dashboard. We will create a component for listing products on the admin dashboard. Let's create a file ProductCompo.js in the static/components folder.


js filename=static/components/ProductCompo.js
const ProductCompo = {
  name: "ProductCompo",
  template: `
    <div class="container d-flex justify-content-center mt-2">
      <div class="row gap-2">
        <div v-for="item in this.$store.state.products" :key="item.id" class="card shadow p-3 mb-5 bg-body-tertiary rounded"
          style="width: 18rem">
          <img :src="'data:image/jpeg;base64,' + item.image" class="card-img-top img-thumbnail" :alt="item.name" />
          <div class="card-body">
            <h5 class="card-title">{{ item.name }}</h5>
            <p class="card-text">Price: &#x20B9; {{item.rpu}}</p>
            <p class="card-text">{{item.description}}</p>
            <button class="btn btn-dark" @click="editPro(item.id)">Edit</button>
          </div>
          <div v-if="item.avg_rate" class="card-footer">
            <i v-for="n in item.avg_rate" :key="n" class="fas fa-star"></i>
          </div>
        </div>
        <div v-if="this.$store.state.products.length == 0">
          <h5>No Products</h5>
        </div>
      </div>
    </div>
  `,
  methods: {
    editPro(id) {
      if (this.$route.path != "/app/admin/pro/edit/" + id) {
        this.$router.push("/app/admin/pro/edit/" + id);
      }
    },
  },
  mounted() {
    this.$store.dispatch("fetchProducts");
  },
};
export default ProductCompo;

Note Base64 is a group of binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation. The main use of Base64 is to:

  1. Encode binary data for transmission: Base64 encoding allows you to transmit binary data, such as images, audio, and video, over text-based protocols like HTTP, email, and JSON.
  2. Browser decodes the Base64 data: The browser decodes the Base64-encoded data using a built-in decoder. This process involves converting the Base64 characters back into their original binary format.

Now we need to add this component in the router file. We will also learn how to create nested routes.

js filename=static/router/index.js

...
import AdminApp from "../views/AdminApp.js"; // [tl! add:start]
import ManagerApp from "../views/ManagerApp.js";
import UserApp from "../views/UserApp.js";
import CreateCatCompo from "../components/CreateCatCompo.js";
import EditCatCompo from "../components/EditCatCompo.js";
import CreateProCompo from "../components/CreateProCompo.js";
import ProductCompo from "../components/ProductCompo.js"; // [tl! add:end]
const router = VueRouter.createRouter({
    history: VueRouter.createWebHistory(),
    routes: [
        ...
        {
          path: "/app/admin",
          component: AdminApp,
          children: [
            { path: "/app/admin", component: ProductCompo },
            { path: "/app/admin/cat/create", component: CreateCatCompo },
            { path: "/app/admin/cat/edit/:id", component: EditCatCompo },
            { path: "/app/admin/pro/create", component: CreateProCompo }
          ],
        },
        {
          path: "/app/manager",
          component: ManagerApp,
          children: [
            { path: "/app/manager", component: ProductCompo }
          ]
        },
        {
          path: "/app/user",
          component: UserApp,
          children: [
            { path: "/app/user", component: ProductUserCompo }
          ]
        }
    ]
});
export default router;

Above you can see I am using children to create nested routes. Also to fetch the products and categories, we will use the fetchProducts and fetchCategories actions in the store file.

js filename=static/store/index.js

const store = new Vuex.Store({
    ...
    actions: { // [tl! add:start]
        // write your code here
      async fetchCategories({ commit }) {
        try {
          const response = await fetch("http://127.0.0.1:5000/get/categories", {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          });
          if (response.status === 200) {
            const data = await response.json();
            console.log(data, "categories fetched");
            commit("setCategories", data);
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      },

      async fetchProducts({ commit }) {
        try {
          const response = await fetch("http://127.0.0.1:5000/get/products", {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          });
          if (response.status === 200) {
            const data = await response.json();
            console.log(data, "products fetched");
            commit("setProducts", data);
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      },
    }, // [tl! add:end]
});
export default store;

Backend Implementation

Now for creating product we will write the backend flask code.

python filename=app.py

...
@main.route('/add/product', methods=['POST'])
@jwt_required()
@role_required(['admin'])
def product():
    if request.method == 'POST':
        name_exist = Product.query.filter_by(name=request.form['name']).first()
        if name_exist:
            return jsonify({'message': "Resource already exists"}), 409
        else:
            name = request.form['name']
            quantity = int(request.form['quantity'])
            manufacture = datetime.strptime(
                request.form['manufacture'], '%Y-%m-%d')
            expiry = datetime.strptime(request.form['expiry'], '%Y-%m-%d')
            rpu = float(request.form['rpu'])
            category_id = float(request.form['category_id'])
            unit = request.form['unit']
            description = request.form['description']

            # Handle file upload
            image = request.files['image'].read()
            new_product = Product(
                name=name,
                quantity=quantity,
                manufacture=manufacture,
                expiry=expiry,
                description=description,
                rpu=rpu,
                unit=unit,
                image=image,
                category_id=category_id
            )
            prod_data = {
                'id': new_product.id,
                'quantity': new_product.quantity,
                'name': new_product.name,
                'manufacture': new_product.manufacture,
                'expiry': new_product.expiry,
                'description': new_product.description,
                'rpu': new_product.rpu,
                'unit': new_product.unit,
                # Assuming image is stored as a base64-encoded string
                'image': base64.b64encode(new_product.image
                                          ).decode('utf-8')
            }
            db.session.add(new_product)
            db.session.commit()
            return jsonify({
                'message': f"Product {request.form['name']} add\
                    successfully in the database",
                'resource': prod_data}), 201
...

Note You can see that we are using base64 for encoding the image data to store in the database.

Not only data we will also have to add add product button on navigation bar. So let's update the static/views/AdminApp.js file.

js filename=static/views/AdminApp.js

...
  <nav class="navbar navbar-expand-lg navbar-dark bg-success">
    ...
              <ul class="navbar-nav ms-auto">
                  ...
                  <li class="nav-item"> <!-- [tl! add:start] -->
                      <a class="nav-link pointer-on-hover" @click="createPro">Add product</a>
                  </li>  <!-- [tl! add:end] -->
              </ul>
    ...
  </nav>
...

Now, you can test out creating a product by running the app.

Edit Product Component

Now let's update the static/components/EditProCompo.js file.

js filename=static/components/EditProCompo.js

const EditProCompo = {
  name: "EditProCompo",
  template: `
  <div class="row justify-content-center m-3 text-color-light">
  <div class="card bg-light" style="width: 36rem;">
    <div class="card-body">
    <div class="d-flex justify-content-end">
      <button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
      </div>
        <h5 class="card-title">Update Product</h5>
        <form @submit.prevent="updateproduct" enctype="multipart/form-data">
        <label class="form-label" for="name">Product Name:</label>
        <input class="form-control" v-model="product.name" type="text" id="name" name="name" required>
        <br>

        <label class="form-label" for="quantity">Quantity:</label>
        <input class="form-control" v-model="product.quantity" type="number" id="quantity" name="quantity" required>
        <br>

        <label class="form-label" for="manufacture">Manufacture Date:</label>
        <input class="form-control" v-model="product.manufacture" type="date" id="manufacture" name="manufacture" required>
        <br>

        <label class="form-label" for="expiry">Expiry Date:</label>
        <input class="form-control" v-model="product.expiry" type="date" id="expiry" name="expiry" required>
        <br>

        <label class="form-label" for="description">description:</label>
        <textarea class="form-control" v-model="product.description" type="text" id="description" name="description" required></textarea>
        <br>

        <label class="form-label" for="rpu">Rate Per Unit:</label>
        <input class="form-control" v-model="product.rpu" type="number" id="rpu" name="rpu" step="0.01" required>
        <br>
        <label class="form-label" for="Select Category">Select Category:</label>
        <select class="form-select" name="Select Category"
            v-model="product.category_id" required>
            <option v-for="category in this.$store.state.categories" :key="category.id" :value="category.id">{{category.name}}</option>
        </select>
        <label class="form-label" for="unit">Unit:</label>
        <select class="form-select" name="Select Unit"
        v-model="product.unit" required>
            <option  value="l">l</option>
            <option  value="ml">ml</option>
            <option  value="g">g</option>
            <option  value="kg">kg</option>
            <option  value="m">m</option>
            <option  value="cm">cm</option>
            <option  value="inch">inch</option>
            <option  value="piece">piece</option>
            <option  value="dozen">dozen</option>
            </select>
        <br>

        <label class="form-label" for="image">Image:</label>
        <input class="form-control" type="file" id="image" @change="handleFileUpload" accept="image/*" required>
        <br>
        <div class="d-flex">
        <button type="submit" class="btn btn-outline-primary me-5">Update</button>
        </div>
      </form>
    </div>
  </div>
</div>
    `,
  data() {
    return {
      product: {
        name: "",
        quantity: 0,
        manufacture: "",
        expiry: "",
        rpu: 0,
        unit: "",
        description: "",
        image: null,
        category_id: "",
      },
    };
  },
  methods: {
    closeCard() {
      if (this.$route.path != "/app/admin") {
        this.$router.push("/app/admin");
      }
    },
    handleFileUpload(event) {
      this.product.image = event.target.files[0];
    },
    async fetchproduct() {
      try {
        const response = await fetch(
          "http://127.0.0.1:5000/update/product/" + this.$route.params.id,
          {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          }
        );
        if (response.status === 200) {
          const data = await response.json();
          this.product = data;
        } else if (response.status === 404) {
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    },
    async updateproduct() {
      if (confirm("Are you sure?")) {
        try {
          const formData = new FormData();
          formData.append("name", this.product.name);
          formData.append("quantity", this.product.quantity);
          formData.append("manufacture", this.product.manufacture);
          formData.append("expiry", this.product.expiry);
          formData.append("rpu", this.product.rpu);
          formData.append("unit", this.product.unit);
          formData.append("description", this.product.description);
          formData.append("image", this.product.image);
          formData.append("category_id", this.product.category_id);
          const response = await fetch(
            "http://127.0.0.1:5000/update/product/" + this.$route.params.id,
            {
              method: "PUT",
              headers: {
                "Content-type": "application/json",
                Authorization: `Bearer ${localStorage.getItem("token")}`,
              },
              body: formData,
            }
          );
          if (response.status === 201) {
            const data = await response.json();
            console.log(data, "printed data");
            if (this.$store.state.authenticatedUser.role === "admin") {
              this.$store.commit("updateProduct", data.resource);
            } else {
              this.$store.commit("addNoti", data.resource);
            }
            this.closeCard();
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    }
  },
  mounted() {
    this.fetchproduct();
  },
};
export default EditProCompo;

Note Notice that FormData is utilized to package and send data to the backend. This is essential when handling multiple data types in the request body. For more details about FormData, refer to the MDN Web Docsdeveloper.mozilla.org/en-US/docs/Web/API/FormData.

Also we will have to include this component in the vue router and vue store.

js filename=static/router/index.js

...
import EditProCompo from "../components/EditProCompo.js"; // [tl! add]
const router = VueRouter.createRouter({
  history: VueRouter.createWebHistory(),
  routes: [
    ...
    {
      path: "/app/admin",
      component: AdminApp,
      children: [
        ...
        { path: "/app/admin/pro/create", component: CreateProCompo },
        { path: "/app/admin/pro/edit/:id", component: EditProCompo } // [tl! add]
      ]
    },
    ...
  ]
});
export default router;

Backend Implementation

python filename=app.py

@main.route('/update/product/<int:prod_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update_product(prod_id):
    if isinstance(prod_id, int):
        if request.method == 'GET':
            product = Product.query.filter_by(id=prod_id).first()
            if product:
                prod_data = {
                    'id': product.id,
                    'quantity': product.quantity,
                    'name': product.name,
                    'manufacture': product.manufacture,
                    'description': product.description,
                    'expiry': product.expiry,
                    'rpu': product.rpu,
                    'unit': product.unit,
                    # Assuming image is stored as a base64-encoded string
                    'image': base64.b64encode(product.image).decode('utf-8')
                }
                return jsonify(prod_data), 200
            else:
                abort(404, message="Not found")
        elif request.method == 'PUT':
            product = Product.query.filter_by(id=prod_id).first()
            if product:
                product.name = request.form['name']
                product.quantity = int(request.form['quantity'])
                product.manufacture = datetime.strptime(
                    request.form['manufacture'], '%Y-%m-%d')
                product.expiry = datetime.strptime(
                    request.form['expiry'], '%Y-%m-%d')
                product.rpu = float(request.form['rpu'])
                product.category_id = float(request.form['category_id'])
                product.unit = request.form['unit']
                product.description = request.form['description']

                # Handle file upload
                product.image = request.files['image'].read()
                db.session.commit()
                prod_data = {
                    'id': product.id,
                    'quantity': product.quantity,
                    'name': product.name,
                    'manufacture': product.manufacture,
                    'expiry': product.expiry,
                    'description': product.description,
                    'rpu': product.rpu,
                    'unit': product.unit,
                    # Assuming image is stored as a base64-encoded string
                    'image': base64.b64encode(
                        product.image).decode('utf-8')
                }
                return jsonify({
                    'message': f"Product {request.form['name']} updated\
                        successfully in the database",
                    'resource': prod_data}), 201
            else:
                return jsonify({'message': "Not found"}), 404
    else:
        return '', 400

Note When working with dates and times, you have two options: you can either store them as datetime objects in the database, or you can store them as strings. Storing them as datetime objects allows you to perform date and time arithmetic directly on the database values, while storing them as strings requires converting them to datetime objects first.

Next, we will add the feature to manage managers by the admin. The admin can approve/hire a manager or dismiss/remove a manager.

Delete Product Component

We need to add the delete button on the EditProCompo.

js filename=static/components/EditProCompo.js

const EditProCompo = {
  name: "EditProCompo",
  template: `
  <div class="row justify-content-center m-3 text-color-light">
  <div class="card bg-light" style="width: 36rem;">
    <div class="card-body">
    <div class="d-flex justify-content-end">
      <button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
      </div>
        <h5 class="card-title">Update Product</h5>
        <form @submit.prevent="updateproduct" enctype="multipart/form-data">
        ...
        <div class="d-flex">
        ...
        <a class="btn btn-outline-danger" @click="deleteproduct">Delete</a> <!-- [tl! add] -->
        </div>
      </form>
    </div>
  </div>
</div>
    `,
  ...
  methods: {
    ...
    , // [tl! add:start]
async deleteproduct() {
      if (confirm("Are you sure?")) {
        try {
          const response = await fetch(
            "http://127.0.0.1:5000/update/product/" + this.$route.params.id,
            {
              method: "DELETE",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${localStorage.getItem("token")}`,
              },
            }
          );
          if (response.status === 201) {
            const data = await response.json();
            console.log(data, "printed data");
            if (this.$store.state.authenticatedUser.role === "admin") {
              this.$store.commit("deleteProduct", data.resource.id);
              alert(data.message);
            } else {
              this.$store.commit("addNoti", data.resource);
              alert(data.message);
            }
            this.closeCard();
          } else {
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    } // [tl! add:end]
  },
  mounted() {
    this.fetchproduct();
  },
};
export default EditProCompo;

Now we can Delete the product.

Backend Implementation

python filename=app.py

@main.route('/update/product/<int:prod_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update_product(prod_id):
    if isinstance(prod_id, int):
        ...
        elif request.method == 'DELETE':
            product = Product.query.filter_by(id=prod_id).first()
                carts = Cart.query.filter_by(product_id=product.id).all()
                for cart in carts:
                    db.session.delete(cart)
                    db.session.commit()
                db.session.delete(product)
                db.session.commit()
                return jsonify(
                    {'message': f"Product {product.name} deleted\
                      successfully from the database",
                      'resource': prod_id}), 201
            return jsonify({'message': "Not found"}), 404
    else:
        return '', 400

Manage Managers

Managers will have access of CRUD operations to manage category/products.

We don't need to create tables or anything related to manager users because the user table already has a role attribute that defines the role of a user. Therefore, we can directly jump to creating the Managers component.

Managers Component

js filename=static/components/ManagersCompo.js

const ManagersCompo = {
  name: "ManagersCompo",
  template: `
  <div class="container">
    <div class="row">
        <!-- Repeat the following structure for each manager -->
        <div class="col-md-4">
            <div v-for="item in this.$store.state.managers" :key="item.id" class="manager-profile">
                <!-- Profile Icon -->
                <img :src="'data:image/jpeg;base64,' + item.image" alt="Manager Profile" class="profile-icon">

                <!-- Basic Info -->
                <div class="basic-info">
                    <p>{{ item.email }}</p>
                    <p>Date of Joining: {{ item.doj }}</p>
                    <p>Years of Service: {{ item.exp }}</p>
                </div>
                <!-- Action Buttons -->
                <div class="action-buttons">
                    <button class="btn btn-danger" @click="deletemanager(item.id)">Delete</button>
                    <button class="btn btn-warning" @click="warn">Send Warning</button>
                </div>
            </div>
            <div v-if="this.$store.state.managers.length==0">
                <h5>No Managers Found</h5>
            </div>
        </div>
    </div>
  </div>
    `,
  methods: {
    warn() {
      if (this.$route.path != "/app/admin/warning") {
        this.$router.push("/app/admin/warning");
      }
    },
    async deletemanager(id) {
      if (confirm("Are you sure?")) {
        try {
          const response = await fetch(
            "http://127.0.0.1:5000/delete/man/" + id,
            {
              method: "DELETE",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${localStorage.getItem("token")}`,
              },
            }
          );
          if (response.status === 200) {
            const data = await response.json();
            console.log(data, "printed data");
            this.$store.commit("deleteManager", data.resource);
            alert(data.message);
          } else {
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    },
  },
  mounted() {
    this.$store.dispatch("fetchManagers");
  },
};
export default ManagersCompo;

Admin have these features:

    1. Approve the requested of new manager.
    1. See all the managers at one page.
    1. Send Warning to the Managers.
    1. Fire/Delete a manager.

Above we have added a route to redirect to wanring page. We will add a view for warning page.

Let's also add fetchManagers action to fetch all the managers. Let's write code in the static/store/index.js file.

js filename=static/store/index.js

const store = new Vuex.Store({
    state: {
      products: [],
      managers: [] // [tl! add]
    },
    mutations: {
      ...
    setManagers: (state, managers) => {
      state.managers = managers;
    }
    }
    actions: {
        // write your code here
      ...
      async fetchManagers({ commit }) {
        try {
          const response = await fetch("http://127.0.0.1:5000/get/all/managers", {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          });
          if (response.status === 200) {
            const data = await response.json();
            console.log(data, "categories fetched");
            commit("setManagers", data.resource);
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    }
});
export default store;

Warning Page

Let's create the send warning feature for admin dashboard.

js filename=static/components/WarningCompo.js

const sendWarning = {
  name: "sendWarning",
  template: `
  <div class="row justify-content-center m-3 text-color-light">
  <div class="card bg-light" style="width: 36rem;">
    <div class="card-body">
    <div class="d-flex justify-content-end">
      <button type="button" class="btn-close" aria-label="Close" @click="closeCard"></button>
      </div>
        <h5 class="card-title">Send Warning</h5>
        <form @submit.prevent="sendWarning" enctype="multipart/form-data">
        <label class="form-label" for="message">message:</label>
        <textarea class="form-control" v-model="message" type="text" id="message" name="message" required></textarea>
        <br>
        <label class="form-label" for="Select Manager">Select Manager:</label>
        <select class="form-select" name="Select Category" id="Select Manager"
            v-model="managers.email" required>
            <option v-for="manager in managers" :key="manager.id" :value="manager.email">{{manager.email}}</option>
        </select>
        <br>
        <input type="submit" class="btn btn-outline-primary" value="Send">
      </form>
    </div>
  </div>
</div>
    `,
  data() {
    return {
      managers: {
        id: "",
        name: "",
        email: "",
      },
      message: "",
    };
  },
  methods: {
    closeCard() {
      if (this.$route.path != "/app/admin") {
        this.$router.push("/app/admin");
      }
    },
    async fetchManagers() {
      try {
        const response = await fetch("http://127.0.0.1:5000/send/alert", {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
        });
        if (response.status === 200) {
          const data = await response.json();
          console.log(data, "managers alerts");
          this.managers = data;
          console.log(this.managers, "printed managers");
        } else if (response.status === 404) {
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    },
    async sendWarning() {
      if (confirm("Are you sure?")) {
        try {
          const response = await fetch("http://127.0.0.1:5000/send/alert", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
            body: JSON.stringify({
              id: this.managers.id,
              message: this.message,
              email: this.managers.email,
            }),
          });
          if (response.status === 200) {
            const data = await response.json();
            console.log(data, "printed data");
            alert(data.message);
            this.closeCard();
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      }
    },
  },
  mounted() {
    this.fetchManagers();
  },
};
export default sendWarning;

The fetchManagers() will have us to fetch the managers data from the backend. The sendWarning() will have us to send the warning to the manager.

Backend Implementation

I will keep admin major operations in a separate file withing a file backendjobs/admin.py.

python filename=static/backendjobs/admin.py

...
from flask import current_app as app

admin_bp = Blueprint('admin_bp', __name__)


@admin_bp.route('/send/alert', methods=['GET', 'POST'])
@jwt_required()
@role_required(['admin'])
def send_alert():
    if request.method == 'GET':
        managers = User.query.filter_by(role='manager').all()
        man_list = []
        for man in managers:
            man_data = {
                'id': man.id,
                'name': man.name,
                'email': man.email,
            }
            man_list.append(man_data)
        return jsonify(man_list), 200
    if request.method == 'POST':
        # will implement the medium to send the alert
        # later.
        return jsonify({'message': "sent"}), 200

I have implemented the logic to send alert, but I want to send the alert via mail which I will implement later.

Backend Implementation

python filename=static/backendjobs/admin.py

...
from flask import current_app as app

admin_bp = Blueprint('admin_bp', __name__)


@admin_bp.route('/delete/man/<int:id>', methods=['DELETE'])
@jwt_required()
@role_required(['admin'])
def delete_man(id):
    man = User.query.filter_by(id=id).first()
    if man:
        db.session.delete(man)
        db.session.commit()
        return jsonify({'message': 'Deleted manager', 'resource': id}), 200
    else:
        return jsonify({'message': 'Not found'}), 404

Logout

We have already implemented the backend code just need to add the logout method in the frontend.

js filename=static/views/AdminApp.js

...
  <nav class="navbar navbar-expand-lg navbar-dark bg-success">
    ...
              <ul class="navbar-nav ms-auto">
                  ...
                  <li class="nav-item"> <!-- [tl! add:start] -->
                      <a class="nav-link pointer-on-hover" @click="logout">logout</a>
                  </li>  <!-- [tl! add:end] -->
              </ul>
    ...
  </nav>
...
methods: {
  ...
  async logout() {
      try {
        const response = await fetch("http://127.0.0.1:5000/logout", {
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
        });
        if (response.status === 200) {
          const data = await response.json();
          this.$store.commit("setAuthenticatedUser", "");
          localStorage.removeItem("token");
          if (this.$route.path != "/app") {
            this.$router.push("/app");
          }
        } else {
          const data = await response.json();
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    }
}

Testing it out

Now we can start the app and test all the feature implemented on this page.

Run the app.

python3 app.py

Commit the changes to the local git.

git add .
git commit -m "added admin features"

Continue to start creating Chirps...