diff --git a/package-lock.json b/package-lock.json index b00e266c5d8bf882654b8e90bb2188ae5fe3869b..52e8f82391ad66170f4ffe771040b0cdab18290f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,13 +14,14 @@ "axios": "^0.26.1", "core-js": "^3.8.3", "cssom": "^0.5.0", + "jwt-decode": "^3.1.2", "roboto-fontface": "*", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuelidate": "^0.7.7", "vuex": "^4.0.0", "vuex-persistedstate": "^4.1.0", - "webfontloader": "^1.0.0" + "webfontloader": "^1.6.28" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -11057,6 +11058,11 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -24450,6 +24456,11 @@ "universalify": "^2.0.0" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", diff --git a/package.json b/package.json index cce55e6cd844586f7c09af08b82704f174640fc8..1a2ddb7600d61ce70a4bf73e48bccf3bfb399e50 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,14 @@ "axios": "^0.26.1", "core-js": "^3.8.3", "cssom": "^0.5.0", + "jwt-decode": "^3.1.2", "roboto-fontface": "*", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuelidate": "^0.7.7", "vuex": "^4.0.0", "vuex-persistedstate": "^4.1.0", - "webfontloader": "^1.0.0" + "webfontloader": "^1.6.28" }, "devDependencies": { "@babel/core": "^7.12.16", diff --git a/src/assets/defaultUserProfileImage.jpg b/src/assets/defaultUserProfileImage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18c8cd70555d36af38f2bf164b0a22740b14cf6b Binary files /dev/null and b/src/assets/defaultUserProfileImage.jpg differ diff --git a/src/components/LoginForm.vue b/src/components/LoginForm.vue index 4bdfe55bb97a36076a5fee4e4e3f0fd711a6502f..a986f45f67e18ee7834405e64a493da89cedc72e 100644 --- a/src/components/LoginForm.vue +++ b/src/components/LoginForm.vue @@ -102,6 +102,7 @@ import useVuelidate from "@vuelidate/core"; import { required, email, helpers } from "@vuelidate/validators"; import { doLogin } from "@/utils/apiutil"; +import { parseUserFromToken } from "@/utils/token-utils"; export default { name: "LoginForm.vue", @@ -165,6 +166,10 @@ export default { else { console.log("Something went wrong"); } + + let user = parseUserFromToken(); + let id = user.account_id; + this.$router.push("/profile/" + id); }, validate() { diff --git a/src/components/UserProfileComponents/LargeProfileCard.vue b/src/components/UserProfileComponents/LargeProfileCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..b27bd17390af37ac6aca878e982ee7b1e46e2c83 --- /dev/null +++ b/src/components/UserProfileComponents/LargeProfileCard.vue @@ -0,0 +1,144 @@ +<template> + <div + class="max-w-sm bg-white rounded-lg border border-gray-200 shadow-md dark:bg-gray-800 dark:border-gray-700" + > + <div v-show="isCurrentUser" class="flex justify-end px-4 pt-4"> + <button + id="dropdownDefault" + data-dropdown-toggle="dropdown" + @click="dropdown = !dropdown" + class="hidden sm:inline-block text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-1.5" + type="button" + > + <svg + class="w-6 h-6" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" + ></path> + </svg> + </button> + + <div + id="dropdown" + v-show="dropdown" + zindex="2" + class="z-10 w-44 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700" + > + <ul class="py-1" aria-labelledby="dropdownDefault"> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Mine gjenstander</router-link + > + </li> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Mine grupper + </router-link> + </li> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Leiehistorikk</router-link + > + </li> + <li> + <router-link + to="/newPassword" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Endre passord</router-link + > + </li> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Slett bruker</router-link + > + </li> + </ul> + </div> + </div> + <div class="flex flex-col items-center pb-10"> + <img + class="mb-3 w-24 h-24 rounded-full shadow-lg" + src="../../assets/defaultUserProfileImage.jpg" + alt="Profile picture" + /> + <h5 class="mb-1 text-xl font-medium text-gray-900 dark:text-white"> + {{ user.first_name }} {{ user.last_name }} + </h5> + <div> + <rating-component :rating="renterRating" :ratingType="'Leietaker'" /> + <rating-component :rating="ownerRating" :ratingType="'Utleier'" /> + </div> + + <div v-show="!isCurrentUser" class="flex mt-4 space-x-3 lg:mt-6"> + <a + href="#" + class="inline-flex items-center py-2 px-4 text-sm font-medium text-center text-gray-900 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700" + >Åpne chat</a + > + </div> + </div> + </div> +</template> + +<script> +import RatingComponent from "@/components/UserProfileComponents/RatingComponent.vue"; +import { parseUserFromToken } from "@/utils/token-utils"; +import { getUser /* getRenterRating, getOwnerRating */ } from "@/utils/apiutil"; +import router from "@/router"; + +export default { + name: "LargeProfileCard", + data() { + return { + user: {}, + currentUser: {}, + id: -1, + isCurrentUser: false, + renterRating: 0, //getRenterRating(this.userID), + ownerRating: 0, //getOwnerRating(this.userID), + dropdown: false, + }; + }, + components: { + RatingComponent, + }, + methods: { + async getUser() { + this.currentUser = parseUserFromToken(); + this.id = router.currentRoute.value.params.id; + if (this.id == this.currentUser.account_id) { + this.isCurrentUser = true; + this.user = this.currentUser; + return; + } + let getuser = await getUser(this.id); + this.user = { + account_id: getuser.userID, + first_name: getuser.firstName, + last_name: getuser.lastName, + }; + }, + getProfilePicture() { + /* if (this.user.picture != "") { + return this.user.picture; + } */ + return "../assets/defaultUserProfileImage.jpg"; + }, + }, + beforeMount() { + this.getUser(); + }, +}; +</script> diff --git a/src/components/UserProfileComponents/RatingComponent.vue b/src/components/UserProfileComponents/RatingComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..f79ff22085c106a75f32e0b8294c7ef3c02e9801 --- /dev/null +++ b/src/components/UserProfileComponents/RatingComponent.vue @@ -0,0 +1,49 @@ +<template> + <ul class="flex justify-center"> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ ratingType }}: + </p> + </li> + <li v-for="i in 5" :key="i"> + <svg + :class="getFill(i)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + </li> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ compRating }} out of 5 + </p> + </li> + </ul> +</template> + +<script> +export default { + name: "RatingComponent", + data() { + return { + compRating: this.rating + 0, + }; + }, + props: { + rating: Number, + ratingType: String, + }, + methods: { + getFill(i) { + if (i <= this.rating) { + return "w-5 h-5 text-yellow-400"; + } + return "w-5 h-5 text-gray-300 dark:text-gray-500"; + }, + }, +}; +</script> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..4da140e21610587c8e88953ed9b17572c9bef98a --- /dev/null +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -0,0 +1,40 @@ +<template> + <div + class="select-none cursor-pointer hover:bg-gray-50 flex flex-1 items-center p-4" + > + <div class="flex flex-col w-10 h-10 justify-center items-center mr-4"> + <router-link to=""> + <img alt="profil" :src="getProfilePicture" /> + </router-link> + </div> + <div class="flex-1 pl-1"> + <div class="font-medium dark:text-white"> + {{ user.first_name }} {{ user.last_name }} + </div> + </div> + <div class="flex flex-row justify-center"> + <button class="w-10 text-right flex justify-end">Åpne chat</button> + <button v-if="admin" class="w-10 text-right flex justify-end"> + Fjern bruker + </button> + </div> + </div> +</template> + +<script> +export default { + name: "UserListItem", + props: { + user: Object, + admin: Boolean, + }, + methods: { + getProfilePicture() { + if (this.user.picture != "") { + return this.user.picture; + } + return "../assets/defaultUserProfileImage.jpg"; + }, + }, +}; +</script> diff --git a/src/router/index.js b/src/router/index.js index cf3844a9c1fffdda6bd0fe3375a0057fc7bc7abf..284f32e9039d6c6cdd0cffed21ddf02be8106929 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,3 +1,4 @@ +import store from "@/store"; import { createRouter, createWebHistory } from "vue-router"; import HomeView from "../views/HomeView.vue"; import LoginView from "../views/LoginView.vue"; @@ -5,18 +6,22 @@ import NewPasswordView from "../views/NewPasswordView"; const routes = [ { - path: "/endre", //Endre før push + path: "/", //Endre før push name: "home", component: HomeView, }, { path: "/about", name: "about", - // route level code-splitting - // this generates a separate chunk (about.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => - import(/* webpackChunkName: "about" */ "../views/AboutView.vue"), + component: () => import("../views/AboutView.vue"), + }, + { + path: "/profile/:id", + name: "profile", + component: () => import("../views/ProfileView.vue"), + beforeEnter: () => { + if (store.state.user.token == null) router.push("login"); + }, }, { path: "/register", @@ -28,7 +33,7 @@ const routes = [ import(/* webpackChunkName: "register" */ "../views/RegisterView.vue"), }, { - path: "/", + path: "/login", name: "login", component: LoginView, }, diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index 16f9020665a454dcae8223a1c59b08bcfc2fc9fd..887d222831d94469e22f88bdef28c13672fb02db 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -1,4 +1,5 @@ import axios from "axios"; +import { tokenHeader } from "./token-utils"; const API_URL = process.env.VUE_APP_BASEURL; @@ -31,3 +32,42 @@ export function registerUser(registerInfo) { }) .catch((err) => console.log(err)); } + +export async function getUser(userid) { + return axios + .get(API_URL + "users/" + userid + "/profile", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +} + +export function getRenterRating(userid) { + return axios + .get(API_URL + "users/" + userid + "", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +} + +export function getOwnerRating(userid) { + return axios + .get(API_URL + "users/" + userid + "", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +} diff --git a/src/utils/token-utils.js b/src/utils/token-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..baf77654f57a548de5a335038ed9fed4628d80e2 --- /dev/null +++ b/src/utils/token-utils.js @@ -0,0 +1,12 @@ +import jwt_decode from "jwt-decode"; +import store from "@/store"; + +export function tokenHeader() { + let token = store.state.user.token; + return { Authorization: token }; +} + +export function parseUserFromToken() { + let token = store.state.user.token; + return jwt_decode(token); +} diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue new file mode 100644 index 0000000000000000000000000000000000000000..cfd2cefd902017ad7b867b7cfe447cce5648cf07 --- /dev/null +++ b/src/views/ProfileView.vue @@ -0,0 +1,14 @@ +<!-- View for looking at different profile display methods --> +<template> + <large-profile-card :isCurrentUser="true" /> +</template> + +<script> +import LargeProfileCard from "@/components/UserProfileComponents/LargeProfileCard.vue"; + +export default { + components: { + LargeProfileCard, + }, +}; +</script> diff --git a/tests/unit/apiutil-user-mock.spec.js b/tests/unit/apiutil-user-mock.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..73612f0f3579f5c04b99d2424a1a57fd62ff31bc --- /dev/null +++ b/tests/unit/apiutil-user-mock.spec.js @@ -0,0 +1,28 @@ +import { getUser } from "@/utils/apiutil"; +import axios from "axios"; + +jest.mock("axios"); + +describe("testing mocking of apiutil.js", () => { + it("check that existing user returns correctly", async () => { + const expectedResponse = { + response: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2NvdW50X2lkIjoiNiIsImV4cCI6MTY1MTEzMDU2NywiZmlyc3RfbmFtZSI6IkFsaWRhIiwiZW1haWwiOiJhbGlkYUB0ZXN0Lm5vIn0.Cp3_qfLhA55j5yaa1WPG97LNtvAZssxo0ROP3VIrHVs", + }; + axios.get.mockImplementation(() => + Promise.resolve({ data: expectedResponse }) + ); + + const userResponse = await getUser(1); + expect(userResponse).not.toEqual({ response: "User not found in DB" }); + }); + it("check that non-existing user returns 404", async () => { + const expectedResponse = { response: "User not found in DB" }; + axios.get.mockImplementation(() => + Promise.resolve({ data: expectedResponse }) + ); + + const userResponse = await getUser(100000); + expect(userResponse).toEqual(expectedResponse); + }); +});