|
| 1 | +/* |
| 2 | + * GNU AGPL-3.0 License |
| 3 | + * |
| 4 | + * Copyright (c) 2021 - present core.ai . All rights reserved. |
| 5 | + * |
| 6 | + * This program is free software: you can redistribute it and/or modify it under |
| 7 | + * the terms of the GNU Affero General Public License as published by the Free |
| 8 | + * Software Foundation, either version 3 of the License, or (at your option) any later version. |
| 9 | + * |
| 10 | + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; |
| 11 | + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| 12 | + * See the GNU Affero General Public License for more details. |
| 13 | + * |
| 14 | + * You should have received a copy of the GNU Affero General Public License along |
| 15 | + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. |
| 16 | + * |
| 17 | + */ |
| 18 | + |
| 19 | + |
| 20 | +define(function (require, exports, module) { |
| 21 | + const EventDispatcher = require("utils/EventDispatcher"), |
| 22 | + PreferencesManager = require("preferences/PreferencesManager"), |
| 23 | + Dialogs = require("widgets/Dialogs"), |
| 24 | + DefaultDialogs = require("widgets/DefaultDialogs"), |
| 25 | + Strings = require("strings"), |
| 26 | + ProfileMenu = require("./profile-menu"); |
| 27 | + |
| 28 | + const KernalModeTrust = window.KernalModeTrust; |
| 29 | + if(!KernalModeTrust){ |
| 30 | + // integrated extensions will have access to kernal mode, but not external extensions |
| 31 | + throw new Error("Login service should have access to KernalModeTrust. Cannot boot without trust ring"); |
| 32 | + } |
| 33 | + const secureExports = {}; |
| 34 | + KernalModeTrust.loginService = secureExports; |
| 35 | + // user profile is something like "apiKey": "uuid...", validationCode: "dfdf", "firstName":"Aa","lastName":"bb", |
| 36 | + // "email":"[email protected]", "customerID":"uuid...","loginTime":1750074393853, |
| 37 | + // "profileIcon":{"color":"#14b8a6","initials":"AB"} |
| 38 | + let userProfile = null; |
| 39 | + let isLoggedInUser = false; |
| 40 | + |
| 41 | + // just used as trigger to notify different windows about user profile changes |
| 42 | + const PREF_USER_PROFILE_VERSION = "userProfileVersion"; |
| 43 | + |
| 44 | + |
| 45 | + function isLoggedIn() { |
| 46 | + return isLoggedInUser; |
| 47 | + } |
| 48 | + |
| 49 | + const ERR_RETRY_LATER = "retry_later"; |
| 50 | + const ERR_INVALID = "invalid"; |
| 51 | + |
| 52 | + /** |
| 53 | + * Resolves the provided API key and verification code to user profile data |
| 54 | + * |
| 55 | + * @param {string} apiKey - The API key to be validated. |
| 56 | + * @param {string} validationCode - The verification code associated with the API key. |
| 57 | + * @return {Promise<Object>} A promise resolving to an object containing the user details if successful, |
| 58 | + * or an error object with the relevant error code (`ERR_RETRY_LATER` or `ERR_INVALID`) if the operation fails. |
| 59 | + * never rejects. |
| 60 | + */ |
| 61 | + async function _resolveAPIKey(apiKey, validationCode) { |
| 62 | + const resolveURL = `${Phoenix.config.account_url}resolveAppSessionID?appSessionID=${apiKey}&validationCode=${validationCode}`; |
| 63 | + if (!navigator.onLine) { |
| 64 | + return {err: ERR_RETRY_LATER}; |
| 65 | + } |
| 66 | + try { |
| 67 | + const response = await fetch(resolveURL); |
| 68 | + if (response.status === 400 || response.status === 404) { |
| 69 | + // 404 api key not found and 400 Bad Request, eg: verification code mismatch |
| 70 | + return {err: ERR_INVALID}; |
| 71 | + } else if (response.ok) { |
| 72 | + const userDetails = await response.json(); |
| 73 | + userDetails.apiKey = apiKey; |
| 74 | + userDetails.validationCode = validationCode; |
| 75 | + return {userDetails}; |
| 76 | + } |
| 77 | + // Other errors like 500 are retriable |
| 78 | + console.log('Other error:', response.status); |
| 79 | + return {err: ERR_RETRY_LATER}; |
| 80 | + } catch (e) { |
| 81 | + console.error(e, "Failed to call resolve API endpoint", resolveURL); |
| 82 | + return {err: ERR_RETRY_LATER}; |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + async function _verifyLogin() { |
| 87 | + const savedUserProfile = await KernalModeTrust.getCredential(KernalModeTrust.CRED_KEY_API); |
| 88 | + if(!savedUserProfile){ |
| 89 | + console.log("No savedUserProfile found. Not logged in"); |
| 90 | + ProfileMenu.setNotLoggedIn(); |
| 91 | + return; |
| 92 | + } |
| 93 | + try { |
| 94 | + userProfile = JSON.parse(savedUserProfile); |
| 95 | + } catch (e) { |
| 96 | + console.error(e, "Failed to parse saved user profile credentials");// this should never happen |
| 97 | + ProfileMenu.setNotLoggedIn(); |
| 98 | + return; // not logged in if parse fails |
| 99 | + } |
| 100 | + isLoggedInUser = true; |
| 101 | + // api key is present, verify if the key is valid. but just show user that we are logged in with |
| 102 | + // stored credentials. |
| 103 | + ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); |
| 104 | + const resolveResponse = await _resolveAPIKey(userProfile.apiKey, userProfile.validationCode); |
| 105 | + if(resolveResponse.userDetails) { |
| 106 | + // a valid user account is in place. update the stored credentials |
| 107 | + userProfile = resolveResponse.userDetails; |
| 108 | + ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); |
| 109 | + await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_API, JSON.stringify(userProfile)); |
| 110 | + return; |
| 111 | + } |
| 112 | + // some error happened. |
| 113 | + if(resolveResponse.err === ERR_INVALID) { // the api key is invalid, we need to logout and tell user |
| 114 | + ProfileMenu.setNotLoggedIn(); |
| 115 | + Dialogs.showModalDialog( |
| 116 | + DefaultDialogs.DIALOG_ID_ERROR, |
| 117 | + Strings.SIGNED_OUT, |
| 118 | + Strings.SIGNED_OUT_MESSAGE |
| 119 | + ); |
| 120 | + await KernalModeTrust.removeCredential(KernalModeTrust.CRED_KEY_API); |
| 121 | + } |
| 122 | + // maybe some intermittent network error, ERR_RETRY_LATER is here. do nothing |
| 123 | + } |
| 124 | + |
| 125 | + function init() { |
| 126 | + ProfileMenu.init(); |
| 127 | + if(!Phoenix.isNativeApp){ |
| 128 | + console.warn("Login service is not supported in browser"); |
| 129 | + return; |
| 130 | + } |
| 131 | + _verifyLogin().catch(console.error);// todo raise metrics |
| 132 | + const pref = PreferencesManager.stateManager.definePreference(PREF_USER_PROFILE_VERSION, 'string', '0') |
| 133 | + .watchExternalChanges(); |
| 134 | + |
| 135 | + } |
| 136 | + |
| 137 | + init(); |
| 138 | + |
| 139 | + // no sensitive apis or events should be triggered from the public exports of this module as extensions |
| 140 | + // can read them. Always use KernalModeTrust.loginService for sensitive apis. |
| 141 | + EventDispatcher.makeEventDispatcher(exports); |
| 142 | + EventDispatcher.makeEventDispatcher(secureExports); |
| 143 | + |
| 144 | + // kernal exports |
| 145 | + secureExports.isLoggedIn = isLoggedIn; |
| 146 | + // public exports |
| 147 | + exports.isLoggedIn = isLoggedIn; |
| 148 | + |
| 149 | +}); |
0 commit comments