mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-11 04:56:33 +00:00
Add one-click login if possible (#1043)
This commit is contained in:
parent
a6c80ac1f0
commit
6d2dde477c
26 changed files with 216 additions and 53 deletions
CHANGELOG.md
cypress
integration
autocompletion.spec.tsbanner.spec.tsdiagrams.spec.tsdocumentTitle.spec.tseditorMode.spec.tsexport.spec.tsfileUpload.spec.tshelpDialog.spec.tshighlightedCodeBlock.spec.tshistory.spec.tsimport.spec.tsintro.spec.tslanguage.spec.tslink.spec.tslinkEmbedder.spec.tsmaxLength.spec.tsprofile.spec.tsquote-extra.spec.tsshortcodes.spec.tssignInButton.spec.tstoolbar.spec.tsyamlArrayDeprecationMessage.spec.ts
support
src
|
@ -85,7 +85,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|||
- The dark-mode is also applied to the read-only-view and can be toggled from there.
|
||||
- Access tokens for the CLI and 3rd-party-clients can be managed in the user profile.
|
||||
- Change editor font to "Fira Code"
|
||||
- Note tags can be set as yaml-array in frontmatter
|
||||
- Note tags can be set as yaml-array in frontmatter.
|
||||
- If only one external login provider is configured, the sign-in button will directly link to it.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Autocompletion', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
cy.get('.CodeMirror')
|
||||
.click()
|
||||
|
|
|
@ -8,6 +8,7 @@ import { banner } from '../support/config'
|
|||
|
||||
describe('Banner', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visit('/')
|
||||
expect(localStorage.getItem('bannerTimeStamp')).to.be.null
|
||||
})
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Diagram codeblock ', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { branding } from '../support/config'
|
|||
const title = 'This is a test title'
|
||||
describe('Document Title', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
|
||||
.should('exist')
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
*/
|
||||
|
||||
describe('Editor mode from URL parameter is used', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
})
|
||||
|
||||
it('mode view', () => {
|
||||
cy.visitTestEditor('view')
|
||||
cy.get('.splitter.left')
|
||||
|
|
|
@ -9,6 +9,7 @@ describe('Export', () => {
|
|||
const testContent = `---\ntitle: ${ testTitle }\n---\nThis is some test content`
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
cy.codemirrorFill(testContent)
|
||||
})
|
||||
|
|
|
@ -8,6 +8,7 @@ const imageUrl = 'http://example.com/non-existing.png'
|
|||
|
||||
describe('File upload', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Help Dialog', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ const findHljsCodeBlock = () => {
|
|||
|
||||
describe('Code', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('History', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visit('/history')
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Import markdown file', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
describe('Intro page', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.intercept('/intro.md', 'test content')
|
||||
cy.visit('/')
|
||||
})
|
||||
|
|
|
@ -8,6 +8,7 @@ import { languages } from '../fixtures/languages'
|
|||
|
||||
describe('Languages', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import '../support/index'
|
|||
|
||||
describe('Links Intro', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
|
@ -53,13 +54,6 @@ describe('Links Intro', () => {
|
|||
cy.url()
|
||||
.should('include', '/new')
|
||||
})
|
||||
|
||||
it('Sign In', () => {
|
||||
cy.get('.btn-success.btn-sm')
|
||||
.click()
|
||||
cy.url()
|
||||
.should('include', '/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Menu Buttons logged in', () => {
|
||||
|
@ -83,7 +77,7 @@ describe('Links Intro', () => {
|
|||
.should('include', '/features')
|
||||
})
|
||||
|
||||
it('Features', () => {
|
||||
it('Profile', () => {
|
||||
cy.get('a.dropdown-item > i.fa-user')
|
||||
.click()
|
||||
cy.url()
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Link gets replaced with embedding: ', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ describe('The status bar text length info', () => {
|
|||
const tooMuchTestContent = `${ dangerTestContent }a`
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('profile page', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.intercept({
|
||||
url: '/api/v2/tokens',
|
||||
method: 'GET'
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Quote extra tags', function () {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('Short code gets replaced or rendered: ', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
114
cypress/integration/signInButton.spec.ts
Normal file
114
cypress/integration/signInButton.spec.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const authProvidersDisabled = {
|
||||
facebook: false,
|
||||
github: false,
|
||||
twitter: false,
|
||||
gitlab: false,
|
||||
dropbox: false,
|
||||
ldap: false,
|
||||
google: false,
|
||||
saml: false,
|
||||
oauth2: false,
|
||||
internal: false,
|
||||
openid: false
|
||||
}
|
||||
|
||||
const initLoggedOutTestWithCustomAuthProviders = (cy: Cypress.cy, enabledProviders: Partial<typeof authProvidersDisabled>) => {
|
||||
cy.loadConfig({
|
||||
authProviders: {
|
||||
...authProvidersDisabled,
|
||||
...enabledProviders
|
||||
}
|
||||
})
|
||||
cy.visit('/')
|
||||
cy.logout()
|
||||
}
|
||||
|
||||
describe('When logged-in, ', () => {
|
||||
it('sign-in button is hidden', () => {
|
||||
cy.loadConfig()
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When logged-out ', () => {
|
||||
describe('and no auth-provider is enabled, ', () => {
|
||||
it('sign-in button is hidden', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('and an interactive auth-provider is enabled, ', () => {
|
||||
it('sign-in button points to login route: internal', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
internal: true
|
||||
})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('be.visible')
|
||||
.should('have.attr', 'href', '/login')
|
||||
})
|
||||
|
||||
it('sign-in button points to login route: ldap', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
ldap: true
|
||||
})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('be.visible')
|
||||
.should('have.attr', 'href', '/login')
|
||||
})
|
||||
|
||||
it('sign-in button points to login route: openid', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
openid: true
|
||||
})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('be.visible')
|
||||
.should('have.attr', 'href', '/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('and only one one-click auth-provider is enabled, ', () => {
|
||||
it('sign-in button points to auth-provider', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
saml: true
|
||||
})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('be.visible')
|
||||
// The absolute URL is used because it is defined as API base URL absolute.
|
||||
.should('have.attr', 'href', 'http://127.0.0.1:3001/api/v2/auth/saml')
|
||||
})
|
||||
})
|
||||
|
||||
describe('and multiple one-click auth-providers are enabled, ', () => {
|
||||
it('sign-in button points to login route', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
saml: true,
|
||||
github: true
|
||||
})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('be.visible')
|
||||
.should('have.attr', 'href', '/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('and one-click- as well as interactive auth-providers are enabled, ', () => {
|
||||
it('sign-in button points to login route', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
saml: true,
|
||||
internal: true
|
||||
})
|
||||
cy.get('[data-cy=sign-in-button]')
|
||||
.should('be.visible')
|
||||
.should('have.attr', 'href', '/login')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -9,6 +9,7 @@ describe('Toolbar Buttons', () => {
|
|||
const testLink = 'http://hedgedoc.org'
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
|
||||
cy.get('.CodeMirror')
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
describe('YAML Array for deprecated syntax of document tags in frontmatter', () => {
|
||||
beforeEach(() => {
|
||||
cy.loadConfig()
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
loadConfig(): Chainable<Window>
|
||||
}
|
||||
}
|
||||
|
||||
export const banner = {
|
||||
text: 'This is the mock banner call',
|
||||
timestamp: '2020-05-22T20:46:08.962Z'
|
||||
|
@ -14,47 +20,54 @@ export const branding = {
|
|||
logo: '/img/acme.png'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/v2/config', {
|
||||
export const authProviders = {
|
||||
facebook: true,
|
||||
github: true,
|
||||
twitter: true,
|
||||
gitlab: true,
|
||||
dropbox: true,
|
||||
ldap: true,
|
||||
google: true,
|
||||
saml: true,
|
||||
oauth2: true,
|
||||
internal: true,
|
||||
openid: true
|
||||
}
|
||||
|
||||
export const config = {
|
||||
allowAnonymous: true,
|
||||
authProviders: authProviders,
|
||||
branding: branding,
|
||||
banner: banner,
|
||||
customAuthNames: {
|
||||
ldap: 'FooBar',
|
||||
oauth2: 'Olaf2',
|
||||
saml: 'aufSAMLn.de'
|
||||
},
|
||||
maxDocumentLength: 200,
|
||||
specialLinks: {
|
||||
privacy: 'https://example.com/privacy',
|
||||
termsOfUse: 'https://example.com/termsOfUse',
|
||||
imprint: 'https://example.com/imprint'
|
||||
},
|
||||
plantumlServer: 'http://mock-plantuml.local',
|
||||
version: {
|
||||
version: 'mock',
|
||||
sourceCodeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
},
|
||||
'iframeCommunication': {
|
||||
'editorOrigin': 'http://127.0.0.1:3001',
|
||||
'rendererOrigin': 'http://127.0.0.1:3001'
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('loadConfig', (additionalConfig?: Partial<typeof config>) => {
|
||||
return cy.intercept('/api/v2/config', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
allowAnonymous: true,
|
||||
authProviders: {
|
||||
facebook: true,
|
||||
github: true,
|
||||
twitter: true,
|
||||
gitlab: true,
|
||||
dropbox: true,
|
||||
ldap: true,
|
||||
google: true,
|
||||
saml: true,
|
||||
oauth2: true,
|
||||
email: true,
|
||||
openid: true
|
||||
},
|
||||
branding: branding,
|
||||
banner: banner,
|
||||
customAuthNames: {
|
||||
ldap: 'FooBar',
|
||||
oauth2: 'Olaf2',
|
||||
saml: 'aufSAMLn.de'
|
||||
},
|
||||
maxDocumentLength: 200,
|
||||
specialLinks: {
|
||||
privacy: 'https://example.com/privacy',
|
||||
termsOfUse: 'https://example.com/termsOfUse',
|
||||
imprint: 'https://example.com/imprint'
|
||||
},
|
||||
plantumlServer: 'http://mock-plantuml.local',
|
||||
version: {
|
||||
version: 'mock',
|
||||
sourceCodeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
},
|
||||
'iframeCommunication': {
|
||||
'editorOrigin': 'http://127.0.0.1:3001',
|
||||
'rendererOrigin': 'http://127.0.0.1:3001'
|
||||
}
|
||||
...config,
|
||||
...additionalConfig
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import { RegisterError } from '../../components/register-page/register-page'
|
||||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
|
||||
export const INTERACTIVE_LOGIN_METHODS = ['internal', 'ldap', 'openid']
|
||||
|
||||
export const doInternalLogin = async (username: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + '/auth/internal', {
|
||||
...defaultFetchConfig,
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import equal from 'fast-deep-equal'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { ButtonProps } from 'react-bootstrap/Button'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
@ -12,16 +13,31 @@ import { useSelector } from 'react-redux'
|
|||
import { LinkContainer } from 'react-router-bootstrap'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { getApiUrl } from '../../../api/utils'
|
||||
import { INTERACTIVE_LOGIN_METHODS } from '../../../api/auth'
|
||||
|
||||
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||
|
||||
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
const anyAuthProviderActive = useSelector((state: ApplicationState) => Object.values(state.config.authProviders)
|
||||
.includes(true))
|
||||
const authProviders = useSelector((state: ApplicationState) => state.config.authProviders, equal)
|
||||
const authEnabled = useMemo(() => Object.values(authProviders).includes(true), [authProviders])
|
||||
|
||||
const loginLink = useMemo(() => {
|
||||
const activeProviders = Object.entries(authProviders)
|
||||
.filter((entry: [string, boolean]) => entry[1])
|
||||
.map(entry => entry[0])
|
||||
const activeOneClickProviders = activeProviders.filter(entry => !INTERACTIVE_LOGIN_METHODS.includes(entry))
|
||||
|
||||
if (activeProviders.length === 1 && activeOneClickProviders.length === 1) {
|
||||
return `${ getApiUrl() }/auth/${ activeOneClickProviders[0] }`
|
||||
}
|
||||
return '/login'
|
||||
}, [authProviders])
|
||||
|
||||
return (
|
||||
<ShowIf condition={ anyAuthProviderActive }>
|
||||
<LinkContainer to="/login" title={ t('login.signIn') }>
|
||||
<ShowIf condition={ authEnabled }>
|
||||
<LinkContainer to={ loginLink } title={ t('login.signIn') }>
|
||||
<Button
|
||||
data-cy={ 'sign-in-button' }
|
||||
variant={ variant || 'success' }
|
||||
|
|
Loading…
Add table
Reference in a new issue