Fix prettier config; run prettier (#6132)

* Fix prettier config; run prettier

* add workaround for double re-render

* add missing fixme

---------

Co-authored-by: Paweł Buchowski <pawel.buchowski@enso.org>
Co-authored-by: Nikita Pekin <nikita@frecency.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
somebody1234 2023-03-31 22:49:34 +10:00 committed by GitHub
parent db87d534e9
commit 0aa7d7ee4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1476 additions and 1547 deletions

View File

@ -1,8 +1,9 @@
overrides: overrides:
- files: - files:
- "*.[j|t]s" - "*.[j|t]s"
- "*.mjs" - "*.[j|t]sx"
- "*.cjs" - "*.m[j|t]s"
- "*.c[j|t]s"
options: options:
printWidth: 100 printWidth: 100
tabWidth: 4 tabWidth: 4

View File

@ -153,10 +153,19 @@ class Main {
* and one for the desktop. Once these are merged, we can't hardcode the * and one for the desktop. Once these are merged, we can't hardcode the
* platform here, and need to detect it from the environment. */ * platform here, and need to detect it from the environment. */
const platform = authentication.Platform.desktop const platform = authentication.Platform.desktop
/** FIXME [PB]: https://github.com/enso-org/cloud-v2/issues/366
* React hooks rerender themselves multiple times. It is resulting in multiple
* Enso main scene being initialized. As a temporary workaround we check whether
* appInstance was already ran. Target solution should move running appInstance
* where it will be called only once. */
let appInstanceRan = false
const onAuthenticated = () => { const onAuthenticated = () => {
hideAuth() hideAuth()
if (!appInstanceRan) {
appInstanceRan = true
void appInstance.run() void appInstance.run()
} }
}
authentication.run(logger, platform, onAuthenticated) authentication.run(logger, platform, onAuthenticated)
} else { } else {
void appInstance.run() void appInstance.run()

View File

@ -3,10 +3,10 @@
* For example, this file contains the {@link SvgIcon} component, which is used by the * For example, this file contains the {@link SvgIcon} component, which is used by the
* `Registration` and `Login` components. */ * `Registration` and `Login` components. */
import * as fontawesome from "@fortawesome/react-fontawesome"; import * as fontawesome from '@fortawesome/react-fontawesome'
import * as fontawesomeIcons from "@fortawesome/free-brands-svg-icons"; import * as fontawesomeIcons from '@fortawesome/free-brands-svg-icons'
import * as icons from "../../components/svg"; import * as icons from '../../components/svg'
// ============= // =============
// === Input === // === Input ===
@ -17,11 +17,11 @@ export function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {
<input <input
{...props} {...props}
className={ className={
"text-sm sm:text-base placeholder-gray-500 pl-10 pr-4 rounded-lg border border-gray-400 " + 'text-sm sm:text-base placeholder-gray-500 pl-10 pr-4 rounded-lg border border-gray-400 ' +
"w-full py-2 focus:outline-none focus:border-blue-400" 'w-full py-2 focus:outline-none focus:border-blue-400'
} }
/> />
); )
} }
// =============== // ===============
@ -29,22 +29,22 @@ export function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {
// =============== // ===============
interface SvgIconProps { interface SvgIconProps {
data: string; data: string
} }
export function SvgIcon(props: SvgIconProps) { export function SvgIcon(props: SvgIconProps) {
return ( return (
<div <div
className={ className={
"inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 " + 'inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 ' +
"text-gray-400" 'text-gray-400'
} }
> >
<span> <span>
<icons.Svg {...props} /> <icons.Svg {...props} />
</span> </span>
</div> </div>
); )
} }
// ======================= // =======================
@ -52,18 +52,18 @@ export function SvgIcon(props: SvgIconProps) {
// ======================= // =======================
interface FontAwesomeIconProps { interface FontAwesomeIconProps {
icon: fontawesomeIcons.IconDefinition; icon: fontawesomeIcons.IconDefinition
} }
export function FontAwesomeIcon(props: FontAwesomeIconProps) { export function FontAwesomeIcon(props: FontAwesomeIconProps) {
return ( return (
<span <span
className={ className={
"absolute left-0 top-0 flex items-center justify-center h-full w-10 " + 'absolute left-0 top-0 flex items-center justify-center h-full w-10 ' +
"text-blue-500" 'text-blue-500'
} }
> >
<fontawesome.FontAwesomeIcon icon={props.icon} /> <fontawesome.FontAwesomeIcon icon={props.icon} />
</span> </span>
); )
} }

View File

@ -1,62 +1,60 @@
/** @file Registration confirmation page for when a user clicks the confirmation link set to their /** @file Registration confirmation page for when a user clicks the confirmation link set to their
* email address. */ * email address. */
import * as react from "react"; import * as react from 'react'
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import toast from "react-hot-toast"; import toast from 'react-hot-toast'
import * as app from "../../components/app"; import * as app from '../../components/app'
import * as auth from "../providers/auth"; import * as auth from '../providers/auth'
import * as loggerProvider from "../../providers/logger"; import * as loggerProvider from '../../providers/logger'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
const REGISTRATION_QUERY_PARAMS = { const REGISTRATION_QUERY_PARAMS = {
verificationCode: "verification_code", verificationCode: 'verification_code',
email: "email", email: 'email',
} as const; } as const
// ============================ // ============================
// === Confirm Registration === // === Confirm Registration ===
// ============================ // ============================
function ConfirmRegistration() { function ConfirmRegistration() {
const logger = loggerProvider.useLogger(); const logger = loggerProvider.useLogger()
const { confirmSignUp } = auth.useAuth(); const { confirmSignUp } = auth.useAuth()
const { search } = router.useLocation(); const { search } = router.useLocation()
const navigate = router.useNavigate(); const navigate = router.useNavigate()
const { verificationCode, email } = parseUrlSearchParams(search); const { verificationCode, email } = parseUrlSearchParams(search)
react.useEffect(() => { react.useEffect(() => {
if (!email || !verificationCode) { if (!email || !verificationCode) {
navigate(app.LOGIN_PATH); navigate(app.LOGIN_PATH)
} else { } else {
confirmSignUp(email, verificationCode) confirmSignUp(email, verificationCode)
.then(() => { .then(() => {
navigate(app.LOGIN_PATH + search.toString()); navigate(app.LOGIN_PATH + search.toString())
}) })
.catch((error) => { .catch(error => {
logger.error("Error while confirming sign-up", error); logger.error('Error while confirming sign-up', error)
toast.error( toast.error(
"Something went wrong! Please try again or contact the administrators." 'Something went wrong! Please try again or contact the administrators.'
); )
navigate(app.LOGIN_PATH); navigate(app.LOGIN_PATH)
}); })
} }
}, []); }, [])
return <></>; return <></>
} }
function parseUrlSearchParams(search: string) { function parseUrlSearchParams(search: string) {
const query = new URLSearchParams(search); const query = new URLSearchParams(search)
const verificationCode = query.get( const verificationCode = query.get(REGISTRATION_QUERY_PARAMS.verificationCode)
REGISTRATION_QUERY_PARAMS.verificationCode const email = query.get(REGISTRATION_QUERY_PARAMS.email)
); return { verificationCode, email }
const email = query.get(REGISTRATION_QUERY_PARAMS.email);
return { verificationCode, email };
} }
export default ConfirmRegistration; export default ConfirmRegistration

View File

@ -1,29 +1,29 @@
/** @file Container responsible for rendering and interactions in first half of forgot password /** @file Container responsible for rendering and interactions in first half of forgot password
* flow. */ * flow. */
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import * as app from "../../components/app"; import * as app from '../../components/app'
import * as auth from "../providers/auth"; import * as auth from '../providers/auth'
import * as common from "./common"; import * as common from './common'
import * as hooks from "../../hooks"; import * as hooks from '../../hooks'
import * as icons from "../../components/svg"; import * as icons from '../../components/svg'
import * as utils from "../../utils"; import * as utils from '../../utils'
// ====================== // ======================
// === ForgotPassword === // === ForgotPassword ===
// ====================== // ======================
function ForgotPassword() { function ForgotPassword() {
const { forgotPassword } = auth.useAuth(); const { forgotPassword } = auth.useAuth()
const [email, bindEmail] = hooks.useInput(""); const [email, bindEmail] = hooks.useInput('')
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300"> <div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
<div <div
className={ className={
"flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full " + 'flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full ' +
"max-w-md" 'max-w-md'
} }
> >
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800"> <div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
@ -32,7 +32,7 @@ function ForgotPassword() {
<div className="mt-10"> <div className="mt-10">
<form <form
onSubmit={utils.handleEvent(async () => { onSubmit={utils.handleEvent(async () => {
await forgotPassword(email); await forgotPassword(email)
})} })}
> >
<div className="flex flex-col mb-6"> <div className="flex flex-col mb-6">
@ -58,9 +58,9 @@ function ForgotPassword() {
<button <button
type="submit" type="submit"
className={ className={
"flex items-center justify-center focus:outline-none text-white text-sm " + 'flex items-center justify-center focus:outline-none text-white text-sm ' +
"sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition " + 'sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
"duration-150 ease-in" 'duration-150 ease-in'
} }
> >
<span className="mr-2 uppercase">Send link</span> <span className="mr-2 uppercase">Send link</span>
@ -75,8 +75,8 @@ function ForgotPassword() {
<router.Link <router.Link
to={app.LOGIN_PATH} to={app.LOGIN_PATH}
className={ className={
"inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs " + 'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs ' +
"text-center" 'text-center'
} }
> >
<span> <span>
@ -87,7 +87,7 @@ function ForgotPassword() {
</div> </div>
</div> </div>
</div> </div>
); )
} }
export default ForgotPassword; export default ForgotPassword

View File

@ -1,62 +1,54 @@
/** @file Login component responsible for rendering and interactions in sign in flow. */ /** @file Login component responsible for rendering and interactions in sign in flow. */
import * as fontawesomeIcons from "@fortawesome/free-brands-svg-icons"; import * as fontawesomeIcons from '@fortawesome/free-brands-svg-icons'
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import * as app from "../../components/app"; import * as app from '../../components/app'
import * as auth from "../providers/auth"; import * as auth from '../providers/auth'
import * as common from "./common"; import * as common from './common'
import * as hooks from "../../hooks"; import * as hooks from '../../hooks'
import * as icons from "../../components/svg"; import * as icons from '../../components/svg'
import * as utils from "../../utils"; import * as utils from '../../utils'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
const BUTTON_CLASS_NAME = const BUTTON_CLASS_NAME =
"relative mt-6 border rounded-md py-2 text-sm text-gray-800 " + 'relative mt-6 border rounded-md py-2 text-sm text-gray-800 ' + 'bg-gray-100 hover:bg-gray-200'
"bg-gray-100 hover:bg-gray-200";
const LOGIN_QUERY_PARAMS = { const LOGIN_QUERY_PARAMS = {
email: "email", email: 'email',
} as const; } as const
// ============= // =============
// === Login === // === Login ===
// ============= // =============
function Login() { function Login() {
const { search } = router.useLocation(); const { search } = router.useLocation()
const { signInWithGoogle, signInWithGitHub, signInWithPassword } = const { signInWithGoogle, signInWithGitHub, signInWithPassword } = auth.useAuth()
auth.useAuth();
const initialEmail = parseUrlSearchParams(search); const initialEmail = parseUrlSearchParams(search)
const [email, bindEmail] = hooks.useInput(initialEmail ?? ""); const [email, bindEmail] = hooks.useInput(initialEmail ?? '')
const [password, bindPassword] = hooks.useInput(""); const [password, bindPassword] = hooks.useInput('')
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300"> <div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
<div <div
className={ className={
"flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md " + 'flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md ' +
"w-full max-w-md" 'w-full max-w-md'
} }
> >
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800"> <div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
Login To Your Account Login To Your Account
</div> </div>
<button <button onClick={utils.handleEvent(signInWithGoogle)} className={BUTTON_CLASS_NAME}>
onClick={utils.handleEvent(signInWithGoogle)}
className={BUTTON_CLASS_NAME}
>
<common.FontAwesomeIcon icon={fontawesomeIcons.faGithub} /> <common.FontAwesomeIcon icon={fontawesomeIcons.faGithub} />
<span>Login with Google</span> <span>Login with Google</span>
</button> </button>
<button <button onClick={utils.handleEvent(signInWithGitHub)} className={BUTTON_CLASS_NAME}>
onClick={utils.handleEvent(signInWithGitHub)}
className={BUTTON_CLASS_NAME}
>
<common.FontAwesomeIcon icon={fontawesomeIcons.faGithub} /> <common.FontAwesomeIcon icon={fontawesomeIcons.faGithub} />
<span>Login with Github</span> <span>Login with Github</span>
</button> </button>
@ -129,9 +121,9 @@ function Login() {
<button <button
type="submit" type="submit"
className={ className={
"flex items-center justify-center focus:outline-none text-white " + 'flex items-center justify-center focus:outline-none text-white ' +
"text-sm sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full " + 'text-sm sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full ' +
"transition duration-150 ease-in" 'transition duration-150 ease-in'
} }
> >
<span className="mr-2 uppercase">Login</span> <span className="mr-2 uppercase">Login</span>
@ -146,8 +138,8 @@ function Login() {
<router.Link <router.Link
to={app.REGISTRATION_PATH} to={app.REGISTRATION_PATH}
className={ className={
"inline-flex items-center font-bold text-blue-500 hover:text-blue-700 " + 'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 ' +
"text-xs text-center" 'text-xs text-center'
} }
> >
<span> <span>
@ -158,13 +150,13 @@ function Login() {
</div> </div>
</div> </div>
</div> </div>
); )
} }
function parseUrlSearchParams(search: string) { function parseUrlSearchParams(search: string) {
const query = new URLSearchParams(search); const query = new URLSearchParams(search)
const email = query.get(LOGIN_QUERY_PARAMS.email); const email = query.get(LOGIN_QUERY_PARAMS.email)
return email; return email
} }
export default Login; export default Login

View File

@ -1,40 +1,40 @@
/** @file Registration container responsible for rendering and interactions in sign up flow. */ /** @file Registration container responsible for rendering and interactions in sign up flow. */
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import toast from "react-hot-toast"; import toast from 'react-hot-toast'
import * as app from "../../components/app"; import * as app from '../../components/app'
import * as auth from "../providers/auth"; import * as auth from '../providers/auth'
import * as common from "./common"; import * as common from './common'
import * as hooks from "../../hooks"; import * as hooks from '../../hooks'
import * as icons from "../../components/svg"; import * as icons from '../../components/svg'
import * as utils from "../../utils"; import * as utils from '../../utils'
// ==================== // ====================
// === Registration === // === Registration ===
// ==================== // ====================
function Registration() { function Registration() {
const { signUp } = auth.useAuth(); const { signUp } = auth.useAuth()
const [email, bindEmail] = hooks.useInput(""); const [email, bindEmail] = hooks.useInput('')
const [password, bindPassword] = hooks.useInput(""); const [password, bindPassword] = hooks.useInput('')
const [confirmPassword, bindConfirmPassword] = hooks.useInput(""); const [confirmPassword, bindConfirmPassword] = hooks.useInput('')
const handleSubmit = () => { const handleSubmit = () => {
/** The password & confirm password fields must match. */ /** The password & confirm password fields must match. */
if (password !== confirmPassword) { if (password !== confirmPassword) {
toast.error("Passwords do not match."); toast.error('Passwords do not match.')
return Promise.resolve(); return Promise.resolve()
} else { } else {
return signUp(email, password); return signUp(email, password)
}
} }
};
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-300 px-4 py-8"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-300 px-4 py-8">
<div <div
className={ className={
"rounded-md bg-white w-full max-w-sm sm:max-w-md border border-gray-200 " + 'rounded-md bg-white w-full max-w-sm sm:max-w-md border border-gray-200 ' +
"shadow-md px-4 py-6 sm:p-8" 'shadow-md px-4 py-6 sm:p-8'
} }
> >
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800"> <div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
@ -104,9 +104,9 @@ function Registration() {
<button <button
type="submit" type="submit"
className={ className={
"flex items-center justify-center focus:outline-none text-white text-sm " + 'flex items-center justify-center focus:outline-none text-white text-sm ' +
"sm:text-base bg-indigo-600 hover:bg-indigo-700 rounded py-2 w-full transition " + 'sm:text-base bg-indigo-600 hover:bg-indigo-700 rounded py-2 w-full transition ' +
"duration-150 ease-in" 'duration-150 ease-in'
} }
> >
<span className="mr-2 uppercase">Register</span> <span className="mr-2 uppercase">Register</span>
@ -121,8 +121,8 @@ function Registration() {
<router.Link <router.Link
to={app.LOGIN_PATH} to={app.LOGIN_PATH}
className={ className={
"inline-flex items-center font-bold text-indigo-500 hover:text-indigo-700 " + 'inline-flex items-center font-bold text-indigo-500 hover:text-indigo-700 ' +
"text-sm text-center" 'text-sm text-center'
} }
> >
<span> <span>
@ -132,7 +132,7 @@ function Registration() {
</router.Link> </router.Link>
</div> </div>
</div> </div>
); )
} }
export default Registration; export default Registration

View File

@ -1,55 +1,54 @@
/** @file Container responsible for rendering and interactions in second half of forgot password /** @file Container responsible for rendering and interactions in second half of forgot password
* flow. */ * flow. */
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import toast from "react-hot-toast"; import toast from 'react-hot-toast'
import * as app from "../../components/app"; import * as app from '../../components/app'
import * as auth from "../providers/auth"; import * as auth from '../providers/auth'
import * as common from "./common"; import * as common from './common'
import * as hooks from "../../hooks"; import * as hooks from '../../hooks'
import * as icons from "../../components/svg"; import * as icons from '../../components/svg'
import * as utils from "../../utils"; import * as utils from '../../utils'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
const RESET_PASSWORD_QUERY_PARAMS = { const RESET_PASSWORD_QUERY_PARAMS = {
email: "email", email: 'email',
verificationCode: "verification_code", verificationCode: 'verification_code',
} as const; } as const
// ===================== // =====================
// === ResetPassword === // === ResetPassword ===
// ===================== // =====================
function ResetPassword() { function ResetPassword() {
const { resetPassword } = auth.useAuth(); const { resetPassword } = auth.useAuth()
const { search } = router.useLocation(); const { search } = router.useLocation()
const { verificationCode: initialCode, email: initialEmail } = const { verificationCode: initialCode, email: initialEmail } = parseUrlSearchParams(search)
parseUrlSearchParams(search);
const [email, bindEmail] = hooks.useInput(initialEmail ?? ""); const [email, bindEmail] = hooks.useInput(initialEmail ?? '')
const [code, bindCode] = hooks.useInput(initialCode ?? ""); const [code, bindCode] = hooks.useInput(initialCode ?? '')
const [newPassword, bindNewPassword] = hooks.useInput(""); const [newPassword, bindNewPassword] = hooks.useInput('')
const [newPasswordConfirm, bindNewPasswordConfirm] = hooks.useInput(""); const [newPasswordConfirm, bindNewPasswordConfirm] = hooks.useInput('')
const handleSubmit = () => { const handleSubmit = () => {
if (newPassword !== newPasswordConfirm) { if (newPassword !== newPasswordConfirm) {
toast.error("Passwords do not match"); toast.error('Passwords do not match')
return Promise.resolve(); return Promise.resolve()
} else { } else {
return resetPassword(email, code, newPassword); return resetPassword(email, code, newPassword)
}
} }
};
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300"> <div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
<div <div
className={ className={
"flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full " + 'flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full ' +
"max-w-md" 'max-w-md'
} }
> >
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800"> <div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
@ -137,9 +136,9 @@ function ResetPassword() {
<button <button
type="submit" type="submit"
className={ className={
"flex items-center justify-center focus:outline-none text-white text-sm " + 'flex items-center justify-center focus:outline-none text-white text-sm ' +
"sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition " + 'sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
"duration-150 ease-in" 'duration-150 ease-in'
} }
> >
<span className="mr-2 uppercase">Reset</span> <span className="mr-2 uppercase">Reset</span>
@ -154,8 +153,8 @@ function ResetPassword() {
<router.Link <router.Link
to={app.LOGIN_PATH} to={app.LOGIN_PATH}
className={ className={
"inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs " + 'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs ' +
"text-center" 'text-center'
} }
> >
<span> <span>
@ -166,16 +165,14 @@ function ResetPassword() {
</div> </div>
</div> </div>
</div> </div>
); )
} }
function parseUrlSearchParams(search: string) { function parseUrlSearchParams(search: string) {
const query = new URLSearchParams(search); const query = new URLSearchParams(search)
const verificationCode = query.get( const verificationCode = query.get(RESET_PASSWORD_QUERY_PARAMS.verificationCode)
RESET_PASSWORD_QUERY_PARAMS.verificationCode const email = query.get(RESET_PASSWORD_QUERY_PARAMS.email)
); return { verificationCode, email }
const email = query.get(RESET_PASSWORD_QUERY_PARAMS.email);
return { verificationCode, email };
} }
export default ResetPassword; export default ResetPassword

View File

@ -1,28 +1,28 @@
/** @file Container responsible for rendering and interactions in setting username flow, after /** @file Container responsible for rendering and interactions in setting username flow, after
* registration. */ * registration. */
import * as auth from "../providers/auth"; import * as auth from '../providers/auth'
import * as common from "./common"; import * as common from './common'
import * as hooks from "../../hooks"; import * as hooks from '../../hooks'
import * as icons from "../../components/svg"; import * as icons from '../../components/svg'
import * as utils from "../../utils"; import * as utils from '../../utils'
// =================== // ===================
// === SetUsername === // === SetUsername ===
// =================== // ===================
function SetUsername() { function SetUsername() {
const { setUsername } = auth.useAuth(); const { setUsername } = auth.useAuth()
const { accessToken, email } = auth.usePartialUserSession(); const { accessToken, email } = auth.usePartialUserSession()
const [username, bindUsername] = hooks.useInput(""); const [username, bindUsername] = hooks.useInput('')
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300"> <div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
<div <div
className={ className={
"flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full " + 'flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full ' +
"max-w-md" 'max-w-md'
} }
> >
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800"> <div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
@ -51,9 +51,9 @@ function SetUsername() {
<button <button
type="submit" type="submit"
className={ className={
"flex items-center justify-center focus:outline-none text-white text-sm " + 'flex items-center justify-center focus:outline-none text-white text-sm ' +
"sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition " + 'sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
"duration-150 ease-in" 'duration-150 ease-in'
} }
> >
<span className="mr-2 uppercase">Set username</span> <span className="mr-2 uppercase">Set username</span>
@ -66,7 +66,7 @@ function SetUsername() {
</div> </div>
</div> </div>
</div> </div>
); )
} }
export default SetUsername; export default SetUsername

View File

@ -3,14 +3,14 @@
* Listening to authentication events is necessary to update the authentication state of the * Listening to authentication events is necessary to update the authentication state of the
* application. For example, if the user signs out, we want to clear the authentication state so * application. For example, if the user signs out, we want to clear the authentication state so
* that the login screen is rendered. */ * that the login screen is rendered. */
import * as amplify from "@aws-amplify/core"; import * as amplify from '@aws-amplify/core'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
/** Name of the string identifying the "hub" that AWS Amplify issues authentication events on. */ /** Name of the string identifying the "hub" that AWS Amplify issues authentication events on. */
const AUTHENTICATION_HUB = "auth"; const AUTHENTICATION_HUB = 'auth'
// ================= // =================
// === AuthEvent === // === AuthEvent ===
@ -22,18 +22,18 @@ const AUTHENTICATION_HUB = "auth";
* when the user signs in or signs out by accessing a page like `enso://auth?code=...&state=...`. */ * when the user signs in or signs out by accessing a page like `enso://auth?code=...&state=...`. */
export enum AuthEvent { export enum AuthEvent {
/** Issued when the user has passed custom OAuth state parameters to some other auth event. */ /** Issued when the user has passed custom OAuth state parameters to some other auth event. */
customOAuthState = "customOAuthState", customOAuthState = 'customOAuthState',
/** Issued when the user completes the sign-in process (via federated identity provider). */ /** Issued when the user completes the sign-in process (via federated identity provider). */
cognitoHostedUi = "cognitoHostedUI", cognitoHostedUi = 'cognitoHostedUI',
/** Issued when the user completes the sign-in process (via email/password). */ /** Issued when the user completes the sign-in process (via email/password). */
signIn = "signIn", signIn = 'signIn',
/** Issued when the user signs out. */ /** Issued when the user signs out. */
signOut = "signOut", signOut = 'signOut',
} }
/** Returns `true` if the given `string` is an {@link AuthEvent}. */ /** Returns `true` if the given `string` is an {@link AuthEvent}. */
function isAuthEvent(value: string): value is AuthEvent { function isAuthEvent(value: string): value is AuthEvent {
return Object.values<string>(AuthEvent).includes(value); return Object.values<string>(AuthEvent).includes(value)
} }
// ================================= // =================================
@ -43,26 +43,24 @@ function isAuthEvent(value: string): value is AuthEvent {
/** Callback called in response to authentication state changes. /** Callback called in response to authentication state changes.
* *
* @see {@link Api["listen"]} */ * @see {@link Api["listen"]} */
export type ListenerCallback = (event: AuthEvent, data?: unknown) => void; export type ListenerCallback = (event: AuthEvent, data?: unknown) => void
/** Unsubscribes the {@link ListenerCallback} from authentication state changes. /** Unsubscribes the {@link ListenerCallback} from authentication state changes.
* *
* @see {@link Api["listen"]} */ * @see {@link Api["listen"]} */
type UnsubscribeFunction = () => void; type UnsubscribeFunction = () => void
/** Used to subscribe to {@link AuthEvent}s. /** Used to subscribe to {@link AuthEvent}s.
* *
* Returns a function that MUST be called before re-subscribing, * Returns a function that MUST be called before re-subscribing,
* to avoid memory leaks or duplicate event handlers. */ * to avoid memory leaks or duplicate event handlers. */
export type ListenFunction = ( export type ListenFunction = (listener: ListenerCallback) => UnsubscribeFunction
listener: ListenerCallback
) => UnsubscribeFunction;
export function registerAuthEventListener(listener: ListenerCallback) { export function registerAuthEventListener(listener: ListenerCallback) {
const callback: amplify.HubCallback = (data) => { const callback: amplify.HubCallback = data => {
if (isAuthEvent(data.payload.event)) { if (isAuthEvent(data.payload.event)) {
listener(data.payload.event, data.payload.data); listener(data.payload.event, data.payload.data)
} }
}; }
return amplify.Hub.listen(AUTHENTICATION_HUB, callback); return amplify.Hub.listen(AUTHENTICATION_HUB, callback)
} }

View File

@ -3,31 +3,31 @@
* Provides an `AuthProvider` component that wraps the entire application, and a `useAuth` hook that * Provides an `AuthProvider` component that wraps the entire application, and a `useAuth` hook that
* can be used from any React component to access the currently logged-in user's session data. The * can be used from any React component to access the currently logged-in user's session data. The
* hook also provides methods for registering a user, logging in, logging out, etc. */ * hook also provides methods for registering a user, logging in, logging out, etc. */
import * as react from "react"; import * as react from 'react'
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import toast from "react-hot-toast"; import toast from 'react-hot-toast'
import * as app from "../../components/app"; import * as app from '../../components/app'
import * as authServiceModule from "../service"; import * as authServiceModule from '../service'
import * as backendService from "../../dashboard/service"; import * as backendService from '../../dashboard/service'
import * as errorModule from "../../error"; import * as errorModule from '../../error'
import * as loggerProvider from "../../providers/logger"; import * as loggerProvider from '../../providers/logger'
import * as sessionProvider from "./session"; import * as sessionProvider from './session'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
const MESSAGES = { const MESSAGES = {
signUpSuccess: "We have sent you an email with further instructions!", signUpSuccess: 'We have sent you an email with further instructions!',
confirmSignUpSuccess: "Your account has been confirmed! Please log in.", confirmSignUpSuccess: 'Your account has been confirmed! Please log in.',
setUsernameSuccess: "Your username has been set!", setUsernameSuccess: 'Your username has been set!',
signInWithPasswordSuccess: "Successfully logged in!", signInWithPasswordSuccess: 'Successfully logged in!',
forgotPasswordSuccess: "We have sent you an email with further instructions!", forgotPasswordSuccess: 'We have sent you an email with further instructions!',
resetPasswordSuccess: "Successfully reset password!", resetPasswordSuccess: 'Successfully reset password!',
signOutSuccess: "Successfully logged out!", signOutSuccess: 'Successfully logged out!',
pleaseWait: "Please wait...", pleaseWait: 'Please wait...',
} as const; } as const
// ============= // =============
// === Types === // === Types ===
@ -35,19 +35,19 @@ const MESSAGES = {
// === UserSession === // === UserSession ===
export type UserSession = FullUserSession | PartialUserSession; export type UserSession = FullUserSession | PartialUserSession
/** Object containing the currently signed-in user's session data. */ /** Object containing the currently signed-in user's session data. */
export interface FullUserSession { export interface FullUserSession {
/** A discriminator for TypeScript to be able to disambiguate between this interface and other /** A discriminator for TypeScript to be able to disambiguate between this interface and other
* `UserSession` variants. */ * `UserSession` variants. */
variant: "full"; variant: 'full'
/** User's JSON Web Token (JWT), used for authenticating and authorizing requests to the API. */ /** User's JSON Web Token (JWT), used for authenticating and authorizing requests to the API. */
accessToken: string; accessToken: string
/** User's email address. */ /** User's email address. */
email: string; email: string
/** User's organization information. */ /** User's organization information. */
organization: backendService.Organization; organization: backendService.Organization
} }
/** Object containing the currently signed-in user's session data, if the user has not yet set their /** Object containing the currently signed-in user's session data, if the user has not yet set their
@ -59,11 +59,11 @@ export interface FullUserSession {
export interface PartialUserSession { export interface PartialUserSession {
/** A discriminator for TypeScript to be able to disambiguate between this interface and other /** A discriminator for TypeScript to be able to disambiguate between this interface and other
* `UserSession` variants. */ * `UserSession` variants. */
variant: "partial"; variant: 'partial'
/** User's JSON Web Token (JWT), used for authenticating and authorizing requests to the API. */ /** User's JSON Web Token (JWT), used for authenticating and authorizing requests to the API. */
accessToken: string; accessToken: string
/** User's email address. */ /** User's email address. */
email: string; email: string
} }
// =================== // ===================
@ -78,27 +78,19 @@ export interface PartialUserSession {
* *
* See {@link Cognito} for details on each of the authentication functions. */ * See {@link Cognito} for details on each of the authentication functions. */
interface AuthContextType { interface AuthContextType {
signUp: (email: string, password: string) => Promise<void>; signUp: (email: string, password: string) => Promise<void>
confirmSignUp: (email: string, code: string) => Promise<void>; confirmSignUp: (email: string, code: string) => Promise<void>
setUsername: ( setUsername: (accessToken: string, username: string, email: string) => Promise<void>
accessToken: string, signInWithGoogle: () => Promise<void>
username: string, signInWithGitHub: () => Promise<void>
email: string signInWithPassword: (email: string, password: string) => Promise<void>
) => Promise<void>; forgotPassword: (email: string) => Promise<void>
signInWithGoogle: () => Promise<void>; resetPassword: (email: string, code: string, password: string) => Promise<void>
signInWithGitHub: () => Promise<void>; signOut: () => Promise<void>
signInWithPassword: (email: string, password: string) => Promise<void>;
forgotPassword: (email: string) => Promise<void>;
resetPassword: (
email: string,
code: string,
password: string
) => Promise<void>;
signOut: () => Promise<void>;
/** Session containing the currently authenticated user's authentication information. /** Session containing the currently authenticated user's authentication information.
* *
* If the user has not signed in, the session will be `null`. */ * If the user has not signed in, the session will be `null`. */
session: UserSession | null; session: UserSession | null
} }
// Eslint doesn't like headings. // Eslint doesn't like headings.
@ -126,30 +118,28 @@ interface AuthContextType {
* So changing the cast would provide no safety guarantees, and would require us to introduce null * So changing the cast would provide no safety guarantees, and would require us to introduce null
* checks everywhere we use the context. */ * checks everywhere we use the context. */
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
const AuthContext = react.createContext<AuthContextType>({} as AuthContextType); const AuthContext = react.createContext<AuthContextType>({} as AuthContextType)
// ==================== // ====================
// === AuthProvider === // === AuthProvider ===
// ==================== // ====================
export interface AuthProviderProps { export interface AuthProviderProps {
authService: authServiceModule.AuthService; authService: authServiceModule.AuthService
/** Callback to execute once the user has authenticated successfully. */ /** Callback to execute once the user has authenticated successfully. */
onAuthenticated: () => void; onAuthenticated: () => void
children: react.ReactNode; children: react.ReactNode
} }
export function AuthProvider(props: AuthProviderProps) { export function AuthProvider(props: AuthProviderProps) {
const { authService, children } = props; const { authService, children } = props
const { cognito } = authService; const { cognito } = authService
const { session } = sessionProvider.useSession(); const { session } = sessionProvider.useSession()
const logger = loggerProvider.useLogger(); const logger = loggerProvider.useLogger()
const navigate = router.useNavigate(); const navigate = router.useNavigate()
const onAuthenticated = react.useCallback(props.onAuthenticated, []); const onAuthenticated = react.useCallback(props.onAuthenticated, [])
const [initialized, setInitialized] = react.useState(false); const [initialized, setInitialized] = react.useState(false)
const [userSession, setUserSession] = react.useState<UserSession | null>( const [userSession, setUserSession] = react.useState<UserSession | null>(null)
null
);
/** Fetch the JWT access token from the session via the AWS Amplify library. /** Fetch the JWT access token from the session via the AWS Amplify library.
* *
@ -159,139 +149,135 @@ export function AuthProvider(props: AuthProviderProps) {
react.useEffect(() => { react.useEffect(() => {
const fetchSession = async () => { const fetchSession = async () => {
if (session.none) { if (session.none) {
setInitialized(true); setInitialized(true)
setUserSession(null); setUserSession(null)
} else { } else {
const { accessToken, email } = session.val; const { accessToken, email } = session.val
const backend = backendService.createBackend(accessToken, logger); const backend = backendService.createBackend(accessToken, logger)
const organization = await backend.getUser(); const organization = await backend.getUser()
let newUserSession: UserSession; let newUserSession: UserSession
if (!organization) { if (!organization) {
newUserSession = { newUserSession = {
variant: "partial", variant: 'partial',
email, email,
accessToken, accessToken,
}; }
} else { } else {
newUserSession = { newUserSession = {
variant: "full", variant: 'full',
email, email,
accessToken, accessToken,
organization, organization,
}; }
/** Execute the callback that should inform the Electron app that the user has logged in. /** Execute the callback that should inform the Electron app that the user has logged in.
* This is done to transition the app from the authentication/dashboard view to the IDE. */ * This is done to transition the app from the authentication/dashboard view to the IDE. */
onAuthenticated(); onAuthenticated()
} }
setUserSession(newUserSession); setUserSession(newUserSession)
setInitialized(true); setInitialized(true)
}
} }
};
fetchSession().catch((error) => { fetchSession().catch(error => {
if (isUserFacingError(error)) { if (isUserFacingError(error)) {
toast.error(error.message); toast.error(error.message)
} else { } else {
logger.error(error); logger.error(error)
} }
}); })
}, [session]); }, [session])
const withLoadingToast = const withLoadingToast =
<T extends unknown[]>(action: (...args: T) => Promise<void>) => <T extends unknown[]>(action: (...args: T) => Promise<void>) =>
async (...args: T) => { async (...args: T) => {
const loadingToast = toast.loading(MESSAGES.pleaseWait); const loadingToast = toast.loading(MESSAGES.pleaseWait)
try { try {
await action(...args); await action(...args)
} finally { } finally {
toast.dismiss(loadingToast); toast.dismiss(loadingToast)
}
} }
};
const signUp = (username: string, password: string) => const signUp = (username: string, password: string) =>
cognito.signUp(username, password).then((result) => { cognito.signUp(username, password).then(result => {
if (result.ok) { if (result.ok) {
toast.success(MESSAGES.signUpSuccess); toast.success(MESSAGES.signUpSuccess)
} else { } else {
toast.error(result.val.message); toast.error(result.val.message)
} }
}); })
const confirmSignUp = async (email: string, code: string) => const confirmSignUp = async (email: string, code: string) =>
cognito.confirmSignUp(email, code).then((result) => { cognito.confirmSignUp(email, code).then(result => {
if (result.err) { if (result.err) {
switch (result.val.kind) { switch (result.val.kind) {
case "UserAlreadyConfirmed": case 'UserAlreadyConfirmed':
break; break
default: default:
throw new errorModule.UnreachableCaseError(result.val.kind); throw new errorModule.UnreachableCaseError(result.val.kind)
} }
} }
toast.success(MESSAGES.confirmSignUpSuccess); toast.success(MESSAGES.confirmSignUpSuccess)
navigate(app.LOGIN_PATH); navigate(app.LOGIN_PATH)
}); })
const signInWithPassword = async (email: string, password: string) => const signInWithPassword = async (email: string, password: string) =>
cognito.signInWithPassword(email, password).then((result) => { cognito.signInWithPassword(email, password).then(result => {
if (result.ok) { if (result.ok) {
toast.success(MESSAGES.signInWithPasswordSuccess); toast.success(MESSAGES.signInWithPasswordSuccess)
} else { } else {
if (result.val.kind === "UserNotFound") { if (result.val.kind === 'UserNotFound') {
navigate(app.REGISTRATION_PATH); navigate(app.REGISTRATION_PATH)
} }
toast.error(result.val.message); toast.error(result.val.message)
} }
}); })
const setUsername = async ( const setUsername = async (accessToken: string, username: string, email: string) => {
accessToken: string,
username: string,
email: string
) => {
const body: backendService.SetUsernameRequestBody = { const body: backendService.SetUsernameRequestBody = {
userName: username, userName: username,
userEmail: email, userEmail: email,
}; }
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/343 /** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/343
* The API client is reinitialised on every request. That is an inefficient way of usage. * The API client is reinitialised on every request. That is an inefficient way of usage.
* Fix it by using React context and implementing it as a singleton. */ * Fix it by using React context and implementing it as a singleton. */
const backend = backendService.createBackend(accessToken, logger); const backend = backendService.createBackend(accessToken, logger)
await backend.setUsername(body); await backend.setUsername(body)
navigate(app.DASHBOARD_PATH); navigate(app.DASHBOARD_PATH)
toast.success(MESSAGES.setUsernameSuccess); toast.success(MESSAGES.setUsernameSuccess)
}; }
const forgotPassword = async (email: string) => const forgotPassword = async (email: string) =>
cognito.forgotPassword(email).then((result) => { cognito.forgotPassword(email).then(result => {
if (result.ok) { if (result.ok) {
toast.success(MESSAGES.forgotPasswordSuccess); toast.success(MESSAGES.forgotPasswordSuccess)
navigate(app.RESET_PASSWORD_PATH); navigate(app.RESET_PASSWORD_PATH)
} else { } else {
toast.error(result.val.message); toast.error(result.val.message)
} }
}); })
const resetPassword = async (email: string, code: string, password: string) => const resetPassword = async (email: string, code: string, password: string) =>
cognito.forgotPasswordSubmit(email, code, password).then((result) => { cognito.forgotPasswordSubmit(email, code, password).then(result => {
if (result.ok) { if (result.ok) {
toast.success(MESSAGES.resetPasswordSuccess); toast.success(MESSAGES.resetPasswordSuccess)
navigate(app.LOGIN_PATH); navigate(app.LOGIN_PATH)
} else { } else {
toast.error(result.val.message); toast.error(result.val.message)
} }
}); })
const signOut = () => const signOut = () =>
cognito.signOut().then(() => { cognito.signOut().then(() => {
toast.success(MESSAGES.signOutSuccess); toast.success(MESSAGES.signOutSuccess)
}); })
const value = { const value = {
signUp: withLoadingToast(signUp), signUp: withLoadingToast(signUp),
@ -304,14 +290,14 @@ export function AuthProvider(props: AuthProviderProps) {
resetPassword: withLoadingToast(resetPassword), resetPassword: withLoadingToast(resetPassword),
signOut, signOut,
session: userSession, session: userSession,
}; }
return ( return (
<AuthContext.Provider value={value}> <AuthContext.Provider value={value}>
{/* Only render the underlying app after we assert for the presence of a current user. */} {/* Only render the underlying app after we assert for the presence of a current user. */}
{initialized && children} {initialized && children}
</AuthContext.Provider> </AuthContext.Provider>
); )
} }
/** Type of an error containing a `string`-typed `message` field. /** Type of an error containing a `string`-typed `message` field.
@ -320,12 +306,12 @@ export function AuthProvider(props: AuthProviderProps) {
* displayed to the user. */ * displayed to the user. */
interface UserFacingError { interface UserFacingError {
/** The user-facing error message. */ /** The user-facing error message. */
message: string; message: string
} }
/** Returns `true` if the value is a {@link UserFacingError}. */ /** Returns `true` if the value is a {@link UserFacingError}. */
function isUserFacingError(value: unknown): value is UserFacingError { function isUserFacingError(value: unknown): value is UserFacingError {
return typeof value === "object" && value != null && "message" in value; return typeof value === 'object' && value != null && 'message' in value
} }
// =============== // ===============
@ -337,7 +323,7 @@ function isUserFacingError(value: unknown): value is UserFacingError {
* Only the hook is exported, and not the context, because we only want to use the hook directly and * Only the hook is exported, and not the context, because we only want to use the hook directly and
* never the context component. */ * never the context component. */
export function useAuth() { export function useAuth() {
return react.useContext(AuthContext); return react.useContext(AuthContext)
} }
// ======================= // =======================
@ -346,12 +332,12 @@ export function useAuth() {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
export function ProtectedLayout() { export function ProtectedLayout() {
const { session } = useAuth(); const { session } = useAuth()
if (!session) { if (!session) {
return <router.Navigate to={app.LOGIN_PATH} />; return <router.Navigate to={app.LOGIN_PATH} />
} else { } else {
return <router.Outlet context={session} />; return <router.Outlet context={session} />
} }
} }
@ -361,14 +347,14 @@ export function ProtectedLayout() {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
export function GuestLayout() { export function GuestLayout() {
const { session } = useAuth(); const { session } = useAuth()
if (session?.variant === "partial") { if (session?.variant === 'partial') {
return <router.Navigate to={app.SET_USERNAME_PATH} />; return <router.Navigate to={app.SET_USERNAME_PATH} />
} else if (session?.variant === "full") { } else if (session?.variant === 'full') {
return <router.Navigate to={app.DASHBOARD_PATH} />; return <router.Navigate to={app.DASHBOARD_PATH} />
} else { } else {
return <router.Outlet />; return <router.Outlet />
} }
} }
@ -377,7 +363,7 @@ export function GuestLayout() {
// ============================= // =============================
export function usePartialUserSession() { export function usePartialUserSession() {
return router.useOutletContext<PartialUserSession>(); return router.useOutletContext<PartialUserSession>()
} }
// ========================== // ==========================
@ -385,5 +371,5 @@ export function usePartialUserSession() {
// ========================== // ==========================
export function useFullUserSession() { export function useFullUserSession() {
return router.useOutletContext<FullUserSession>(); return router.useOutletContext<FullUserSession>()
} }

View File

@ -1,27 +1,27 @@
/** @file Provider for the {@link SessionContextType}, which contains information about the /** @file Provider for the {@link SessionContextType}, which contains information about the
* currently authenticated user's session. */ * currently authenticated user's session. */
import * as react from "react"; import * as react from 'react'
import * as results from "ts-results"; import * as results from 'ts-results'
import * as cognito from "../cognito"; import * as cognito from '../cognito'
import * as error from "../../error"; import * as error from '../../error'
import * as hooks from "../../hooks"; import * as hooks from '../../hooks'
import * as listen from "../listen"; import * as listen from '../listen'
// ====================== // ======================
// === SessionContext === // === SessionContext ===
// ====================== // ======================
interface SessionContextType { interface SessionContextType {
session: results.Option<cognito.UserSession>; session: results.Option<cognito.UserSession>
} }
/** See {@link AuthContext} for safety details. */ /** See {@link AuthContext} for safety details. */
const SessionContext = react.createContext<SessionContextType>( const SessionContext = react.createContext<SessionContextType>(
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
{} as SessionContextType {} as SessionContextType
); )
// ======================= // =======================
// === SessionProvider === // === SessionProvider ===
@ -39,31 +39,30 @@ interface SessionProviderProps {
* obtained by reading the window location at the time that authentication is instantiated. This * obtained by reading the window location at the time that authentication is instantiated. This
* is guaranteed to be the correct location, since authentication is instantiated when the content * is guaranteed to be the correct location, since authentication is instantiated when the content
* is initially served. */ * is initially served. */
mainPageUrl: URL; mainPageUrl: URL
registerAuthEventListener: listen.ListenFunction; registerAuthEventListener: listen.ListenFunction
userSession: () => Promise<results.Option<cognito.UserSession>>; userSession: () => Promise<results.Option<cognito.UserSession>>
children: react.ReactNode; children: react.ReactNode
} }
export function SessionProvider(props: SessionProviderProps) { export function SessionProvider(props: SessionProviderProps) {
const { mainPageUrl, children, userSession, registerAuthEventListener } = const { mainPageUrl, children, userSession, registerAuthEventListener } = props
props;
/** Flag used to avoid rendering child components until we've fetched the user's session at least /** Flag used to avoid rendering child components until we've fetched the user's session at least
* once. Avoids flash of the login screen when the user is already logged in. */ * once. Avoids flash of the login screen when the user is already logged in. */
const [initialized, setInitialized] = react.useState(false); const [initialized, setInitialized] = react.useState(false)
/** Produces a new object every time. /** Produces a new object every time.
* This is not equal to any other empty object because objects are compared by reference. * This is not equal to any other empty object because objects are compared by reference.
* Because it is not equal to the old value, React re-renders the component. */ * Because it is not equal to the old value, React re-renders the component. */
function newRefresh() { function newRefresh() {
return {}; return {}
} }
/** State that, when set, forces a refresh of the user session. This is useful when a /** State that, when set, forces a refresh of the user session. This is useful when a
* user has just logged in (so their cached credentials are out of date). Should be used via the * user has just logged in (so their cached credentials are out of date). Should be used via the
* `refreshSession` function. */ * `refreshSession` function. */
const [refresh, setRefresh] = react.useState(newRefresh()); const [refresh, setRefresh] = react.useState(newRefresh())
/** Forces a refresh of the user session. /** Forces a refresh of the user session.
* *
@ -71,8 +70,8 @@ export function SessionProvider(props: SessionProviderProps) {
* For example, this should be called after signing out. Calling this will result in a re-render * For example, this should be called after signing out. Calling this will result in a re-render
* of the whole page, which is why it should only be done when necessary. */ * of the whole page, which is why it should only be done when necessary. */
const refreshSession = () => { const refreshSession = () => {
setRefresh(newRefresh()); setRefresh(newRefresh())
}; }
/** Register an async effect that will fetch the user's session whenever the `refresh` state is /** Register an async effect that will fetch the user's session whenever the `refresh` state is
* incremented. This is useful when a user has just logged in (as their cached credentials are * incremented. This is useful when a user has just logged in (as their cached credentials are
@ -80,12 +79,12 @@ export function SessionProvider(props: SessionProviderProps) {
const session = hooks.useAsyncEffect( const session = hooks.useAsyncEffect(
results.None, results.None,
async () => { async () => {
const innerSession = await userSession(); const innerSession = await userSession()
setInitialized(true); setInitialized(true)
return innerSession; return innerSession
}, },
[refresh, userSession] [refresh, userSession]
); )
/** Register an effect that will listen for authentication events. When the event occurs, we /** Register an effect that will listen for authentication events. When the event occurs, we
* will refresh or clear the user's session, forcing a re-render of the page with the new * will refresh or clear the user's session, forcing a re-render of the page with the new
@ -94,12 +93,12 @@ export function SessionProvider(props: SessionProviderProps) {
* For example, if a user clicks the signout button, this will clear the user's session, which * For example, if a user clicks the signout button, this will clear the user's session, which
* means we want the login screen to render (which is a child of this provider). */ * means we want the login screen to render (which is a child of this provider). */
react.useEffect(() => { react.useEffect(() => {
const listener: listen.ListenerCallback = (event) => { const listener: listen.ListenerCallback = event => {
switch (event) { switch (event) {
case listen.AuthEvent.signIn: case listen.AuthEvent.signIn:
case listen.AuthEvent.signOut: { case listen.AuthEvent.signOut: {
refreshSession(); refreshSession()
break; break
} }
case listen.AuthEvent.customOAuthState: case listen.AuthEvent.customOAuthState:
case listen.AuthEvent.cognitoHostedUi: { case listen.AuthEvent.cognitoHostedUi: {
@ -110,30 +109,28 @@ export function SessionProvider(props: SessionProviderProps) {
* *
* See: * See:
* https://github.com/aws-amplify/amplify-js/issues/3391#issuecomment-756473970 */ * https://github.com/aws-amplify/amplify-js/issues/3391#issuecomment-756473970 */
window.history.replaceState({}, "", mainPageUrl); window.history.replaceState({}, '', mainPageUrl)
refreshSession(); refreshSession()
break; break
} }
default: { default: {
throw new error.UnreachableCaseError(event); throw new error.UnreachableCaseError(event)
}
} }
} }
};
const cancel = registerAuthEventListener(listener); const cancel = registerAuthEventListener(listener)
/** Return the `cancel` function from the `useEffect`, which ensures that the listener is /** Return the `cancel` function from the `useEffect`, which ensures that the listener is
* cleaned up between renders. This must be done because the `useEffect` will be called * cleaned up between renders. This must be done because the `useEffect` will be called
* multiple times during the lifetime of the component. */ * multiple times during the lifetime of the component. */
return cancel; return cancel
}, [registerAuthEventListener]); }, [registerAuthEventListener])
const value = { session }; const value = { session }
return ( return (
<SessionContext.Provider value={value}> <SessionContext.Provider value={value}>{initialized && children}</SessionContext.Provider>
{initialized && children} )
</SessionContext.Provider>
);
} }
// ================== // ==================
@ -141,5 +138,5 @@ export function SessionProvider(props: SessionProviderProps) {
// ================== // ==================
export function useSession() { export function useSession() {
return react.useContext(SessionContext); return react.useContext(SessionContext)
} }

View File

@ -1,18 +1,18 @@
/** @file Provides an {@link AuthService} which consists of an underyling {@link Cognito} API /** @file Provides an {@link AuthService} which consists of an underyling {@link Cognito} API
* wrapper, along with some convenience callbacks to make URL redirects for the authentication flows * wrapper, along with some convenience callbacks to make URL redirects for the authentication flows
* work with Electron. */ * work with Electron. */
import * as amplify from "@aws-amplify/auth"; import * as amplify from '@aws-amplify/auth'
import * as common from "enso-common"; import * as common from 'enso-common'
import * as app from "../components/app"; import * as app from '../components/app'
import * as auth from "./config"; import * as auth from './config'
import * as cognito from "./cognito"; import * as cognito from './cognito'
import * as config from "../config"; import * as config from '../config'
import * as listen from "./listen"; import * as listen from './listen'
import * as loggerProvider from "../providers/logger"; import * as loggerProvider from '../providers/logger'
import * as platformModule from "../platform"; import * as platformModule from '../platform'
import * as utils from "../utils"; import * as utils from '../utils'
// ================= // =================
// === Constants === // === Constants ===
@ -20,25 +20,23 @@ import * as utils from "../utils";
/** Pathname of the {@link URL} for deep links to the sign in page, after a redirect from a /** Pathname of the {@link URL} for deep links to the sign in page, after a redirect from a
* federated identity provider. */ * federated identity provider. */
const SIGN_IN_PATHNAME = "//auth"; const SIGN_IN_PATHNAME = '//auth'
/** Pathname of the {@link URL} for deep links to the sign out page, after a redirect from a /** Pathname of the {@link URL} for deep links to the sign out page, after a redirect from a
* federated identity provider. */ * federated identity provider. */
const SIGN_OUT_PATHNAME = "//auth"; const SIGN_OUT_PATHNAME = '//auth'
/** Pathname of the {@link URL} for deep links to the registration confirmation page, after a /** Pathname of the {@link URL} for deep links to the registration confirmation page, after a
* redirect from an account verification email. */ * redirect from an account verification email. */
const CONFIRM_REGISTRATION_PATHNAME = "//auth/confirmation"; const CONFIRM_REGISTRATION_PATHNAME = '//auth/confirmation'
/** Pathname of the {@link URL} for deep links to the login page, after a redirect from a reset /** Pathname of the {@link URL} for deep links to the login page, after a redirect from a reset
* password email. */ * password email. */
const LOGIN_PATHNAME = "//auth/login"; const LOGIN_PATHNAME = '//auth/login'
/** URL used as the OAuth redirect when running in the desktop app. */ /** URL used as the OAuth redirect when running in the desktop app. */
const DESKTOP_REDIRECT = utils.brand<auth.OAuthRedirect>( const DESKTOP_REDIRECT = utils.brand<auth.OAuthRedirect>(`${common.DEEP_LINK_SCHEME}://auth`)
`${common.DEEP_LINK_SCHEME}://auth`
);
/** Map from platform to the OAuth redirect URL that should be used for that platform. */ /** Map from platform to the OAuth redirect URL that should be used for that platform. */
const PLATFORM_TO_CONFIG: Record< const PLATFORM_TO_CONFIG: Record<
platformModule.Platform, platformModule.Platform,
Pick<auth.AmplifyConfig, "redirectSignIn" | "redirectSignOut"> Pick<auth.AmplifyConfig, 'redirectSignIn' | 'redirectSignOut'>
> = { > = {
[platformModule.Platform.desktop]: { [platformModule.Platform.desktop]: {
redirectSignIn: DESKTOP_REDIRECT, redirectSignIn: DESKTOP_REDIRECT,
@ -48,39 +46,33 @@ const PLATFORM_TO_CONFIG: Record<
redirectSignIn: config.ACTIVE_CONFIG.cloudRedirect, redirectSignIn: config.ACTIVE_CONFIG.cloudRedirect,
redirectSignOut: config.ACTIVE_CONFIG.cloudRedirect, redirectSignOut: config.ACTIVE_CONFIG.cloudRedirect,
}, },
}; }
const BASE_AMPLIFY_CONFIG = { const BASE_AMPLIFY_CONFIG = {
region: auth.AWS_REGION, region: auth.AWS_REGION,
scope: auth.OAUTH_SCOPES, scope: auth.OAUTH_SCOPES,
responseType: auth.OAUTH_RESPONSE_TYPE, responseType: auth.OAUTH_RESPONSE_TYPE,
} satisfies Partial<auth.AmplifyConfig>; } satisfies Partial<auth.AmplifyConfig>
/** Collection of configuration details for Amplify user pools, sorted by deployment environment. */ /** Collection of configuration details for Amplify user pools, sorted by deployment environment. */
const AMPLIFY_CONFIGS = { const AMPLIFY_CONFIGS = {
/** Configuration for @pbuchu's Cognito user pool. */ /** Configuration for @pbuchu's Cognito user pool. */
pbuchu: { pbuchu: {
userPoolId: utils.brand<auth.UserPoolId>("eu-west-1_jSF1RbgPK"), userPoolId: utils.brand<auth.UserPoolId>('eu-west-1_jSF1RbgPK'),
userPoolWebClientId: utils.brand<auth.UserPoolWebClientId>( userPoolWebClientId: utils.brand<auth.UserPoolWebClientId>('1bnib0jfon3aqc5g3lkia2infr'),
"1bnib0jfon3aqc5g3lkia2infr" domain: utils.brand<auth.OAuthDomain>('pb-enso-domain.auth.eu-west-1.amazoncognito.com'),
),
domain: utils.brand<auth.OAuthDomain>(
"pb-enso-domain.auth.eu-west-1.amazoncognito.com"
),
...BASE_AMPLIFY_CONFIG, ...BASE_AMPLIFY_CONFIG,
} satisfies Partial<auth.AmplifyConfig>, } satisfies Partial<auth.AmplifyConfig>,
/** Configuration for the production Cognito user pool. */ /** Configuration for the production Cognito user pool. */
production: { production: {
userPoolId: utils.brand<auth.UserPoolId>("eu-west-1_9Kycu2SbD"), userPoolId: utils.brand<auth.UserPoolId>('eu-west-1_9Kycu2SbD'),
userPoolWebClientId: utils.brand<auth.UserPoolWebClientId>( userPoolWebClientId: utils.brand<auth.UserPoolWebClientId>('4j9bfs8e7415erf82l129v0qhe'),
"4j9bfs8e7415erf82l129v0qhe"
),
domain: utils.brand<auth.OAuthDomain>( domain: utils.brand<auth.OAuthDomain>(
"production-enso-domain.auth.eu-west-1.amazoncognito.com" 'production-enso-domain.auth.eu-west-1.amazoncognito.com'
), ),
...BASE_AMPLIFY_CONFIG, ...BASE_AMPLIFY_CONFIG,
} satisfies Partial<auth.AmplifyConfig>, } satisfies Partial<auth.AmplifyConfig>,
}; }
// ================== // ==================
// === AuthConfig === // === AuthConfig ===
@ -89,14 +81,14 @@ const AMPLIFY_CONFIGS = {
/** Configuration for the authentication service. */ /** Configuration for the authentication service. */
export interface AuthConfig { export interface AuthConfig {
/** Logger for the authentication service. */ /** Logger for the authentication service. */
logger: loggerProvider.Logger; logger: loggerProvider.Logger
/** Whether the application is running on a desktop (i.e., versus in the Cloud). */ /** Whether the application is running on a desktop (i.e., versus in the Cloud). */
platform: platformModule.Platform; platform: platformModule.Platform
/** Function to navigate to a given (relative) URL. /** Function to navigate to a given (relative) URL.
* *
* Used to redirect to pages like the password reset page with the query parameters set in the * Used to redirect to pages like the password reset page with the query parameters set in the
* URL (e.g., `?verification_code=...`). */ * URL (e.g., `?verification_code=...`). */
navigate: (url: string) => void; navigate: (url: string) => void
} }
// =================== // ===================
@ -106,9 +98,9 @@ export interface AuthConfig {
/** API for the authentication service. */ /** API for the authentication service. */
export interface AuthService { export interface AuthService {
/** @see {@link cognito.Cognito}. */ /** @see {@link cognito.Cognito}. */
cognito: cognito.Cognito; cognito: cognito.Cognito
/** @see {@link listen.ListenFunction} */ /** @see {@link listen.ListenFunction} */
registerAuthEventListener: listen.ListenFunction; registerAuthEventListener: listen.ListenFunction
} }
/** Creates an instance of the authentication service. /** Creates an instance of the authentication service.
@ -118,13 +110,13 @@ export interface AuthService {
* This function should only be called once, and the returned service should be used throughout the * This function should only be called once, and the returned service should be used throughout the
* application. This is because it performs global configuration of the Amplify library. */ * application. This is because it performs global configuration of the Amplify library. */
export function initAuthService(authConfig: AuthConfig): AuthService { export function initAuthService(authConfig: AuthConfig): AuthService {
const { logger, platform, navigate } = authConfig; const { logger, platform, navigate } = authConfig
const amplifyConfig = loadAmplifyConfig(logger, platform, navigate); const amplifyConfig = loadAmplifyConfig(logger, platform, navigate)
const cognitoClient = new cognito.Cognito(logger, platform, amplifyConfig); const cognitoClient = new cognito.Cognito(logger, platform, amplifyConfig)
return { return {
cognito: cognitoClient, cognito: cognitoClient,
registerAuthEventListener: listen.registerAuthEventListener, registerAuthEventListener: listen.registerAuthEventListener,
}; }
} }
function loadAmplifyConfig( function loadAmplifyConfig(
@ -133,8 +125,8 @@ function loadAmplifyConfig(
navigate: (url: string) => void navigate: (url: string) => void
): auth.AmplifyConfig { ): auth.AmplifyConfig {
/** Load the environment-specific Amplify configuration. */ /** Load the environment-specific Amplify configuration. */
const baseConfig = AMPLIFY_CONFIGS[config.ENVIRONMENT]; const baseConfig = AMPLIFY_CONFIGS[config.ENVIRONMENT]
let urlOpener = null; let urlOpener = null
if (platform === platformModule.Platform.desktop) { if (platform === platformModule.Platform.desktop) {
/** If we're running on the desktop, we want to override the default URL opener for OAuth /** If we're running on the desktop, we want to override the default URL opener for OAuth
* flows. This is because the default URL opener opens the URL in the desktop app itself, * flows. This is because the default URL opener opens the URL in the desktop app itself,
@ -144,23 +136,23 @@ function loadAmplifyConfig(
* - users trust their system browser with their credentials more than they trust our app; * - users trust their system browser with their credentials more than they trust our app;
* - our app can keep itself on the relevant page until the user is sent back to it (i.e., * - our app can keep itself on the relevant page until the user is sent back to it (i.e.,
* we avoid unnecessary reloads/refreshes caused by redirects. */ * we avoid unnecessary reloads/refreshes caused by redirects. */
urlOpener = openUrlWithExternalBrowser; urlOpener = openUrlWithExternalBrowser
/** To handle redirects back to the application from the system browser, we also need to /** To handle redirects back to the application from the system browser, we also need to
* register a custom URL handler. */ * register a custom URL handler. */
setDeepLinkHandler(logger, navigate); setDeepLinkHandler(logger, navigate)
} }
/** Load the platform-specific Amplify configuration. */ /** Load the platform-specific Amplify configuration. */
const platformConfig = PLATFORM_TO_CONFIG[platform]; const platformConfig = PLATFORM_TO_CONFIG[platform]
return { return {
...baseConfig, ...baseConfig,
...platformConfig, ...platformConfig,
urlOpener, urlOpener,
}; }
} }
function openUrlWithExternalBrowser(url: string) { function openUrlWithExternalBrowser(url: string) {
window.authenticationApi.openUrlInSystemBrowser(url); window.authenticationApi.openUrlInSystemBrowser(url)
} }
/** Set the callback that will be invoked when a deep link to the application is opened. /** Set the callback that will be invoked when a deep link to the application is opened.
@ -180,20 +172,17 @@ function openUrlWithExternalBrowser(url: string) {
* *
* All URLs that don't have a pathname that starts with {@link AUTHENTICATION_PATHNAME_BASE} will be * All URLs that don't have a pathname that starts with {@link AUTHENTICATION_PATHNAME_BASE} will be
* ignored by this handler. */ * ignored by this handler. */
function setDeepLinkHandler( function setDeepLinkHandler(logger: loggerProvider.Logger, navigate: (url: string) => void) {
logger: loggerProvider.Logger,
navigate: (url: string) => void
) {
const onDeepLink = (url: string) => { const onDeepLink = (url: string) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url)
switch (parsedUrl.pathname) { switch (parsedUrl.pathname) {
/** If the user is being redirected after clicking the registration confirmation link in their /** If the user is being redirected after clicking the registration confirmation link in their
* email, then the URL will be for the confirmation page path. */ * email, then the URL will be for the confirmation page path. */
case CONFIRM_REGISTRATION_PATHNAME: { case CONFIRM_REGISTRATION_PATHNAME: {
const redirectUrl = `${app.CONFIRM_REGISTRATION_PATH}${parsedUrl.search}`; const redirectUrl = `${app.CONFIRM_REGISTRATION_PATH}${parsedUrl.search}`
navigate(redirectUrl); navigate(redirectUrl)
break; break
} }
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/339 /** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/339
* Don't use `enso://auth` for both authentication redirect & signout redirect so we don't * Don't use `enso://auth` for both authentication redirect & signout redirect so we don't
@ -201,31 +190,31 @@ function setDeepLinkHandler(
case SIGN_OUT_PATHNAME: case SIGN_OUT_PATHNAME:
case SIGN_IN_PATHNAME: case SIGN_IN_PATHNAME:
/** If the user is being redirected after a sign-out, then no query args will be present. */ /** If the user is being redirected after a sign-out, then no query args will be present. */
if (parsedUrl.search === "") { if (parsedUrl.search === '') {
navigate(app.LOGIN_PATH); navigate(app.LOGIN_PATH)
} else { } else {
handleAuthResponse(url); handleAuthResponse(url)
} }
break; break
/** If the user is being redirected after finishing the password reset flow, then the URL will /** If the user is being redirected after finishing the password reset flow, then the URL will
* be for the login page. */ * be for the login page. */
case LOGIN_PATHNAME: case LOGIN_PATHNAME:
navigate(app.LOGIN_PATH); navigate(app.LOGIN_PATH)
break; break
/** If the user is being redirected from a password reset email, then we need to navigate to /** If the user is being redirected from a password reset email, then we need to navigate to
* the password reset page, with the verification code and email passed in the URL so they can * the password reset page, with the verification code and email passed in the URL so they can
* be filled in automatically. */ * be filled in automatically. */
case app.RESET_PASSWORD_PATH: { case app.RESET_PASSWORD_PATH: {
const resetPasswordRedirectUrl = `${app.RESET_PASSWORD_PATH}${parsedUrl.search}`; const resetPasswordRedirectUrl = `${app.RESET_PASSWORD_PATH}${parsedUrl.search}`
navigate(resetPasswordRedirectUrl); navigate(resetPasswordRedirectUrl)
break; break
} }
default: default:
logger.error(`${url} is an unrecognized deep link. Ignoring.`); logger.error(`${url} is an unrecognized deep link. Ignoring.`)
}
} }
};
window.authenticationApi.setDeepLinkHandler(onDeepLink); window.authenticationApi.setDeepLinkHandler(onDeepLink)
} }
/** When the user is being redirected from a federated identity provider, then we need to pass the /** When the user is being redirected from a federated identity provider, then we need to pass the
@ -243,8 +232,8 @@ function handleAuthResponse(url: string) {
* the original `window.history.replaceState` function, which is not bound to the * the original `window.history.replaceState` function, which is not bound to the
* `window.history` object. */ * `window.history` object. */
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const replaceState = window.history.replaceState; const replaceState = window.history.replaceState
window.history.replaceState = () => false; window.history.replaceState = () => false
try { try {
/** # Safety /** # Safety
* *
@ -254,10 +243,10 @@ function handleAuthResponse(url: string) {
* are intentionally not part of the public AWS Amplify API. */ * are intentionally not part of the public AWS Amplify API. */
// @ts-expect-error `_handleAuthResponse` is a private method without typings. // @ts-expect-error `_handleAuthResponse` is a private method without typings.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
await amplify.Auth._handleAuthResponse(url); await amplify.Auth._handleAuthResponse(url)
} finally { } finally {
/** Restore the original `window.location.replaceState` function. */ /** Restore the original `window.location.replaceState` function. */
window.history.replaceState = replaceState; window.history.replaceState = replaceState
} }
})(); })()
} }

View File

@ -34,41 +34,41 @@
* {@link router.Route}s require fully authenticated users (c.f. * {@link router.Route}s require fully authenticated users (c.f.
* {@link authProvider.FullUserSession}). */ * {@link authProvider.FullUserSession}). */
import * as react from "react"; import * as react from 'react'
import * as router from "react-router-dom"; import * as router from 'react-router-dom'
import * as toast from "react-hot-toast"; import * as toast from 'react-hot-toast'
import * as authProvider from "../authentication/providers/auth"; import * as authProvider from '../authentication/providers/auth'
import * as authService from "../authentication/service"; import * as authService from '../authentication/service'
import * as loggerProvider from "../providers/logger"; import * as loggerProvider from '../providers/logger'
import * as platformModule from "../platform"; import * as platformModule from '../platform'
import * as session from "../authentication/providers/session"; import * as session from '../authentication/providers/session'
import ConfirmRegistration from "../authentication/components/confirmRegistration"; import ConfirmRegistration from '../authentication/components/confirmRegistration'
import Dashboard from "../dashboard/components/dashboard"; import Dashboard from '../dashboard/components/dashboard'
import ForgotPassword from "../authentication/components/forgotPassword"; import ForgotPassword from '../authentication/components/forgotPassword'
import Login from "../authentication/components/login"; import Login from '../authentication/components/login'
import Registration from "../authentication/components/registration"; import Registration from '../authentication/components/registration'
import ResetPassword from "../authentication/components/resetPassword"; import ResetPassword from '../authentication/components/resetPassword'
import SetUsername from "../authentication/components/setUsername"; import SetUsername from '../authentication/components/setUsername'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
/** Path to the root of the app (i.e., the Cloud dashboard). */ /** Path to the root of the app (i.e., the Cloud dashboard). */
export const DASHBOARD_PATH = "/"; export const DASHBOARD_PATH = '/'
/** Path to the login page. */ /** Path to the login page. */
export const LOGIN_PATH = "/login"; export const LOGIN_PATH = '/login'
/** Path to the registration page. */ /** Path to the registration page. */
export const REGISTRATION_PATH = "/registration"; export const REGISTRATION_PATH = '/registration'
/** Path to the confirm registration page. */ /** Path to the confirm registration page. */
export const CONFIRM_REGISTRATION_PATH = "/confirmation"; export const CONFIRM_REGISTRATION_PATH = '/confirmation'
/** Path to the forgot password page. */ /** Path to the forgot password page. */
export const FORGOT_PASSWORD_PATH = "/forgot-password"; export const FORGOT_PASSWORD_PATH = '/forgot-password'
/** Path to the reset password page. */ /** Path to the reset password page. */
export const RESET_PASSWORD_PATH = "/password-reset"; export const RESET_PASSWORD_PATH = '/password-reset'
/** Path to the set username page. */ /** Path to the set username page. */
export const SET_USERNAME_PATH = "/set-username"; export const SET_USERNAME_PATH = '/set-username'
// =========== // ===========
// === App === // === App ===
@ -77,9 +77,9 @@ export const SET_USERNAME_PATH = "/set-username";
/** Global configuration for the `App` component. */ /** Global configuration for the `App` component. */
export interface AppProps { export interface AppProps {
/** Logger to use for logging. */ /** Logger to use for logging. */
logger: loggerProvider.Logger; logger: loggerProvider.Logger
platform: platformModule.Platform; platform: platformModule.Platform
onAuthenticated: () => void; onAuthenticated: () => void
} }
/** Component called by the parent module, returning the root React component for this /** Component called by the parent module, returning the root React component for this
@ -88,13 +88,11 @@ export interface AppProps {
* This component handles all the initialization and rendering of the app, and manages the app's * This component handles all the initialization and rendering of the app, and manages the app's
* routes. It also initializes an `AuthProvider` that will be used by the rest of the app. */ * routes. It also initializes an `AuthProvider` that will be used by the rest of the app. */
function App(props: AppProps) { function App(props: AppProps) {
const { platform } = props; const { platform } = props
// This is a React component even though it does not contain JSX. // This is a React component even though it does not contain JSX.
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
const Router = const Router =
platform === platformModule.Platform.desktop platform === platformModule.Platform.desktop ? router.MemoryRouter : router.BrowserRouter
? router.MemoryRouter
: router.BrowserRouter;
/** Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider` /** Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
* will redirect the user between the login/register pages and the dashboard. */ * will redirect the user between the login/register pages and the dashboard. */
return ( return (
@ -104,7 +102,7 @@ function App(props: AppProps) {
<AppRouter {...props} /> <AppRouter {...props} />
</Router> </Router>
</> </>
); )
} }
// ================= // =================
@ -117,18 +115,15 @@ function App(props: AppProps) {
* because the {@link AppRouter} relies on React hooks, which can't be used in the same React * because the {@link AppRouter} relies on React hooks, which can't be used in the same React
* component as the component that defines the provider. */ * component as the component that defines the provider. */
function AppRouter(props: AppProps) { function AppRouter(props: AppProps) {
const { logger, onAuthenticated } = props; const { logger, onAuthenticated } = props
const navigate = router.useNavigate(); const navigate = router.useNavigate()
const mainPageUrl = new URL(window.location.href); const mainPageUrl = new URL(window.location.href)
const memoizedAuthService = react.useMemo(() => { const memoizedAuthService = react.useMemo(() => {
const authConfig = { navigate, ...props }; const authConfig = { navigate, ...props }
return authService.initAuthService(authConfig); return authService.initAuthService(authConfig)
}, [navigate, props]); }, [navigate, props])
const userSession = memoizedAuthService.cognito.userSession.bind( const userSession = memoizedAuthService.cognito.userSession.bind(memoizedAuthService.cognito)
memoizedAuthService.cognito const registerAuthEventListener = memoizedAuthService.registerAuthEventListener
);
const registerAuthEventListener =
memoizedAuthService.registerAuthEventListener;
return ( return (
<loggerProvider.LoggerProvider logger={logger}> <loggerProvider.LoggerProvider logger={logger}>
<session.SessionProvider <session.SessionProvider
@ -144,19 +139,13 @@ function AppRouter(props: AppProps) {
<react.Fragment> <react.Fragment>
{/* Login & registration pages are visible to unauthenticated users. */} {/* Login & registration pages are visible to unauthenticated users. */}
<router.Route element={<authProvider.GuestLayout />}> <router.Route element={<authProvider.GuestLayout />}>
<router.Route <router.Route path={REGISTRATION_PATH} element={<Registration />} />
path={REGISTRATION_PATH}
element={<Registration />}
/>
<router.Route path={LOGIN_PATH} element={<Login />} /> <router.Route path={LOGIN_PATH} element={<Login />} />
</router.Route> </router.Route>
{/* Protected pages are visible to authenticated users. */} {/* Protected pages are visible to authenticated users. */}
<router.Route element={<authProvider.ProtectedLayout />}> <router.Route element={<authProvider.ProtectedLayout />}>
<router.Route path={DASHBOARD_PATH} element={<Dashboard />} /> <router.Route path={DASHBOARD_PATH} element={<Dashboard />} />
<router.Route <router.Route path={SET_USERNAME_PATH} element={<SetUsername />} />
path={SET_USERNAME_PATH}
element={<SetUsername />}
/>
</router.Route> </router.Route>
{/* Other pages are visible to unauthenticated and authenticated users. */} {/* Other pages are visible to unauthenticated and authenticated users. */}
<router.Route <router.Route
@ -167,16 +156,13 @@ function AppRouter(props: AppProps) {
path={FORGOT_PASSWORD_PATH} path={FORGOT_PASSWORD_PATH}
element={<ForgotPassword />} element={<ForgotPassword />}
/> />
<router.Route <router.Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
path={RESET_PASSWORD_PATH}
element={<ResetPassword />}
/>
</react.Fragment> </react.Fragment>
</router.Routes> </router.Routes>
</authProvider.AuthProvider> </authProvider.AuthProvider>
</session.SessionProvider> </session.SessionProvider>
</loggerProvider.LoggerProvider> </loggerProvider.LoggerProvider>
); )
} }
export default App; export default App

View File

@ -12,21 +12,20 @@
export const PATHS = { export const PATHS = {
/** Path data for the `@` icon SVG. */ /** Path data for the `@` icon SVG. */
at: at:
"M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 " + 'M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 ' +
"8.959 0 01-4.5 1.207", '8.959 0 01-4.5 1.207',
/** Path data for the lock icon SVG. */ /** Path data for the lock icon SVG. */
lock: lock:
"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 " + 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 ' +
"0 00-8 0v4h8z", '0 00-8 0v4h8z',
/** Path data for the "right arrow" icon SVG. */ /** Path data for the "right arrow" icon SVG. */
rightArrow: "M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z", rightArrow: 'M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z',
/** Path data for the "create account" icon SVG. */ /** Path data for the "create account" icon SVG. */
createAccount: createAccount:
"M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z", 'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z',
/** Path data for the "go back" icon SVG. */ /** Path data for the "go back" icon SVG. */
goBack: goBack: 'M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1',
"M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1", } as const
} as const;
// =========== // ===========
// === Svg === // === Svg ===
@ -34,7 +33,7 @@ export const PATHS = {
/** Props for the `Svg` component. */ /** Props for the `Svg` component. */
interface Props { interface Props {
data: string; data: string
} }
/** Component for rendering SVG icons. /** Component for rendering SVG icons.
@ -54,5 +53,5 @@ export function Svg(props: Props) {
> >
<path d={props.data} /> <path d={props.data} />
</svg> </svg>
); )
} }

View File

@ -1,22 +1,22 @@
/** @file Main dashboard component, responsible for listing user's projects as well as other /** @file Main dashboard component, responsible for listing user's projects as well as other
* interactive components. */ * interactive components. */
import * as auth from "../../authentication/providers/auth"; import * as auth from '../../authentication/providers/auth'
// ================= // =================
// === Dashboard === // === Dashboard ===
// ================= // =================
function Dashboard() { function Dashboard() {
const { signOut } = auth.useAuth(); const { signOut } = auth.useAuth()
const { accessToken } = auth.useFullUserSession(); const { accessToken } = auth.useFullUserSession()
return ( return (
<> <>
<h1>This is a placeholder page for the cloud dashboard.</h1> <h1>This is a placeholder page for the cloud dashboard.</h1>
<p>Access token: {accessToken}</p> <p>Access token: {accessToken}</p>
<button onClick={signOut}>Log out</button> <button onClick={signOut}>Log out</button>
</> </>
); )
} }
export default Dashboard; export default Dashboard

View File

@ -2,18 +2,18 @@
* *
* Each exported function in the {@link Backend} in this module corresponds to an API endpoint. The * Each exported function in the {@link Backend} in this module corresponds to an API endpoint. The
* functions are asynchronous and return a `Promise` that resolves to the response from the API. */ * functions are asynchronous and return a `Promise` that resolves to the response from the API. */
import * as config from "../config"; import * as config from '../config'
import * as http from "../http"; import * as http from '../http'
import * as loggerProvider from "../providers/logger"; import * as loggerProvider from '../providers/logger'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
/** Relative HTTP path to the "set username" endpoint of the Cloud backend API. */ /** Relative HTTP path to the "set username" endpoint of the Cloud backend API. */
const SET_USER_NAME_PATH = "users"; const SET_USER_NAME_PATH = 'users'
/** Relative HTTP path to the "get user" endpoint of the Cloud backend API. */ /** Relative HTTP path to the "get user" endpoint of the Cloud backend API. */
const GET_USER_PATH = "users/me"; const GET_USER_PATH = 'users/me'
// ============= // =============
// === Types === // === Types ===
@ -21,15 +21,15 @@ const GET_USER_PATH = "users/me";
/** A user/organization in the application. These are the primary owners of a project. */ /** A user/organization in the application. These are the primary owners of a project. */
export interface Organization { export interface Organization {
id: string; id: string
userEmail: string; userEmail: string
name: string; name: string
} }
/** HTTP request body for the "set username" endpoint. */ /** HTTP request body for the "set username" endpoint. */
export interface SetUsernameRequestBody { export interface SetUsernameRequestBody {
userName: string; userName: string
userEmail: string; userEmail: string
} }
// =============== // ===============
@ -47,53 +47,48 @@ export class Backend {
) { ) {
/** All of our API endpoints are authenticated, so we expect the `Authorization` header to be /** All of our API endpoints are authenticated, so we expect the `Authorization` header to be
* set. */ * set. */
if (!this.client.defaultHeaders?.has("Authorization")) { if (!this.client.defaultHeaders?.has('Authorization')) {
throw new Error("Authorization header not set."); throw new Error('Authorization header not set.')
} }
} }
/** Returns a {@link RequestBuilder} for an HTTP GET request to the given path. */ /** Returns a {@link RequestBuilder} for an HTTP GET request to the given path. */
get<T = void>(path: string) { get<T = void>(path: string) {
return this.client.get<T>(`${config.ACTIVE_CONFIG.apiUrl}/${path}`); return this.client.get<T>(`${config.ACTIVE_CONFIG.apiUrl}/${path}`)
} }
/** Returns a {@link RequestBuilder} for an HTTP POST request to the given path. */ /** Returns a {@link RequestBuilder} for an HTTP POST request to the given path. */
post<T = void>(path: string, payload: object) { post<T = void>(path: string, payload: object) {
return this.client.post<T>( return this.client.post<T>(`${config.ACTIVE_CONFIG.apiUrl}/${path}`, payload)
`${config.ACTIVE_CONFIG.apiUrl}/${path}`,
payload
);
} }
/** Logs the error that occurred and throws a new one with a more user-friendly message. */ /** Logs the error that occurred and throws a new one with a more user-friendly message. */
errorHandler(message: string) { errorHandler(message: string) {
return (error: Error) => { return (error: Error) => {
this.logger.error(error.message); this.logger.error(error.message)
throw new Error(message); throw new Error(message)
}; }
} }
/** Sets the username of the current user, on the Cloud backend API. */ /** Sets the username of the current user, on the Cloud backend API. */
setUsername(body: SetUsernameRequestBody): Promise<Organization> { setUsername(body: SetUsernameRequestBody): Promise<Organization> {
return this.post<Organization>(SET_USER_NAME_PATH, body).then((response) => return this.post<Organization>(SET_USER_NAME_PATH, body).then(response => response.json())
response.json()
);
} }
/** Returns organization info for the current user, from the Cloud backend API. /** Returns organization info for the current user, from the Cloud backend API.
* *
* @returns `null` if status code 401 or 404 was received. */ * @returns `null` if status code 401 or 404 was received. */
getUser(): Promise<Organization | null> { getUser(): Promise<Organization | null> {
return this.get<Organization>(GET_USER_PATH).then((response) => { return this.get<Organization>(GET_USER_PATH).then(response => {
if ( if (
response.status === http.HttpStatus.unauthorized || response.status === http.HttpStatus.unauthorized ||
response.status === http.HttpStatus.notFound response.status === http.HttpStatus.notFound
) { ) {
return null; return null
} else { } else {
return response.json(); return response.json()
} }
}); })
} }
} }
@ -107,12 +102,9 @@ export class Backend {
* This is a hack to quickly create the backend in the format we want, until we get the provider * This is a hack to quickly create the backend in the format we want, until we get the provider
* working. This should be removed entirely in favour of creating the backend once and using it from * working. This should be removed entirely in favour of creating the backend once and using it from
* the context. */ * the context. */
export function createBackend( export function createBackend(accessToken: string, logger: loggerProvider.Logger): Backend {
accessToken: string, const headers = new Headers()
logger: loggerProvider.Logger headers.append('Authorization', `Bearer ${accessToken}`)
): Backend { const client = new http.Client(headers)
const headers = new Headers(); return new Backend(client, logger)
headers.append("Authorization", `Bearer ${accessToken}`);
const client = new http.Client(headers);
return new Backend(client, logger);
} }

View File

@ -1,7 +1,7 @@
/** @file Module containing common custom React hooks used throughout out Dashboard. */ /** @file Module containing common custom React hooks used throughout out Dashboard. */
import * as react from "react"; import * as react from 'react'
import * as loggerProvider from "./providers/logger"; import * as loggerProvider from './providers/logger'
// ============ // ============
// === Bind === // === Bind ===
@ -21,8 +21,8 @@ import * as loggerProvider from "./providers/logger";
* <input {...bind} /> * <input {...bind} />
* ``` */ * ``` */
interface Bind { interface Bind {
value: string; value: string
onChange: (value: react.ChangeEvent<HTMLInputElement>) => void; onChange: (value: react.ChangeEvent<HTMLInputElement>) => void
} }
// ================ // ================
@ -37,12 +37,12 @@ interface Bind {
* use the `value` prop and the `onChange` event handler. However, this can be tedious to do for * use the `value` prop and the `onChange` event handler. However, this can be tedious to do for
* every input field, so we can use a custom hook to handle this for us. */ * every input field, so we can use a custom hook to handle this for us. */
export function useInput(initialValue: string): [string, Bind] { export function useInput(initialValue: string): [string, Bind] {
const [value, setValue] = react.useState(initialValue); const [value, setValue] = react.useState(initialValue)
const onChange = (event: react.ChangeEvent<HTMLInputElement>) => { const onChange = (event: react.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value); setValue(event.target.value)
}; }
const bind = { value, onChange }; const bind = { value, onChange }
return [value, bind]; return [value, bind]
} }
// ====================== // ======================
@ -69,33 +69,33 @@ export function useAsyncEffect<T>(
fetch: (signal: AbortSignal) => Promise<T>, fetch: (signal: AbortSignal) => Promise<T>,
deps?: react.DependencyList deps?: react.DependencyList
): T { ): T {
const logger = loggerProvider.useLogger(); const logger = loggerProvider.useLogger()
const [value, setValue] = react.useState<T>(initialValue); const [value, setValue] = react.useState<T>(initialValue)
react.useEffect(() => { react.useEffect(() => {
const controller = new AbortController(); const controller = new AbortController()
const { signal } = controller; const { signal } = controller
/** Declare the async data fetching function. */ /** Declare the async data fetching function. */
const load = async () => { const load = async () => {
const result = await fetch(signal); const result = await fetch(signal)
/** Set state with the result only if this effect has not been aborted. This prevents race /** Set state with the result only if this effect has not been aborted. This prevents race
* conditions by making it so that only the latest async fetch will update the state on * conditions by making it so that only the latest async fetch will update the state on
* completion. */ * completion. */
if (!signal.aborted) { if (!signal.aborted) {
setValue(result); setValue(result)
}
} }
};
load().catch((error) => { load().catch(error => {
logger.error("Error while fetching data", error); logger.error('Error while fetching data', error)
}); })
/** Cancel any future `setValue` calls. */ /** Cancel any future `setValue` calls. */
return () => { return () => {
controller.abort(); controller.abort()
}; }
}, deps); }, deps)
return value; return value
} }

View File

@ -20,10 +20,10 @@ export enum HttpStatus {
/** HTTP method variants that can be used in an HTTP request. */ /** HTTP method variants that can be used in an HTTP request. */
enum HttpMethod { enum HttpMethod {
get = "GET", get = 'GET',
post = "POST", post = 'POST',
put = "PUT", put = 'PUT',
delete = "DELETE", delete = 'DELETE',
} }
// ============== // ==============
@ -32,20 +32,17 @@ enum HttpMethod {
/** A helper function to convert a `Blob` to a base64-encoded string. */ /** A helper function to convert a `Blob` to a base64-encoded string. */
function blobToBase64(blob: Blob) { function blobToBase64(blob: Blob) {
return new Promise<string>((resolve) => { return new Promise<string>(resolve => {
const reader = new FileReader(); const reader = new FileReader()
reader.onload = () => { reader.onload = () => {
resolve( resolve(
// This cast is always safe because we read as data URL (a string). // This cast is always safe because we read as data URL (a string).
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
(reader.result as string).replace( (reader.result as string).replace(/^data:application\/octet-stream;base64,/, '')
/^data:application\/octet-stream;base64,/,
""
) )
); }
}; reader.readAsDataURL(blob)
reader.readAsDataURL(blob); })
});
} }
/** An HTTP client that can be used to create and send HTTP requests asynchronously. */ /** An HTTP client that can be used to create and send HTTP requests asynchronously. */
@ -60,17 +57,12 @@ export class Client {
/** Sends an HTTP GET request to the specified URL. */ /** Sends an HTTP GET request to the specified URL. */
get<T = void>(url: string) { get<T = void>(url: string) {
return this.request<T>(HttpMethod.get, url); return this.request<T>(HttpMethod.get, url)
} }
/** Sends a JSON HTTP POST request to the specified URL. */ /** Sends a JSON HTTP POST request to the specified URL. */
post<T = void>(url: string, payload: object) { post<T = void>(url: string, payload: object) {
return this.request<T>( return this.request<T>(HttpMethod.post, url, JSON.stringify(payload), 'application/json')
HttpMethod.post,
url,
JSON.stringify(payload),
"application/json"
);
} }
/** Sends a base64-encoded binary HTTP POST request to the specified URL. */ /** Sends a base64-encoded binary HTTP POST request to the specified URL. */
@ -79,23 +71,18 @@ export class Client {
HttpMethod.post, HttpMethod.post,
url, url,
await blobToBase64(payload), await blobToBase64(payload),
"application/octet-stream" 'application/octet-stream'
); )
} }
/** Sends a JSON HTTP PUT request to the specified URL. */ /** Sends a JSON HTTP PUT request to the specified URL. */
put<T = void>(url: string, payload: object) { put<T = void>(url: string, payload: object) {
return this.request<T>( return this.request<T>(HttpMethod.put, url, JSON.stringify(payload), 'application/json')
HttpMethod.put,
url,
JSON.stringify(payload),
"application/json"
);
} }
/** Sends an HTTP DELETE request to the specified URL. */ /** Sends an HTTP DELETE request to the specified URL. */
delete<T = void>(url: string) { delete<T = void>(url: string) {
return this.request<T>(HttpMethod.delete, url); return this.request<T>(HttpMethod.delete, url)
} }
/** Executes an HTTP request to the specified URL, with the given HTTP method. */ /** Executes an HTTP request to the specified URL, with the given HTTP method. */
@ -105,14 +92,14 @@ export class Client {
payload?: string, payload?: string,
mimetype?: string mimetype?: string
) { ) {
const defaultHeaders = this.defaultHeaders ?? []; const defaultHeaders = this.defaultHeaders ?? []
const headers = new Headers(defaultHeaders); const headers = new Headers(defaultHeaders)
if (payload) { if (payload) {
const contentType = mimetype ?? "application/json"; const contentType = mimetype ?? 'application/json'
headers.set("Content-Type", contentType); headers.set('Content-Type', contentType)
} }
interface ResponseWithTypedJson<U> extends Response { interface ResponseWithTypedJson<U> extends Response {
json: () => Promise<U>; json: () => Promise<U>
} }
// This is an UNSAFE type assertion, however this is a HTTP client // This is an UNSAFE type assertion, however this is a HTTP client
// and should only be used to query APIs with known response types. // and should only be used to query APIs with known response types.
@ -121,6 +108,6 @@ export class Client {
method, method,
headers, headers,
...(payload ? { body: payload } : {}), ...(payload ? { body: payload } : {}),
}) as Promise<ResponseWithTypedJson<T>>; }) as Promise<ResponseWithTypedJson<T>>
} }
} }

