Compare commits

...

2 commits

Author SHA1 Message Date
a3ac22d694 Add devcontainer configuration
All checks were successful
forgejo/Procyon/procyon/pipeline/pr-develop This commit looks good
forgejo/Procyon/procyon/pipeline/head This commit looks good
2023-11-09 10:31:10 +01:00
b4ebc22edd Update auth hooks + minor updates
Some checks are pending
forgejo/Procyon/procyon/pipeline/head Build started...
forgejo/Procyon/procyon/pipeline/pr-develop This commit looks good
2023-11-09 10:29:09 +01:00
25 changed files with 124 additions and 68 deletions

1
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1 @@
FROM local/nodejs-dev:n18_j11

View file

@ -0,0 +1,6 @@
{
"name": "procyon",
"workspaceFolder": "/home/project/procyon",
"dockerComposeFile": "docker-compose.yml",
"service": "procyon",
}

View file

@ -0,0 +1,12 @@
version: "3"
name: procyon
services:
procyon:
build:
context: .
container_name: procyon
volumes:
- ../:/home/project/procyon
command: sleep infinity

View file

@ -1,6 +1,6 @@
import { isJSON } from '@procyon/utils';
import { FetchTokensParams } from './types';
import { UseFetchTokensParams } from './types';
export const fetchTokens = async ({
baseUrl,
@ -9,7 +9,7 @@ export const fetchTokens = async ({
code,
grantType,
redirectUri,
}: FetchTokensParams) => {
}: UseFetchTokensParams) => {
try {
const response = await fetch(`${baseUrl}${tokenEndpoint}`, {
method: 'post',

View file

@ -0,0 +1,8 @@
import { useSelector } from 'react-redux';
import { AUTH_REDUCER_NAME, AuthRootState } from '@procyon/auth/types';
import { ROOT_REDUCER_NAME } from '@procyon/constants/redux';
export const useAuthAttributes = () => {
return useSelector<AuthRootState>((state) => state[ROOT_REDUCER_NAME][AUTH_REDUCER_NAME]);
};

View file

@ -5,9 +5,9 @@ import { useLocation } from 'wouter';
import { setAuthTokens } from '@procyon/auth/slice';
import { fetchTokens } from '../actions';
import { UseAuthParams } from '../types';
import { UseLoginParams } from '../types';
export const useAuthCode = ({ baseUrl, clientId, tokenEndpoint, redirectUri }: UseAuthParams) => {
export const useFetchTokens = ({ baseUrl, clientId, tokenEndpoint, redirectUri }: UseLoginParams) => {
const dispatch = useDispatch<any>();
const [, setLocation] = useLocation();

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { UseAuthParams } from '../types';
import { UseLoginParams } from '../types';
const generateState = () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
@ -9,7 +9,7 @@ const generateState = () =>
return v.toString(16);
});
export const useLogin = ({ baseUrl, clientId, authEndpoint, redirectUri }: UseAuthParams) => {
export const useLogin = ({ baseUrl, clientId, authEndpoint, redirectUri }: UseLoginParams) => {
return useCallback(() => {
const state = generateState();
const urlParams: Record<string, string> = {

View file

@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { addSeconds } from 'date-fns';
import { AUTH_REDUCER_NAME, AuthAttributes, AuthRootState, AuthTokenPayload, ModifyAuthAttribute } from './types';
import { AUTH_REDUCER_NAME, AuthAttributes, AuthRootState, AuthTokens, ModifyAuthAttributePayload } from './types';
const initialState: AuthAttributes = {
authenticated: undefined,
@ -19,7 +19,7 @@ const authSlice = createSlice({
name: AUTH_REDUCER_NAME,
initialState,
reducers: {
setAuthTokens: (_, action: PayloadAction<AuthTokenPayload>) => {
setAuthTokens: (_, action: PayloadAction<AuthTokens>) => {
const { accessExpiresIn, refreshExpiresIn } = action.payload;
return {
...action.payload,
@ -34,7 +34,7 @@ const authSlice = createSlice({
...action.payload,
};
},
updateAuthAttribute: (state, action: PayloadAction<ModifyAuthAttribute>) => {
updateAuthAttribute: (state, action: PayloadAction<ModifyAuthAttributePayload>) => {
state[action.payload.key] = action.payload.value;
},
deleteAuthAttributes: () => {
@ -50,9 +50,10 @@ export const setAuthAttribute = authSlice.actions.setAuthAttribute as <T>(
) => PayloadAction<AuthAttributes<T>>;
export const updateAuthAttribute = authSlice.actions.updateAuthAttribute as <T>(
attrs: ModifyAuthAttribute<T>
) => PayloadAction<ModifyAuthAttribute<T>>;
attrs: ModifyAuthAttributePayload<T>
) => PayloadAction<ModifyAuthAttributePayload<T>>;
export const getAuthAttribute = (state: AuthRootState, name: keyof AuthAttributes) => state.procyon.auth[name];
export const getAuthAttribute = <T>(state: AuthRootState, name: keyof AuthAttributes<T>) =>
state.procyon.auth[name as any];
export default authSlice.reducer;

View file

@ -2,7 +2,7 @@ import { ROOT_REDUCER_NAME } from '@procyon/constants/redux';
export const AUTH_REDUCER_NAME = 'auth';
export type AuthTokenPayload = {
export type AuthTokens = {
accessExpiresIn?: number;
accessToken?: string;
idToken?: string;
@ -11,14 +11,14 @@ export type AuthTokenPayload = {
sessionState?: string;
};
export type AuthAttributes<CustomAttributes = unknown> = {
export type AuthAttributes<C = unknown> = {
accessExpiresTime?: string;
authenticated?: boolean;
refreshExpiresTime?: string;
} & CustomAttributes &
AuthTokenPayload;
} & C &
AuthTokens;
export type ModifyAuthAttribute<P = unknown> = {
export type ModifyAuthAttributePayload<P = unknown> = {
key: keyof AuthAttributes<P>;
value: any;
};
@ -29,7 +29,7 @@ export type AuthRootState = {
};
};
export type UseAuthParams = {
export type UseLoginParams = {
authEndpoint: string;
baseUrl: string;
clientId: string;
@ -38,7 +38,7 @@ export type UseAuthParams = {
tokenEndpoint: string;
};
export type FetchTokensParams = Pick<UseAuthParams, 'baseUrl' | 'tokenEndpoint' | 'clientId'> & {
export type UseFetchTokensParams = Pick<UseLoginParams, 'baseUrl' | 'tokenEndpoint' | 'clientId'> & {
code: string;
grantType: 'authorization_code';
redirectUri: string;

View file

@ -69,7 +69,8 @@ export const DatePicker: React.FC<DatePickerProps<Date>> = (props) => {
label={label}
value={value ? formatFn(value, props.format ?? DefaultDateFormat) : undefined}
{...omit(['value', 'onClick'], others)}
leftIcon={<TodayIcon onClick={handleOpenModal} className="cursor-pointer" />}
leftIcon={<TodayIcon onClick={handleOpenModal} className="cursor-pointer text-primary-200" />}
placeholder={format ?? DefaultDateFormat}
/>
);
};

View file

@ -0,0 +1,10 @@
// icon:button-cursor | Material Design Icons https://materialdesignicons.com/ | Austin Andrews
import * as React from 'react';
export function ButtonCursorIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" height="1em" width="1em" {...props}>
<path d="M18.1 15.3c-.1.1-.3.2-.4.3l-2.4.4 1.7 3.6c.2.4 0 .8-.4 1l-2.8 1.3c-.1.1-.2.1-.3.1-.3 0-.6-.2-.7-.4L11.2 18l-1.9 1.5c-.1.1-.3.2-.5.2-.4 0-.8-.3-.8-.8V7.5c0-.5.3-.8.8-.8.2 0 .4.1.5.2l8.7 7.4c.3.2.4.7.1 1M6 12H4V4h16v8h-1.6l2.2 1.9c.8-.3 1.3-1 1.3-1.9V4c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h2v-2z" />
</svg>
);
}

View file

@ -3,22 +3,8 @@ import * as React from 'react';
export function ClockTimeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em" {...props}>
<path
fill="none"
stroke="currentColor"
strokeMiterlimit={10}
strokeWidth={32}
d="M256 64C150 64 64 150 64 256s86 192 192 192 192-86 192-192S362 64 256 64z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
d="M256 128v144h96"
/>
<svg viewBox="0 0 24 24" fill="currentColor" height="1em" width="1em" {...props}>
<path d="M12 20c4.4 0 8-3.6 8-8s-3.6-8-8-8-8 3.6-8 8 3.6 8 8 8m0-18c5.5 0 10 4.5 10 10s-4.5 10-10 10S2 17.5 2 12 6.5 2 12 2m3.3 14.2L14 17l-3-5.2V7h1.5v4.4l2.8 4.8z" />
</svg>
);
}

View file

@ -0,0 +1,20 @@
// icon:pointer | Lucide https://lucide.dev/ | Lucide
import * as React from 'react';
export function PointerIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
height="1em"
width="1em"
{...props}>
<path d="M22 14a8 8 0 01-8 8M18 11v-1a2 2 0 00-2-2v0a2 2 0 00-2 2v0M14 10V9a2 2 0 00-2-2v0a2 2 0 00-2 2v1M10 9.5V4a2 2 0 00-2-2v0a2 2 0 00-2 2v10" />
<path d="M18 11a2 2 0 114 0v3a8 8 0 01-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 012.83-2.82L7 15" />
</svg>
);
}

View file

@ -122,7 +122,7 @@ const List: React.FC<IProps> = ({
<div
key={option.code}
id={`list-${name}_${i}`}
className={clsx('list', className, {
className={clsx('list', {
'list-compact': compact,
'list--focus': index === i,
'list--active': selectedItemCode === option.code,
@ -146,7 +146,7 @@ const List: React.FC<IProps> = ({
<TextField name="search" label="Hledat" onChange={handleSearch} autoFocus />
</div>
)}
<div className={clsx('list-container')} onMouseLeave={setCurrentOption(-1)}>
<div className={clsx('list-container', className)} onMouseLeave={setCurrentOption(-1)}>
{items}
</div>
</div>

View file

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { find, propEq, propOr } from 'ramda';
import { PointerIcon } from '@procyon/components/Icons/Pointer';
import { ModalId } from '@procyon/components/Modal/context';
import { useModal } from '@procyon/components/Modal/hooks';
import { InputElementType } from '@procyon/types/form';
@ -14,12 +15,11 @@ export type SelectBoxProps<V = Option> = Omit<
autoComplete?: boolean;
options: Option[];
} & InputElementType<V>,
'onFocus' | 'onClick'
'onFocus' | 'onClick' | 'onBlur'
>;
export const Selectbox: React.FC<SelectBoxProps> = (props) => {
const { value, options, onBlur, onChange, name, label, autoComplete, ...others } =
props;
const { value, options, onChange, name, label, autoComplete, ...others } = props;
const { openModal, closeModal, registerModal } = useModal();
@ -32,10 +32,6 @@ export const Selectbox: React.FC<SelectBoxProps> = (props) => {
}
}, [value, options]);
const handleBlur = (_: string, e: React.FocusEvent<HTMLInputElement>): void => {
onBlur?.(selectedOption ?? undefined, e);
};
const handleOptionClick = (option: Option): void => {
setSelectedOption(option);
onChange?.(option, null);
@ -81,11 +77,9 @@ export const Selectbox: React.FC<SelectBoxProps> = (props) => {
{...others}
name={name}
label={label}
onClick={handleOpenModal}
onFocus={handleOpenModal}
onBlur={handleBlur}
value={propOr('', 'name', selectedOption)}
readonly
leftIcon={<PointerIcon className="cursor-pointer text-primary-200" onClick={handleOpenModal} />}
/>
);
};

View file

@ -29,6 +29,7 @@ export const TextField: React.FC<TextFieldProps> = (props) => {
status,
autoFocus,
mask,
placeholder,
multiline,
leftIcon,
rightIcon,
@ -139,6 +140,7 @@ export const TextField: React.FC<TextFieldProps> = (props) => {
className={clsx(passProps.className, {
'field-input--nolabel': !label,
})}
placeholder={placeholder}
autoComplete="off"
type={type}
/>

View file

@ -42,6 +42,7 @@ const TimeList: React.FC<TimeListProps> = ({ title, onChange, min, max, step, se
selectedCode={selectedCode}
scrollToCode={scrollToCode}
enableAutoScroll
className="h-60"
/>
</>
);

View file

@ -46,7 +46,7 @@ export const TimePicker: FC<TimePickerProps<Date>> = (props) => {
registerModal({
id: name,
title: label,
style: { width: 250 },
style: { width: 200 },
Component: modalComponent,
});
openModal(name);
@ -73,7 +73,9 @@ export const TimePicker: FC<TimePickerProps<Date>> = (props) => {
name={name}
label={label}
value={value ? format(value, 'HH:mm') : undefined}
leftIcon={<ClockTimeIcon onClick={handleOpenModal} className="cursor-pointer" />}
leftIcon={<ClockTimeIcon onClick={handleOpenModal} className="cursor-pointer text-primary-200" />}
placeholder="HH:mm"
mask="99:99"
/>
);
};

View file

@ -6,7 +6,7 @@ import { Grid, GridSizeEnum } from '@procyon/components/Grid';
import { GridCol } from '@procyon/components/GridCol';
import TrashIcon from '@procyon/components/Icons/Trash';
import { useModal } from '@procyon/components/Modal/hooks';
import { StatusEnum } from '@procyon/types/common';
import { SizeEnum, StatusEnum } from '@procyon/types/common';
import { Option } from '@procyon/types/options';
import TimeList from './TimeList';
@ -99,10 +99,16 @@ export const TimePickerModal: React.FC<TimePickerModalProps> = (props) => {
</Grid>
<Grid cols={{ [base]: 2 }} className="mt-4">
<GridCol>
<Button label="Použít" status={StatusEnum.success} onClick={handleChangeValue} />
<Button size={SizeEnum.sm} label="Použít" status={StatusEnum.success} onClick={handleChangeValue} />
</GridCol>
<GridCol>
<Button label={<TrashIcon fontSize={20} />} status={StatusEnum.error} blank onClick={handleClearValue} />
<Button
label={<TrashIcon fontSize={20} />}
size={SizeEnum.sm}
status={StatusEnum.error}
blank
onClick={handleClearValue}
/>
</GridCol>
</Grid>
</>

View file

@ -48,7 +48,6 @@ export const SelectBoxField: FC<SelectBoxFieldProps> = (props) => {
name={field.name}
value={field.value}
onChange={handleChange}
onBlur={field.onBlur}
{...(error ? { status: StatusEnum.error } : {})}
/>
<div className="field-validationMessage">{error?.message}</div>

View file

@ -43,7 +43,7 @@ const field = (theme) => ({
},
'&-input': {
backgroundColor: 'white',
fontSize: '18px',
fontSize: '16px',
outline: 'none',
cursor: 'text',
width: 'calc(100% - 0.75rem)',
@ -57,7 +57,7 @@ const field = (theme) => ({
},
'.checkbox-container, .radiobutton-container': {
'.field-checkbox, .field-radio': {
fontSize: '18px',
fontSize: '16px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',

View file

@ -10,6 +10,7 @@ export type InputElementType<Value> = {
onChange?: (value: Value | undefined, e: React.MouseEvent | React.ChangeEvent | null) => void;
onClick?: (value: Value, e: React.MouseEvent) => void;
onFocus?: (value: Value, e: React.FocusEvent) => void;
placeholder?: string;
status?: StatusEnum;
value?: Value;
};

View file

@ -12,7 +12,7 @@ import hookCode from './helpers/hook?raw';
`@procyon/auth` module for authenticated.
For work with IdP (Identity Provider) server use `useLogin` and `useAuthCode` hooks.
For work with IdP (Identity Provider) server use `useLogin` and `useFetchTokens` hooks.
<Unstyled>
<Message status={StatusEnum.info}>

View file

@ -2,7 +2,7 @@ import { Meta } from '@storybook/blocks';
<Meta title="Auth/Redux" />
# Redux slice
# Redux
## Actions
@ -10,13 +10,7 @@ import { Meta } from '@storybook/blocks';
`import { setAuthAttribute } from @procyon/auth/slice`
Update all avaible stored data in `auth` reducer.
### `setAuthTokens`
`import { setAuthTokens } from @procyon/auth/slice`
Set new tokens from IdP. Action will automaticaly calculate datetime, when tokens will be revoked.
Update all available stored data in `auth` reducer.
### `updateAuthAttribute`
@ -30,6 +24,12 @@ Update specific value in `auth` reducer.
Remove all data from reducer = revoke session.
### `setAuthTokens`
`import { setAuthTokens } from @procyon/auth/slice`
Set new tokens from IdP. Action will automatically calculate datetime, when tokens will be revoked.
## Selectors
### getAuthAttribute
@ -37,3 +37,9 @@ Remove all data from reducer = revoke session.
`import { getAuthAttribute } from @procyon/auth/slice`
Return stored value for specific key under `auth` reducer.
## Hooks
### useAuthAttributes
Insted of `getAuthAttribute` you can use `useAuthAttributes` hook for obtain all stored attributes.

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { useLocation } from 'wouter';
import { useAuthCode } from '@procyon/auth/hook/useAuthCode';
import { useFetchTokens } from '@procyon/auth/hook/useFetchTokens';
import { useLogin } from '@procyon/auth/hook/useLogin';
const authConfig = {
@ -17,13 +17,13 @@ export const Component: FC = () => {
const [location] = useLocation();
const doLogin = useLogin(authConfig);
const changeCodeForTokens = useAuthCode(authConfig);
const obtainTokens = useFetchTokens(authConfig);
if (location === '/login/callback') {
const params = new URLSearchParams(window.location.href);
const code = params.get('code') ?? null;
if (code) {
changeCodeForTokens(code);
obtainTokens(code);
}
return null;
}