enso/app/ide-desktop/lib/client/src/authentication.ts
somebody1234 3d045a7ceb
Dashboard directory interactivity (#6279)
* turn object into var

* add todo

* fix style

* fixes

* remove forgot password + reset password

* remove signout

* remove setusername

* remove login

* remove registration

* fix comments

* re-enable flag

* rename div

* add comment

* fix lints

* remove tailwind conf

* Revert "remove registration"

This reverts commit 02439c9b70.

* Revert "remove login"

This reverts commit 8e6f9c1112.

* Revert "remove setusername"

This reverts commit 84721bcccd.

* Revert "remove signout"

This reverts commit 08a96d3796.

* Revert "remove forgot password + reset password"

This reverts commit e52f51a762.

* remove opener

* move opener

* tmp

* prettier

* expand docs

* tmp

* replace react-scripts with craco

* add tailwindcss

* switch to brands

* tmp

* tmp

* tmp

* fixmes

* fixmes

* fixmes

* fixmes

* fixmes

* fixes for e-hern's comments

* use abortcontroller

* add docs

* fixes

* revert craco, fix windows build

* remove from gitignore

* remove unnecessary check

* tmp

* augment window

* tmptmp

* split errors back up

* tmp

* tmp

* prettier

* fix

* Fix lints

* Prepare for addition for `as T` lint

* Add lint for early returns

* Address review issues

* Fix lints

* remove withrouter

* fix file length

* fixes

* fixes

* remove dashboard

* fix

* use switch

* prettier

* fixes

* prettier

* fixes

* run prettier

* run prettier

* run prettier

* fix main page url

* allow node.js debugging

* fix lints

* change not equal

* prettier

* Remove references to withRouter; fix lints

* Run prettier

* Add cloud endpoints

* Add JSON-RPC endpoints

* Add dashboard skeleton

* Add components and edit dashboard

* Run prettier

* (WIP) Add cloud endpoints

* Add rpc endpoints

* Address review issues

* Formatting and minor fixes for `newtype.ts`

* Address review issues

* Rename `Brand` to `NewtypeVariant`

* Rename `Brand` to `NewtypeVariant`

* Fix formatting in `newtype.ts`

* Switch dashboard to esbuild

* Minor fixes; move Tailwind generation into esbuild-config

* Fix watching `content/` and `client/`

* Bump esbuild binary versions; minor dependency list fixes

* Add dashboard skeleton

* Run prettier

* Fixes; rename "npm run dev" to "npm run watch-dashboard"

* Avoid writing esbuild outputs to disk for `dashboard/`

* Convert watch-dashboard to be fully in-memory; rebuild css files on change

* Remove obsolete FIXME

* Remove unused constants

* Run prettier

* add missing styles

* Fixes

* Fix the fixes

* Run prettier

* Fixes; use nesting plugin to wrap tailwind preflight

* Remove testing flag from client/watch

* Minor fixes

* Run prettier

* Export newtypes

* Make css rebuild when tailwind config changes

* Fix endpoints

* Finish copying changes over

* Remove duplicate type definitions

* Fix bundling for dashboard

* Fix dashboard/bundle.ts erroring when build directory does not exist

* Move CSS to Tailwind config

* Run prettier

* Update endpoints

* Fix esbuild binary package names

* Remove redundant "npx" prefix from build scripts

* Remove unused dependency

* Begin adding interactivity

* workaround for mac freeze

* Fix modal bugs

* Begin implementing forms, split forms and modals into new files

* Get form UI working

* add missing sections

* Minor fixes, save current directory to localStorage

* Fixes for drop-to-upload

Note: currently it is opening in a new tab instead of actually uploading

* Address review issue

* Fix prettier config; run prettier

* Fix live-reload of `npm run watch-dashboard`

* (WIP)

* Fix service worker for client-side routing

* Add close button to asset creation forms; fix saving directory to localStorage

* Remove workaround for backend bug when listing directories

* Fix drop-to-upload

* Fix sizing

* Fix spacing, add fixed paths

* WIP: fix toast notification styles, begin adding context menu

* WIP: Add context menu, minor fixes

* Fix authentication on desktop IDE

* Allow unused locals and parameters in tsconfig.json

* Run prettier

* Fix TypeScript errors

* Fix modals; minor refactor

* Implement context menus for labels; fixes

* Add modal provider and switch all modals to provider; fixes

* Fix modals and user icon size

* Fixes

* Remove obsolete files from incorrect merge

* Address review issues

* Fix type error

* Stop removing `#root`

* Fixes for cloud

* Implement search on frontend side

* Fix race condition related to `directoryId`

* Hide directories, files and secrets tables on desktop IDE

* Fix lint errors

* Properly update visible projects when a project is created

* Pass directory id to create project

* Hide column display switcher; remove placeholder column data

---------

Co-authored-by: Nikita Pekin <nikita@frecency.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Paweł Buchowski <pawel.buchowski@enso.org>
2023-04-26 11:52:13 +02:00

188 lines
8.9 KiB
TypeScript

/** @file Definition of the Electron-specific parts of the authentication flows of the IDE.
*
* # Overview of Authentication/Authorization
*
* Actions like creating projects, opening projects, uploading files to the cloud, etc. require the
* user to be authenticated and authorized. Authenticated means that the user has an account that
* the application recognizes, and that the user has provided their credentials to prove that they
* are who they say they are. Authorized means that the user has the necessary permissions to
* perform the action.
*
* Authentication and authorization are provided by the user logging in with their credentials,
* which we exchange for a JSON Web Token (JWT). The JWT is sent with every HTTP request to the
* backend.
*
* The authentication module of the dashboard and IDE handles these flows:
* - registering a new user account,
* - signing in to an existing user account (in exchange for an access token),
* - signing out of the user account,
* - setting the user's username (i.e., display name used in place of their email address),
* - changing/resetting the user's password,
* - etc.
*
* # Electron Inter-Process Communication (IPC)
*
* If the user is signing in through a federated identity provider (e.g., Google or GitHub), the
* authentication flows need be able to to:
* - redirect the user from the IDE to external sources (e.g., system web browser), and
* - redirect the user from external sources to the IDE (e.g., system web browser, email client).
*
* The main Electron process can launch the system web browser. The dashboard and IDE are sandboxed,
* so they can not launch the system web browser. By registering Inter-Process Communication (IPC)
* listeners in the Electron app, we can bridge this gap, and allow the dashboad + IDE to emit
* events that signal to the main Electron process to open URLs in the system web browser.
*
* ## Redirect To System Web Browser
*
* The user must use the system browser to complete sensitive flows such as signup and signin. These
* flows should not be done in the app as the user cannot be expected to trust the app with their
* credentials.
*
* To redirect the user from the IDE to an external source:
* 1. Call the {@link initIpc} function to register a listener for
* {@link ipc.Channel.openUrlInSystemBrowser} IPC events.
* 2. Emit an {@link ipc.Channel.openUrlInSystemBrowser} event. The listener registered in the
* {@link initIpc} function will use the {@link opener} library to open the event's {@link URL}
* argument in the system web browser, in a cross-platform way.
*
* ## Redirect To IDE
*
* The user must be redirected back to the IDE from the system web browser after completing a
* sensitive flow such as signup or signin. The user may also be redirected to the IDE from an
* external source such as an email client after verifying their email address.
*
* To handle these redirects, we use deep links. Deep links are URLs that are used to redirect the
* user to a specific page in the application. To handle deep links, we use a custom URL protocol
* scheme.
*
* To prepare the application to handle deep links:
* - Register a custom URL protocol scheme with the OS (c.f., `electron-builder-config.ts`).
* - Define a listener for Electron {@link OPEN_URL_EVENT}s (c.f., {@link initOpenUrlListener}).
* - Define a listener for {@link ipc.Channel.openDeepLink} events (c.f., `preload.ts`).
*
* Then when the user clicks on a deep link from an external source to the IDE:
* - The OS redirects the user to the application.
* - The application emits an Electron {@link OPEN_URL_EVENT}.
* - The {@link OPEN_URL_EVENT} listener checks if the {@link URL} is a deep link.
* - If the {@link URL} is a deep link, the {@link OPEN_URL_EVENT} listener prevents Electron from
* handling the event.
* - The {@link OPEN_URL_EVENT} listener then emits an {@link ipc.Channel.openDeepLink} event.
* - The {@link ipc.Channel.openDeepLink} listener registered by the dashboard receives the event.
* Then it parses the {@link URL} from the event's {@link URL} argument. Then it uses the
* {@link URL} to redirect the user to the dashboard, to the page specified in the {@link URL}'s
* `pathname`. */
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import * as electron from 'electron'
import opener from 'opener'
import * as common from 'enso-common'
import * as contentConfig from 'enso-content-config'
import * as urlAssociations from 'url-associations'
import * as ipc from 'ipc'
const logger = contentConfig.logger
// ========================================
// === Initialize Authentication Module ===
// ========================================
/** Configures all the functionality that must be set up in the Electron app to support
* authentication-related flows. Must be called in the Electron app `whenReady` event.
*
* @param window - A function that returns the main Electron window. This argument is a lambda and
* not a variable because the main window is not available when this function is called. This module
* does not use the `window` until after it is initialized, so while the lambda may return `null` in
* theory, it never will in practice. */
export function initModule(window: () => electron.BrowserWindow) {
initIpc()
initOpenUrlListener(window)
initSaveAccessTokenListener()
}
/** Registers an Inter-Process Communication (IPC) channel between the Electron application and the
* served website.
*
* This channel listens for {@link ipc.Channel.openUrlInSystemBrowser} events. When this kind of
* event is fired, this listener will assume that the first and only argument of the event is a URL.
* This listener will then attempt to open the URL in a cross-platform way. The intent is to open
* the URL in the system browser.
*
* This functionality is necessary because we don't want to run the OAuth flow in the app. Users
* don't trust Electron apps to handle their credentials. */
function initIpc() {
electron.ipcMain.on(ipc.Channel.openUrlInSystemBrowser, (_event, url: string) => {
logger.log(`Opening URL in system browser: '${url}'.`)
urlAssociations.setAsUrlHandler()
opener(url)
})
}
/** Registers a listener that fires a callback for `open-url` events, when the URL is a deep link.
*
* This listener is used to open a page in *this* application window, when the user is
* redirected to a URL with a protocol supported by this application.
*
* All URLs that aren't deep links (i.e., URLs that don't use the {@link common.DEEP_LINK_SCHEME}
* protocol) will be ignored by this handler. Non-deep link URLs will be handled by Electron. */
function initOpenUrlListener(window: () => electron.BrowserWindow) {
urlAssociations.registerUrlCallback(url => {
onOpenUrl(url, window)
})
}
/**
* Handles the 'open-url' event by parsing the received URL, checking if it is a deep link, and
* sending it to the appropriate BrowserWindow via IPC.
*
* @param url - The URL to handle.
* @param window - A function that returns the BrowserWindow to send the parsed URL to.
*/
export function onOpenUrl(url: URL, window: () => electron.BrowserWindow) {
logger.log(`Received 'open-url' event for '${url.toString()}'.`)
if (url.protocol !== `${common.DEEP_LINK_SCHEME}:`) {
logger.error(`'${url.toString()}' is not a deep link, ignoring.`)
} else {
logger.log(`'${url.toString()}' is a deep link, sending to renderer.`)
window().webContents.send(ipc.Channel.openDeepLink, url.toString())
}
}
/** Registers a listener that fires a callback for `save-access-token` events.
*
* This listener is used to save given access token to credentials file to be later used by enso backend.
*
* Credentials file is placed in users home directory in `.enso` subdirectory in `credentials` file. */
function initSaveAccessTokenListener() {
electron.ipcMain.on(ipc.Channel.saveAccessToken, (event, accessToken: string) => {
/** Enso home directory for credentials file. */
const ensoCredentialsDirectoryName = '.enso'
/** Enso credentials file. */
const ensoCredentialsFileName = 'credentials'
/** System agnostic credentials directory home path. */
const ensoCredentialsHomePath = path.join(os.homedir(), ensoCredentialsDirectoryName)
fs.mkdir(ensoCredentialsHomePath, { recursive: true }, error => {
if (error) {
logger.error(`Couldn't create ${ensoCredentialsDirectoryName} directory.`)
} else {
fs.writeFile(
path.join(ensoCredentialsHomePath, ensoCredentialsFileName),
accessToken,
innerError => {
if (innerError) {
logger.error(`Could not write to ${ensoCredentialsFileName} file.`)
}
}
)
}
})
event.preventDefault()
})
}