Skip to content

Commit

Permalink
fix: handle new login endpoints [LIBS-600] (#846)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomzemp authored Apr 29, 2024
1 parent 980750a commit 4512825
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 52 deletions.
28 changes: 23 additions & 5 deletions adapter/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-03-14T15:31:40.141Z\n"
"PO-Revision-Date: 2024-03-14T15:31:40.141Z\n"
"POT-Creation-Date: 2024-04-24T13:58:13.591Z\n"
"PO-Revision-Date: 2024-04-24T13:58:13.591Z\n"

msgid "Save your data"
msgstr "Save your data"
Expand Down Expand Up @@ -48,6 +48,9 @@ msgstr "Something went wrong"
msgid "Redirect to safe login mode"
msgstr "Redirect to safe login mode"

msgid "Redirect to safe login mode"
msgstr "Redirect to safe login mode"

msgid "Hide technical details"
msgstr "Hide technical details"

Expand All @@ -60,9 +63,27 @@ msgstr "The following information may be requested by technical support."
msgid "Copy technical details to clipboard"
msgstr "Copy technical details to clipboard"

msgid "Signing in..."
msgstr "Signing in..."

msgid "Sign in"
msgstr "Sign in"

msgid "Going to app..."
msgstr "Going to app..."

msgid "Go to app"
msgstr "Go to app"

msgid "Please sign in"
msgstr "Please sign in"

msgid "Specify server"
msgstr "Specify server"

msgid "Could not log in"
msgstr "Could not log in"

msgid "Server"
msgstr "Server"

Expand All @@ -71,6 +92,3 @@ msgstr "Username"

msgid "Password"
msgstr "Password"

msgid "Sign in"
msgstr "Sign in"
7 changes: 4 additions & 3 deletions adapter/src/components/LoginAppWrapper.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useConfig } from '@dhis2/app-runtime'
import PropTypes from 'prop-types'
import React from 'react'
import { useSystemDefaultLocale } from '../utils/useLocale.js'
Expand All @@ -6,8 +7,9 @@ import { ErrorBoundary } from './ErrorBoundary.js'
import { LoadingMask } from './LoadingMask.js'
import { styles } from './styles/AppWrapper.style.js'

