feat: enable user to update his name and data (#332)
* feat: enable user to update his name and data issue #307 * docs: add JSDocs * test: fix sidebar tests * test: fix test cases * test: add account test cases * test: add user controller and auth service tests
This commit is contained in:
Generated
+31
@@ -14,6 +14,7 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"bootstrap": "^5.3.3",
|
||||
"dompurify": "^3.2.4",
|
||||
"i18next": "^24.2.2",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
@@ -2315,6 +2316,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||
@@ -3648,6 +3656,15 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -9923,6 +9940,12 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"optional": true
|
||||
},
|
||||
"@types/warning": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||
@@ -10801,6 +10824,14 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||
"requires": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"bootstrap": "^5.3.3",
|
||||
"dompurify": "^3.2.4",
|
||||
"i18next": "^24.2.2",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
|
||||
@@ -9,12 +9,19 @@ import i18n from '../../i18n';
|
||||
|
||||
const authContextMock = {
|
||||
signed: true,
|
||||
user: undefined,
|
||||
user: {
|
||||
userId: 1,
|
||||
name: 'Ricardo',
|
||||
email: 'ricardo@campos.com',
|
||||
admin: false,
|
||||
createdAt: new Date()
|
||||
},
|
||||
checkCurrentAuthUser: vi.fn(),
|
||||
signIn: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
register: vi.fn(),
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
updateUser: vi.fn(),
|
||||
};
|
||||
|
||||
describe('Sidebar Component', () => {
|
||||
@@ -32,7 +39,7 @@ describe('Sidebar Component', () => {
|
||||
|
||||
it('should render the Sidebar component', () => {
|
||||
const { getByText } = renderSidebar();
|
||||
expect(getByText('Ricardo Campos')).toBeDefined();
|
||||
expect(getByText('Ricardo')).toBeDefined();
|
||||
expect(getByText('Main Menu')).toBeDefined();
|
||||
expect(getByText('Preferences')).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -11,10 +11,12 @@ import ApiConfig from '../../api-service/apiConfig';
|
||||
|
||||
vi.mock('../../api-service/api');
|
||||
|
||||
const changeLanguageMock = vi.fn();
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
i18n: {
|
||||
changeLanguage: vi.fn(),
|
||||
changeLanguage: changeLanguageMock,
|
||||
},
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
@@ -27,12 +29,19 @@ vi.mock('react-i18next', () => ({
|
||||
|
||||
const authContextMock = {
|
||||
signed: true,
|
||||
user: { email: 'test@example.com' },
|
||||
user: {
|
||||
userId: 1,
|
||||
name: 'Ricardo',
|
||||
email: 'test@example.com',
|
||||
admin: false,
|
||||
createdAt: new Date()
|
||||
},
|
||||
checkCurrentAuthUser: vi.fn(),
|
||||
signIn: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
register: vi.fn(),
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
updateUser: vi.fn()
|
||||
};
|
||||
|
||||
describe('Account Component', () => {
|
||||
@@ -50,18 +59,15 @@ describe('Account Component', () => {
|
||||
|
||||
it('should render the Account component', () => {
|
||||
const { getByText } = renderAccount();
|
||||
expect(getByText('account_my_account_tittle')).toBeDefined();
|
||||
expect(getByText('account_my_account_hello')).toBeDefined();
|
||||
expect(getByText('account_my_account_logged')).toBeDefined();
|
||||
expect(getByText('test@example.com')).toBeDefined();
|
||||
expect(getByText('Change your info')).toBeDefined();
|
||||
});
|
||||
|
||||
// it('should change language when a language button is clicked', () => {
|
||||
// const { getByText } = renderAccount();
|
||||
// const languageButton = getByText('account_app_lang_tittle');
|
||||
// fireEvent.click(languageButton);
|
||||
// expect(changeLanguageMock).toHaveBeenCalled();
|
||||
// });
|
||||
it('should change language when a language button is clicked', () => {
|
||||
const { getByTestId } = renderAccount();
|
||||
const languageButton = getByTestId('language-button-pt_br');
|
||||
fireEvent.click(languageButton);
|
||||
expect(changeLanguageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show alert when delete button is clicked', () => {
|
||||
const { getByText } = renderAccount();
|
||||
@@ -82,4 +88,28 @@ describe('Account Component', () => {
|
||||
expect(authContextMock.signOut).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit the form with correct patchPayload', async () => {
|
||||
const mockPatchJSON = vi.spyOn(api, 'patchJSON').mockResolvedValue(authContextMock.user);
|
||||
|
||||
const { getByLabelText, getByText, getByTestId } = renderAccount();
|
||||
|
||||
fireEvent.change(getByLabelText(/First name/i), { target: { value: 'Jane' } });
|
||||
fireEvent.change(getByLabelText(/Email/i), { target: { value: 'jane.doe@example.com' } });
|
||||
fireEvent.change(getByTestId('account-password-one'), { target: { value: 'password123' } });
|
||||
fireEvent.change(getByLabelText(/Repeat password/i), { target: { value: 'password123' } });
|
||||
|
||||
fireEvent.click(getByText(/Save profile information/i));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPatchJSON).toHaveBeenCalledWith(expect.any(String), {
|
||||
name: 'Jane',
|
||||
email: 'jane.doe@example.com',
|
||||
password: 'password123',
|
||||
passwordAgain: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
mockPatchJSON.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,12 +28,19 @@ vi.mock('react-i18next', () => ({
|
||||
|
||||
const authContextMock = {
|
||||
signed: true,
|
||||
user: { email: 'test@example.com' },
|
||||
user: {
|
||||
userId: 1,
|
||||
name: 'Ricardo',
|
||||
email: 'test@example.com',
|
||||
admin: false,
|
||||
createdAt: new Date()
|
||||
},
|
||||
checkCurrentAuthUser: vi.fn(),
|
||||
signIn: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
register: vi.fn(),
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
updateUser: vi.fn(),
|
||||
};
|
||||
|
||||
describe('TaskAdd Component', () => {
|
||||
|
||||
@@ -16,8 +16,9 @@ const ApiConfig = {
|
||||
|
||||
homeUrl: `${server}/rest/home`,
|
||||
|
||||
notesUrl: `${server}/rest/notes`
|
||||
notesUrl: `${server}/rest/notes`,
|
||||
|
||||
userUrl: `${server}/rest/users`
|
||||
};
|
||||
|
||||
export default ApiConfig;
|
||||
|
||||
@@ -13,7 +13,7 @@ type Series = {
|
||||
};
|
||||
const data: Series[] = [
|
||||
{
|
||||
label: 'Commpleted tasks',
|
||||
label: 'Completed tasks',
|
||||
data: [
|
||||
{
|
||||
date: 'S', // Sunday
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Col, Form, InputGroup, Row } from 'react-bootstrap';
|
||||
import * as Icons from 'react-bootstrap-icons';
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
interface Props {
|
||||
labelText: string;
|
||||
iconName: IconName;
|
||||
required: boolean;
|
||||
type?: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
data_testid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FormInput component renders a form input with a label, icon, and optional password toggle.
|
||||
*
|
||||
* @param {Props} props - The properties for the FormInput component.
|
||||
* @param {string} props.labelText - The text for the form label.
|
||||
* @param {IconName} props.iconName - The name of the icon to display.
|
||||
* @param {boolean} props.required - Whether the input is required.
|
||||
* @param {string} [props.type] - The type of the input (e.g., 'text', 'password').
|
||||
* @param {string} props.name - The name of the input.
|
||||
* @param {string} [props.placeholder] - The placeholder text for the input.
|
||||
* @param {string} props.value - The value of the input.
|
||||
* @param {(e: React.ChangeEvent<HTMLInputElement>) void} props.onChange - The change event handler for the input.
|
||||
* @returns {React.ReactNode} The rendered FormInput component.
|
||||
*/
|
||||
function FormInput(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
const [showingPwd, setShowingPwd] = useState<boolean>(false);
|
||||
const [formType, setFormType] = useState<string>(props.type ? props.type : 'text');
|
||||
const Icon = Icons[props.iconName];
|
||||
|
||||
/**
|
||||
* Toggle the password visibility.
|
||||
*
|
||||
* @param {React.MouseEvent<Element, MouseEvent>} e the click event.
|
||||
*/
|
||||
const toggleShowPassword = (e: React.MouseEvent<Element, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
console.log('clicked to show password');
|
||||
setShowingPwd((prevValue: boolean) => !prevValue);
|
||||
setFormType((previous: string) => previous === 'text' ? 'password' : 'text');
|
||||
};
|
||||
|
||||
useEffect(() => {}, [showingPwd, formType]);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<Form.Group className="mb-3" controlId={`form-input-${props.name}`}>
|
||||
<Form.Label>
|
||||
{props.labelText}
|
||||
{props.type && props.type === 'password' && (
|
||||
<small>
|
||||
<a href="#" onClick={toggleShowPassword}>
|
||||
{showingPwd ? ' (Hide)' : ' (Show)'}
|
||||
</a>
|
||||
</small>
|
||||
)}
|
||||
</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<Icon />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
required={props.required}
|
||||
type={formType}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder ? props.placeholder : ''}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
data-testid={props.data_testid}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormInput;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Nav } from 'react-bootstrap';
|
||||
import { NavLink } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,18 +8,32 @@ import NavButton from '../NavButton';
|
||||
import { env } from '../../env';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
* Sidebar component renders the sidebar navigation menu.
|
||||
*
|
||||
* @returns {React.ReactNode} The rendered Sidebar component.
|
||||
*/
|
||||
function Sidebar(): React.ReactNode {
|
||||
const { signOut } = useContext(AuthContext);
|
||||
const { signOut, user } = useContext(AuthContext);
|
||||
const { t } = useTranslation();
|
||||
const build = `Build: ${env.VITE_BUILD}`;
|
||||
const [current, setCurrent] = useState<string>('/home');
|
||||
|
||||
// Note: when selected, change class to plus-jakarta-sans-thin and add background
|
||||
|
||||
const goOut = () => {
|
||||
/**
|
||||
* Handles the sign-out action.
|
||||
*/
|
||||
const goOut = (): void => {
|
||||
signOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the color for the selected navigation link.
|
||||
*
|
||||
* @param {string} path - The path of the navigation link.
|
||||
* @returns {string} The color for the selected navigation link.
|
||||
*/
|
||||
const getSelectedColor = (path: string): string => {
|
||||
if (path === current) {
|
||||
return '#4CD964';
|
||||
@@ -27,15 +41,22 @@ function Sidebar(): React.ReactNode {
|
||||
return '#6A8996';
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the navigation link click event.
|
||||
*
|
||||
* @param {string} menu - The menu path.
|
||||
*/
|
||||
const navLinkClicked = (menu: string): void => {
|
||||
setCurrent(menu);
|
||||
};
|
||||
|
||||
useEffect(() => {}, [user]);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column vh-100 bg-light sidebar">
|
||||
<div className="sidebar-header plus-jakarta-sans-bold">
|
||||
<img width="45" src={UserIcon} alt="User icon" />
|
||||
<span>Ricardo Campos</span>
|
||||
<span>{user?.name ? user?.name : 'User'}</span>
|
||||
</div>
|
||||
|
||||
<div className="header-spacer"></div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { createContext } from 'react';
|
||||
import { User } from '../types/User';
|
||||
import { UserResponse } from '../types/UserResponse';
|
||||
|
||||
export interface AuthContextData {
|
||||
signed: boolean;
|
||||
user: User | undefined;
|
||||
user: UserResponse | undefined;
|
||||
checkCurrentAuthUser: (pathname: string) => Promise<void>;
|
||||
signIn: (email: string, password: string) => Promise<string>;
|
||||
signOut: () => void;
|
||||
register: (email: string, password: string) => Promise<string>;
|
||||
isAdmin: boolean;
|
||||
updateUser: (userUpdated: UserResponse) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextData>({} as AuthContextData);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { User } from '../types/User';
|
||||
import AuthContext, { AuthContextData } from './AuthContext';
|
||||
import { API_TOKEN, REDIRECT_PATH, USER_DATA } from '../app-constants/app-constants';
|
||||
import { SigninResponse } from '../types/SigninResponse';
|
||||
import api from '../api-service/api';
|
||||
import ApiConfig from '../api-service/apiConfig';
|
||||
import { UserResponse } from '../types/UserResponse';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
|
||||
const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Props) => {
|
||||
const [signed, setSigned] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<User | undefined>();
|
||||
const [user, setUser] = useState<UserResponse | undefined>();
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false);
|
||||
const [intervalInstance, setIntervalInstance] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
@@ -44,7 +44,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const updateUserSession = (userPriv: User | null, bearerToken: string): User => {
|
||||
const updateUserSession = (userPriv: UserResponse | null, bearerToken: string): UserResponse | null => {
|
||||
if (userPriv) {
|
||||
localStorage.setItem(USER_DATA, JSON.stringify(userPriv));
|
||||
}
|
||||
@@ -59,14 +59,16 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
return JSON.parse(savedUser);
|
||||
}
|
||||
|
||||
return { email: 'undefined' };
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkCurrentAuthUser = async (pathname: string): Promise<void> => {
|
||||
const bearerToken: SigninResponse | undefined = await fetchCurrentSession(pathname);
|
||||
if (bearerToken && bearerToken.token) {
|
||||
const userLocal = updateUserSession(null, bearerToken.token);
|
||||
setUser(userLocal);
|
||||
if (userLocal) {
|
||||
setUser(userLocal);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,8 +76,12 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
try {
|
||||
const payload = { email, password };
|
||||
const registerResponse: SigninResponse = await api.putJSON(ApiConfig.registerUrl, payload);
|
||||
const currentUser: User = {
|
||||
email
|
||||
const currentUser: UserResponse = {
|
||||
userId: registerResponse.userId,
|
||||
name: registerResponse.name,
|
||||
email: registerResponse.email,
|
||||
admin: registerResponse.admin,
|
||||
createdAt: new Date(registerResponse.createdAt)
|
||||
};
|
||||
|
||||
setSigned(true);
|
||||
@@ -95,8 +101,12 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
try {
|
||||
const payload = { email, password };
|
||||
const registerResponse: SigninResponse = await api.postJSON(ApiConfig.signInUrl, payload);
|
||||
const currentUser: User = {
|
||||
email
|
||||
const currentUser: UserResponse = {
|
||||
userId: registerResponse.userId,
|
||||
name: registerResponse.name,
|
||||
email: registerResponse.email,
|
||||
admin: registerResponse.admin,
|
||||
createdAt: new Date(registerResponse.createdAt)
|
||||
};
|
||||
|
||||
setSigned(true);
|
||||
@@ -126,7 +136,9 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
const bearerToken: SigninResponse | undefined = await fetchCurrentSession('/');
|
||||
if (bearerToken) {
|
||||
const userLocal = updateUserSession(null, bearerToken.token);
|
||||
setUser(userLocal);
|
||||
if (userLocal) {
|
||||
setUser(userLocal);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
@@ -148,6 +160,11 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
setIntervalInstance(instance);
|
||||
}
|
||||
|
||||
const updateUser = (userUpdated: UserResponse): void => {
|
||||
setUser(userUpdated);
|
||||
localStorage.setItem(USER_DATA, JSON.stringify(userUpdated));
|
||||
};
|
||||
|
||||
const contextValue: AuthContextData = useMemo(() => ({
|
||||
signed,
|
||||
user,
|
||||
@@ -155,8 +172,9 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
signIn,
|
||||
signOut,
|
||||
register,
|
||||
isAdmin
|
||||
}), [signed, user, checkCurrentAuthUser, signIn, signOut, register, isAdmin]);
|
||||
isAdmin,
|
||||
updateUser
|
||||
}), [signed, user, checkCurrentAuthUser, signIn, signOut, register, isAdmin, updateUser]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
|
||||
@@ -176,4 +176,19 @@ a:hover, .btn-link:hover {
|
||||
|
||||
.about-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.home-hello {
|
||||
font-size: 28px;
|
||||
color: #233C46;
|
||||
}
|
||||
|
||||
.home-subtitle {
|
||||
color: #A1A1A1;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.home-productive {
|
||||
color: #4CD964;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export type SigninResponse = {
|
||||
userId: number;
|
||||
name: string;
|
||||
email: string;
|
||||
admin: boolean;
|
||||
createdAt: Date;
|
||||
token: string;
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export type User = {
|
||||
email: string;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export type UserPatchRequest = {
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
password: string | null;
|
||||
passwordAgain: string | null;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export type UserResponse = {
|
||||
userId: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
admin: boolean;
|
||||
createdAt: Date;
|
||||
};
|
||||
@@ -1,12 +1,18 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Alert, Button, Card, Col, Container, Row } from 'react-bootstrap';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Alert, Button, Col, Container, Form, Row } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FileText, Person, ShieldCheck } from 'react-bootstrap-icons';
|
||||
import { FileText, Person } from 'react-bootstrap-icons';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { clearStorage, setDefaultLang } from '../../storage-service/storage';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
import { LangAvailable, languages } from '../../constants/languages_available';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import FormInput from '../../components/FormInput';
|
||||
import { translateServerResponse } from '../../utils/TranslatorUtils';
|
||||
import { UserPatchRequest } from '../../types/UserPatchRequest';
|
||||
import { UserResponse } from '../../types/UserResponse';
|
||||
import './styles.css';
|
||||
|
||||
/**
|
||||
* Account page component.
|
||||
@@ -16,15 +22,30 @@ import ApiConfig from '../../api-service/apiConfig';
|
||||
* @returns {React.ReactNode} The Account page component.
|
||||
*/
|
||||
function Account(): React.ReactNode {
|
||||
const { signOut, user } = useContext(AuthContext);
|
||||
const { signOut, user, updateUser } = useContext(AuthContext);
|
||||
const { i18n, t } = useTranslation();
|
||||
const [showAlert, setShowAlert] = useState<boolean>(false);
|
||||
const [validated, setValidated] = useState<boolean>(false);
|
||||
const [formInvalid, setFormInvalid] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [userName, setUserName] = useState<string>('');
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const [userPassword, setUserPassword] = useState<string>('');
|
||||
const [userPasswordAgain, setUserPasswordAgain] = useState<string>('');
|
||||
|
||||
const handleLanguage = (lang: string) => {
|
||||
/**
|
||||
* Handle the language change.
|
||||
*
|
||||
* @param {string} lang the language to change to.
|
||||
*/
|
||||
const handleLanguage = (lang: string): void => {
|
||||
i18n.changeLanguage(lang);
|
||||
setDefaultLang(lang);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the user account
|
||||
*/
|
||||
const deleteAccount = async (): Promise<void> => {
|
||||
setShowAlert(false);
|
||||
await api.postJSON(ApiConfig.deleteAccountUrl, {});
|
||||
@@ -32,38 +53,189 @@ function Account(): React.ReactNode {
|
||||
clearStorage();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles errors by setting the error message and form invalid state.
|
||||
*
|
||||
* @param {unknown} e - The error to handle.
|
||||
*/
|
||||
const handleError = (e: unknown): void => {
|
||||
if (typeof e === 'string') {
|
||||
setErrorMessage(translateServerResponse(e, i18n.language));
|
||||
setFormInvalid(true);
|
||||
}
|
||||
else if (e instanceof Error) {
|
||||
setErrorMessage(translateServerResponse(e.message, i18n.language));
|
||||
setFormInvalid(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Patches a user into.
|
||||
*
|
||||
* @param {UserPatchRequest} payload - The user data to patch.
|
||||
* @returns {Promise<boolean>} True if the task was added successfully, false otherwise.
|
||||
*/
|
||||
const patchUserInfo = async (payload: UserPatchRequest): Promise<UserResponse | undefined> => {
|
||||
try {
|
||||
return await api.patchJSON(ApiConfig.userUrl, payload) as UserResponse;
|
||||
}
|
||||
catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the input fields to their default values.
|
||||
*/
|
||||
const resetInputs = (userUpdated: UserResponse): void => {
|
||||
setUserName(userUpdated.name ? userUpdated.name : '');
|
||||
setUserEmail(userUpdated.email);
|
||||
updateUser(userUpdated);
|
||||
|
||||
setValidated(false);
|
||||
setFormInvalid(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setValidated(true);
|
||||
|
||||
const form = event.currentTarget;
|
||||
|
||||
const patchPayload: UserPatchRequest = {
|
||||
name: userName ? DOMPurify.sanitize(userName) : null,
|
||||
email: userEmail,
|
||||
password: userPassword,
|
||||
passwordAgain: userPasswordAgain
|
||||
};
|
||||
|
||||
const updated: UserResponse | undefined = await patchUserInfo(patchPayload);
|
||||
if (updated) {
|
||||
form.reset();
|
||||
resetInputs(updated);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {}, [user]);
|
||||
|
||||
return (
|
||||
<Container className="mb-5 main-margin">
|
||||
<Row className="justify-content-center mb-4">
|
||||
<Container>
|
||||
<h1 className="poppins-regular home-hello main-margin">
|
||||
My
|
||||
{' '}
|
||||
<b>Account</b>
|
||||
</h1>
|
||||
<p className="poppins-regular home-subtitle">
|
||||
{t('account_my_account_hello')}
|
||||
</p>
|
||||
|
||||
<Row className="mb-3">
|
||||
<Col xs={12}>
|
||||
<Card className="p-4 shadow-sm">
|
||||
<h2 className="mb-4">
|
||||
<Person />
|
||||
{' '}
|
||||
{t('account_my_account_tittle')}
|
||||
</h2>
|
||||
<p>{t('account_my_account_hello')}</p>
|
||||
<p data-testid="logged-in-as">
|
||||
{t('account_my_account_logged')}
|
||||
{' '}
|
||||
<b>
|
||||
{user?.email}
|
||||
</b>
|
||||
</p>
|
||||
</Card>
|
||||
<h2 className="poppins-regular">Update and Manage, Your</h2>
|
||||
<h2 className="poppins-bold home-productive">Information</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="justify-content-center mb-4">
|
||||
<Col xs={12}>
|
||||
<Card className="p-4 shadow-sm">
|
||||
<h2 className="mb-4">
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<div className="user-info-card">
|
||||
<div className="title">
|
||||
<Person />
|
||||
{' '}
|
||||
Change your info
|
||||
</div>
|
||||
<span className="description">
|
||||
Update only what you need. Blank fields will not be updated
|
||||
</span>
|
||||
|
||||
{formInvalid
|
||||
? (
|
||||
<Alert variant="danger" data-testid="add-task-error-message">
|
||||
{ errorMessage }
|
||||
</Alert>
|
||||
)
|
||||
: null}
|
||||
|
||||
<Form noValidate validated={validated} onSubmit={handleSubmit} className="mt-4">
|
||||
{/* User name */}
|
||||
<FormInput
|
||||
labelText="First name"
|
||||
iconName="Person"
|
||||
required={false}
|
||||
name="name"
|
||||
placeholder={user?.name ? user.name : ''}
|
||||
value={userName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* User email */}
|
||||
<FormInput
|
||||
labelText="Email"
|
||||
iconName="At"
|
||||
required={false}
|
||||
name="email"
|
||||
placeholder={user?.email}
|
||||
value={userEmail}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* User password */}
|
||||
<FormInput
|
||||
labelText="Password"
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="password"
|
||||
value={userPassword}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPassword(e.target.value);
|
||||
}}
|
||||
data_testid="account-password-one"
|
||||
/>
|
||||
|
||||
{/* User password again */}
|
||||
<FormInput
|
||||
labelText="Repeat password"
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="passwordAgain"
|
||||
value={userPasswordAgain}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPasswordAgain(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
Save profile information
|
||||
</Button>
|
||||
</Form>
|
||||
<hr />
|
||||
<p>
|
||||
If you want to display your picture, we support Gravatar.
|
||||
Please head to
|
||||
{' '}
|
||||
<a href="#">Gravatar</a>
|
||||
{' '}
|
||||
to register or update your profile picture.
|
||||
</p>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<div className="user-info-card">
|
||||
<div className="title">
|
||||
<FileText />
|
||||
{' '}
|
||||
{t('account_app_lang_tittle')}
|
||||
</h2>
|
||||
<p>{t('account_app_lang_description')}</p>
|
||||
<div className="my-3">
|
||||
Change the app language
|
||||
</div>
|
||||
<span className="description">{t('account_app_lang_description')}</span>
|
||||
<div className="mt-4 mb-2">Available languages</div>
|
||||
<div>
|
||||
{languages.map((lang: LangAvailable) => (
|
||||
<Button
|
||||
key={lang.key}
|
||||
@@ -71,23 +243,38 @@ function Account(): React.ReactNode {
|
||||
variant="outline-primary"
|
||||
className="btn-sm me-3"
|
||||
onClick={() => handleLanguage(lang.lang)}
|
||||
data-testid={`language-button-${lang.lang}`}
|
||||
>
|
||||
{lang.lang === 'pt_br' && (
|
||||
<span>🇧🇷 </span>
|
||||
)}
|
||||
{lang.lang === 'en' && (
|
||||
<span>🇺🇸 </span>
|
||||
)}
|
||||
{lang.lang === 'es' && (
|
||||
<span>🇪🇸 </span>
|
||||
)}
|
||||
{lang.lang === 'ru' && (
|
||||
<span>🇷🇺 </span>
|
||||
)}
|
||||
{t(lang.key)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="justify-content-center mb-4">
|
||||
<Row className="my-4">
|
||||
<Col xs={12}>
|
||||
<Card className="p-4 shadow-sm">
|
||||
<h2 className="mb-4">
|
||||
<ShieldCheck />
|
||||
{' '}
|
||||
{t('account_privacy_little')}
|
||||
</h2>
|
||||
<h3 className="poppins-regular">Your Privacy</h3>
|
||||
<h4 className="poppins-bold home-productive">Matters</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<div className="user-info-card">
|
||||
<p>{t('account_privacy_text')}</p>
|
||||
<Button
|
||||
variant="danger"
|
||||
@@ -106,7 +293,7 @@ function Account(): React.ReactNode {
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.user-info-card {
|
||||
background-color: #fff;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: #6a8996;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { NavLink } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import { handleDefaultLang } from '../../lang-service/LangHandler';
|
||||
import { translateServerResponse } from '../../utils/TranslatorUtils';
|
||||
import CompletedTasks from '../../components/CompletedTasks';
|
||||
import TaskProgress from '../../components/TaskProgress';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
@@ -32,13 +33,19 @@ import './style.css';
|
||||
* @returns {React.ReactNode} The Home page component.
|
||||
*/
|
||||
function Home(): React.ReactNode {
|
||||
const { user } = useContext(AuthContext);
|
||||
const { i18n, t } = useTranslation();
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [validated, setValidated] = useState<boolean>(false);
|
||||
const [formInvalid, setFormInvalid] = useState<boolean>(false);
|
||||
const [searchResults, setSearchResults] = useState<HomeSearchResponse | null>(null);
|
||||
const [name, setName] = useState<string>('Ricardo');
|
||||
const { i18n, t } = useTranslation();
|
||||
const [name, setName] = useState<string>(user?.name ? user?.name : 'User');
|
||||
|
||||
/**
|
||||
* Handles the error by setting the error message.
|
||||
*
|
||||
* @param {unknown} e - The error to handle.
|
||||
*/
|
||||
const handleError = (e: unknown): void => {
|
||||
if (typeof e === 'string') {
|
||||
setErrorMessage(translateServerResponse(e, i18n.language));
|
||||
@@ -48,6 +55,12 @@ function Home(): React.ReactNode {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a term.
|
||||
*
|
||||
* @param {string}
|
||||
* @returns {Promise<boolean>} Whether the search was successful.
|
||||
*/
|
||||
const searchTerm = async (term: string): Promise<boolean> => {
|
||||
try {
|
||||
const response: HomeSearchResponse = await api.getJSON(`${ApiConfig.homeUrl}/search?term=${term}`);
|
||||
@@ -60,6 +73,11 @@ function Home(): React.ReactNode {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the search form submission.
|
||||
*
|
||||
* @param {React.FormEvent<HTMLFormElement>} event - The form submission event.
|
||||
*/
|
||||
const handleSearch = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -81,8 +99,8 @@ function Home(): React.ReactNode {
|
||||
|
||||
useEffect(() => {
|
||||
handleDefaultLang();
|
||||
setName('Ricardo');
|
||||
}, []);
|
||||
setName(user?.name ? user?.name : 'User');
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
.home-hello {
|
||||
margin-top: 2rem;
|
||||
font-size: 28px;
|
||||
color: #233C46;
|
||||
}
|
||||
|
||||
.home-subtitle {
|
||||
color: #A1A1A1;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.home-productive {
|
||||
color: #4CD964;
|
||||
}
|
||||
|
||||
.home-card {
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.home-new-item {
|
||||
border: none;
|
||||
border-radius: 15px !important;
|
||||
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
Col,
|
||||
Container,
|
||||
Form,
|
||||
InputGroup,
|
||||
Row
|
||||
} from 'react-bootstrap';
|
||||
import { CalendarCheck, Hash, PencilFill } from 'react-bootstrap-icons';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import TaskNoteRequest from '../../types/TaskNoteRequest';
|
||||
import { TaskResponse } from '../../types/TaskResponse';
|
||||
@@ -17,6 +15,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import { translateServerResponse } from '../../utils/TranslatorUtils';
|
||||
import FormInput from '../../components/FormInput';
|
||||
|
||||
type TaskAction = 'add' | 'edit';
|
||||
|
||||
@@ -214,94 +213,61 @@ function TaskAdd(): React.ReactNode {
|
||||
: null}
|
||||
|
||||
<Form noValidate validated={validated} onSubmit={handleSubmit}>
|
||||
<Row>
|
||||
<Col xs={9}>
|
||||
<Form.Group className="mb-3" controlId="task-form-description">
|
||||
<Form.Label>{t('task_form_desc_label')}</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<PencilFill />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
name="description"
|
||||
placeholder={t('task_form_desc_placeholder')}
|
||||
value={taskDescription}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTaskDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* Description */}
|
||||
<FormInput
|
||||
labelText={t('task_form_desc_label')}
|
||||
iconName="PencilFill"
|
||||
required={true}
|
||||
type="text"
|
||||
name="description"
|
||||
placeholder={t('task_form_desc_placeholder')}
|
||||
value={taskDescription}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTaskDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
<Col xs={9}>
|
||||
<Form.Group className="mb-3" controlId="task-form-url">
|
||||
<Form.Label>{t('task_form_url_label')}</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>@</InputGroup.Text>
|
||||
<Form.Control
|
||||
required={false}
|
||||
type="text"
|
||||
name="url"
|
||||
placeholder={t('task_form_url_placeholder')}
|
||||
value={taskUrl}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTaskUrl(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* Task URL */}
|
||||
<FormInput
|
||||
labelText={t('task_form_url_label')}
|
||||
iconName="At"
|
||||
required={false}
|
||||
type="text"
|
||||
name="url"
|
||||
placeholder={t('task_form_url_placeholder')}
|
||||
value={taskUrl}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTaskUrl(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<Form.Group className="mb-3" controlId="task-form-due-date">
|
||||
<Form.Label>{t('task_form_duedate_label')}</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<CalendarCheck />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
required={false}
|
||||
type="text"
|
||||
name="dueDate"
|
||||
placeholder={t('task_form_duedate_placeholder')}
|
||||
value={dueDate}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDueDate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col sm={6}>
|
||||
<Form.Group className="mb-3" controlId="task-form-tag">
|
||||
<Form.Label>Tag</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<Hash />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
required={false}
|
||||
type="text"
|
||||
name="tag"
|
||||
placeholder="my-tag"
|
||||
value={tag}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* Due date */}
|
||||
<FormInput
|
||||
labelText={t('task_form_duedate_label')}
|
||||
iconName="CalendarCheck"
|
||||
required={false}
|
||||
type="text"
|
||||
name="dueDate"
|
||||
placeholder={t('task_form_duedate_placeholder')}
|
||||
value={dueDate}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDueDate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Tag */}
|
||||
<FormInput
|
||||
labelText="Tag"
|
||||
iconName="Hash"
|
||||
required={false}
|
||||
type="text"
|
||||
name="tag"
|
||||
placeholder="my-tag"
|
||||
value={tag}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Check
|
||||
type="switch"
|
||||
|
||||
+8
-8
@@ -4,7 +4,7 @@ import br.com.tasknoteapp.server.exception.InvalidCredentialsException;
|
||||
import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException;
|
||||
import br.com.tasknoteapp.server.exception.UserNotFoundException;
|
||||
import br.com.tasknoteapp.server.request.LoginRequest;
|
||||
import br.com.tasknoteapp.server.response.JwtAuthenticationResponse;
|
||||
import br.com.tasknoteapp.server.response.UserResponseWithToken;
|
||||
import br.com.tasknoteapp.server.service.AuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
@@ -55,10 +55,10 @@ public class AuthenticationController {
|
||||
description = "Email already in use",
|
||||
content = @Content(schema = @Schema(implementation = Void.class)))
|
||||
})
|
||||
public ResponseEntity<JwtAuthenticationResponse> signUp(
|
||||
public ResponseEntity<UserResponseWithToken> signUp(
|
||||
@RequestBody @Valid LoginRequest loginRequest) {
|
||||
String token = authService.signUpNewUser(loginRequest);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(new JwtAuthenticationResponse(token));
|
||||
UserResponseWithToken response = authService.signUpNewUser(loginRequest);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,11 +88,11 @@ public class AuthenticationController {
|
||||
description = "User not found",
|
||||
content = @Content(schema = @Schema(implementation = Void.class)))
|
||||
})
|
||||
public JwtAuthenticationResponse signIn(@RequestBody @Valid LoginRequest loginRequest) {
|
||||
String token = authService.signInUser(loginRequest);
|
||||
if (Objects.isNull(token)) {
|
||||
public ResponseEntity<UserResponseWithToken> signIn(@RequestBody @Valid LoginRequest loginRequest) {
|
||||
UserResponseWithToken response = authService.signInUser(loginRequest);
|
||||
if (Objects.isNull(response)) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
return new JwtAuthenticationResponse(token);
|
||||
return ResponseEntity.ok().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package br.com.tasknoteapp.server.controller;
|
||||
|
||||
import br.com.tasknoteapp.server.request.UserPatchRequest;
|
||||
import br.com.tasknoteapp.server.response.UserResponse;
|
||||
import br.com.tasknoteapp.server.service.AuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -7,9 +8,13 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -47,4 +52,36 @@ public class UserController {
|
||||
public List<UserResponse> getAllUsers() {
|
||||
return authService.getAllUsers();
|
||||
}
|
||||
|
||||
@PatchMapping
|
||||
@Operation(
|
||||
summary = "Patch the user data",
|
||||
description = "Patch all user information. Empty fields will not be updated",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Task successfully patched",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = UserResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Forbidden. Access Denied",
|
||||
content = @Content(schema = @Schema(implementation = Void.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Task not found",
|
||||
content = @Content(schema = @Schema(implementation = Void.class)))
|
||||
})
|
||||
public ResponseEntity<UserResponse> patchUserInfo(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "User data to be patched.",
|
||||
required = true)
|
||||
@RequestBody
|
||||
@Valid
|
||||
UserPatchRequest taskRequest) {
|
||||
UserResponse patched = authService.patchUserInfo(taskRequest);
|
||||
return ResponseEntity.ok().body(patched);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ public class UserEntity implements UserDetails {
|
||||
@Column(name = "inactivated_at", nullable = true)
|
||||
private LocalDateTime inactivatedAt;
|
||||
|
||||
@Column(name = "name", length = 20)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "user")
|
||||
private List<TaskEntity> tasks;
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package br.com.tasknoteapp.server.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/** This record represents a user patch payload. */
|
||||
@Schema(description = "User patch payload.")
|
||||
public record UserPatchRequest(
|
||||
@Schema(description = "User first name. Optional.") String name,
|
||||
@Schema(description = "User email. Optional.") String email,
|
||||
@Schema(description = "User password. Optional.") String password,
|
||||
@Schema(description = "User password again. Optional.") String passwordAgain) {}
|
||||
@@ -1,14 +1,14 @@
|
||||
package br.com.tasknoteapp.server.response;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
|
||||
/** This record represents a User Response object. */
|
||||
@Schema(description = "This record represents a User Response object.")
|
||||
public record UserResponse(
|
||||
@Schema(description = "The id of the user", example = "1") Long userId,
|
||||
@Schema(description = "The name of the user", example = "John") String name,
|
||||
@Schema(description = "The email of the user", example = "user@domain.com") String email,
|
||||
@Schema(description = "The admin status of the user", example = "false") Boolean admin,
|
||||
@Schema(description = "The created date and time of the user", example = "2023-01-01T00:00:00")
|
||||
@@ -18,7 +18,6 @@ public record UserResponse(
|
||||
example = "2023-01-01T00:00:00")
|
||||
LocalDateTime inactivatedAt) {
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link UserResponse} instance from a {@link UserEntity}.
|
||||
*
|
||||
@@ -28,6 +27,7 @@ public record UserResponse(
|
||||
public static UserResponse fromEntity(UserEntity user) {
|
||||
return new UserResponse(
|
||||
user.getId(),
|
||||
user.getName(),
|
||||
user.getEmail(),
|
||||
user.getAdmin(),
|
||||
user.getCreatedAt(),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package br.com.tasknoteapp.server.response;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** This record represents a User Response object. */
|
||||
@Schema(description = "This record represents a User Response with token object.")
|
||||
public record UserResponseWithToken(
|
||||
@Schema(description = "The id of the user", example = "1") Long userId,
|
||||
@Schema(description = "The name of the user", example = "John") String name,
|
||||
@Schema(description = "The email of the user", example = "user@domain.com") String email,
|
||||
@Schema(description = "The admin status of the user", example = "false") Boolean admin,
|
||||
@Schema(description = "The created date and time of the user", example = "2023-01-01T00:00:00")
|
||||
LocalDateTime createdAt,
|
||||
@Schema(
|
||||
description = "The inactivated date and time of the user",
|
||||
example = "2023-01-01T00:00:00")
|
||||
LocalDateTime inactivatedAt,
|
||||
@Schema(description = "The token created upon login") String token) {
|
||||
|
||||
/**
|
||||
* Create a {@link UserResponseWithToken} instance from a {@link UserEntity}.
|
||||
*
|
||||
* @param user The user entity instance with user info to be used as source.
|
||||
* @param token The token created upon registration or login.
|
||||
* @return UserResponse instance.
|
||||
*/
|
||||
public static UserResponseWithToken fromEntity(UserEntity user, String token) {
|
||||
return new UserResponseWithToken(
|
||||
user.getId(),
|
||||
user.getName(),
|
||||
user.getEmail(),
|
||||
user.getAdmin(),
|
||||
user.getCreatedAt(),
|
||||
user.getInactivatedAt(),
|
||||
token);
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,16 @@ import br.com.tasknoteapp.server.exception.UserNotFoundException;
|
||||
import br.com.tasknoteapp.server.repository.UserPwdLimitRepository;
|
||||
import br.com.tasknoteapp.server.repository.UserRepository;
|
||||
import br.com.tasknoteapp.server.request.LoginRequest;
|
||||
import br.com.tasknoteapp.server.request.UserPatchRequest;
|
||||
import br.com.tasknoteapp.server.response.UserResponse;
|
||||
import br.com.tasknoteapp.server.response.UserResponseWithToken;
|
||||
import br.com.tasknoteapp.server.util.AuthUtil;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -54,7 +57,7 @@ public class AuthService {
|
||||
* @param login User details with email and password.
|
||||
* @return Token
|
||||
*/
|
||||
public String signUpNewUser(LoginRequest login) {
|
||||
public UserResponseWithToken signUpNewUser(LoginRequest login) {
|
||||
log.info("Signing up new user! {}", login.email());
|
||||
|
||||
if (findByEmail(login.email()).isPresent()) {
|
||||
@@ -76,7 +79,7 @@ public class AuthService {
|
||||
String token = jwtService.generateToken(user.getEmail());
|
||||
|
||||
log.info("User created! Token {}", token);
|
||||
return token;
|
||||
return UserResponseWithToken.fromEntity(user, token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +114,7 @@ public class AuthService {
|
||||
* @return Token
|
||||
*/
|
||||
@Transactional
|
||||
public String signInUser(LoginRequest login) {
|
||||
public UserResponseWithToken signInUser(LoginRequest login) {
|
||||
log.info("Signing in user! {}", login.email());
|
||||
|
||||
Optional<UserEntity> user = findByEmail(login.email());
|
||||
@@ -130,7 +133,7 @@ public class AuthService {
|
||||
log.info("User authenticated! Token {}", token);
|
||||
|
||||
userPwdLimitRepository.deleteAllForUser(user.get().getId());
|
||||
return token;
|
||||
return UserResponseWithToken.fromEntity(user.get(), token);
|
||||
} catch (BadCredentialsException e) {
|
||||
log.error("BadCredentialsException when logging in user {}", user.get().getId());
|
||||
|
||||
@@ -213,12 +216,46 @@ public class AuthService {
|
||||
userPwdLimitRepository.deleteAllForUser(currentUser.getId());
|
||||
userRepository.delete(currentUser);
|
||||
|
||||
return new UserResponse(
|
||||
currentUser.getId(),
|
||||
currentUser.getEmail(),
|
||||
currentUser.getAdmin(),
|
||||
currentUser.getCreatedAt(),
|
||||
currentUser.getInactivatedAt());
|
||||
return UserResponse.fromEntity(currentUser);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse patchUserInfo(UserPatchRequest patchRequest) {
|
||||
Optional<String> currentUserEmail = authUtil.getCurrentUserEmail();
|
||||
String email = currentUserEmail.orElseThrow();
|
||||
UserEntity currentUser = findByEmail(email).orElseThrow();
|
||||
boolean shouldUpdate = false;
|
||||
|
||||
if (!Objects.isNull(patchRequest.name()) && !patchRequest.name().isBlank()) {
|
||||
currentUser.setName(patchRequest.name().trim());
|
||||
shouldUpdate = true;
|
||||
}
|
||||
if (!Objects.isNull(patchRequest.email()) && !patchRequest.email().isBlank()) {
|
||||
currentUser.setEmail(patchRequest.email().trim());
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
boolean updatePassword =
|
||||
!Objects.isNull(patchRequest.password())
|
||||
&& !patchRequest.password().isBlank()
|
||||
&& !Objects.isNull(patchRequest.passwordAgain())
|
||||
&& !patchRequest.passwordAgain().isBlank();
|
||||
|
||||
if (updatePassword) {
|
||||
Optional<String> passwordValidation = authUtil.validatePassword(patchRequest.password());
|
||||
if (passwordValidation.isPresent()) {
|
||||
throw new BadPasswordException(passwordValidation.get());
|
||||
}
|
||||
|
||||
currentUser.setPassword(passwordEncoder.encode(patchRequest.password()));
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
userRepository.save(currentUser);
|
||||
}
|
||||
|
||||
return UserResponse.fromEntity(currentUser);
|
||||
}
|
||||
|
||||
private void checkLoginAttemptLimit(Long userId) {
|
||||
|
||||
@@ -122,7 +122,7 @@ public class TaskService {
|
||||
|
||||
TaskEntity taskEntity = task.get();
|
||||
if (!Objects.isNull(patch.description()) && !patch.description().isBlank()) {
|
||||
taskEntity.setDescription(patch.description());
|
||||
taskEntity.setDescription(patch.description().trim());
|
||||
}
|
||||
if (!Objects.isNull(patch.done())) {
|
||||
taskEntity.setDone(patch.done());
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table tasknote.users
|
||||
add name varchar(20) null;
|
||||
+20
-3
@@ -9,7 +9,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
|
||||
import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException;
|
||||
import br.com.tasknoteapp.server.request.LoginRequest;
|
||||
import br.com.tasknoteapp.server.response.UserResponseWithToken;
|
||||
import br.com.tasknoteapp.server.service.AuthService;
|
||||
import java.time.LocalDateTime;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -33,7 +35,10 @@ class AuthenticationControllerTest {
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456");
|
||||
final String token = "xaxbxcxdx1x2x3A@";
|
||||
|
||||
when(authService.signUpNewUser(request)).thenReturn(token);
|
||||
UserResponseWithToken response =
|
||||
new UserResponseWithToken(
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, token);
|
||||
when(authService.signUpNewUser(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
"""
|
||||
@@ -51,6 +56,9 @@ class AuthenticationControllerTest {
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.content(jsonString))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.userId").value(response.userId()))
|
||||
.andExpect(jsonPath("$.email").value(response.email()))
|
||||
.andExpect(jsonPath("$.admin").value(response.admin()))
|
||||
.andExpect(jsonPath("$.token").value(token))
|
||||
.andReturn();
|
||||
}
|
||||
@@ -61,7 +69,10 @@ class AuthenticationControllerTest {
|
||||
LoginRequest request = new LoginRequest("user@domain..com", "abcde123456");
|
||||
final String token = "xaxbxcxdx1x2x3@A";
|
||||
|
||||
when(authService.signUpNewUser(request)).thenReturn(token);
|
||||
UserResponseWithToken response =
|
||||
new UserResponseWithToken(
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, token);
|
||||
when(authService.signUpNewUser(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
"""
|
||||
@@ -114,7 +125,10 @@ class AuthenticationControllerTest {
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456");
|
||||
final String token = "xaxbxcxdx1x2x3A@";
|
||||
|
||||
when(authService.signInUser(request)).thenReturn(token);
|
||||
UserResponseWithToken response =
|
||||
new UserResponseWithToken(
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, token);
|
||||
when(authService.signInUser(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
"""
|
||||
@@ -132,6 +146,9 @@ class AuthenticationControllerTest {
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.content(jsonString))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.userId").value(response.userId()))
|
||||
.andExpect(jsonPath("$.email").value(response.email()))
|
||||
.andExpect(jsonPath("$.admin").value(response.admin()))
|
||||
.andExpect(jsonPath("$.token").value(token))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package br.com.tasknoteapp.server.controller;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import br.com.tasknoteapp.server.request.UserPatchRequest;
|
||||
import br.com.tasknoteapp.server.response.UserResponse;
|
||||
import br.com.tasknoteapp.server.service.AuthService;
|
||||
import java.util.List;
|
||||
@@ -31,7 +33,7 @@ class UserControllerTest {
|
||||
@DisplayName("Get all users happy path should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void getAllUsers_happyPath_shouldSucceed() throws Exception {
|
||||
UserResponse userResponse = new UserResponse(1L, "email@test.com", false, null, null);
|
||||
UserResponse userResponse = new UserResponse(1L, "John", "email@test.com", false, null, null);
|
||||
when(authService.getAllUsers()).thenReturn(List.of(userResponse));
|
||||
|
||||
mockMvc
|
||||
@@ -59,4 +61,34 @@ class UserControllerTest {
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Patch user info happy path should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void patchUserInfo_happyPath_shouldSucceed() throws Exception {
|
||||
UserResponse response = new UserResponse(1L, "John", "email@example.com", false, null, null);
|
||||
UserPatchRequest request = new UserPatchRequest("John Doe", response.email(), null, null);
|
||||
when(authService.patchUserInfo(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
"""
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "email@example.com"
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
patch("/rest/users")
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.content(jsonString))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.userId").value(response.userId()))
|
||||
.andExpect(jsonPath("$.email").value(response.email()))
|
||||
.andExpect(jsonPath("$.admin").value(response.admin()))
|
||||
.andReturn();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ class UserSessionControllerTest {
|
||||
@DisplayName("Delete account happy path should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void deleteAccount_happyPath_shouldSucceed() throws Exception {
|
||||
UserResponse response = new UserResponse(1L, "email@test.com", false, null, null);
|
||||
UserResponse response = new UserResponse(1L, "John", "email@test.com", false, null, null);
|
||||
when(userSessionService.deleteCurrentUserAccount()).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
|
||||
@@ -9,15 +9,17 @@ import static org.mockito.Mockito.when;
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import br.com.tasknoteapp.server.entity.UserPwdLimitEntity;
|
||||
import br.com.tasknoteapp.server.exception.BadPasswordException;
|
||||
import br.com.tasknoteapp.server.exception.MaxLoginLimitAttemptException;
|
||||
import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException;
|
||||
import br.com.tasknoteapp.server.exception.InvalidCredentialsException;
|
||||
import br.com.tasknoteapp.server.exception.MaxLoginLimitAttemptException;
|
||||
import br.com.tasknoteapp.server.exception.UserForbiddenException;
|
||||
import br.com.tasknoteapp.server.exception.UserNotFoundException;
|
||||
import br.com.tasknoteapp.server.exception.InvalidCredentialsException;
|
||||
import br.com.tasknoteapp.server.repository.UserPwdLimitRepository;
|
||||
import br.com.tasknoteapp.server.repository.UserRepository;
|
||||
import br.com.tasknoteapp.server.request.LoginRequest;
|
||||
import br.com.tasknoteapp.server.request.UserPatchRequest;
|
||||
import br.com.tasknoteapp.server.response.UserResponse;
|
||||
import br.com.tasknoteapp.server.response.UserResponseWithToken;
|
||||
import br.com.tasknoteapp.server.util.AuthUtil;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -80,11 +82,12 @@ class AuthServiceTest {
|
||||
when(userRepository.save(any())).thenReturn(entity);
|
||||
when(jwtService.generateToken(request.email())).thenReturn("a1b2c3");
|
||||
|
||||
String token = authService.signUpNewUser(request);
|
||||
UserResponseWithToken token = authService.signUpNewUser(request);
|
||||
|
||||
Assertions.assertNotNull(token);
|
||||
Assertions.assertFalse(token.isBlank());
|
||||
Assertions.assertEquals("a1b2c3", token);
|
||||
Assertions.assertFalse(token.token().isBlank());
|
||||
Assertions.assertEquals("a1b2c3", token.token());
|
||||
Assertions.assertEquals(entity.getEmail(), token.email());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -191,10 +194,10 @@ class AuthServiceTest {
|
||||
|
||||
doNothing().when(userPwdLimitRepository).deleteAllForUser(existing.getId());
|
||||
|
||||
String token = authService.signInUser(request);
|
||||
UserResponseWithToken token = authService.signInUser(request);
|
||||
|
||||
Assertions.assertNotNull(token);
|
||||
Assertions.assertEquals("a1b2c3", token);
|
||||
Assertions.assertEquals("a1b2c3", token.token());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -248,7 +251,7 @@ class AuthServiceTest {
|
||||
when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort)).thenReturn(List.of());
|
||||
when(authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("Wrong"));
|
||||
|
||||
String token = authService.signInUser(request);
|
||||
UserResponseWithToken token = authService.signInUser(request);
|
||||
|
||||
Assertions.assertNull(token);
|
||||
verify(userPwdLimitRepository, times(1)).save(any());
|
||||
@@ -285,9 +288,11 @@ class AuthServiceTest {
|
||||
void getAllUsers_noCurrentUser_shouldFail() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.empty());
|
||||
|
||||
Assertions.assertThrows(UserForbiddenException.class, () -> {
|
||||
authService.getAllUsers();
|
||||
});
|
||||
Assertions.assertThrows(
|
||||
UserForbiddenException.class,
|
||||
() -> {
|
||||
authService.getAllUsers();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -297,9 +302,11 @@ class AuthServiceTest {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(email));
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.empty());
|
||||
|
||||
Assertions.assertThrows(UserForbiddenException.class, () -> {
|
||||
authService.getAllUsers();
|
||||
});
|
||||
Assertions.assertThrows(
|
||||
UserForbiddenException.class,
|
||||
() -> {
|
||||
authService.getAllUsers();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -314,9 +321,11 @@ class AuthServiceTest {
|
||||
existing.setAdmin(false);
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
|
||||
|
||||
Assertions.assertThrows(UserForbiddenException.class, () -> {
|
||||
authService.getAllUsers();
|
||||
});
|
||||
Assertions.assertThrows(
|
||||
UserForbiddenException.class,
|
||||
() -> {
|
||||
authService.getAllUsers();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -356,4 +365,54 @@ class AuthServiceTest {
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Patch user info patch user name and email should succeed")
|
||||
void pathUserInfo_nameAndEmail_shouldSucceed() {
|
||||
String email = "user@domain.com";
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(email));
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
existing.setId(919L);
|
||||
existing.setName(null);
|
||||
existing.setEmail(email);
|
||||
existing.setAdmin(false);
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
|
||||
|
||||
when(userRepository.save(any())).thenReturn(existing);
|
||||
|
||||
UserPatchRequest patchRequest = new UserPatchRequest("Kong", "newemail@domain.com", null, null);
|
||||
UserResponse response = authService.patchUserInfo(patchRequest);
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals("Kong", response.name());
|
||||
Assertions.assertEquals("newemail@domain.com", response.email());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Patch user info patch user password should succeed")
|
||||
void pathUserInfo_password_shouldSucceed() {
|
||||
String email = "user@domain.com";
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(email));
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
existing.setId(919L);
|
||||
existing.setName(null);
|
||||
existing.setEmail(email);
|
||||
existing.setAdmin(false);
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
|
||||
|
||||
when(userRepository.save(any())).thenReturn(existing);
|
||||
|
||||
String newPassword = "TestHackedPw@difficult!#:)";
|
||||
UserPatchRequest patchRequest = new UserPatchRequest("Kong", "newemail@domain.com", newPassword, newPassword);
|
||||
|
||||
when(authUtil.validatePassword(patchRequest.password())).thenReturn(Optional.empty());
|
||||
|
||||
UserResponse response = authService.patchUserInfo(patchRequest);
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals("Kong", response.name());
|
||||
Assertions.assertEquals("newemail@domain.com", response.email());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user