Skip to content
Snippets Groups Projects
Commit f37c675d authored by Harry Linrui XU's avatar Harry Linrui XU
Browse files

merge: merge with dev

parents bc5cb85f a80afdca
No related branches found
No related tags found
3 merge requests!66Final merge,!49Create spare component,!4Pipeline fix
Pipeline #282827 failed
Showing
with 547 additions and 444 deletions
/*import { useUserStore } from '../../src/stores/userStore'
describe('Goals and Challenges Page Load', () => { describe('Goals and Challenges Page Load', () => {
let userStore;
beforeEach(() => { beforeEach(() => {
// Add console log to trace API calls // Add console log to trace API calls
cy.on('window:before:load', (win) => { cy.on('window:before:load', (win) => {
cy.spy(win.console, 'log'); cy.spy(win.console, 'log');
}); });
cy.window().then((win) => {
win.sessionStorage.setItem('accessToken', 'validAccessToken');
win.localStorage.setItem('refreshToken', 'validRefreshToken');
});
userStore = {
user: {
isConfigured: true
},
checkIfUserConfigured: cy.stub().resolves(),
};
cy.stub(window, useUserStore()).returns(userStore);
// Mock the API responses that are called on component mount // Mock the API responses that are called on component mount
cy.intercept('GET', '/goals', { cy.intercept('GET', '/goals', {
statusCode: 200, statusCode: 200,
...@@ -115,3 +133,4 @@ describe('Goals and Challenges Page Load', () => { ...@@ -115,3 +133,4 @@ describe('Goals and Challenges Page Load', () => {
}); });
}); });
*/
\ No newline at end of file
...@@ -10,7 +10,7 @@ const showNavBar = computed(() => { ...@@ -10,7 +10,7 @@ const showNavBar = computed(() => {
return !( return !(
route.path == '/' || route.path == '/' ||
route.path == '/registrer' || route.path == '/registrer' ||
route.path == '/logginn' || route.path.startsWith('/logginn') ||
route.path == '/forgotPassword' || route.path == '/forgotPassword' ||
route.path.startsWith('/konfigurasjon') route.path.startsWith('/konfigurasjon')
) )
......
...@@ -30,12 +30,6 @@ ...@@ -30,12 +30,6 @@
--section-gap: 160px; --section-gap: 160px;
} }
@media (prefers-color-scheme: dark) {
:root {
/* TODO: add dark mode colors */
}
}
*, *,
*::before, *::before,
*::after { *::after {
......
...@@ -25,20 +25,25 @@ ...@@ -25,20 +25,25 @@
import { defineProps, ref } from 'vue' import { defineProps, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
interface Props { const props = defineProps({
buttonText: string buttonText: String,
type: 'goal' | 'challenge' type: String,
} showModal: Boolean
})
const emit = defineEmits(['update:showModal'])
const router = useRouter() const router = useRouter()
const props = defineProps<Props>()
const btnText = ref(props.buttonText) const btnText = ref(props.buttonText)
const routeToGoalOrChallenge = () => { const routeToGoalOrChallenge = () => {
if (props.type === 'goal') { if (props.type === 'goal') {
router.push('/sparemaal') router.push('/sparemaal')
} else { } else if (props.type === 'challenge') {
router.push('/spareutfordringer') router.push('/spareutfordringer')
} else if (props.type === 'generatedChallenge') {
emit('update:showModal', true)
} }
} }
</script> </script>
File moved
...@@ -13,7 +13,14 @@ ...@@ -13,7 +13,14 @@
import { defineEmits, defineProps } from 'vue' import { defineEmits, defineProps } from 'vue'
const props = defineProps({ const props = defineProps({
disabled: Boolean disabled: {
type: Boolean,
default: false
},
text: {
type: String,
default: 'Fortsett'
}
}) })
const emit = defineEmits(['click']) const emit = defineEmits(['click'])
......
...@@ -23,10 +23,6 @@ const submitForm = () => { ...@@ -23,10 +23,6 @@ const submitForm = () => {
userStore.login(username.value, password.value) userStore.login(username.value, password.value)
} }
const bioLogin = () => {
userStore.bioLogin(username.value)
}
const toggleShowPassword = () => { const toggleShowPassword = () => {
showPassword.value = !showPassword.value showPassword.value = !showPassword.value
} }
...@@ -121,7 +117,6 @@ watch( ...@@ -121,7 +117,6 @@ watch(
Logg inn Logg inn
</button> </button>
<p>{{ errorMessage }}</p> <p>{{ errorMessage }}</p>
<button @click="bioLogin">biologin</button>
</div> </div>
</div> </div>
<modal-component <modal-component
......
...@@ -15,7 +15,7 @@ const errorMessage = ref<string>('') ...@@ -15,7 +15,7 @@ const errorMessage = ref<string>('')
const userStore = useUserStore() const userStore = useUserStore()
const nameRegex = /^[æÆøØåÅa-zA-Z,.'-][æÆøØåÅa-zA-Z ,.'-]{1,29}$/ const nameRegex = /^[æÆøØåÅa-zA-Z,.'-][æÆøØåÅa-zA-Z ,.'-]{0,29}$/
const emailRegex = const emailRegex =
/^[æÆøØåÅa-zA-Z0-9_+&*-]+(?:\.[æÆøØåÅa-zA-Z0-9_+&*-]+)*@(?:[æÆøØåÅa-zA-Z0-9-]+\.)+[æÆøØåÅa-zA-Z]{2,7}$/ /^[æÆøØåÅa-zA-Z0-9_+&*-]+(?:\.[æÆøØåÅa-zA-Z0-9_+&*-]+)*@(?:[æÆøØåÅa-zA-Z0-9-]+\.)+[æÆøØåÅa-zA-Z]{2,7}$/
const usernameRegex = /^[ÆØÅæøåA-Za-z][æÆøØåÅA-Za-z0-9_]{2,29}$/ const usernameRegex = /^[ÆØÅæøåA-Za-z][æÆøØåÅA-Za-z0-9_]{2,29}$/
......
<template>
<div
v-if="showModal"
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div class="relative bg-white pt-10 p-4 rounded-lg shadow-xl" style="width: 40rem">
<button @click="closeModal" class="absolute top-0 right-0 m-2 text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<div v-if="generatedChallenges.length > 0">
<div class="text-center font-bold text-3xl mb-4 mt-2">
Personlig tilpassede spareutfordringer:
</div>
<div class="grid grid-cols-7 sm:grid-cols-11 gap-2 p-3 pb-1 border-b-2">
<span class="font-bold col-span-2 md:col-span-3 sm:text-lg pt-1 mb-0"
>Tittel</span
>
<span class="font-bold col-span-2 md:col-span-2 sm:text-lg pt-1 mb-0"
>Målsum</span
>
<span
class="font-bold col-span-2 md:col-span-1 sm:text-lg pt-1 pr-1 md:pr-3 mb-0"
>Frist</span
>
<span class="col-span-2"></span>
</div>
<div class="space-y-2">
<div
v-for="(challenge, index) in generatedChallenges"
:key="index"
:class="{ 'bg-gray-100': index % 2 === 0 }"
class="grid grid-cols-7 md:grid-cols-7 sm:grid-cols-2 lg:grid-cols-7 gap-4 items-center border p-3 rounded mt-[-8px]"
>
<span class="break-words col-span-2 md:col-span-1 lg:col-span-2 text-lg">{{
challenge.title
}}</span>
<span class="col-span-2 md:col-span-2 lg:col-span-1 text-lg">{{
challenge.target
}}</span>
<span class="col-span-2 md:col-span-1 lg:col-span-2 text-lg">{{
challenge.due
}}</span>
<div
class="col-span-7 sm:col-start-3 sm:col-span-2 md:col-span-2 lg:col-span-2 flex items-center justify-end space-x-2"
>
<span v-if="challenge.isAccepted" class="font-bold text-lg"
>Godtatt!</span
>
<button
@click="acceptChallenge(challenge)"
class="text-white font-bold py-1 px-4 mt-[-14px] sm:mt-0"
>
Godta
</button>
</div>
</div>
</div>
</div>
<div v-else class="text-center text-2xl font-bold mt-1">
Ingen nye spareutfordringer enda ... sjekk igjen senere!
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import authInterceptor from '@/services/authInterceptor'
import type { AxiosResponse } from 'axios'
interface Challenge {
title: string
target: number
due: string
dueFull: string
isAccepted: boolean
perPurchase?: number
description?: string
type?: string
}
const showModal = ref(true)
const generatedChallenges = reactive<Challenge[]>([])
async function fetchGeneratedChallenges() {
try {
const response: AxiosResponse = await authInterceptor.get('/challenges/generate')
if (response.status === 200) {
generatedChallenges.splice(
0,
generatedChallenges.length,
...response.data.map((ch: any) => ({
...ch,
due: new Date(ch.due).toISOString().split('T')[0],
dueFull: ch.due,
isAccepted: false
}))
)
} else {
generatedChallenges.splice(0, generatedChallenges.length)
}
} catch (error) {
console.error('Error fetching challenges:', error)
}
}
onMounted(() => {
fetchGeneratedChallenges()
localStorage.setItem('lastModalShow', Date.now().toString())
})
function acceptChallenge(challenge: Challenge) {
if (!challenge) {
console.error('No challenge data provided to acceptChallenge function.')
return
}
const postData = {
title: challenge.title,
saved: 0,
target: challenge.target,
perPurchase: challenge.perPurchase,
description: challenge.description,
due: challenge.dueFull,
type: challenge.type
}
authInterceptor
.post('/challenges', postData)
.then((response: AxiosResponse) => {
challenge.isAccepted = true
})
.catch((error) => {
console.error('Failed to save challenge:', error)
})
}
const closeModal = () => {
showModal.value = false
}
</script>
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</router-link> </router-link>
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<ButtonDisplayStreak></ButtonDisplayStreak> <ButtonDisplayStreak />
</div> </div>
</div> </div>
<div v-if="!isHamburger" class="flex flex-row gap-10"> <div v-if="!isHamburger" class="flex flex-row gap-10">
......
import { mount, VueWrapper } from '@vue/test-utils'
import NavBar from '@/components/NavBarComponent.vue'
import router from '@/router'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
vi.stubGlobal('scrollTo', vi.fn())
// Mocking Axios correctly using `importOriginal`
const mocks = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn()
}))
vi.mock('axios', async (importActual) => {
const actual = await importActual<typeof import('axios')>()
return {
default: {
...actual.default,
create: vi.fn(() => ({
...actual.default.create(),
get: mocks.get,
post: mocks.post
}))
}
}
})
describe('NavBar Routing', () => {
let wrapper: VueWrapper<any>
beforeEach(async () => {
const pinia = createPinia()
setActivePinia(pinia)
wrapper = mount(NavBar, {
global: {
plugins: [router, pinia]
}
})
await router.push('/')
await router.isReady()
await nextTick()
})
it('renders without errors', () => {
expect(wrapper.exists()).toBe(true)
})
it('displays correct active route for home link on full screen', async () => {
global.innerWidth = 1200
await router.push('/hjem')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for goals link on full screen', async () => {
global.innerWidth = 1200
await router.push('/sparemaal')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for challenges link on full screen', async () => {
global.innerWidth = 1200
await router.push('/spareutfordringer')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for profile link on full screen', async () => {
global.innerWidth = 1200
await router.push('/profil')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for home link when the hamburger menu is open', async () => {
global.innerWidth = 1000
wrapper.vm.hamburgerOpen = true
await wrapper.vm.$nextTick()
await router.push('/hjem')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for goals link when the hamburger menu is open', async () => {
global.innerWidth = 1000
wrapper.vm.hamburgerOpen = true
await wrapper.vm.$nextTick()
await router.push('/sparemaal')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for challenges link when the hamburger menu is open', async () => {
global.innerWidth = 1000
wrapper.vm.hamburgerOpen = true
await wrapper.vm.$nextTick()
await router.push('/spareutfordringer')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
it('displays correct active route for profile link when the hamburger menu is open', async () => {
global.innerWidth = 1000
wrapper.vm.hamburgerOpen = true
await wrapper.vm.$nextTick()
await router.push('/profil')
await router.isReady()
expect(wrapper.find('.router-link-exact-active').exists()).toBe(true)
})
})
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/userStore'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
...@@ -18,6 +19,11 @@ const router = createRouter({ ...@@ -18,6 +19,11 @@ const router = createRouter({
name: 'login', name: 'login',
component: () => import('@/views/RegisterLoginView.vue') component: () => import('@/views/RegisterLoginView.vue')
}, },
{
path: '/logginn/:username',
name: 'login-bio',
component: () => import('@/views/BiometricLoginView.vue')
},
{ {
path: '/registrer', path: '/registrer',
name: 'register', name: 'register',
...@@ -31,12 +37,12 @@ const router = createRouter({ ...@@ -31,12 +37,12 @@ const router = createRouter({
{ {
path: '/profil', path: '/profil',
name: 'profile', name: 'profile',
component: () => import('@/views/ProfileView.vue') component: () => import('@/views/ViewProfileView.vue')
}, },
{ {
path: '/profil/rediger', path: '/profil/rediger',
name: 'edit-profile', name: 'edit-profile',
component: () => import('@/views/EditProfileView.vue') component: () => import('@/views/ManageProfileView.vue')
}, },
{ {
path: '/sparemaal', path: '/sparemaal',
...@@ -108,25 +114,15 @@ const router = createRouter({ ...@@ -108,25 +114,15 @@ const router = createRouter({
name: 'configurations6', name: 'configurations6',
component: () => import('@/views/ConfigAccountNumberView.vue') component: () => import('@/views/ConfigAccountNumberView.vue')
}, },
{
path: '/forsteSparemaal',
name: 'firstSavingGoal',
component: () => import('@/views/FirstSavingGoalView.vue')
},
{
path: '/forsteSpareutfordring',
name: 'firstSavingChallengde',
component: () => import('@/views/FirstSavingChallengeView.vue')
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'not-found', name: 'not-found',
component: () => import('@/views/NotFoundView.vue') component: () => import('@/views/NotFoundView.vue')
}, },
{ {
path: '/addAlternativeLogin', path: '/konfigurasjonBiometri',
name: 'addAlternativeLogin', name: 'configure-biometric',
component: () => import('@/views/AddAlternativeLogin.vue') component: () => import('@/views/ConfigBiometricView.vue')
} }
], ],
scrollBehavior() { scrollBehavior() {
...@@ -134,4 +130,67 @@ const router = createRouter({ ...@@ -134,4 +130,67 @@ const router = createRouter({
} }
}) })
router.beforeEach(async (to, from, next) => {
const publicPages = [
{ name: 'login' },
{ name: 'login-bio' },
{ name: 'register' },
{ name: 'resetPassword' },
{ name: 'start' }
]
const configPages = [
{ name: 'configure-biometric' },
{ name: 'configurations1' },
{ name: 'configurations2' },
{ name: 'configurations3' },
{ name: 'configurations4' },
{ name: 'configurations5' },
{ name: 'configurations6' }
]
const authRequired = !publicPages.some((page) => page.name === to.name)
const loginCredentials = sessionStorage.getItem('accessToken')
const bioCredentials = localStorage.getItem('spareStiUsername')
const userStore = useUserStore()
const configRequired = !configPages.some((page) => page.name === to.name)
if (!loginCredentials) {
if (bioCredentials && to.name !== 'login-bio') {
console.log('Bio login')
await router.replace({ name: 'login-bio', params: { username: bioCredentials } })
return next({ name: 'login-bio', params: { username: bioCredentials } })
} else if (authRequired && !bioCredentials && to.name !== 'login') {
console.log('Normal login')
await router.replace({ name: 'login' })
return next({ name: 'login' })
} else if (!authRequired) {
console.log('Public page')
next()
}
} else {
if (userStore.user.isConfigured == false) {
await userStore.checkIfUserConfigured()
}
const isConfigured = userStore.user.isConfigured
if (configRequired && !isConfigured) {
await router.replace({ name: 'configure-biometric' })
return next({ name: 'configure-biometric' })
} else if (!configRequired && isConfigured) {
await router.replace({ name: 'home' })
return next({ name: 'home' })
}
if (!authRequired) {
await router.replace({ name: 'home' })
return next({ name: 'home' })
}
}
return next()
})
export default router export default router
...@@ -33,28 +33,15 @@ authInterceptor.interceptors.response.use( ...@@ -33,28 +33,15 @@ authInterceptor.interceptors.response.use(
!originalRequest._retry !originalRequest._retry
) { ) {
originalRequest._retry = true originalRequest._retry = true
const refreshToken = localStorage.getItem('refreshToken')
axios sessionStorage.removeItem('accessToken')
.post('/auth/renewToken', null, { const username = localStorage.getItem('spareStiUsername')
headers: {
Authorization: `Bearer ${refreshToken}` if (!username) {
} await router.push({ name: 'login' })
}) } else {
.then((response) => { await router.push({ name: 'login-bio', params: { username: username } })
sessionStorage.setItem('accessToken', response.data.accessToken) }
authInterceptor.defaults.headers['Authorization'] =
`Bearer ${response.data.accessToken}`
return authInterceptor(originalRequest)
})
.catch((err) => {
router.push({ name: 'login' })
return Promise.reject(err)
})
}
// Specific handler for 404 errors
if (error.response?.status === 404) {
console.error('Requested resource not found:', error.config.url)
// Optionally redirect or inform the user, depending on the context
} }
return Promise.reject(error) return Promise.reject(error)
} }
......
import { defineStore } from 'pinia'
import { ref } from 'vue'
import authInterceptor from '@/services/authInterceptor'
import { AxiosError } from 'axios'
export const useAccountStore = defineStore('account', {
state: () => ({
errorMessage: ref<string>('')
}),
actions: {
async postAccount(accountType: 'SAVING' | 'SPENDING', accNumber: string, balance: number) {
const payload = {
accountType,
accNumber,
balance
}
try {
const response = await authInterceptor.post('/accounts', payload)
console.log('Success:', response.data)
} catch (error) {
console.error('Error posting account:', error)
this.handleAxiosError(error)
}
},
handleAxiosError(error: any) {
const axiosError = error as AxiosError
if (axiosError.response && axiosError.response.data) {
const errorData = axiosError.response.data as { message: string }
} else {
this.errorMessage = 'An unexpected error occurred'
}
}
}
})
import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { defineStore } from 'pinia'
import authInterceptor from '@/services/authInterceptor' import authInterceptor from '@/services/authInterceptor'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
export const useUserConfigStore = defineStore('userConfig', { export const useUserConfigStore = defineStore('userConfig', () => {
state: () => ({ const role = ref('USER')
role: 'USER', const experience = ref('')
experience: 'VERY_HIGH', const motivation = ref('')
motivation: 'VERY_HIGH', const challengeTypeConfigs = ref(
challengeTypeConfigs: [] as { [] as {
type: string type: string
specificAmount: number specificAmount: number
generalAmount: number generalAmount: number
}[], }[]
errorMessage: ref<string>('') )
}), const accounts = ref({
actions: { savings: '',
setExperience(value: string) { spending: ''
this.experience = value })
}, const errorMessage = ref<string>('')
setMotivation(value: string) {
this.motivation = value const setExperience = (value: string) => {
}, experience.value = value
addChallengeTypeConfig(type: string, specificAmount: number, generalAmount: number) { }
this.challengeTypeConfigs.push({ type, specificAmount, generalAmount })
},
postUserConfig() {
const payload = {
experience: this.experience,
motivation: this.motivation,
challengeTypeConfigs: Array.from(this.challengeTypeConfigs)
}
authInterceptor const setMotivation = (value: string) => {
.post('/config/challenge', payload) motivation.value = value
.then((response) => { }
console.log('Success:', response.data)
}) const addChallengeTypeConfig = (
.catch((error) => { type: string,
const axiosError = error as AxiosError specificAmount: number,
if (axiosError.response && axiosError.response.data) { generalAmount: number
const errorData = axiosError.response.data as { message: string } ) => {
this.errorMessage = errorData.message || 'An error occurred' challengeTypeConfigs.value.push({ type, specificAmount, generalAmount })
} else { }
this.errorMessage = 'An unexpected error occurred'
} const postAccount = async (
console.error('Axios error:', this.errorMessage) accountType: 'SAVING' | 'SPENDING',
}) accNumber: string,
balance: number
) => {
const payload = {
accountType,
accNumber,
balance
}
await authInterceptor
.post('/accounts', payload)
.then((response) => {
console.log('Success:', response.data)
})
.catch((error) => {
const axiosError = error as AxiosError
errorMessage.value =
(axiosError.response?.data as string) ||
'An error occurred while posting account'
console.error('Error posting account:', errorMessage.value)
})
}
const postUserConfig = async () => {
const payload = {
experience: experience.value,
motivation: motivation.value,
challengeTypeConfigs: Array.from(challengeTypeConfigs.value)
} }
await authInterceptor
.post('/config/challenge', payload)
.then((response) => {
console.log('Success:', response.data)
})
.catch((error) => {
const axiosError = error as AxiosError
errorMessage.value =
(axiosError.response?.data as string) ||
'An error occurred while updating configuration'
console.error('Error updating configuration:', errorMessage.value)
})
}
return {
role,
experience,
motivation,
challengeTypeConfigs,
accounts,
errorMessage,
setExperience,
setMotivation,
addChallengeTypeConfig,
postAccount,
postUserConfig
} }
}) })
...@@ -14,7 +14,8 @@ export const useUserStore = defineStore('user', () => { ...@@ -14,7 +14,8 @@ export const useUserStore = defineStore('user', () => {
const defaultUser: User = { const defaultUser: User = {
firstname: 'Firstname', firstname: 'Firstname',
lastname: 'Lastname', lastname: 'Lastname',
username: 'Username' username: 'Username',
isConfigured: false
} }
const user = ref<User>(defaultUser) const user = ref<User>(defaultUser)
...@@ -30,7 +31,7 @@ export const useUserStore = defineStore('user', () => { ...@@ -30,7 +31,7 @@ export const useUserStore = defineStore('user', () => {
) => { ) => {
await axios await axios
.post(`http://localhost:8080/auth/register`, { .post(`http://localhost:8080/auth/register`, {
firstName: firstname, //TODO rename all instances of firstname to firstName firstName: firstname,
lastName: lastname, lastName: lastname,
email: email, email: email,
username: username, username: username,
...@@ -38,13 +39,12 @@ export const useUserStore = defineStore('user', () => { ...@@ -38,13 +39,12 @@ export const useUserStore = defineStore('user', () => {
}) })
.then((response) => { .then((response) => {
sessionStorage.setItem('accessToken', response.data.accessToken) sessionStorage.setItem('accessToken', response.data.accessToken)
localStorage.setItem('refreshToken', response.data.refreshToken)
user.value.firstname = firstname user.value.firstname = firstname
user.value.lastname = lastname user.value.lastname = lastname
user.value.username = username user.value.username = username
router.push({ name: 'addAlternativeLogin' }) router.push({ name: 'configure-biometric' })
}) })
.catch((error) => { .catch((error) => {
const axiosError = error as AxiosError const axiosError = error as AxiosError
...@@ -60,13 +60,22 @@ export const useUserStore = defineStore('user', () => { ...@@ -60,13 +60,22 @@ export const useUserStore = defineStore('user', () => {
}) })
.then((response) => { .then((response) => {
sessionStorage.setItem('accessToken', response.data.accessToken) sessionStorage.setItem('accessToken', response.data.accessToken)
localStorage.setItem('refreshToken', response.data.refreshToken)
user.value.firstname = response.data.firstName user.value.firstname = response.data.firstName
user.value.lastname = response.data.lastName user.value.lastname = response.data.lastName
user.value.username = response.data.username user.value.username = response.data.username
router.push({ name: 'home' }) authInterceptor('/profile').then((profileResponse) => {
if (profileResponse.data.hasPasskey === true) {
localStorage.setItem('spareStiUsername', username)
}
})
checkIfUserConfigured()
user.value.isConfigured
? router.push({ name: 'home' })
: router.push({ name: 'configure-biometric' })
}) })
.catch((error) => { .catch((error) => {
const axiosError = error as AxiosError const axiosError = error as AxiosError
...@@ -77,160 +86,167 @@ export const useUserStore = defineStore('user', () => { ...@@ -77,160 +86,167 @@ export const useUserStore = defineStore('user', () => {
const logout = () => { const logout = () => {
console.log('Logging out') console.log('Logging out')
sessionStorage.removeItem('accessToken') sessionStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken') localStorage.removeItem('spareStiUsername')
user.value = defaultUser user.value = defaultUser
router.push({ name: 'login' }) router.push({ name: 'login' })
} }
const getUserStreak = async () => {
try { const getUserStreak = () => {
const response = await authInterceptor('/profile/streak') authInterceptor('/profile/streak')
if (response.data) { .then((response) => {
streak.value = response.data streak.value = response.data
console.log('Fetched Challenges:', streak.value) })
} else { .catch((error) => {
console.error('Error fetching challenges:', error)
streak.value = undefined streak.value = undefined
console.error('No challenge content found:', response.data) })
}
} catch (error) {
console.error('Error fetching challenges:', error)
streak.value = undefined // Ensure challenges is always an array
}
} }
const bioRegister = async () => { const bioRegister = async () => {
try { authInterceptor
const response = await authInterceptor.post('/auth/bioRegistration') .post('/auth/bioRegistration')
initialCheckStatus(response) .then((response) => {
initialCheckStatus(response)
const credentialCreateJson: CredentialCreationOptions = response.data
const credentialCreateJson: CredentialCreationOptions = response.data
const credentialCreateOptions: CredentialCreationOptions = {
publicKey: { const credentialCreateOptions: CredentialCreationOptions = {
...credentialCreateJson.publicKey, publicKey: {
challenge: base64urlToUint8array( ...credentialCreateJson.publicKey,
credentialCreateJson.publicKey.challenge as unknown as string challenge: base64urlToUint8array(
), credentialCreateJson.publicKey.challenge as unknown as string
user: { ),
...credentialCreateJson.publicKey.user, user: {
id: base64urlToUint8array( ...credentialCreateJson.publicKey.user,
credentialCreateJson.publicKey.user.id as unknown as string id: base64urlToUint8array(
) credentialCreateJson.publicKey.user.id as unknown as string
)
},
excludeCredentials: credentialCreateJson.publicKey.excludeCredentials?.map(
(credential) => ({
...credential,
id: base64urlToUint8array(credential.id as unknown as string)
})
),
extensions: credentialCreateJson.publicKey.extensions
}
}
return navigator.credentials.create(
credentialCreateOptions
) as Promise<PublicKeyCredential>
})
.then((publicKeyCredential) => {
const publicKeyResponse =
publicKeyCredential.response as AuthenticatorAttestationResponse
const encodedResult = {
type: publicKeyCredential.type,
id: publicKeyCredential.id,
response: {
attestationObject: uint8arrayToBase64url(
publicKeyResponse.attestationObject
),
clientDataJSON: uint8arrayToBase64url(publicKeyResponse.clientDataJSON),
transports: publicKeyResponse.getTransports?.() || []
}, },
excludeCredentials: credentialCreateJson.publicKey.excludeCredentials?.map( clientExtensionResults: publicKeyCredential.getClientExtensionResults()
(credential) => ({
...credential,
id: base64urlToUint8array(credential.id as unknown as string)
})
),
extensions: credentialCreateJson.publicKey.extensions
} }
}
return authInterceptor.post('/auth/finishBioRegistration', {
const publicKeyCredential = (await navigator.credentials.create( credential: JSON.stringify(encodedResult)
credentialCreateOptions
)) as PublicKeyCredential
const publicKeyResponse =
publicKeyCredential.response as AuthenticatorAttestationResponse
const encodedResult = {
type: publicKeyCredential.type,
id: publicKeyCredential.id,
response: {
attestationObject: uint8arrayToBase64url(publicKeyResponse.attestationObject),
clientDataJSON: uint8arrayToBase64url(publicKeyResponse.clientDataJSON),
transports: publicKeyResponse.getTransports?.() || []
},
clientExtensionResults: publicKeyCredential.getClientExtensionResults()
}
await authInterceptor
.post('/auth/finishBioRegistration', { credential: JSON.stringify(encodedResult) })
.then((response) => {
router.push({ name: 'configurations1' })
}) })
} catch (error) { })
router.push({ name: 'configurations1' }) .then(() => {
console.error(error) localStorage.setItem('spareStiUsername', user.value.username)
} })
.catch((error) => {
console.error(error)
})
} }
const bioLogin = async (username: string) => { const bioLogin = (username: string) => {
try { axios
const request = await axios.post(`http://localhost:8080/auth/bioLogin/${username}`) .post(`http://localhost:8080/auth/bioLogin/${username}`)
.then((request) => {
initialCheckStatus(request) initialCheckStatus(request)
console.log(request) console.log(request)
const credentialGetJson: CredentialRequestOptions = request.data const credentialGetJson: CredentialRequestOptions = request.data
console.log(credentialGetJson) console.log(credentialGetJson)
const credentialGetOptions: CredentialRequestOptions = { const credentialGetOptions: CredentialRequestOptions = {
publicKey: { publicKey: {
...credentialGetJson.publicKey, ...credentialGetJson.publicKey,
allowCredentials: allowCredentials: credentialGetJson.publicKey.allowCredentials?.map(
credentialGetJson.publicKey.allowCredentials && (credential) => ({
credentialGetJson.publicKey.allowCredentials.map((credential) => ({ ...credential,
...credential, id: base64urlToUint8array(credential.id as unknown as string)
id: base64urlToUint8array(credential.id as unknown as string) })
})), ),
challenge: base64urlToUint8array( challenge: base64urlToUint8array(
credentialGetJson.publicKey.challenge as unknown as string credentialGetJson.publicKey.challenge as unknown as string
), ),
extensions: credentialGetJson.publicKey.extensions extensions: credentialGetJson.publicKey.extensions
}
} }
}
return navigator.credentials.get(
const publicKeyCredential = (await navigator.credentials.get( credentialGetOptions
credentialGetOptions ) as Promise<PublicKeyCredential>
)) as PublicKeyCredential })
.then((publicKeyCredential) => {
// Extract response data based on the type of credential const response = publicKeyCredential.response as AuthenticatorAssertionResponse
const response = publicKeyCredential.response as AuthenticatorAssertionResponse
const encodedResult = {
const encodedResult = { type: publicKeyCredential.type,
type: publicKeyCredential.type, id: publicKeyCredential.id,
id: publicKeyCredential.id, response: {
response: { authenticatorData:
authenticatorData: response.authenticatorData &&
response.authenticatorData && uint8arrayToBase64url(response.authenticatorData),
uint8arrayToBase64url(response.authenticatorData), clientDataJSON:
clientDataJSON: response.clientDataJSON &&
response.clientDataJSON && uint8arrayToBase64url(response.clientDataJSON), uint8arrayToBase64url(response.clientDataJSON),
signature: response.signature && uint8arrayToBase64url(response.signature),
signature: response.signature && uint8arrayToBase64url(response.signature), userHandle:
userHandle: response.userHandle && uint8arrayToBase64url(response.userHandle) response.userHandle && uint8arrayToBase64url(response.userHandle)
}, },
clientExtensionResults: publicKeyCredential.getClientExtensionResults() clientExtensionResults: publicKeyCredential.getClientExtensionResults()
} }
console.log(encodedResult) console.log(encodedResult)
await axios return axios.post(`http://localhost:8080/auth/finishBioLogin/${username}`, {
.post(`http://localhost:8080/auth/finishBioLogin/${username}`, {
credential: JSON.stringify(encodedResult) credential: JSON.stringify(encodedResult)
}) })
.then((response) => { })
sessionStorage.setItem('accessToken', response.data.accessToken) .then((response) => {
localStorage.setItem('refreshToken', response.data.refreshToken) sessionStorage.setItem('accessToken', response.data.accessToken)
user.value.firstname = response.data.firstName user.value.firstname = response.data.firstName
user.value.lastname = response.data.lastName user.value.lastname = response.data.lastName
user.value.username = response.data.username user.value.username = response.data.username
router.push({ name: 'home' }) router.push({ name: 'home' })
}) })
.catch((error) => { .catch((error) => {
const axiosError = error as AxiosError console.error(error)
errorMessage.value = })
(axiosError.response?.data as string) || 'An error occurred' }
console.log('hei :' + errorMessage.value)
}) const checkIfUserConfigured = async () => {
} catch (error) { await authInterceptor('/config')
// Handle errors .then((response) => {
console.log(error) console.log('User configured: ' + user.value.isConfigured)
} user.value.isConfigured = response.data.challengeConfig != null
})
.catch(() => {
user.value.isConfigured = false
})
} }
return { return {
user,
checkIfUserConfigured,
register, register,
login, login,
logout, logout,
......
...@@ -16,4 +16,5 @@ export interface Profile { ...@@ -16,4 +16,5 @@ export interface Profile {
balance?: number balance?: number
} }
badges?: object[] badges?: object[]
hasPasskey?: boolean
} }
...@@ -2,4 +2,6 @@ export interface User { ...@@ -2,4 +2,6 @@ export interface User {
firstname: string firstname: string
lastname: string lastname: string
username: string username: string
isConfigured: boolean
isBiometric?: boolean
} }
<template>
<div class="alt-login-main">
<h1>Alternativ innlogging</h1>
<div class="img-div">
<img src="@/assets/bioAuthTouch.png" alt="bioAuthTouch" />
<img src="@/assets/bioAuthFace.png" alt="bioAuthFace" />
</div>
<h2>Vil du logge på med touch eller face id?</h2>
<div class="btn-div">
<button @click="router.push('konfigurasjonSteg1')">Senere</button>
<button @click="userStore.bioRegister()">OK</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/userStore'
import router from '@/router'
const userStore = useUserStore()
</script>
<style scoped>
.alt-login-main {
max-width: 800px;
display: flex;
flex-direction: column;
align-items: center;
}
.img-div {
display: flex;
justify-content: center;
}
img {
width: 30%;
}
img:first-child {
margin-right: 20px;
}
button {
margin: 10px;
width: 100px;
height: 40px;
}
</style>
<script lang="ts" setup>
import { useRoute } from 'vue-router'
import router from '@/router'
import { useUserStore } from '@/stores/userStore'
const route = useRoute()
const username = route.params.username as string
const removeBioCredential = () => {
localStorage.removeItem('spareStiUsername')
router.push({ name: 'login' })
}
const bioLogin = () => {
useUserStore().bioLogin(username)
}
</script>
<template>
<div class="flex flex-col items-center h-screen gap-5 my-10">
<h1>Hei {{ username }}, velkommen tilbake</h1>
<button @click="bioLogin">Biometrisk login</button>
<p>Ikke deg? Eller funker ikke biometrisk innlogging?</p>
<button @click="removeBioCredential">Logg inn med brukernavn og passord</button>
</div>
</template>
<style scoped></style>
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