// Copyright (C) 2024 Vaughn Nugent
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import { AxiosInstance } from "axios";
import { get, useTimeoutFn, set } from "@vueuse/core";
import { computed, shallowRef } from "vue";
import { clone, defer, delay } from "lodash";
import { IMfaFlowContinuiation, totpMfaProcessor, useMfaLogin, usePkiAuth, useSession, useUser,
type IMfaSubmission, type IMfaMessage, type WebMessage
} from "@vnuge/vnlib.browser";
import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, popupAndOptionsOnly, popupOnly } from "./framework";
import { waitForChangeFn } from "./util";
import type { ClientStatus, Watchable } from "./types";
import type { AppSettings } from "./settings";
import type { JsonObject } from "type-fest";
export interface ProectedHandler {
(message: T): Promise
}
export interface MessageHandler {
(message: T): Promise
}
export interface ApiMessageHandler {
(message: T, apiHandle: { axios: AxiosInstance }): Promise
}
export interface UserApi extends FeatureApi, Watchable {
login(username: string, password?: string): Promise
logout: () => Promise
getProfile: () => Promise
getStatus: () => Promise
submitMfa: (submission: IMfaSubmission) => Promise
}
export const useAuthApi = (): IFeatureExport => {
return {
background: ({ state }:BgRuntime): UserApi =>{
const { loggedIn, clearLoginState } = useSession();
const { currentConfig } = state
const { logout, getProfile, heartbeat, userName } = useUser();
const currentPkiPath = computed(() => `${currentConfig.value.accountBasePath}/pki`)
//Use pki login controls
const pkiAuth = usePkiAuth(currentPkiPath as any)
const { login } = useMfaLogin([ totpMfaProcessor() ])
//We can send post messages to the server heartbeat endpoint to get status
const runHeartbeat = async () => {
//Only run if the api thinks its logged in, and config is enabled
if (!loggedIn.value || currentConfig.value.heartbeat !== true) {
return
}
try {
//Post against the heartbeat endpoint
await heartbeat()
}
catch (error: any) {
if (error.response?.status === 401 || error.response?.status === 403) {
//If we get a 401, the user is no longer logged in
clearLoginState()
}
}
}
const mfaUpgrade = (() => {
const store = shallowRef(null)
const message = computed(() =>{
if(!store.value){
return null
}
//clone the continuation to send to the popup
const cl = clone>(store.value)
//Remove the submit method from the continuation
delete cl.submit;
return cl as IMfaMessage
})
const { start, stop } = useTimeoutFn(() => set(store, null), 360 * 1000)
return{
setContiuation(cont: IMfaFlowContinuiation){
//Store continuation for later
set(store, cont)
//Restart cleanup timer
start()
},
continuation: message,
async submit(submission: IMfaSubmission){
const cont = get(store)
if(!cont){
throw new Error('MFA login expired')
}
const response = await cont.submit(submission)
response.getResultOrThrow()
//Stop timer
stop()
//clear the continuation
defer(() => set(store, null))
}
}
})()
//Configure interval to run every 5 minutes to update the status
setInterval(runHeartbeat, 60 * 1000);
delay(runHeartbeat, 1000) //Delay 1 second to allow the extension to load
return {
waitForChange: waitForChangeFn([currentConfig, loggedIn, userName, mfaUpgrade.continuation]),
login: popupOnly(async (usernameOrToken: string, password?: string): Promise => {
if(password){
const result = await login(usernameOrToken, password)
if ('getResultOrThrow' in result){
(result as WebMessage).getResultOrThrow()
}
if((result as IMfaFlowContinuiation).submit){
//Capture continuation, store for submission for later, and set the continuation
mfaUpgrade.setContiuation(result as IMfaFlowContinuiation);
return true;
}
//Otherwise normal login
}
else{
//Perform login
await pkiAuth.login(usernameOrToken)
}
//load profile
getProfile()
return true;
}),
logout: popupOnly(async (): Promise => {
//Perform logout
await logout()
//Cleanup after logout
clearLoginState()
}),
submitMfa: popupOnly(async (submission: IMfaSubmission): Promise => {
const cont = get(mfaUpgrade.continuation)
if(!cont || cont.expired){
return false;
}
//Submit the continuation
await mfaUpgrade.submit(submission);
//load profile
getProfile()
return true;
}),
getProfile: popupAndOptionsOnly(getProfile),
async getStatus (){
return {
//Logged in if the cookie is set and the api flag is set
loggedIn: get(loggedIn),
//username
userName: get(userName),
//mfa status
mfaStatus: get(mfaUpgrade.continuation)
} as ClientStatus
},
}
},
foreground: exportForegroundApi([
'login',
'logout',
'getProfile',
'getStatus',
'waitForChange',
'submitMfa',
]),
}
}