View File

@ -10,19 +10,19 @@
// as per the above comment. // as per the above comment.
// @ts-expect-error See above comment for why this import is needed. // @ts-expect-error See above comment for why this import is needed.
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-restricted-syntax // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-restricted-syntax
import * as React from "react"; import * as React from 'react'
import * as reactDOM from "react-dom/client"; import * as reactDOM from 'react-dom/client'
import * as loggerProvider from "./providers/logger"; import * as loggerProvider from './providers/logger'
import * as platformModule from "./platform"; import * as platformModule from './platform'
import App, * as app from "./components/app"; import App, * as app from './components/app'
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
/** The `id` attribute of the root element that the app will be rendered into. */ /** The `id` attribute of the root element that the app will be rendered into. */
const ROOT_ELEMENT_ID = "dashboard"; const ROOT_ELEMENT_ID = 'dashboard'
// =========== // ===========
// === run === // === run ===
@ -41,18 +41,18 @@ export function run(
platform: platformModule.Platform, platform: platformModule.Platform,
onAuthenticated: () => void onAuthenticated: () => void
) { ) {
logger.log("Starting authentication/dashboard UI."); logger.log('Starting authentication/dashboard UI.')
/** The root element that the authentication/dashboard app will be rendered into. */ /** The root element that the authentication/dashboard app will be rendered into. */
const root = document.getElementById(ROOT_ELEMENT_ID); const root = document.getElementById(ROOT_ELEMENT_ID)
if (root == null) { if (root == null) {
logger.error(`Could not find root element with ID '${ROOT_ELEMENT_ID}'.`); logger.error(`Could not find root element with ID '${ROOT_ELEMENT_ID}'.`)
} else { } else {
const props = { logger, platform, onAuthenticated }; const props = { logger, platform, onAuthenticated }
reactDOM.createRoot(root).render(<App {...props} />); reactDOM.createRoot(root).render(<App {...props} />)
} }
} }
export type AppProps = app.AppProps; export type AppProps = app.AppProps
// This export should be `PascalCase` because it is a re-export. // This export should be `PascalCase` because it is a re-export.
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
export const Platform = platformModule.Platform; export const Platform = platformModule.Platform

