Skip to content

Commit 931e1d0

Browse files
authored
feat(admin-ui): support multi-repository SNAuth sessions (#1652)
* feat(admin-ui): scope SNAuth sessions by repository * feat(admin-ui): add SNAuth repository switcher
1 parent 44174a5 commit 931e1d0

17 files changed

Lines changed: 549 additions & 78 deletions

apps/sensenet/src/components/app-providers.tsx

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { PathHelper } from '@sensenet/client-utils'
21
import { InjectorContext, LoggerContextProvider } from '@sensenet/hooks-react'
32
import React, { ReactNode, Suspense, useCallback, useEffect, useState } from 'react'
43
import { BrowserRouter } from 'react-router-dom'
@@ -8,6 +7,7 @@ import {
87
LocalizationProvider,
98
PersonalSettingsContextProvider,
109
RepositoryProvider,
10+
RepositorySwitchContext,
1111
ResponsiveContextProvider,
1212
ThemeProvider,
1313
} from '../context'
@@ -22,6 +22,12 @@ import {
2222
NavigationCommandProvider,
2323
SearchCommandProvider,
2424
} from '../services'
25+
import {
26+
clearActiveRepositorySelection,
27+
hasSnAuthRepositoryTokens,
28+
normalizeRepositoryUrl,
29+
startSnAuthRepositoryLogin,
30+
} from '../services/repository-session'
2531
import { DialogProvider } from './dialogs/dialog-provider'
2632

2733
import { GridLoadingProvider } from './grid/Providers/GridLoadingProvider'
@@ -34,28 +40,56 @@ export type AppProvidersProps = {
3440
}
3541

3642
export default function AppProviders({ children }: AppProvidersProps) {
37-
const initAuthType: AuthServerType = (window.localStorage.getItem('authType') as AuthServerType) ?? 'IdentityServer'
43+
const initAuthType: AuthServerType =
44+
(window.localStorage.getItem('authType') as AuthServerType) ?? defaultAuthConfig.authType
3845
const [authType, setAuthType] = useState<'IdentityServer' | 'SNAuth'>(initAuthType)
3946
const [url, setUrl] = useState<string>('')
4047

48+
const selectRepository = useCallback((providedUrl: string) => {
49+
const normalizedUrl = normalizeRepositoryUrl(providedUrl)
50+
51+
clearActiveRepositorySelection()
52+
startSnAuthRepositoryLogin(normalizedUrl)
53+
setUrl(normalizedUrl)
54+
}, [])
55+
4156
const changeAuthType = useCallback((providedUrl: string) => {
42-
setUrl(PathHelper.ensureDefaultSchema(providedUrl))
57+
const normalizedUrl = normalizeRepositoryUrl(providedUrl)
58+
59+
setUrl(normalizedUrl)
4360
setAuthType((prev) => {
4461
const newAuthType = prev === 'IdentityServer' ? 'SNAuth' : 'IdentityServer'
62+
if (newAuthType === 'SNAuth') {
63+
startSnAuthRepositoryLogin(normalizedUrl)
64+
}
4565
window.localStorage.setItem('authType', newAuthType)
4666
return newAuthType
4767
})
4868
}, [])
4969

70+
const switchRepository = useCallback((providedUrl: string) => {
71+
const normalizedUrl = normalizeRepositoryUrl(providedUrl)
72+
73+
if (!hasSnAuthRepositoryTokens(normalizedUrl)) {
74+
startSnAuthRepositoryLogin(normalizedUrl)
75+
}
76+
77+
window.localStorage.setItem('authType', 'SNAuth')
78+
setAuthType('SNAuth')
79+
setUrl(normalizedUrl)
80+
}, [])
81+
5082
useEffect(() => {
51-
const IsAuthKey = localStorage.getItem(authConfigKeyIS)
52-
const SnAuthKey = localStorage.getItem(authConfigKeySN)
53-
if (IsAuthKey || SnAuthKey) return
5483
const repoUrl = new URL(window.location.href).searchParams.get('repoUrl')
5584
if (repoUrl) {
56-
changeAuthType(repoUrl)
85+
selectRepository(repoUrl)
86+
return
5787
}
58-
}, [changeAuthType])
88+
89+
const IsAuthKey = localStorage.getItem(authConfigKeyIS)
90+
const SnAuthKey = localStorage.getItem(authConfigKeySN)
91+
if (IsAuthKey || SnAuthKey) return
92+
}, [selectRepository])
5993

