mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-03 08:28:54 -04:00
feat(auth): refactor auth, add oidc
Some checks are pending
Docker / build-and-push (frontend) (push) Waiting to run
Docker / build-and-push (backend) (push) Waiting to run
Deploy HD2 docs to Netlify / Deploys to netlify (push) Waiting to run
E2E Tests / backend-sqlite (push) Waiting to run
E2E Tests / backend-mariadb (push) Waiting to run
E2E Tests / backend-postgres (push) Waiting to run
E2E Tests / Build test build of frontend (push) Waiting to run
E2E Tests / frontend-cypress (1) (push) Blocked by required conditions
E2E Tests / frontend-cypress (2) (push) Blocked by required conditions
E2E Tests / frontend-cypress (3) (push) Blocked by required conditions
Lint and check format / Lint files and check formatting (push) Waiting to run
REUSE Compliance Check / reuse (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Static Analysis / Njsscan code scanning (push) Waiting to run
Static Analysis / CodeQL analysis (push) Waiting to run
Run tests & build / Test and build with NodeJS 20 (push) Waiting to run
Some checks are pending
Docker / build-and-push (frontend) (push) Waiting to run
Docker / build-and-push (backend) (push) Waiting to run
Deploy HD2 docs to Netlify / Deploys to netlify (push) Waiting to run
E2E Tests / backend-sqlite (push) Waiting to run
E2E Tests / backend-mariadb (push) Waiting to run
E2E Tests / backend-postgres (push) Waiting to run
E2E Tests / Build test build of frontend (push) Waiting to run
E2E Tests / frontend-cypress (1) (push) Blocked by required conditions
E2E Tests / frontend-cypress (2) (push) Blocked by required conditions
E2E Tests / frontend-cypress (3) (push) Blocked by required conditions
Lint and check format / Lint files and check formatting (push) Waiting to run
REUSE Compliance Check / reuse (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Static Analysis / Njsscan code scanning (push) Waiting to run
Static Analysis / CodeQL analysis (push) Waiting to run
Run tests & build / Test and build with NodeJS 20 (push) Waiting to run
Thanks to all HedgeDoc team members for the time discussing, helping with weird Nest issues, providing feedback and suggestions! Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
1609f3e01f
commit
7f665fae4b
109 changed files with 2927 additions and 1700 deletions
|
@ -1,16 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
|
||||
|
||||
interface DisplayNameFieldProps extends CommonFieldProps {
|
||||
initialValue?: string
|
||||
onValidityChange?: (valid: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,10 +21,21 @@ interface DisplayNameFieldProps extends CommonFieldProps {
|
|||
* @param onChange Hook that is called when the entered display name changes.
|
||||
* @param value The currently entered display name.
|
||||
* @param initialValue The initial input field value.
|
||||
* @param onValidityChange Callback that is called when the validity of the field changes.
|
||||
*/
|
||||
export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, value, initialValue }) => {
|
||||
export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
initialValue,
|
||||
onValidityChange
|
||||
}) => {
|
||||
const isValid = useMemo(() => value.trim() !== '' && value !== initialValue, [value, initialValue])
|
||||
const placeholderText = useTranslatedText('profile.displayName')
|
||||
const profileEditsAllowed = useFrontendConfig().allowProfileEdits
|
||||
|
||||
useEffect(() => {
|
||||
onValidityChange?.(isValid)
|
||||
}, [isValid, onValidityChange])
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
|
@ -37,6 +50,7 @@ export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, va
|
|||
onChange={onChange}
|
||||
placeholder={placeholderText}
|
||||
autoComplete='name'
|
||||
disabled={!profileEditsAllowed}
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ChangeEvent } from 'react'
|
||||
import type { ChangeEventHandler } from 'react'
|
||||
|
||||
export interface CommonFieldProps {
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||
value: string
|
||||
export interface CommonFieldProps<ValueType = undefined> {
|
||||
onChange: ValueType extends undefined ? ChangeEventHandler : (set: ValueType) => void
|
||||
value: ValueType extends undefined ? string : ValueType
|
||||
hasError?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Trans } from 'react-i18next'
|
|||
* @param value The currently entered password.
|
||||
*/
|
||||
export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value, hasError = false }) => {
|
||||
const isValid = useMemo(() => value.trim() !== '', [value])
|
||||
const isValid = useMemo(() => value.length >= 6, [value])
|
||||
|
||||
const placeholderText = useTranslatedText('login.auth.password')
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { useCallback } from 'react'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useAvatarUrl } from '../user-avatar/hooks/use-avatar-url'
|
||||
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
|
||||
|
||||
export enum ProfilePictureChoice {
|
||||
PROVIDER,
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
export interface ProfilePictureSelectFieldProps extends CommonFieldProps<ProfilePictureChoice> {
|
||||
onChange: (choice: ProfilePictureChoice) => void
|
||||
value: ProfilePictureChoice
|
||||
pictureUrl?: string
|
||||
username: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A field to select the profile picture.
|
||||
* @param onChange The callback to call when the value changes.
|
||||
* @param pictureUrl The URL of the picture provided by the identity provider.
|
||||
* @param username The username of the user.
|
||||
* @param value The current value of the field.
|
||||
*/
|
||||
export const ProfilePictureSelectField: React.FC<ProfilePictureSelectFieldProps> = ({
|
||||
onChange,
|
||||
pictureUrl,
|
||||
username,
|
||||
value
|
||||
}) => {
|
||||
const fallbackUrl = useAvatarUrl(undefined, username)
|
||||
const profileEditsAllowed = useFrontendConfig().allowProfileEdits
|
||||
const onSetProviderPicture = useCallback(() => {
|
||||
if (value !== ProfilePictureChoice.PROVIDER) {
|
||||
onChange(ProfilePictureChoice.PROVIDER)
|
||||
}
|
||||
}, [onChange, value])
|
||||
const onSetFallbackPicture = useCallback(() => {
|
||||
if (value !== ProfilePictureChoice.FALLBACK) {
|
||||
onChange(ProfilePictureChoice.FALLBACK)
|
||||
}
|
||||
}, [onChange, value])
|
||||
|
||||
if (!profileEditsAllowed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.selectProfilePicture.title' />
|
||||
</Form.Label>
|
||||
{pictureUrl && (
|
||||
<Form.Check className={'d-flex gap-2 align-items-center mb-3'} type='radio'>
|
||||
<Form.Check.Input
|
||||
type={'radio'}
|
||||
checked={value === ProfilePictureChoice.PROVIDER}
|
||||
onChange={onSetProviderPicture}
|
||||
/>
|
||||
<Form.Check.Label>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={pictureUrl} alt={'Profile picture provided by the identity provider'} height={48} width={48} />
|
||||
</Form.Check.Label>
|
||||
</Form.Check>
|
||||
)}
|
||||
<Form.Check className={'d-flex gap-2 align-items-center'} type='radio'>
|
||||
<Form.Check.Input
|
||||
type='radio'
|
||||
checked={value === ProfilePictureChoice.FALLBACK}
|
||||
onChange={onSetFallbackPicture}
|
||||
/>
|
||||
<Form.Check.Label>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={'Fallback profile picture'} src={fallbackUrl} height={48} width={48} />
|
||||
</Form.Check.Label>
|
||||
</Form.Check>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='profile.selectProfilePicture.info' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -11,6 +11,7 @@ import { Form } from 'react-bootstrap'
|
|||
export interface UsernameFieldProps extends CommonFieldProps {
|
||||
isInvalid?: boolean
|
||||
isValid?: boolean
|
||||
onValidityChange?: (valid: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +22,7 @@ export interface UsernameFieldProps extends CommonFieldProps {
|
|||
* @param isValid Is a valid field or not
|
||||
* @param isInvalid Adds error style to label
|
||||
*/
|
||||
export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, isValid, isInvalid }) => {
|
||||
export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, isValid, isInvalid, disabled }) => {
|
||||
const placeholderText = useTranslatedText('login.auth.username')
|
||||
|
||||
return (
|
||||
|
@ -29,10 +30,12 @@ export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, i
|
|||
type='text'
|
||||
size='sm'
|
||||
value={value}
|
||||
maxLength={64}
|
||||
isValid={isValid}
|
||||
isInvalid={isInvalid}
|
||||
onChange={onChange}
|
||||
placeholder={placeholderText}
|
||||
disabled={disabled}
|
||||
autoComplete='username'
|
||||
autoFocus={true}
|
||||
required
|
||||
|
|
|
@ -1,29 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { UsernameFieldProps } from './username-field'
|
||||
import { UsernameField } from './username-field'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useDebounce } from 'react-use'
|
||||
import { checkUsernameAvailability } from '../../../api/auth'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
|
||||
import { REGEX_USERNAME } from '@hedgedoc/commons'
|
||||
|
||||
const logger = new Logger('UsernameLabelField')
|
||||
|
||||
/**
|
||||
* Wraps and contains label and info for UsernameField
|
||||
*
|
||||
* @param onChange Callback that is called when the entered username changes.
|
||||
* @param value The currently entered username.
|
||||
* @param isValid Is a valid field or not
|
||||
* @param isInvalid Adds error style to label
|
||||
* @param onValidityChange Callback that is called when the validity of the field changes.
|
||||
* @param props Additional props for the UsernameField.
|
||||
*/
|
||||
export const UsernameLabelField: React.FC<UsernameFieldProps> = (props) => {
|
||||
export const UsernameLabelField: React.FC<UsernameFieldProps> = ({ value, onValidityChange, ...props }) => {
|
||||
useTranslation()
|
||||
const [usernameValid, setUsernameValid] = useState(false)
|
||||
const [usernameInvalid, setUsernameInvalid] = useState(false)
|
||||
const usernameChoosingAllowed = useFrontendConfig().allowChooseUsername
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (value === '') {
|
||||
setUsernameValid(false)
|
||||
setUsernameInvalid(false)
|
||||
return
|
||||
}
|
||||
if (!REGEX_USERNAME.test(value)) {
|
||||
setUsernameValid(false)
|
||||
setUsernameInvalid(true)
|
||||
return
|
||||
}
|
||||
checkUsernameAvailability(value)
|
||||
.then((available) => {
|
||||
setUsernameValid(available)
|
||||
setUsernameInvalid(!available)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Failed to check username availability', error)
|
||||
setUsernameValid(false)
|
||||
setUsernameInvalid(false)
|
||||
})
|
||||
},
|
||||
500,
|
||||
[value]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
onValidityChange?.(usernameValid && !usernameInvalid)
|
||||
}, [usernameValid, usernameInvalid, onValidityChange])
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='login.auth.username' />
|
||||
</Form.Label>
|
||||
<UsernameField {...props} />
|
||||
<UsernameField
|
||||
value={value}
|
||||
{...props}
|
||||
disabled={!usernameChoosingAllowed}
|
||||
isInvalid={usernameInvalid}
|
||||
isValid={usernameValid}
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='login.register.usernameInfo' />
|
||||
</Form.Text>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -12,17 +12,17 @@ import * as identicon from '@dicebear/identicon'
|
|||
* When an empty or no photoUrl is given, a random avatar is generated from the displayName.
|
||||
*
|
||||
* @param photoUrl The photo url of the user to use. Maybe empty or not set.
|
||||
* @param displayName The display name of the user to use as input to the random avatar.
|
||||
* @param username The username of the user to use as input to the random avatar.
|
||||
* @return The correct avatar url for the user.
|
||||
*/
|
||||
export const useAvatarUrl = (photoUrl: string | undefined, displayName: string): string => {
|
||||
export const useAvatarUrl = (photoUrl: string | undefined, username: string): string => {
|
||||
return useMemo(() => {
|
||||
if (photoUrl && photoUrl.trim() !== '') {
|
||||
return photoUrl
|
||||
}
|
||||
const avatar = createAvatar(identicon, {
|
||||
seed: displayName
|
||||
seed: username
|
||||
})
|
||||
return avatar.toDataUri()
|
||||
}, [photoUrl, displayName])
|
||||
}, [photoUrl, username])
|
||||
}
|
||||
|
|
|
@ -19,5 +19,5 @@ export interface UserAvatarForUserProps extends Omit<UserAvatarProps, 'photoUrl'
|
|||
* @param props remaining avatar props
|
||||
*/
|
||||
export const UserAvatarForUser: React.FC<UserAvatarForUserProps> = ({ user, ...props }) => {
|
||||
return <UserAvatar displayName={user.displayName} photoUrl={user.photoUrl} {...props} />
|
||||
return <UserAvatar displayName={user.displayName} photoUrl={user.photoUrl} username={user.username} {...props} />
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { getUser } from '../../../api/users'
|
||||
import { getUserInfo } from '../../../api/users'
|
||||
import { AsyncLoadingBoundary } from '../async-loading-boundary/async-loading-boundary'
|
||||
import type { UserAvatarProps } from './user-avatar'
|
||||
import { UserAvatar } from './user-avatar'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAsync } from 'react-use'
|
||||
|
||||
|
@ -28,15 +28,18 @@ export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ us
|
|||
const { t } = useTranslation()
|
||||
const { error, value, loading } = useAsync(async (): Promise<{ displayName: string; photo?: string }> => {
|
||||
return username
|
||||
? await getUser(username)
|
||||
? await getUserInfo(username)
|
||||
: {
|
||||
displayName: t('common.guestUser')
|
||||
}
|
||||
}, [username, t])
|
||||
|
||||
const avatar = useMemo(() => {
|
||||
return !value ? <Fragment /> : <UserAvatar displayName={value.displayName} photoUrl={value.photo} {...props} />
|
||||
}, [props, value])
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
return <UserAvatar displayName={value.displayName} photoUrl={value.photo} username={username} {...props} />
|
||||
}, [props, value, username])
|
||||
|
||||
return (
|
||||
<AsyncLoadingBoundary loading={loading || !value} error={error} componentName={'UserAvatarForUsername'}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -14,6 +14,7 @@ export interface UserAvatarProps {
|
|||
showName?: boolean
|
||||
photoUrl?: string
|
||||
displayName: string
|
||||
username?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +30,8 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
|||
displayName,
|
||||
size,
|
||||
additionalClasses = '',
|
||||
showName = true
|
||||
showName = true,
|
||||
username
|
||||
}) => {
|
||||
const imageSize = useMemo(() => {
|
||||
switch (size) {
|
||||
|
@ -42,7 +44,7 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
|||
}
|
||||
}, [size])
|
||||
|
||||
const avatarUrl = useAvatarUrl(photoUrl, displayName)
|
||||
const avatarUrl = useAvatarUrl(photoUrl, username ?? displayName)
|
||||
|
||||
const imageTranslateOptions = useMemo(
|
||||
() => ({
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { removeUserPermission, setUserPermission } from '../../../../../../api/permissions'
|
||||
import { getUser } from '../../../../../../api/users'
|
||||
import { getUserInfo } from '../../../../../../api/users'
|
||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||
import { setNotePermissionsFromServer } from '../../../../../../redux/note-details/methods'
|
||||
import { UserAvatarForUser } from '../../../../../common/user-avatar/user-avatar-for-user'
|
||||
|
@ -79,7 +79,7 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
|||
}, [noteId, entry.username, showErrorNotification])
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
return await getUser(entry.username)
|
||||
return await getUserInfo(entry.username)
|
||||
}, [entry.username])
|
||||
|
||||
if (!value) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RevisionDetails } from '../../../../../../api/revisions/types'
|
||||
import { getUser } from '../../../../../../api/users'
|
||||
import { getUserInfo } from '../../../../../../api/users'
|
||||
import type { UserInfo } from '../../../../../../api/users/types'
|
||||
import { download } from '../../../../../common/download/download'
|
||||
|
||||
|
@ -34,7 +34,7 @@ export const getUserDataForRevision = async (usernames: string[]): Promise<UserI
|
|||
const users: UserInfo[] = []
|
||||
const usersToFetch = Math.min(usernames.length, DISPLAY_MAX_USERS_PER_REVISION) - 1
|
||||
for (let i = 0; i <= usersToFetch; i++) {
|
||||
const user = await getUser(usernames[i])
|
||||
const user = await getUserInfo(usernames[i])
|
||||
users.push(user)
|
||||
}
|
||||
return users
|
||||
|
|
|
@ -25,7 +25,7 @@ export const SignOutDropdownButton: React.FC = () => {
|
|||
const onSignOut = useCallback(() => {
|
||||
clearUser()
|
||||
doLogout()
|
||||
.then(() => router.push('/login'))
|
||||
.then((logoutResponse) => router.push(logoutResponse.redirect))
|
||||
.catch(showErrorNotification('login.logoutFailed'))
|
||||
}, [showErrorNotification, router])
|
||||
|
||||
|
|
44
frontend/src/components/layout/login-layout.tsx
Normal file
44
frontend/src/components/layout/login-layout.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import { Col, Container, Row } from 'react-bootstrap'
|
||||
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import { HedgeDocLogoVertical } from '../common/hedge-doc-logo/hedge-doc-logo-vertical'
|
||||
import { LogoSize } from '../common/hedge-doc-logo/logo-size'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { CustomBranding } from '../common/custom-branding/custom-branding'
|
||||
import { IntroCustomContent } from '../intro-page/intro-custom-content'
|
||||
|
||||
/**
|
||||
* Layout for the login page with the intro content on the left and children on the right.
|
||||
* @param children The content to show on the right
|
||||
*/
|
||||
export const LoginLayout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={8}>
|
||||
<EditorToRendererCommunicatorContextProvider>
|
||||
<div className={'d-flex flex-column align-items-center mt-3'}>
|
||||
<HedgeDocLogoVertical size={LogoSize.BIG} autoTextColor={true} />
|
||||
<h5>
|
||||
<Trans i18nKey='app.slogan' />
|
||||
</h5>
|
||||
<div className={'mb-5'}>
|
||||
<CustomBranding />
|
||||
</div>
|
||||
<IntroCustomContent />
|
||||
</div>
|
||||
</EditorToRendererCommunicatorContextProvider>
|
||||
</Col>
|
||||
<Col xs={4} className={'pt-3 d-flex gap-3 flex-column'}>
|
||||
{children}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -13,6 +13,7 @@ import { Alert, Button, Card, Form } from 'react-bootstrap'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { fetchAndSetUser } from '../utils/fetch-and-set-user'
|
||||
import { PasswordField } from '../password-field'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export interface ViaLdapProps {
|
||||
providerName: string
|
||||
|
@ -25,18 +26,32 @@ export interface ViaLdapProps {
|
|||
export const LdapLoginCard: React.FC<ViaLdapProps> = ({ providerName, identifier }) => {
|
||||
useTranslation()
|
||||
|
||||
const router = useRouter()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const onLoginSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
let redirect = false
|
||||
doLdapLogin(identifier, username, password)
|
||||
.then(() => fetchAndSetUser())
|
||||
.catch((error: Error) => setError(error.message))
|
||||
.then((response) => {
|
||||
if (response.newUser) {
|
||||
router.push('/new-user')
|
||||
} else {
|
||||
redirect = true
|
||||
return fetchAndSetUser()
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (redirect) {
|
||||
router.push('/')
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => setError(String(error)))
|
||||
event.preventDefault()
|
||||
},
|
||||
[username, password, identifier]
|
||||
[username, password, identifier, router]
|
||||
)
|
||||
|
||||
const onUsernameChange = useLowercaseOnInputChange(setUsername)
|
||||
|
@ -49,10 +64,10 @@ export const LdapLoginCard: React.FC<ViaLdapProps> = ({ providerName, identifier
|
|||
<Trans i18nKey='login.signInVia' values={{ service: providerName }} />
|
||||
</Card.Title>
|
||||
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
||||
<UsernameField onChange={onUsernameChange} isValid={!!error} value={username} />
|
||||
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />
|
||||
<PasswordField onChange={onPasswordChange} isInvalid={!!error} />
|
||||
<Alert className='small' show={!!error} variant='danger'>
|
||||
<Trans i18nKey={error} />
|
||||
{error}
|
||||
</Alert>
|
||||
|
||||
<Button type='submit' variant='primary'>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -53,7 +53,7 @@ export const LocalLoginCardBody: React.FC = () => {
|
|||
</Card.Title>
|
||||
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
||||
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />
|
||||
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||
<PasswordField onChange={onPasswordChange} isInvalid={!!error} />
|
||||
<Alert className='small' show={!!error} variant='danger'>
|
||||
<Trans i18nKey={error} />
|
||||
</Alert>
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
'use client'
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
|
@ -25,9 +19,10 @@ import { UsernameLabelField } from '../../../common/fields/username-label-field'
|
|||
import { DisplayNameField } from '../../../common/fields/display-name-field'
|
||||
import { NewPasswordField } from '../../../common/fields/new-password-field'
|
||||
import { PasswordAgainField } from '../../../common/fields/password-again-field'
|
||||
import { RegisterInfos } from '../../../register-page/register-infos'
|
||||
import { RegisterError } from '../../../register-page/register-error'
|
||||
import { RegisterInfos } from './register-infos'
|
||||
import { RegisterError } from './register-error'
|
||||
import { fetchAndSetUser } from '../../utils/fetch-and-set-user'
|
||||
import { useGetPostLoginRedirectUrl } from '../../utils/use-get-post-login-redirect-url'
|
||||
|
||||
/**
|
||||
* Renders the registration process with fields for username, display name, password, password retype and information about terms and conditions.
|
||||
|
@ -43,29 +38,34 @@ export const LocalRegisterForm: NextPage = () => {
|
|||
const [error, setError] = useState<ApiError>()
|
||||
|
||||
const { dispatchUiNotification } = useUiNotifications()
|
||||
const postLoginRedirectUrl = useGetPostLoginRedirectUrl()
|
||||
|
||||
const doRegisterSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
doLocalRegister(username, displayName, password)
|
||||
.then(() => fetchAndSetUser())
|
||||
.then(() => dispatchUiNotification('login.register.success.title', 'login.register.success.message', {}))
|
||||
.then(() => router.push('/history'))
|
||||
.then(() => router.push(postLoginRedirectUrl))
|
||||
.catch((error: ApiError) => setError(error))
|
||||
event.preventDefault()
|
||||
},
|
||||
[username, displayName, password, dispatchUiNotification, router]
|
||||
[username, displayName, password, dispatchUiNotification, router, postLoginRedirectUrl]
|
||||
)
|
||||
|
||||
const ready = useMemo(() => {
|
||||
return username.trim() !== '' && displayName.trim() !== '' && password.trim() !== '' && password === passwordAgain
|
||||
return (
|
||||
username.length >= 3 &&
|
||||
username.length <= 64 &&
|
||||
displayName.trim() !== '' &&
|
||||
password.length >= 6 &&
|
||||
password === passwordAgain
|
||||
)
|
||||
}, [username, password, displayName, passwordAgain])
|
||||
|
||||
const isWeakPassword = useMemo(() => {
|
||||
return error?.backendErrorName === 'PasswordTooWeakError'
|
||||
}, [error])
|
||||
|
||||
const isValidUsername = useMemo(() => Boolean(username.trim()), [username])
|
||||
|
||||
const onUsernameChange = useLowercaseOnInputChange(setUsername)
|
||||
const onDisplayNameChange = useOnInputChange(setDisplayName)
|
||||
const onPasswordChange = useOnInputChange(setPassword)
|
||||
|
@ -73,7 +73,7 @@ export const LocalRegisterForm: NextPage = () => {
|
|||
|
||||
return (
|
||||
<Form onSubmit={doRegisterSubmit} className={'d-flex flex-column gap-3'}>
|
||||
<UsernameLabelField onChange={onUsernameChange} value={username} isValid={isValidUsername} />
|
||||
<UsernameLabelField onChange={onUsernameChange} value={username} />
|
||||
<DisplayNameField onChange={onDisplayNameChange} value={displayName} />
|
||||
<NewPasswordField onChange={onPasswordChange} value={password} hasError={isWeakPassword} />
|
||||
<PasswordAgainField
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ErrorToI18nKeyMapper } from '../../api/common/error-to-i18n-key-mapper'
|
||||
import { ErrorToI18nKeyMapper } from '../../../../api/common/error-to-i18n-key-mapper'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useFrontendConfig } from '../common/frontend-config-context/use-frontend-config'
|
||||
import { TranslatedExternalLink } from '../common/links/translated-external-link'
|
||||
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
|
||||
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
|
||||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
124
frontend/src/components/login-page/new-user/new-user-card.tsx
Normal file
124
frontend/src/components/login-page/new-user/new-user-card.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Card, Form } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useAsync } from 'react-use'
|
||||
import { cancelPendingUser, confirmPendingUser, getPendingUserInfo } from '../../../api/auth/pending-user'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||
import { UsernameLabelField } from '../../common/fields/username-label-field'
|
||||
import { DisplayNameField } from '../../common/fields/display-name-field'
|
||||
import { ProfilePictureChoice, ProfilePictureSelectField } from '../../common/fields/profile-picture-select-field'
|
||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||
import { fetchAndSetUser } from '../utils/fetch-and-set-user'
|
||||
|
||||
/**
|
||||
* The card where a new user can enter their user information.
|
||||
*/
|
||||
export const NewUserCard: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
const { value, error, loading } = useAsync(getPendingUserInfo, [])
|
||||
const [username, setUsername] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [pictureChoice, setPictureChoice] = useState(ProfilePictureChoice.FALLBACK)
|
||||
const [isUsernameSubmittable, setIsUsernameSubmittable] = useState(false)
|
||||
const [isDisplayNameSubmittable, setIsDisplayNameSubmittable] = useState(false)
|
||||
|
||||
const isSubmittable = useMemo(() => {
|
||||
return isUsernameSubmittable && isDisplayNameSubmittable
|
||||
}, [isUsernameSubmittable, isDisplayNameSubmittable])
|
||||
|
||||
const onChangeUsername = useOnInputChange(setUsername)
|
||||
const onChangeDisplayName = useOnInputChange(setDisplayName)
|
||||
|
||||
const submitUserdata = useCallback(() => {
|
||||
confirmPendingUser({
|
||||
username,
|
||||
displayName,
|
||||
profilePicture: pictureChoice === ProfilePictureChoice.PROVIDER ? value?.photoUrl : undefined
|
||||
})
|
||||
.then(() => fetchAndSetUser())
|
||||
.then(() => {
|
||||
router.push('/')
|
||||
})
|
||||
.catch(showErrorNotification('login.welcome.error'))
|
||||
}, [username, displayName, pictureChoice, router, showErrorNotification, value?.photoUrl])
|
||||
|
||||
const cancelUserCreation = useCallback(() => {
|
||||
cancelPendingUser()
|
||||
.catch(showErrorNotification('login.welcome.cancelError'))
|
||||
.finally(() => {
|
||||
router.push('/login')
|
||||
})
|
||||
}, [router, showErrorNotification])
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
showErrorNotification('login.welcome.error')(error)
|
||||
router.push('/login')
|
||||
}
|
||||
}, [error, router, showErrorNotification])
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
setUsername(value.username ?? '')
|
||||
setDisplayName(value.displayName ?? '')
|
||||
if (value.photoUrl) {
|
||||
setPictureChoice(ProfilePictureChoice.PROVIDER)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
if (!value && !loading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Body>
|
||||
{loading && <p>Loading...</p>}
|
||||
<Card.Title>
|
||||
{displayName !== '' ? (
|
||||
<Trans i18nKey={'login.welcome.title'} values={{ name: displayName }} />
|
||||
) : (
|
||||
<Trans i18nKey={'login.welcome.titleFallback'} />
|
||||
)}
|
||||
</Card.Title>
|
||||
<Trans i18nKey={'login.welcome.description'} />
|
||||
<hr />
|
||||
<Form onSubmit={submitUserdata} className={'d-flex flex-column gap-3'}>
|
||||
<DisplayNameField
|
||||
onChange={onChangeDisplayName}
|
||||
value={displayName}
|
||||
onValidityChange={setIsDisplayNameSubmittable}
|
||||
/>
|
||||
<UsernameLabelField
|
||||
onChange={onChangeUsername}
|
||||
value={username}
|
||||
onValidityChange={setIsUsernameSubmittable}
|
||||
/>
|
||||
<ProfilePictureSelectField
|
||||
onChange={setPictureChoice}
|
||||
value={pictureChoice}
|
||||
pictureUrl={value?.photoUrl}
|
||||
username={username}
|
||||
/>
|
||||
<div className={'d-flex gap-3'}>
|
||||
<Button variant={'secondary'} type={'button'} className={'w-50'} onClick={cancelUserCreation}>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</Button>
|
||||
<Button variant={'success'} type={'submit'} className={'w-50'} disabled={!isSubmittable}>
|
||||
<Trans i18nKey={'common.continue'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
|
@ -1,21 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import styles from './via-one-click.module.scss'
|
||||
import type { Icon } from 'react-bootstrap-icons'
|
||||
import {
|
||||
Exclamation as IconExclamation,
|
||||
Github as IconGithub,
|
||||
Google as IconGoogle,
|
||||
People as IconPeople,
|
||||
PersonRolodex as IconPersonRolodex
|
||||
PersonRolodex as IconPersonRolodex,
|
||||
Microsoft as IconMicrosoft,
|
||||
Paypal as IconPaypal,
|
||||
Discord as IconDiscord,
|
||||
Facebook as IconFacebook,
|
||||
Mastodon as IconMastodon
|
||||
} from 'react-bootstrap-icons'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import type { AuthProvider } from '../../../api/config/types'
|
||||
import { AuthProviderType } from '../../../api/config/types'
|
||||
import { IconGitlab } from '../../common/icons/additional/icon-gitlab'
|
||||
import styles from './one-click-login-button.module.scss'
|
||||
|
||||
export interface OneClickMetadata {
|
||||
name: string
|
||||
|
@ -24,10 +28,6 @@ export interface OneClickMetadata {
|
|||
url: string
|
||||
}
|
||||
|
||||
const getBackendAuthUrl = (providerIdentifer: string): string => {
|
||||
return `/auth/${providerIdentifer}`
|
||||
}
|
||||
|
||||
const logger = new Logger('GetOneClickProviderMetadata')
|
||||
|
||||
/**
|
||||
|
@ -37,49 +37,57 @@ const logger = new Logger('GetOneClickProviderMetadata')
|
|||
* @return Name, icon, URL and CSS class of the given provider for rendering a login button.
|
||||
*/
|
||||
export const getOneClickProviderMetadata = (provider: AuthProvider): OneClickMetadata => {
|
||||
switch (provider.type) {
|
||||
case AuthProviderType.GITHUB:
|
||||
return {
|
||||
name: 'GitHub',
|
||||
icon: IconGithub,
|
||||
className: styles['btn-social-github'],
|
||||
url: getBackendAuthUrl('github')
|
||||
}
|
||||
case AuthProviderType.GITLAB:
|
||||
return {
|
||||
name: provider.providerName,
|
||||
icon: IconGitlab,
|
||||
className: styles['btn-social-gitlab'],
|
||||
url: getBackendAuthUrl(provider.identifier)
|
||||
}
|
||||
case AuthProviderType.GOOGLE:
|
||||
return {
|
||||
name: 'Google',
|
||||
icon: IconGoogle,
|
||||
className: styles['btn-social-google'],
|
||||
url: getBackendAuthUrl('google')
|
||||
}
|
||||
case AuthProviderType.OAUTH2:
|
||||
return {
|
||||
name: provider.providerName,
|
||||
icon: IconPersonRolodex,
|
||||
className: 'btn-primary',
|
||||
url: getBackendAuthUrl(provider.identifier)
|
||||
}
|
||||
case AuthProviderType.SAML:
|
||||
return {
|
||||
name: provider.providerName,
|
||||
icon: IconPeople,
|
||||
className: 'btn-success',
|
||||
url: getBackendAuthUrl(provider.identifier)
|
||||
}
|
||||
default:
|
||||
logger.warn('Metadata for one-click-provider does not exist', provider)
|
||||
return {
|
||||
name: '',
|
||||
icon: IconExclamation,
|
||||
className: '',
|
||||
url: '#'
|
||||
}
|
||||
if (provider.type !== AuthProviderType.OIDC) {
|
||||
logger.warn('Metadata for one-click-provider does not exist', provider)
|
||||
return {
|
||||
name: '',
|
||||
icon: IconExclamation,
|
||||
className: '',
|
||||
url: '#'
|
||||
}
|
||||
}
|
||||
let icon: Icon = IconPersonRolodex
|
||||
let className: string = 'btn-primary'
|
||||
switch (provider.theme) {
|
||||
case 'github':
|
||||
className = styles.github
|
||||
icon = IconGithub
|
||||
break
|
||||
case 'google':
|
||||
className = styles.google
|
||||
icon = IconGoogle
|
||||
break
|
||||
case 'gitlab':
|
||||
className = styles.gitlab
|
||||
icon = IconGitlab
|
||||
break
|
||||
case 'facebook':
|
||||
className = styles.facebook
|
||||
icon = IconFacebook
|
||||
break
|
||||
case 'mastodon':
|
||||
className = styles.mastodon
|
||||
icon = IconMastodon
|
||||
break
|
||||
case 'discord':
|
||||
className = styles.discord
|
||||
icon = IconDiscord
|
||||
break
|
||||
case 'paypal':
|
||||
className = styles.paypal
|
||||
icon = IconPaypal
|
||||
break
|
||||
case 'azure':
|
||||
case 'microsoft':
|
||||
case 'outlook':
|
||||
className = styles.azure
|
||||
icon = IconMicrosoft
|
||||
break
|
||||
}
|
||||
return {
|
||||
name: provider.providerName,
|
||||
icon,
|
||||
className,
|
||||
url: `/api/private/auth/oidc/${provider.identifier}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@function brightness($color) {
|
||||
@return ((red($color) * 299) + (green($color) * 587) + (blue($color) * 114)) / 1000;
|
||||
}
|
||||
|
||||
@mixin button($color) {
|
||||
$font-color: if(brightness($color) > 128, #000000, #ffffff);
|
||||
color: $font-color;
|
||||
background-color: $color;
|
||||
&:hover {
|
||||
background-color: darken($color, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
.github {
|
||||
@include button(#444444);
|
||||
}
|
||||
|
||||
.gitlab {
|
||||
@include button(#FA7035);
|
||||
}
|
||||
|
||||
.google {
|
||||
@include button(#DD4B39);
|
||||
}
|
||||
|
||||
.azure {
|
||||
@include button(#008AD7);
|
||||
}
|
||||
|
||||
.facebook {
|
||||
@include button(#0165E1);
|
||||
}
|
||||
|
||||
.mastodon {
|
||||
@include button(#563ACC);
|
||||
}
|
||||
|
||||
.discord {
|
||||
@include button(#5865F2);
|
||||
}
|
||||
|
||||
.paypal {
|
||||
@include button(#00457C);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@mixin button($color) {
|
||||
color: #ffffff;
|
||||
background-color: $color;
|
||||
&:hover {
|
||||
background-color: darken($color, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-social-github {
|
||||
@include button(#444444);
|
||||
}
|
||||
|
||||
.btn-social-gitlab {
|
||||
@include button(#FA7035);
|
||||
}
|
||||
|
||||
.btn-social-google {
|
||||
@include button(#DD4B39);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -10,7 +10,7 @@ import { useTranslatedText } from '../../hooks/common/use-translated-text'
|
|||
|
||||
export interface AuthFieldProps {
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||
invalid: boolean
|
||||
isInvalid: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,13 +19,13 @@ export interface AuthFieldProps {
|
|||
* @param onChange Hook that is called when the entered password changes.
|
||||
* @param invalid True when the entered password is invalid, false otherwise.
|
||||
*/
|
||||
export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
|
||||
export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, isInvalid }) => {
|
||||
const placeholderText = useTranslatedText('login.auth.password')
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
isInvalid={invalid}
|
||||
isInvalid={isInvalid}
|
||||
type='password'
|
||||
size='sm'
|
||||
placeholder={placeholderText}
|
||||
|
|
|
@ -6,19 +6,13 @@
|
|||
|
||||
import React from 'react'
|
||||
import { Redirect } from '../common/redirect'
|
||||
import { useSingleStringUrlParameter } from '../../hooks/common/use-single-string-url-parameter'
|
||||
|
||||
const defaultFallback = '/history'
|
||||
import { useGetPostLoginRedirectUrl } from './utils/use-get-post-login-redirect-url'
|
||||
|
||||
/**
|
||||
* Redirects the browser to the relative URL that is provided via "redirectBackTo" URL parameter.
|
||||
* If no parameter has been provided or if the URL is not relative then "/history" will be used.
|
||||
* If no parameter has been provided or if the URL is not relative, then "/history" will be used.
|
||||
*/
|
||||
export const RedirectToParamOrHistory: React.FC = () => {
|
||||
const redirectBackUrl = useSingleStringUrlParameter('redirectBackTo', defaultFallback)
|
||||
|
||||
const cleanedUrl =
|
||||
redirectBackUrl.startsWith('/') && !redirectBackUrl.startsWith('//') ? redirectBackUrl : defaultFallback
|
||||
|
||||
return <Redirect to={cleanedUrl} replace={true} />
|
||||
const redirectUrl = useGetPostLoginRedirectUrl()
|
||||
return <Redirect to={redirectUrl} replace={true} />
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useSingleStringUrlParameter } from '../../../hooks/common/use-single-string-url-parameter'
|
||||
|
||||
const defaultFallback = '/history'
|
||||
|
||||
/**
|
||||
* Returns the URL that the user should be redirected to after logging in.
|
||||
* If no parameter has been provided or if the URL is not relative, then "/history" will be used.
|
||||
*/
|
||||
export const useGetPostLoginRedirectUrl = (): string => {
|
||||
const redirectBackUrl = useSingleStringUrlParameter('redirectBackTo', defaultFallback)
|
||||
return redirectBackUrl.startsWith('/') && !redirectBackUrl.startsWith('//') ? redirectBackUrl : defaultFallback
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue