2025-05-12 22:53:43 +02:00

309 lines
13 KiB
JavaScript

import { defineStore } from 'pinia'
import { notify } from "@kyvg/vue3-notification";
import { apiWrapper } from '@/helpers/fetch-wrapper'
import router from '../router'
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
import {base64_url_encode} from "@/helpers/encoding";
export const authStore = defineStore('auth',{
state: () => ({
// initialize state from local storage to enable user to stay logged in
user: JSON.parse(localStorage.getItem('user')),
providers: [],
returnUrl: localStorage.getItem('returnUrl'),
webAuthnCredentials: [],
fetching: false,
}),
getters: {
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
User: (state) => state.user,
LoginProviders: (state) => state.providers,
IsAuthenticated: (state) => state.user != null,
IsAdmin: (state) => state.user?.IsAdmin || false,
ReturnUrl: (state) => state.returnUrl || '/',
IsWebAuthnEnabled: (state) => {
if (state.webAuthnCredentials) {
return state.webAuthnCredentials.length > 0
}
return false
},
WebAuthnCredentials: (state) => state.webAuthnCredentials || [],
isFetching: (state) => state.fetching,
},
actions: {
SetReturnUrl(link) {
this.returnUrl = link
localStorage.setItem('returnUrl', link)
},
ResetReturnUrl() {
this.returnUrl = null
localStorage.removeItem('returnUrl')
},
// LoadProviders always returns a fulfilled promise, even if the request failed.
async LoadProviders() {
apiWrapper.get(`/auth/providers`)
.then(providers => this.providers = providers)
.catch(error => {
this.providers = []
console.log("Failed to load auth providers: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load external authentication providers!",
})
})
},
// LoadSession returns promise that might have been rejected if the session was not authenticated.
async LoadSession() {
return apiWrapper.get(`/auth/session`)
.then(session => {
if (session.LoggedIn === true) {
this.ResetReturnUrl()
this.setUserInfo(session)
return session.UserIdentifier
} else {
this.setUserInfo(null)
return Promise.reject(new Error('session not authenticated'))
}
})
.catch(err => {
this.setUserInfo(null)
return Promise.reject(err)
})
},
// LoadWebAuthnCredentials returns promise that might have been rejected if the session was not authenticated.
async LoadWebAuthnCredentials() {
this.fetching = true
return apiWrapper.get(`/auth/webauthn/credentials`)
.then(credentials => {
this.setWebAuthnCredentials(credentials)
})
.catch(error => {
this.setWebAuthnCredentials([])
console.log("Failed to load webauthn credentials:", error)
notify({
title: "Backend Connection Failure",
text: error,
type: 'error',
})
})
},
// Login returns promise that might have been rejected if the login attempt was not successful.
async Login(username, password) {
return apiWrapper.post(`/auth/login`, { username, password })
.then(user => {
this.ResetReturnUrl()
this.setUserInfo(user)
return user.Identifier
})
.catch(err => {
console.log("Login failed:", err)
this.setUserInfo(null)
return Promise.reject(new Error("login failed"))
})
},
async Logout() {
this.setUserInfo(null)
this.ResetReturnUrl() // just to be sure^^
try {
await apiWrapper.post(`/auth/logout`)
} catch (e) {
console.log("Logout request failed:", e)
}
notify({
title: "Logged Out",
text: "Logout successful!",
type: "warn",
})
await router.push('/login')
},
async RegisterWebAuthn() {
// check if the browser supports WebAuthn
if (!browserSupportsWebAuthn()) {
console.error("WebAuthn is not supported by this browser.");
notify({
title: "WebAuthn not supported",
text: "This browser does not support WebAuthn.",
type: 'error'
});
return Promise.reject(new Error("WebAuthn not supported"));
}
this.fetching = true
console.log("Starting WebAuthn registration...")
await apiWrapper.post(`/auth/webauthn/register/start`, {})
.then(optionsJSON => {
notify({
title: "Passkey registration",
text: "Starting passkey registration, follow the instructions in the browser."
});
console.log("Started WebAuthn registration with options: ", optionsJSON)
return startRegistration({ optionsJSON: optionsJSON.publicKey }).then(attResp => {
console.log("Finishing WebAuthn registration...")
return apiWrapper.post(`/auth/webauthn/register/finish`, attResp)
.then(credentials => {
console.log("Passkey registration finished successfully: ", credentials)
this.setWebAuthnCredentials(credentials)
notify({
title: "Passkey registration",
text: "A new passkey has been registered successfully!",
type: 'success'
});
})
.catch(err => {
this.fetching = false
console.error("Failed to register passkey:", err);
notify({
title: "Passkey registration failed",
text: err,
type: 'error'
});
})
}).catch(err => {
this.fetching = false
console.error("Failed to start WebAuthn registration:", err);
notify({
title: "Failed to start Passkey registration",
text: err,
type: 'error'
});
})
})
.catch(err => {
this.fetching = false
console.error("Failed to start WebAuthn registration:", err);
notify({
title: "Failed to start WebAuthn registration",
text: err,
type: 'error'
});
})
},
async DeleteWebAuthnCredential(credentialId) {
this.fetching = true
return apiWrapper.delete(`/auth/webauthn/credential/${base64_url_encode(credentialId)}`)
.then(credentials => {
this.setWebAuthnCredentials(credentials)
notify({
title: "Success",
text: "Passkey deleted successfully!",
type: 'success',
})
})
.catch(err => {
this.fetching = false
console.error("Failed to delete webauthn credential:", err);
notify({
title: "Backend Connection Failure",
text: err,
type: 'error',
})
})
},
async RenameWebAuthnCredential(credential) {
this.fetching = true
return apiWrapper.put(`/auth/webauthn/credential/${base64_url_encode(credential.ID)}`, {
Name: credential.Name,
})
.then(credentials => {
this.setWebAuthnCredentials(credentials)
notify({
title: "Success",
text: "Passkey renamed successfully!",
type: 'success',
})
})
.catch(err => {
this.fetching = false
console.error("Failed to rename webauthn credential", credential.ID, ":", err);
notify({
title: "Backend Connection Failure",
text: err,
type: 'error',
})
})
},
async LoginWebAuthn() {
// check if the browser supports WebAuthn
if (!browserSupportsWebAuthn()) {
console.error("WebAuthn is not supported by this browser.");
notify({
title: "WebAuthn not supported",
text: "This browser does not support WebAuthn.",
type: 'error'
});
return Promise.reject(new Error("WebAuthn not supported"));
}
this.fetching = true
console.log("Starting WebAuthn login...")
await apiWrapper.post(`/auth/webauthn/login/start`, {})
.then(optionsJSON => {
console.log("Started WebAuthn login with options: ", optionsJSON)
return startAuthentication({ optionsJSON: optionsJSON.publicKey }).then(asseResp => {
console.log("Finishing WebAuthn login ...")
return apiWrapper.post(`/auth/webauthn/login/finish`, asseResp)
.then(user => {
console.log("Passkey login finished successfully for user:", user.Identifier)
this.ResetReturnUrl()
this.setUserInfo(user)
return user.Identifier
})
.catch(err => {
console.error("Failed to login with passkey:", err)
this.setUserInfo(null)
return Promise.reject(new Error("login failed"))
})
}).catch(err => {
console.error("Failed to finish passkey login:", err)
this.setUserInfo(null)
return Promise.reject(new Error("login failed"))
})
})
.catch(err => {
console.error("Failed to start passkey login:", err)
this.setUserInfo(null)
return Promise.reject(new Error("login failed"))
})
},
// -- internal setters
setUserInfo(userInfo) {
// store user details and jwt in local storage to keep user logged in between page refreshes
if (userInfo) {
if ('UserIdentifier' in userInfo) { // session object
this.user = {
Identifier: userInfo['UserIdentifier'],
Firstname: userInfo['UserFirstname'],
Lastname: userInfo['UserLastname'],
Email: userInfo['UserEmail'],
IsAdmin: userInfo['IsAdmin']
}
} else { // user object
this.user = {
Identifier: userInfo['Identifier'],
Firstname: userInfo['Firstname'],
Lastname: userInfo['Lastname'],
Email: userInfo['Email'],
IsAdmin: userInfo['IsAdmin']
}
}
localStorage.setItem('user', JSON.stringify(this.user))
} else {
this.user = null
localStorage.removeItem('user')
}
},
setWebAuthnCredentials(credentials) {
this.fetching = false
this.webAuthnCredentials = credentials
}
}
});