View File

@ -1,6 +1,6 @@
/** @file Defines the React provider for the {@link Logger} interface, along with a hook to use the /** @file Defines the React provider for the {@link Logger} interface, along with a hook to use the
* provider via the shared React context. */ * provider via the shared React context. */
import * as react from "react"; import * as react from 'react'
// ============== // ==============
// === Logger === // === Logger ===
@ -12,9 +12,9 @@ import * as react from "react";
* provided by the EnsoGL packager. */ * provided by the EnsoGL packager. */
export interface Logger { export interface Logger {
/** Logs a message to the console. */ /** Logs a message to the console. */
log: (message: unknown, ...optionalParams: unknown[]) => void; log: (message: unknown, ...optionalParams: unknown[]) => void
/** Logs an error message to the console. */ /** Logs an error message to the console. */
error: (message: unknown, ...optionalParams: unknown[]) => void; error: (message: unknown, ...optionalParams: unknown[]) => void
} }
// ===================== // =====================
@ -23,22 +23,20 @@ export interface Logger {
/** See {@link AuthContext} for safety details. */ /** See {@link AuthContext} for safety details. */
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
const LoggerContext = react.createContext<Logger>({} as Logger); const LoggerContext = react.createContext<Logger>({} as Logger)
// ====================== // ======================
// === LoggerProvider === // === LoggerProvider ===
// ====================== // ======================
interface LoggerProviderProps { interface LoggerProviderProps {
children: react.ReactNode; children: react.ReactNode
logger: Logger; logger: Logger
} }
export function LoggerProvider(props: LoggerProviderProps) { export function LoggerProvider(props: LoggerProviderProps) {
const { children, logger } = props; const { children, logger } = props
return ( return <LoggerContext.Provider value={logger}>{children}</LoggerContext.Provider>
<LoggerContext.Provider value={logger}>{children}</LoggerContext.Provider>
);
} }
// ================= // =================
@ -46,5 +44,5 @@ export function LoggerProvider(props: LoggerProviderProps) {
// ================= // =================
export function useLogger() { export function useLogger() {
return react.useContext(LoggerContext); return react.useContext(LoggerContext)
} }

View File

@ -1,15 +1,15 @@
/** @file Index file declaring main DOM structure for the app. */ /** @file Index file declaring main DOM structure for the app. */
import * as authentication from "enso-authentication"; import * as authentication from 'enso-authentication'
import * as platform from "./authentication/src/platform"; import * as platform from './authentication/src/platform'
const logger = console; const logger = console
/** This package is a standalone React app (i.e., IDE deployed to the Cloud), so we're not /** This package is a standalone React app (i.e., IDE deployed to the Cloud), so we're not
* running on the desktop. */ * running on the desktop. */
const PLATFORM = platform.Platform.cloud; const PLATFORM = platform.Platform.cloud
// The `onAuthenticated` parameter is required but we don't need it, so we pass an empty function. // The `onAuthenticated` parameter is required but we don't need it, so we pass an empty function.
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
function onAuthenticated() {} function onAuthenticated() {}
authentication.run(logger, PLATFORM, onAuthenticated); authentication.run(logger, PLATFORM, onAuthenticated)