export const LoginAppWrapper = ({ url, children }) => {
export const LoginAppWrapper = ({ children }) => {
const { loading: localeLoading } = useSystemDefaultLocale()
const { baseUrl } = useConfig()
// cannot check current user for a loginApp (no api/me)

if (localeLoading) {
Expand All @@ -21,7 +23,7 @@ export const LoginAppWrapper = ({ url, children }) => {
<ErrorBoundary
onRetry={() => window.location.reload()}
loginApp={true}
baseURL={url}
baseURL={baseUrl}
>
{children}
</ErrorBoundary>
Expand All @@ -33,5 +35,4 @@ export const LoginAppWrapper = ({ url, children }) => {

LoginAppWrapper.propTypes = {
children: PropTypes.node,
url: PropTypes.string,
}
160 changes: 122 additions & 38 deletions adapter/src/components/LoginModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,136 @@ import {
ModalActions,
Button,
InputField,
NoticeBox,
} from '@dhis2/ui'
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import i18n from '../locales/index.js'
import { post } from '../utils/api.js'
import { get, post, postJSON } from '../utils/api.js'
import { styles } from './styles/LoginModal.style.js'

// Check if base URL is set statically as an env var (typical in production)
const staticUrl = process.env.REACT_APP_DHIS2_BASE_URL

export const LoginModal = ({ appName, baseUrl }) => {
const getIsNewLoginAPIAvailable = async (server) => {
try {
// if loginConfig is available, the instance can use new endpoints
await get(`${server}/api/loginConfig`)
return true
} catch (e) {
// if loginConfig is not available, the instance must use old endpoints
console.error(e)
return false
}
}

const loginWithNewEndpoints = async ({
server,
username,
password,
setError,
setIsLoggingIn,
}) => {
try {
await postJSON(
`${server}/api/auth/login`,
JSON.stringify({
username: encodeURIComponent(username),
password: encodeURIComponent(password),
})
)
window.location.reload()
} catch (e) {
setError(e)
setIsLoggingIn(false)
}
}

const loginWithOldEndpoints = async ({ server, username, password }) => {
try {
await post(
`${server}/dhis-web-commons-security/login.action`,
`j_username=${encodeURIComponent(
username
)}&j_password=${encodeURIComponent(password)}`
)
} catch (e) {
console.error(e)
} finally {
window.location.reload()
}
}

export const LoginModal = ({ appName, baseUrl, loginApp = false }) => {
const [server, setServer] = useState(baseUrl || '')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [isDirty, setIsDirty] = useState(false)
const [error, setError] = useState(null)
const [isLoggingIn, setIsLoggingIn] = useState(false)

const isValid = (val) => val && val.length >= 2
const getSignInButtonText = ({ loginApp, isLoggingIn }) => {
if (!loginApp) {
return isLoggingIn ? i18n.t('Signing in...') : i18n.t('Sign in')
}
return isLoggingIn ? i18n.t('Going to app...') : i18n.t('Go to app')
}

const onSubmit = async (e) => {
e.preventDefault()
setIsDirty(true)
if (isValid(server) && isValid(username) && isValid(password)) {
if (
isValid(server) &&
((isValid(username) && isValid(password)) || loginApp)
) {
setIsLoggingIn(true)
if (!staticUrl) {
// keep the localStorage value here -- it's still used in some
// obscure cases, like in the cypress network shim
window.localStorage.DHIS2_BASE_URL = server
await setBaseUrlByAppName({ appName, baseUrl: server })
if (loginApp) {
window.location.reload()
}
}
try {
await post(
`${server}/dhis-web-commons-security/login.action`,
`j_username=${encodeURIComponent(
username
)}&j_password=${encodeURIComponent(password)}`
)
} catch (e) {
console.log(
'TODO: This will always error and cancel the request until we get a real login endpoint!'
)
}

// TODO: Hacky solution... this shouldn't require a reload
window.location.reload()
const isNewLoginAPIAvailable = await getIsNewLoginAPIAvailable(
server
)

if (isNewLoginAPIAvailable) {
loginWithNewEndpoints({
server,
username,
password,
setError,
setIsLoggingIn,
})
} else {
loginWithOldEndpoints({ server, username, password })
}
}
}

return (
<Modal open small dataTest="dhis2-adapter-loginmodal">
<style jsx>{styles}</style>
<form onSubmit={onSubmit}>
<ModalTitle>{i18n.t('Please sign in')}</ModalTitle>
<ModalTitle>
{!loginApp
? i18n.t('Please sign in')
: i18n.t('Specify server')}
</ModalTitle>

<ModalContent>
{error && (
<div className="errorNotification">
<NoticeBox error title={i18n.t('Could not log in')}>
{error?.message}
</NoticeBox>
</div>
)}
{!staticUrl && (
<InputField
dataTest="dhis2-adapter-loginserver"
Expand All @@ -68,35 +147,39 @@ export const LoginModal = ({ appName, baseUrl }) => {
onChange={(input) => setServer(input.value)}
/>
)}
{!loginApp && (
<>
<InputField
dataTest="dhis2-adapter-loginname"
error={isDirty && !isValid(username)}
label={i18n.t('Username')}
name="j_username"
type="text"
value={username}
onChange={(input) => setUsername(input.value)}
/>

<InputField
dataTest="dhis2-adapter-loginname"
error={isDirty && !isValid(username)}
label={i18n.t('Username')}
name="j_username"
type="text"
value={username}
onChange={(input) => setUsername(input.value)}
/>

<InputField
dataTest="dhis2-adapter-loginpassword"
error={isDirty && !isValid(password)}
label={i18n.t('Password')}
name="j_password"
type="password"
value={password}
onChange={(input) => setPassword(input.value)}
/>
<InputField
dataTest="dhis2-adapter-loginpassword"
error={isDirty && !isValid(password)}
label={i18n.t('Password')}
name="j_password"
type="password"
value={password}
onChange={(input) => setPassword(input.value)}
/>
</>
)}
</ModalContent>

<ModalActions>
<Button
primary
dataTest="dhis2-adapter-loginsubmit"
type="submit"
disabled={isLoggingIn}
>
{i18n.t('Sign in')}
{getSignInButtonText({ loginApp, isLoggingIn })}
</Button>
</ModalActions>
</form>
Expand All @@ -106,4 +189,5 @@ export const LoginModal = ({ appName, baseUrl }) => {
LoginModal.propTypes = {
appName: PropTypes.string,
baseUrl: PropTypes.string,
loginApp: PropTypes.bool,
}
10 changes: 6 additions & 4 deletions adapter/src/components/ServerVersionProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,12 @@ export const ServerVersionProvider = ({

// This needs to come before 'loading' case to show modal at correct times
if (systemInfoState.error || baseUrlState.error) {
return !loginApp ? (
<LoginModal appName={appName} baseUrl={baseUrl} />
) : (
<p>Specify DHIS2_BASE_URL environment variable</p>
return (
<LoginModal
appName={appName}
baseUrl={baseUrl}
loginApp={loginApp}
/>
)
}

Expand Down
7 changes: 7 additions & 0 deletions adapter/src/components/styles/LoginModal.style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import css from 'styled-jsx/css'

export const styles = css`
.errorNotification {
margin-block: 8px;
}
`
2 changes: 1 addition & 1 deletion adapter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const AppAdapter = ({
loginApp={loginApp}
plugin={false}
>
<LoginAppWrapper url={url}>{children}</LoginAppWrapper>
<LoginAppWrapper>{children}</LoginAppWrapper>
</ServerVersionProvider>
</ErrorBoundary>
)
Expand Down
18 changes: 17 additions & 1 deletion adapter/src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ const request = (url, options) => {
})
.then((response) => {
if (response.status !== 200) {
reject('Request failed ' + response.statusText)
response
.json()
.then((json) => {
reject(json)
})
.catch(() => {
reject('Request failed ' + response.statusText)
})
return
}
try {
Expand Down Expand Up @@ -42,3 +49,12 @@ export const post = (url, body) =>
'Content-Type': 'application/x-www-form-urlencoded',
},
})

export const postJSON = (url, body) =>
request(url, {
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
},
})

0 comments on commit 4512825

Please sign in to comment.