From 6c04e1b034c2c29192b54fa5b0084aa815c63c82 Mon Sep 17 00:00:00 2001 From: Sondre Malerud <sondrmal@stud.ntnu.no> Date: Mon, 24 Apr 2023 13:27:09 +0200 Subject: [PATCH] Velg profil --- .env | 1 + cypress/e2e/example.cy.js | 8 -- cypress/e2e/login.cy.js | 16 +++ package-lock.json | 15 +++ package.json | 2 + src/App.vue | 13 -- src/assets/base.css | 13 -- src/components/icons/logo.png | Bin 0 -> 17006 bytes src/main.js | 6 +- src/router/index.js | 16 ++- src/stores/authStore.js | 32 +++++ src/stores/counter.js | 12 -- src/util/API.js | 106 ++++++++++++++++ src/views/AboutView.vue | 15 --- src/views/LoginView.vue | 114 ++++++++++++++++++ src/views/SelectProfileView.vue | 113 +++++++++++++++++ src/views/__tests__/LoginView.spec.js | 16 +++ src/views/__tests__/SelectProfileView.spec.js | 28 +++++ 18 files changed, 458 insertions(+), 68 deletions(-) create mode 100644 .env delete mode 100644 cypress/e2e/example.cy.js create mode 100644 cypress/e2e/login.cy.js create mode 100644 src/components/icons/logo.png create mode 100644 src/stores/authStore.js delete mode 100644 src/stores/counter.js create mode 100644 src/util/API.js delete mode 100644 src/views/AboutView.vue create mode 100644 src/views/LoginView.vue create mode 100644 src/views/SelectProfileView.vue create mode 100644 src/views/__tests__/LoginView.spec.js create mode 100644 src/views/__tests__/SelectProfileView.spec.js diff --git a/.env b/.env new file mode 100644 index 0000000..d6e1cfd --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_BACKEND_URL = "http://localhost:8080" \ No newline at end of file diff --git a/cypress/e2e/example.cy.js b/cypress/e2e/example.cy.js deleted file mode 100644 index 7a8c909..0000000 --- a/cypress/e2e/example.cy.js +++ /dev/null @@ -1,8 +0,0 @@ -// https://docs.cypress.io/api/introduction/api.html - -describe('My First Test', () => { - it('visits the app root url', () => { - cy.visit('/') - cy.contains('h1', 'You did it!') - }) -}) diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js new file mode 100644 index 0000000..50b8b97 --- /dev/null +++ b/cypress/e2e/login.cy.js @@ -0,0 +1,16 @@ +describe('Login fails with wrong credentials', () => { + it('passes', () => { + cy.visit('http://localhost:4173/login') + cy.get('#login-button').trigger('click') + cy.get('#error-message').contains("Kunne ikke logge inn!") + + cy.get('#email-input').type('en bruker som ikke finnes') + cy.get('#login-button').trigger('click') + cy.get('#error-message').contains("Kunne ikke logge inn!") + + cy.get('#password-input').type('hei') + cy.get('#login-button').trigger('click') + cy.get('#error-message').contains("Kunne ikke logge inn!") + }) + +}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 67f061c..c8c1d66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "jwt-decode": "^3.1.2", "pinia": "^2.0.28", + "pinia-plugin-persistedstate": "^3.1.0", "vue": "^3.2.45", "vue-router": "^4.1.6" }, @@ -2355,6 +2357,11 @@ "verror": "1.10.0" } }, + "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/lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -2815,6 +2822,14 @@ } } }, + "node_modules/pinia-plugin-persistedstate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.1.0.tgz", + "integrity": "sha512-8UN+vYMEPBdgNLwceY08mi5olI0wkYaEb8b6hD6xW7SnBRuPydWHlEhZvUWgNb/ibuf4PvufpvtS+dmhYjJQOw==", + "peerDependencies": { + "pinia": "^2.0.0" + } + }, "node_modules/pinia/node_modules/vue-demi": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.0.tgz", diff --git a/package.json b/package.json index 0839dc4..b392ca4 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress run --e2e'" }, "dependencies": { + "jwt-decode": "^3.1.2", "pinia": "^2.0.28", + "pinia-plugin-persistedstate": "^3.1.0", "vue": "^3.2.45", "vue-router": "^4.1.6" }, diff --git a/src/App.vue b/src/App.vue index e864195..e953209 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,19 +4,6 @@ import HelloWorld from './components/HelloWorld.vue' </script> <template> - <header> - <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> - - <div class="wrapper"> - <HelloWorld msg="You did it!" /> - - <nav> - <RouterLink to="/">Home</RouterLink> - <RouterLink to="/about">About</RouterLink> - </nav> - </div> - </header> - <RouterView /> </template> diff --git a/src/assets/base.css b/src/assets/base.css index 71dc55a..a0d4bbf 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -36,19 +36,6 @@ --section-gap: 160px; } -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--vt-c-black); - --color-background-soft: var(--vt-c-black-soft); - --color-background-mute: var(--vt-c-black-mute); - - --color-border: var(--vt-c-divider-dark-2); - --color-border-hover: var(--vt-c-divider-dark-1); - - --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); - } -} *, *::before, diff --git a/src/components/icons/logo.png b/src/components/icons/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..932a31c570979e3d4453e761d6e0a97f9b9938da GIT binary patch literal 17006 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 literal 0 HcmV?d00001 diff --git a/src/main.js b/src/main.js index 4fb24b7..26fcc4d 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' +import piniaPluginPersistedState from "pinia-plugin-persistedstate"; import App from './App.vue' import router from './router' @@ -8,7 +9,10 @@ import './assets/main.css' const app = createApp(App) -app.use(createPinia()) +const pinia = createPinia(); +pinia.use(piniaPluginPersistedState); + +app.use(pinia) app.use(router) app.mount('#app') diff --git a/src/router/index.js b/src/router/index.js index a49ae50..5d1e0df 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,5 +1,7 @@ import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' +import LoginView from '../views/LoginView.vue' +import SelectProfileView from '../views/SelectProfileView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -10,12 +12,14 @@ const router = createRouter({ 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('../views/AboutView.vue') + path: '/login', + name: 'login', + component: LoginView + }, + { + path: '/selectProfile', + name: "selectProfile", + component: SelectProfileView } ] }) diff --git a/src/stores/authStore.js b/src/stores/authStore.js new file mode 100644 index 0000000..79696ca --- /dev/null +++ b/src/stores/authStore.js @@ -0,0 +1,32 @@ +import { defineStore } from "pinia"; +export const useAuthStore = defineStore("auth", { + state: () => { + return { + token: "", + user: {}, + profile: {}, + }; + }, + persist: { + storage: localStorage + }, + getters: { + isLoggedIn() { + return this.token.length > 0 + } + }, + actions: { + setToken(token) { + this.token = token; + }, + setUser(user) { + this.user = user; + }, + logout() { + this.$reset(); + }, + setProfile(profile) { + this.profile = profile; + } + } +}); diff --git a/src/stores/counter.js b/src/stores/counter.js deleted file mode 100644 index b6757ba..0000000 --- a/src/stores/counter.js +++ /dev/null @@ -1,12 +0,0 @@ -import { ref, computed } from 'vue' -import { defineStore } from 'pinia' - -export const useCounterStore = defineStore('counter', () => { - const count = ref(0) - const doubleCount = computed(() => count.value * 2) - function increment() { - count.value++ - } - - return { count, doubleCount, increment } -}) diff --git a/src/util/API.js b/src/util/API.js new file mode 100644 index 0000000..b932e12 --- /dev/null +++ b/src/util/API.js @@ -0,0 +1,106 @@ +import axios from "axios"; +import { useAuthStore } from "@/stores/authStore.js"; +import jwt_decode from "jwt-decode"; +import router from "@/router/index"; + +export const API = { + + /** + * API method to send a login request. + * If login succeeds, the logged in User and their token + * is saved to the Pinia AuthStore + * + * @param email email address of the user to log in as + * @param password password to log in with + * @returns a Result with whether the login attempt succeeded + */ + login: async (request) => { + const authStore = useAuthStore(); + let token; + + return axios.post( + `${import.meta.env.VITE_BACKEND_URL}/login`, + request, + ) + .then(async (response) => { + token = response.data; + const id = (jwt_decode(token)).id; + + return API.getAccount(id, token) + .then((user) => { + authStore.setUser(user); + authStore.setToken(token); + return; + }) + .catch(() => { + throw new Error(); + }); + }) + .catch(() => { + throw new Error(); + }); + }, + + + /** + * API method to get a account by their ID + * @param id ID number of the account to retrieve + * @returns A promise that resolves to a User if the API call succeeds, + * or is rejected if the API call fails + */ + getAccount: async (id, token) => { + return axios.get(`${import.meta.env.VITE_BACKEND_URL}/account/${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((response) => { + return response.data; + }) + .catch(() => { + throw new Error("Account not found or not accessible"); + }); + }, + + // Sends the user into the home page logged in as the profile they clicked on + selectProfile: async (id) => { + const authStore = useAuthStore() + return axios.get(`${import.meta.env.VITE_BACKEND_URL}/profile/${id}`, { + headers: { Authorization: `Bearer ${authStore.token}` }, + }) + .then((response) => { + authStore.setProfile(response.data) + router.push("/") + + }) + .catch(() => { + throw new Error("Profile not found or not accessible") + }) + + + }, + + // Sends the user into the "register profile" view + addProfile: async () => { + console.log("todo"); + }, + + // Returns all profiles to the logged in user + getProfiles: async () => { + const authStore = useAuthStore(); + if (!authStore.isLoggedIn) { + throw new Error(); + } + + + return axios.get(import.meta.env.VITE_BACKEND_URL + '/profile', { + headers: { Authorization: "Bearer " + authStore.token }, + }, + ) + .then(response => { + + console.log(response.data) + return response.data + }).catch(() => { + throw new Error(); + }); + } +} diff --git a/src/views/AboutView.vue b/src/views/AboutView.vue deleted file mode 100644 index 756ad2a..0000000 --- a/src/views/AboutView.vue +++ /dev/null @@ -1,15 +0,0 @@ -<template> - <div class="about"> - <h1>This is an about page</h1> - </div> -</template> - -<style> -@media (min-width: 1024px) { - .about { - min-height: 100vh; - display: flex; - align-items: center; - } -} -</style> diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue new file mode 100644 index 0000000..1a6a64b --- /dev/null +++ b/src/views/LoginView.vue @@ -0,0 +1,114 @@ +<script> + import { API } from '@/util/API.js'; + import router from '@/router/index.js'; + + export default { + data() { + return { + welcomemsg: "Velkommen tilbake", + email: "", + password: "", + errormsg: "", + } + }, + methods: { + login() { + //todo: implement when API is up + API.login({email: this.email, password: this.password}).then(() => { + router.push("/selectProfile"); + }) + .catch(() => { + this.errormsg = "Kunne ikke logge inn! Sjekk brukernavn og passord, og prøv igjen"; + }); + } + } + } + +</script> + +<template> + <main> + <div class="login-container"> + <img id="logo" src="../components/icons/logo.png" alt="Logo"> + <h1>{{ welcomemsg }}</h1> + <form @submit.prevent="login"> + <div class="field-container"> + <label for="email">E-post</label> + <input id="email-input" name="email" type="text" v-model="email" /> + </div> + + <div class="field-container"> + <label for="password">Passord</label> + <input id="password-input" name="password" type="password" v-model="password" /> + </div> + + <p id="error-message">{{ errormsg }}</p> + <button @click="login" id="login-button">Logg inn</button> + </form> + + <p><RouterLink to="/newuser">Ny bruker</RouterLink> - <a href="#">Glemt passord?</a></p> + </div> + </main> +</template> + +<style> + +.login-container { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + min-width: 300px; + margin-top: 40px; +} + +form { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.field-container { + padding: 10px; + display: flex; + flex-direction: column; +} + +input { + height: 40px; + font-size: 16px; + padding-left: 10px; +} + +label { + font-size: 18px; +} + +#login-button { + background-color: #00663C; + color: #FFFFFF; + border-radius: 5px; + border-style: none; + width: 150px; + height: 40px; + font-size: 18px; + font-weight: bold; + margin: 20px; +} + +#logo { + width: 100px; + height: 100px; +} + +@media (min-width: 1024px) { + .login-container { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + } +} +</style> diff --git a/src/views/SelectProfileView.vue b/src/views/SelectProfileView.vue new file mode 100644 index 0000000..6dc0b9f --- /dev/null +++ b/src/views/SelectProfileView.vue @@ -0,0 +1,113 @@ +<script> + import { API } from '@/util/API.js'; + + export default { + data() { + return { + profiles: [] + } + }, + methods: { + // Sends the user into the home page logged in as the profile they clicked on + selectProfile(id) { + API.selectProfile(id); + }, + + // Sends the user into the "register profile" view + addProfile() { + API.addProfile(); + }, + + // Receives all profiles from this user + async getProfiles() { + await API.getProfiles() + .then(response => {this.profiles = response}) + .catch(() => new Error()); + } + }, + + mounted() { + this.getProfiles(); + } + } + +</script> + + +<template> + <div class="container"> + <h1>Hvem bruker appen?</h1> + + <div class="icons"> + <div v-for="profile in this.profiles" @click=selectProfile(profile.id) class="icon"> + + <img v-if="profile.profileImageUrl == ''" src="https://t4.ftcdn.net/jpg/02/15/84/43/360_F_215844325_ttX9YiIIyeaR7Ne6EaLLjMAmy4GvPC69.jpg" alt="profile image"> + <img v-else :src=profile.profileImageUrl alt="profile image"> + <p>{{profile.name}}</p> + </div> + + </div> + + <div class="add"> + <button @click="addProfile">+</button> + </div> + </div> +</template> + + +<style scoped> + + .container { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + gap: 20px; + min-width: 296px; + margin-top: 40px; + } + + .icons { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 20px; + max-width: 550px; + + } + + .icon { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + font-size: 20px; + } + + .icon:hover { + background-color: #d5d5d5; + border-radius: 10%; + } + + img { + height: 130px; + width: 130px; + border-radius: 50%; + } + + + button { + border-radius: 50%; + border-style: none; + width: 50px; + height: 50px; + font-size: 50px; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 10px; + } + +</style> \ No newline at end of file diff --git a/src/views/__tests__/LoginView.spec.js b/src/views/__tests__/LoginView.spec.js new file mode 100644 index 0000000..64e4c4d --- /dev/null +++ b/src/views/__tests__/LoginView.spec.js @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest' + +import { mount } from '@vue/test-utils' +import LoginView from '../LoginView.vue' + +describe('Login', () => { + it('renders properly', () => { + const wrapper = mount(LoginView) + expect(wrapper.text()).toContain('E-post') + }) + + it('login button exists', () => { + const wrapper = mount(LoginView) + wrapper.find('#login-button').exists() + }) +}) \ No newline at end of file diff --git a/src/views/__tests__/SelectProfileView.spec.js b/src/views/__tests__/SelectProfileView.spec.js new file mode 100644 index 0000000..eb41136 --- /dev/null +++ b/src/views/__tests__/SelectProfileView.spec.js @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest' + +import { mount } from '@vue/test-utils' +import SelectProfileView from '../SelectProfileView.vue' + +describe('Select profile', () => { + it('renders properly', () => { + const wrapper = mount(SelectProfileView) + expect(wrapper.text()).toContain('Hvem bruker appen?') + expect(wrapper.text()).toContain('+') + }) + + it('loads with one profile', () => { + const wrapper = mount(SelectProfileView, { + data() { + return { + profiles: [{ + id: -1, + name: "test", + profileImageUrl: "", + }] + } + } + }) + expect(wrapper.text()).toContain("test") + + }) +}) \ No newline at end of file -- GitLab