6094
snInjector
6195
.getInstance(CommandProviderManager)
@@ -76,31 +110,33 @@ export default function AppProviders({ children }: AppProvidersProps) {
76110
<GridLoadingProvider>
77111
<TreeLoadingProvider>
78112
<ThemeProvider>
79-
{authType === 'IdentityServer' ? (
80-
<RepositoryProvider url={url} changeAuthType={changeAuthType}>
81-
<ShareProvider>
82-
<ISAuthProvider>
83-
<ResponsiveContextProvider>
84-
<ExpandedItemsProvider>
85-
<DialogProvider>{children}</DialogProvider>
86-
</ExpandedItemsProvider>
87-
</ResponsiveContextProvider>
88-
</ISAuthProvider>
89-
</ShareProvider>
90-
</RepositoryProvider>
91-
) : (
92-
<SnAuthRepositoryProvider url={url} changeAuthType={changeAuthType}>
93-
<ShareProvider>
94-
<SNAuthProvider>
95-
<ResponsiveContextProvider>
96-
<ExpandedItemsProvider>
97-
<DialogProvider>{children}</DialogProvider>
98-
</ExpandedItemsProvider>
99-
</ResponsiveContextProvider>
100-
</SNAuthProvider>
101-
</ShareProvider>
102-
</SnAuthRepositoryProvider>
103-
)}
113+
<RepositorySwitchContext.Provider value={{ authType, switchRepository }}>
114+
{authType === 'IdentityServer' ? (
115+
<RepositoryProvider url={url} changeAuthType={changeAuthType}>
116+
<ShareProvider>
117+
<ISAuthProvider>
118+
<ResponsiveContextProvider>
119+
<ExpandedItemsProvider>
120+
<DialogProvider>{children}</DialogProvider>
121+
</ExpandedItemsProvider>
122+
</ResponsiveContextProvider>
123+
</ISAuthProvider>
124+
</ShareProvider>
125+
</RepositoryProvider>
126+
) : (
127+
<SnAuthRepositoryProvider url={url} changeAuthType={changeAuthType}>
128+
<ShareProvider>
129+
<SNAuthProvider>
130+
<ResponsiveContextProvider>
131+
<ExpandedItemsProvider>
132+
<DialogProvider>{children}</DialogProvider>
133+
</ExpandedItemsProvider>
134+
</ResponsiveContextProvider>
135+
</SNAuthProvider>
136+
</ShareProvider>
137+
</SnAuthRepositoryProvider>
138+
)}
139+
</RepositorySwitchContext.Provider>
104140
</ThemeProvider>
105141
</TreeLoadingProvider>
106142
</GridLoadingProvider>

apps/sensenet/src/components/app.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
11
import { CssBaseline, StylesProvider } from '@material-ui/core'
22
import React from 'react'
3+
import { clearActiveRepositorySelection } from '../services/repository-session'
34
import AppProviders from './app-providers'
45
import { Dialogs } from './dialogs'
56
import { ErrorBoundary } from './error-boundary'
67
import { DesktopLayout } from './layout/DesktopLayout'
78
import { MainRouter } from './MainRouter'
89
import { NotificationComponent } from './NotificationComponent'
910

11+
const AppErrorFallback = () => {
12+
const switchRepository = () => {
13+
clearActiveRepositorySelection()
14+
window.location.assign('/')
15+
}
16+
17+
return (
18+
<div style={{ boxSizing: 'border-box', minHeight: '100vh', padding: 32 }}>
19+
<h1>Something went wrong</h1>
20+
<p>The current repository could not be loaded. You can reload the page or choose another repository.</p>
21+
<button type="button" onClick={() => window.location.reload()} style={{ marginRight: 16 }}>
22+
Reload
23+
</button>
24+
<button type="button" onClick={switchRepository}>
25+
Switch repository
26+
</button>
27+
</div>
28+
)
29+
}
30+
1031
export function App() {
1132
return (
12-
<ErrorBoundary>
33+
<ErrorBoundary FallbackComponent={AppErrorFallback}>
1334
<AppProviders>
1435
<CssBaseline />
1536
<StylesProvider injectFirst>

apps/sensenet/src/components/dialogs/logout.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Button, DialogActions, DialogContent, DialogContentText } from '@material-ui/core'
22
import { useRepository } from '@sensenet/hooks-react'
33
import React from 'react'
4-
import { authConfigKey } from '../../context'
54
import { useAuth } from '../../context/auth-provider'
65
import { useGlobalStyles } from '../../globalStyles'
76
import { useLocalization } from '../../hooks'
7+
import { clearActiveRepositorySelection } from '../../services/repository-session'
88
import { Icon } from '../Icon'
99
import { DialogTitle, useDialog } from '.'
1010

@@ -38,6 +38,14 @@ export function LogoutDialog() {
3838
</DialogContentText>
3939
</DialogContent>
4040
<DialogActions>
41+
<Button
42+
aria-label={localization.switchRepositoryButtonTitle}
43+
onClick={() => {
44+
clearActiveRepositorySelection()
45+
window.location.assign('/')
46+
}}>
47+
{localization.switchRepositoryButtonTitle}
48+
</Button>
4149
<Button
4250
aria-label={localization.logoutCancel}
4351
className={globalClasses.cancelButton}
@@ -49,7 +57,7 @@ export function LogoutDialog() {
4957
color="primary"
5058
variant="contained"
5159
onClick={() => {
52-
window.localStorage.removeItem(authConfigKey)
60+
clearActiveRepositorySelection()
5361
logout()
5462
}}
5563
autoFocus={true}>

apps/sensenet/src/components/drawer/PermanentDrawer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useDrawerItems, useLocalization } from '../../hooks'
1515
import { AddButton } from '../AddButton'
1616
import { SearchButton } from '../search-button'
1717
import { PermanentDrawerItem } from './PermanentDrawerItem'
18+
import { RepositorySelector } from './repository-selector'
1819

1920
const useStyles = makeStyles((theme: Theme) => {
2021
return createStyles({
@@ -109,6 +110,7 @@ export const PermanentDrawer = () => {
109110
</ListItemIcon>
110111
</ListItem>
111112
) : null}
113+
{opened ? <RepositorySelector /> : null}
112114
{matchPath(location.pathname, PATHS.savedQueries.appPath) ? <SearchButton isOpened={opened} /> : null}{' '}
113115
{matchPath(location.pathname, [
114116
PATHS.content.appPath,

apps/sensenet/src/components/drawer/TemporaryDrawer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ResponsiveContext, ResponsivePersonalSettings } from '../../context'
1818
import { useDrawerItems, useLocalization, useTheme } from '../../hooks'
1919
import { LogoutButton } from '../LogoutButton'
2020
import { UserAvatar } from '../UserAvatar'
21+
import { RepositorySelector } from './repository-selector'
2122

2223
type TemporaryDrawerProps = {
2324
isOpened: boolean
@@ -60,6 +61,7 @@ export const TemporaryDrawer = (props: TemporaryDrawerProps) => {
6061
transition: 'width 100ms ease-in-out',
6162
}}>
6263
<div style={{ paddingTop: '1em' }}>
64+
<RepositorySelector />
6365
{items.map((item, index) => {
6466
const isActive = matchPath(location.pathname, item.url)
6567
return isActive ? (
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { FormControl, InputLabel, makeStyles, MenuItem, Select, Theme } from '@material-ui/core'
2+
import { useRepository } from '@sensenet/hooks-react'
3+
import React, { useEffect, useState } from 'react'
4+
import { useHistory } from 'react-router-dom'
5+
import { PATHS } from '../../application-paths'
6+
import { useRepositorySwitch } from '../../context'
7+
import { useLocalization } from '../../hooks'
8+
import {
9+
getAuthenticatedSnAuthRepositorySessions,
10+
normalizeRepositoryUrl,
11+
snAuthRepositorySessionsChangedEvent,
12+
} from '../../services/repository-session'
13+
14+
const useStyles = makeStyles((theme: Theme) => ({
15+
root: {
16+
padding: theme.spacing(1, 1, 0),
17+
},
18+
formControl: {
19+
width: '100%',
20+
},
21+
}))
22+
23+
const getRepositoryHost = (repoUrl: string) => {
24+
try {
25+
return new URL(repoUrl).host
26+
} catch {
27+
return repoUrl
28+
}
29+
}
30+
31+
export const RepositorySelector = () => {
32+
const classes = useStyles()
33+
const history = useHistory()
34+
const localization = useLocalization().repositorySelector
35+
const repository = useRepository()
36+
const { authType, switchRepository } = useRepositorySwitch()
37+
const currentRepositoryUrl = normalizeRepositoryUrl(repository.configuration.repositoryUrl)
38+
const [repositorySessions, setRepositorySessions] = useState(getAuthenticatedSnAuthRepositorySessions)
39+
40+
useEffect(() => {
41+
const refreshRepositorySessions = () => setRepositorySessions(getAuthenticatedSnAuthRepositorySessions())
42+
43+
window.addEventListener(snAuthRepositorySessionsChangedEvent, refreshRepositorySessions)
44+
45+
return () => window.removeEventListener(snAuthRepositorySessionsChangedEvent, refreshRepositorySessions)
46+
}, [])
47+
48+
if (authType !== 'SNAuth' || repositorySessions.length < 2) {
49+
return null
50+
}
51+
52+
return (
53+
<div className={classes.root}>
54+
<FormControl className={classes.formControl} variant="outlined" size="small">
55+
<InputLabel id="repository-selector-label">{localization.activeRepository}</InputLabel>
56+
<Select
57+
labelId="repository-selector-label"
58+
label={localization.activeRepository}
59+
value={currentRepositoryUrl}
60+
onChange={(ev) => {
61+
const nextRepositoryUrl = ev.target.value as string
62+
63+
if (nextRepositoryUrl === currentRepositoryUrl) {
64+
return
65+
}
66+
67+
switchRepository(nextRepositoryUrl)
68+
history.push(PATHS.landingPath.appPath)
69+
}}>
70+
{repositorySessions.map((repositorySession) => (
71+
<MenuItem key={repositorySession.repoUrl} value={repositorySession.repoUrl}>
72+
{getRepositoryHost(repositorySession.repoUrl)}
73+
</MenuItem>
74+
))}
75+
</Select>
76+
</FormControl>
77+
</div>
78+
)
79+
}

apps/sensenet/src/components/login/login-page.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
createStyles,
77
Grid,
88
InputLabel,
9+
List,
10+
ListItem,
11+
ListItemText,
912
makeStyles,
1013
TextField,
1114
TextFieldProps,
@@ -50,9 +53,16 @@ const DEVDEMO_URL = `https://dev.demo.sensenet.com`
5053
type LoginPageProps = {
5154
handleSubmit: (url: string) => void
5255
isLoginInProgress: boolean
56+
repositoryOptions?: Array<{ repoUrl: string; lastUsed?: string }>
57+
handleSelectRepository?: (url: string) => void
5358
}
5459

55-
export default function LoginPage({ handleSubmit, isLoginInProgress }: LoginPageProps) {
60+
export default function LoginPage({
61+
handleSubmit,
62+
isLoginInProgress,
63+
repositoryOptions = [],
64+
handleSelectRepository,
65+
}: LoginPageProps) {
5666
const classes = useStyles()
5767
const globalClasses = useGlobalStyles()
5868
const localization = useLocalization().login
@@ -143,6 +153,31 @@ export default function LoginPage({ handleSubmit, isLoginInProgress }: LoginPage
143153
</Button>
144154
</form>
145155
</Grid>
156+
{repositoryOptions.length > 0 && handleSelectRepository ? (
157+
<Grid item style={{ marginTop: 32 }}>
158+
<Typography align="center" variant="subtitle1" component="p" className={classes.loginSubtitle}>
159+
{localization.recentRepositories}
160+
</Typography>
161+
<List dense={true}>
162+
{repositoryOptions.map((repositoryOption) => (
163+
<ListItem
164+
button={true}
165+
disabled={isLoginInProgress}
166+
key={repositoryOption.repoUrl}
167+
onClick={() => handleSelectRepository(repositoryOption.repoUrl)}>
168+
<ListItemText
169+
primary={repositoryOption.repoUrl}
170+
secondary={
171+
repositoryOption.lastUsed
172+
? localization.lastUsedRepository(new Date(repositoryOption.lastUsed).toLocaleString())
173+
: undefined
174+
}
175+
/>
176+
</ListItem>
177+
))}
178+
</List>
179+
</Grid>
180+
) : null}
146181
</Grid>
147182
</Container>
148183
</>

0 commit comments

Comments
 (0)