Skip to content
Snippets Groups Projects
Commit 79316272 authored by Ingrid Martinsheimen Egge's avatar Ingrid Martinsheimen Egge :cow2:
Browse files

merge with main

parents 745f12f8 51315409
No related branches found
No related tags found
1 merge request!21Merge profilinnstillinger into main
.env 0 → 100644
VITE_BACKEND_URL = "http://localhost:8080"
\ No newline at end of file
image: node:latest
image: cypress/base:latest
stages:
- build
......@@ -24,3 +24,8 @@ unit-test-job: # This job runs in the test stage.
stage: test # It only starts when the job in the build stage completes successfully.
script:
- npm run test:unit
e2e-test-job: # This job runs in the test stage.
stage: test # It only starts when the job in the build stage completes successfully.
script:
- npm run test:e2e:dev
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
This diff is collapsed.
......@@ -8,24 +8,20 @@
"preview": "vite preview",
"test:unit": "vitest --environment jsdom --root src/",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'"
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress run --e2e'"
},
"dependencies": {
"@iconify/iconify": "^3.1.0",
"async": "^3.2.4",
"jwt-decode": "^3.1.2",
"pinia": "^2.0.28",
"sass": "^1.62.0",
"pinia-plugin-persistedstate": "^3.1.0",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@iconify/vue": "^4.1.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/test-utils": "^2.2.6",
"cypress": "^12.10.0",
"cypress": "^12.0.2",
"jsdom": "^20.0.3",
"node-sass": "^8.0.0",
"sass-loader": "^13.2.2",
"start-server-and-test": "^1.15.2",
"vite": "^4.0.0",
"vitest": "^0.25.6"
......
......@@ -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,
......
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')
......@@ -3,6 +3,9 @@ import HomeView from "@/views/HomeView.vue";
import ProfileSettings from "@/views/ProfileSettings.vue";
import MissingPage from "@/views/MissingPage.vue";
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),
......@@ -13,6 +16,15 @@ const router = createRouter({
component: HomeView
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/selectProfile',
name: "selectProfile",
component: SelectProfileView
},
path: '/profileSettings',
name: 'profileSettings',
component: ProfileSettings
......
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;
}
}
});
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 }
})
import axios from "axios";
import { useAuthStore } from "@/stores/authStore.js";
import jwt_decode from "jwt-decode";
import router from "@/router/index";
export const API = {
/**
* Fetches all available foodpreference options
* 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
*/
getFoodpreferences: async () => {
return axios.get(`${import.meta.env.VITE_BACKEND_URL}/foodpreferences`)
.then((response) => {
return response.data;
}).catch(() => {
throw new Error();
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();
});
},
/**
*
* @param id id of the account to retrieve
* @returns {Promise<*>}
* Fetches all available foodpreference options
*/
getAccount: async (id) => {
return axios.get(`${import.meta.env.VITE_BACKEND_URL}/account/{id}`)
getFoodpreferences: async () => {
return axios.get(`${import.meta.env.VITE_BACKEND_URL}/foodpreferences`)
.then((response) => {
return response.data;
}).catch(() => {
......@@ -73,6 +162,4 @@ export const API = {
}
<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>
<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
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
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment