Skip to content

Commit

Permalink
added basic login screen and functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Corepex committed May 6, 2024
1 parent 7a53da7 commit 25e83bf
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 15 deletions.
13 changes: 12 additions & 1 deletion assets/js/src/app/api/pimcore/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { RootState } from '@Pimcore/components/login-form/store'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
baseQuery: fetchBaseQuery({
baseUrl: '/',
prepareHeaders: (headers, { getState }) => {
// By default, if we have a token in the store, let's use that for authenticated requests
const token = (getState() as RootState).auth?.token
if (token !== null) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
}
}),
endpoints: () => ({})
})
37 changes: 37 additions & 0 deletions assets/js/src/app/auth/authSlice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { type User } from '@Pimcore/components/login-form/services/auth'
import { type RootState } from '@Pimcore/components/login-form/store'

interface AuthState {
user: User | null
token: string | null
}

const initialState: AuthState = {
user: null,
token: null

}

const slice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials: (
state,
{
payload: { user, token }
}: PayloadAction<{ user: User, token: string }>
) => {
state.user = user
state.token = token
}
}
})

export const { setCredentials } = slice.actions

export const authReducer = slice.reducer

export const selectCurrentUser = (state: RootState): User | null => state.auth.user
9 changes: 9 additions & 0 deletions assets/js/src/components/login-form/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
import {selectCurrentUser} from "@Pimcore/app/auth/authSlice";

export const useAuth = () => {
const user = useSelector(selectCurrentUser)

return useMemo(() => ({ user }), [user])
}
43 changes: 43 additions & 0 deletions assets/js/src/components/login-form/login-form-style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createStyles } from 'antd-style'

export const useStyle = createStyles(({ token, css }) => {
return {
form: css`
form {
display: flex;
flex-direction: column;
gap: 8px;
font-family: Lato, sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 22px;
.flex-space {
display: flex;
justify-content: space-between;
align-items: center;
}
.ant-btn-link {
color: ${token.colorPrimary};
&:hover {
color: ${token.colorPrimaryHover};
}
}
}
.login__additional-logins {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
.ant-btn {
width: 100%;
}
}
`
}
})
96 changes: 96 additions & 0 deletions assets/js/src/components/login-form/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Button, Checkbox, Input } from 'antd'
import React from 'react'
import { EyeInvisibleOutlined, EyeTwoTone, UserOutlined } from '@ant-design/icons'
import { useStyle } from '@Pimcore/components/login-form/login-form-style'
import { type LoginRequest, useLoginMutation } from '@Pimcore/components/login-form/services/auth'
import { useDispatch } from 'react-redux'
import { setCredentials } from '@Pimcore/app/auth/authSlice'
import { useMessage } from '@Pimcore/components/message/useMessage'

export interface IAdditionalLogins {
key: string
name: string
link: string
}

interface ILoginFormProps {
additionalLogins?: IAdditionalLogins[]
}

export const LoginForm = ({ additionalLogins }: ILoginFormProps): React.JSX.Element => {
const dispatch = useDispatch()
const { styles } = useStyle()
const [messageApi, contextHolder] = useMessage()

const [formState, setFormState] = React.useState<LoginRequest>({
username: '',
password: ''
})

const [login, { isLoading }] = useLoginMutation()

const handleAuthentication = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
try {
e.preventDefault()
const user = await login(formState).unwrap()
dispatch(setCredentials(user))

console.log('worked', user)
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
messageApi.error({
content: error.data.message
})
}
}

return (
<>
{contextHolder}
<div className={ styles.form }>
<form onSubmit={ handleAuthentication }>
<Input
onChange={ (e) => { setFormState({ ...formState, username: e.target.value }) } }
placeholder="Username"
prefix={ <UserOutlined /> }
/>
<Input.Password
iconRender={ (visible) => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />) }
onChange={ (e) => { setFormState({ ...formState, password: e.target.value }) } }
placeholder="Password"
/>
<div className={ 'flex-space' }>
<Checkbox>
Remember me
</Checkbox>
<Button type={ 'link' }>Forgot password</Button>
</div>

<Button
htmlType="submit"
loading={ isLoading }
type="primary"
>
Log in
</Button>
</form>

{Array.isArray(additionalLogins) && (
<div className={ 'login__additional-logins' }>
<p>or</p>

{additionalLogins?.map((login) => (
<Button
href={ login.link }
key={ login.key }
type={ 'primary' }
>
{login.name}
</Button>
))}
</div>
)}
</div>
</>
)
}
37 changes: 37 additions & 0 deletions assets/js/src/components/login-form/services/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { api } from '@Pimcore/app/api/pimcore'

export interface User {
username: string
// token: string
}

export interface UserResponse {
token: string
lifetime: number
user: User
}

export interface LoginRequest {
username: string
password: string
}

export const authApi = api
.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation<UserResponse, LoginRequest>({
query: (credentials) => ({
url: 'studio/api/login',
method: 'POST',
body: credentials
})
}),
protected: builder.mutation<{ message: string }, undefined>({
query: () => 'protected'
})
}),
overrideExisting: false
})

export { authApi as api }
export const { useLoginMutation, useProtectedMutation } = authApi
15 changes: 15 additions & 0 deletions assets/js/src/components/login-form/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { configureStore } from '@reduxjs/toolkit'
import { authReducer } from '@Pimcore/app/auth/authSlice';
import {api} from "@Pimcore/components/login-form/services/auth";

export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
auth: authReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
10 changes: 3 additions & 7 deletions assets/js/src/modules/app/app-view.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import React, { StrictMode } from 'react'
import { GlobalProvider } from './global-provider'
import { BaseLayoutView } from '@Pimcore/modules/app/base-layout/base-layout-view'
import { App as AntApp } from 'antd'
import { TranslationsLoaderContainer } from '@Pimcore/modules/app/translations/translations-loader-container'
import { Background } from '@Pimcore/components/background/background'
import { router } from '@Pimcore/router/router'
import { RouterProvider } from 'react-router-dom'

export const AppView = (): React.JSX.Element => {
return (
<>
<StrictMode>
<GlobalProvider>
<AntApp>
<Background />
<TranslationsLoaderContainer>
<BaseLayoutView />
</TranslationsLoaderContainer>
<RouterProvider router={ router } />
</AntApp>
</GlobalProvider>
</StrictMode>
Expand Down
12 changes: 7 additions & 5 deletions assets/js/src/modules/app/global-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ export interface GlobalProviderProps {

export const GlobalProvider = ({ children }: GlobalProviderProps): React.JSX.Element => {
return (
<ThemeProvider>
<Provider store={ store }>
{children}
</Provider>
</ThemeProvider>
<>
<ThemeProvider>
<Provider store={ store }>
{children}
</Provider>
</ThemeProvider>
</>
)
}
15 changes: 15 additions & 0 deletions assets/js/src/router/layouts/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import { Background } from '@Pimcore/components/background/background'
import { TranslationsLoaderContainer } from '@Pimcore/modules/app/translations/translations-loader-container'
import { BaseLayoutView } from '@Pimcore/modules/app/base-layout/base-layout-view'

export default function DefaultLayout (): React.JSX.Element {
return (
<>
<Background />
<TranslationsLoaderContainer>
<BaseLayoutView />
</TranslationsLoaderContainer>
</>
)
}
64 changes: 64 additions & 0 deletions assets/js/src/router/layouts/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react'
import { createStyles } from 'antd-style'
import { type IAdditionalLogins, LoginForm } from '@Pimcore/components/login-form/login-form'

const useStyle = createStyles(({ token, css }) => {
return {
loginPage: css`
display: flex;
align-items: center;
background: url(/bundles/pimcorestudioui/img/login-bg.png) lightgray 50% / cover no-repeat;
position: absolute;
inset: 0;
overflow: hidden;
`,
loginWidget: css`
display: flex;
flex-direction: column;
width: 503px;
height: 608px;
flex-shrink: 0;
border-radius: 8px;
background: linear-gradient(335deg, rgba(255, 255, 255, 0.86) 1.72%, rgba(57, 14, 97, 0.86) 158.36%);
padding: 83px 100px 0 100px;
margin-left: 80px;
/* Component/Button/primaryShadow */
box-shadow: 0px 2px 0px 0px rgba(114, 46, 209, 0.10);
img {
margin-bottom: 50px
}
`
}
})

export default function LoginLayout (): React.JSX.Element {
const { styles } = useStyle()
const additionalLogins: IAdditionalLogins[] = [
{
key: 'google',
name: 'Log in with Google',
link: '/admin/login/google'
},
{
key: 'github',
name: 'Log in with GitHub',
link: '/admin/login/github'
}
]

return (
<div
className={ styles.loginPage }
>
<div className={ styles.loginWidget }>
<img
alt={ 'Pimcore Logo' }
src={ '/bundles/pimcorestudioui/img/logo.png' }
/>
<LoginForm additionalLogins={ additionalLogins } />
</div>
</div>
)
}
Loading

0 comments on commit 25e83bf

Please sign in to comment.