Fix/410 fix languages (#483)
* feat: handle languages and save it in the backend. Issue #418. Issue #410 * test: fix test cases * test: add test cases in the backend
This commit is contained in:
@@ -55,5 +55,6 @@ describe('Portuguese Utils unit tests', () => {
|
||||
expect(translateServerResponse(keys[17], 'pt_br')).toBe('Identificação incorreta ou faltando');
|
||||
expect(translateServerResponse(keys[18], 'pt_br')).toBe('Informação errada ou incompleta!');
|
||||
expect(translateServerResponse(keys[19], 'pt_br')).toBe('E-mail ou senha inválidos!');
|
||||
expect(translateServerResponse(keys[20], 'pt_br')).toBe('Nada para atualizar!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,5 +76,6 @@ describe('Russian Utils unit tests', () => {
|
||||
expect(translateServerResponse(keys[17], 'ru')).toBe('Неправильная или отсутствующая идентификация');
|
||||
expect(translateServerResponse(keys[18], 'ru')).toBe('Неверная или отсутствующая информация!');
|
||||
expect(translateServerResponse(keys[19], 'ru')).toBe('Неправильный пользователь или пароль');
|
||||
expect(translateServerResponse(keys[20], 'ru')).toBe('Нечего обновлять!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,5 +54,7 @@ describe('Spanish Utils unit tests', () => {
|
||||
expect(translateServerResponse(keys[16], 'es')).toBe('Error desconocido');
|
||||
expect(translateServerResponse(keys[17], 'es')).toBe('Identificación incorrecta o faltante');
|
||||
expect(translateServerResponse(keys[18], 'es')).toBe('¡Información incorrecta o incompleta!');
|
||||
expect(translateServerResponse(keys[19], 'es')).toBe('¡Usuario o contraseña incorrectos!');
|
||||
expect(translateServerResponse(keys[20], 'es')).toBe('¡Nada que actualizar!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,8 @@ const authContextMock = {
|
||||
email: 'test@example.com',
|
||||
admin: false,
|
||||
createdAt: new Date(),
|
||||
gravatarImageUrl: 'http://image.com'
|
||||
gravatarImageUrl: 'http://image.com',
|
||||
lang: 'en'
|
||||
},
|
||||
checkCurrentAuthUser: vi.fn(),
|
||||
signIn: vi.fn(),
|
||||
@@ -61,7 +62,7 @@ describe('Account Component', () => {
|
||||
|
||||
it('should render the Account component', () => {
|
||||
const { getByText } = renderAccount();
|
||||
expect(getByText('Update only what you need. Blank fields will not be updated')).toBeDefined();
|
||||
expect(getByText('account_data_update_header')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should change language when a language button is clicked', () => {
|
||||
@@ -96,12 +97,12 @@ describe('Account Component', () => {
|
||||
|
||||
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(getByLabelText('account_form_first_name_label'), { target: { value: 'Jane' } });
|
||||
fireEvent.change(getByLabelText('login_email_label'), { 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.change(getByLabelText(/register_password_repeat_label/i), { target: { value: 'password123' } });
|
||||
|
||||
fireEvent.click(getByText(/Save/i));
|
||||
fireEvent.click(getByText('account_form_save'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPatchJSON).toHaveBeenCalledWith(expect.any(String), {
|
||||
@@ -109,6 +110,7 @@ describe('Account Component', () => {
|
||||
email: 'jane.doe@example.com',
|
||||
password: 'password123',
|
||||
passwordAgain: 'password123',
|
||||
lang: ''
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,10 +120,10 @@ describe('Account Component', () => {
|
||||
it('should render text based on new contentHeader component', () => {
|
||||
const { getByText } = renderAccount();
|
||||
|
||||
expect(getByText('My')).toBeDefined();
|
||||
expect(getByText('Account')).toBeDefined();
|
||||
expect(getByText('account_header_my')).toBeDefined();
|
||||
expect(getByText('account_header_account')).toBeDefined();
|
||||
expect(getByText('account_my_account_hello')).toBeDefined();
|
||||
expect(getByText('Update and Manage, Your')).toBeDefined();
|
||||
expect(getByText('Data')).toBeDefined();
|
||||
expect(getByText('account_header_update_manage')).toBeDefined();
|
||||
expect(getByText('account_header_data')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode {
|
||||
const [passwordAgain, setPasswordAgain] = useState<string>('');
|
||||
const [secondsLeft, setSecondsLeft] = useState<number>(0);
|
||||
const [isResendEnabled, setIsResendEnabled] = useState(true);
|
||||
const [langToSave, setLangToSave] = useState<string>('');
|
||||
|
||||
/**
|
||||
* Navigates to the specified page.
|
||||
@@ -87,7 +88,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode {
|
||||
goTo('/home');
|
||||
}
|
||||
else if (prefix === 'register') {
|
||||
await register(email, password, passwordAgain);
|
||||
await register({ email, password, passwordAgain, lang: langToSave });
|
||||
// Do not clear the email, because user might request to resend
|
||||
setPassword('');
|
||||
setPasswordAgain('');
|
||||
@@ -139,7 +140,8 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleDefaultLang();
|
||||
const lang = handleDefaultLang();
|
||||
setLangToSave(lang);
|
||||
}, [formInvalid]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -114,6 +114,11 @@ const enTranslations = {
|
||||
note_table_btn_edit: 'Edit',
|
||||
note_table_btn_delete: 'Delete',
|
||||
|
||||
about_page_title_one: 'About the',
|
||||
about_page_title_two: 'TaskNote App',
|
||||
about_page_title_three: 'Tasks and notes made',
|
||||
about_page_title_four: 'Easy',
|
||||
about_page_subtitle: 'Find more information about us and the app',
|
||||
about_app_title: 'About TaskNote',
|
||||
about_app_description: `TaskNote is your go-to application for managing
|
||||
tasks and notes in one convenient place. Whether you're keeping track of
|
||||
@@ -146,13 +151,26 @@ const enTranslations = {
|
||||
passionate about building applications that make life easier and more
|
||||
organized. You can reach out to me at `,
|
||||
about_dev_description_two: ' for any questions or feedback.',
|
||||
about_buy_coffee_one: 'You can also ',
|
||||
about_buy_coffee_link: 'Buy me a coffee',
|
||||
|
||||
account_my_account_title: 'My Account',
|
||||
account_header_my: 'My',
|
||||
account_header_account: 'Account',
|
||||
account_header_update_manage: 'Update and Manage, Your',
|
||||
account_header_data: 'Data',
|
||||
account_data_update_header: 'Update only what you need. Blank fields will not be updated',
|
||||
account_form_first_name_label: 'Fist name',
|
||||
account_form_first_name_placeholder: 'Your name',
|
||||
account_form_save: 'Save',
|
||||
account_form_gravatar_one: 'If you want to display your picture, we support Gravatar. Please head to ',
|
||||
account_form_gravatar_two: ' to register or update your profile picture. Once updated, please wait a few seconds to see it here.',
|
||||
account_my_account_hello: 'Hello! This is where you can manage your preferences',
|
||||
account_my_account_logged: 'You\'re logged in as: ',
|
||||
account_app_lang_title: 'App Language',
|
||||
account_app_lang_title: 'Change the app language',
|
||||
account_app_lang_description: 'You can choose one the languages below',
|
||||
account_app_lang_available: 'Available languages',
|
||||
account_privacy_little: 'Your Privacy Matters',
|
||||
account_privacy_subtitle: 'You decide when to delete your data',
|
||||
account_privacy_text: `We're committed to protecting your privacy and
|
||||
giving you full control over your data. You can request complete account
|
||||
deletion at any time. Once processed, all your personal information will be
|
||||
|
||||
@@ -84,6 +84,7 @@ export const serverResponsesTranslations: Record<string, string> = {
|
||||
UNKNOWN_pt_br: 'Erro desconhecido',
|
||||
WRONG_IDENTIFICATION_pt_br: 'Identificação incorreta ou faltando',
|
||||
WRONG_OR_MISSING_INFO_pt_br: 'Informação errada ou incompleta!',
|
||||
NOTHING_TO_UPDATE_pt_br: 'Nada para atualizar!',
|
||||
|
||||
BAD_PASSWORD_3_es: 'Contraseña inválida: La contraseña debe tener al menos 8 caracteres, 1 mayúscula y 1 carácter especial',
|
||||
BAD_PASSWORD_2_es: 'Contraseña inválida: La contraseña debe tener al menos 1 mayúscula y 1 carácter especial',
|
||||
@@ -105,6 +106,7 @@ export const serverResponsesTranslations: Record<string, string> = {
|
||||
UNKNOWN_es: 'Error desconocido',
|
||||
WRONG_IDENTIFICATION_es: 'Identificación incorrecta o faltante',
|
||||
WRONG_OR_MISSING_INFO_es: '¡Información incorrecta o incompleta!',
|
||||
NOTHING_TO_UPDATE_es: '¡Nada que actualizar!',
|
||||
|
||||
BAD_PASSWORD_3_ru: 'Неправильный пароль: Пароль должен содержать не менее 8 символов, 1 заглавную букву, 1 специальный символ.',
|
||||
BAD_PASSWORD_2_ru: 'Неправильный пароль: Пароль должен содержать как минимум 1 заглавную букву и 1 специальный символ.',
|
||||
@@ -125,5 +127,6 @@ export const serverResponsesTranslations: Record<string, string> = {
|
||||
RECOVER_PASSWORD_ru: 'Если введенный вами адрес электронной почты связан с учетной записью, вы вскоре получите ссылку для сброса пароля.',
|
||||
UNKNOWN_ru: 'Неизвестная ошибка',
|
||||
WRONG_IDENTIFICATION_ru: 'Неправильная или отсутствующая идентификация',
|
||||
WRONG_OR_MISSING_INFO_ru: 'Неверная или отсутствующая информация!'
|
||||
WRONG_OR_MISSING_INFO_ru: 'Неверная или отсутствующая информация!',
|
||||
NOTHING_TO_UPDATE_ru: 'Нечего обновлять!'
|
||||
};
|
||||
|
||||
@@ -114,6 +114,11 @@ const ptBrTranslations = {
|
||||
note_table_btn_edit: 'Alterar',
|
||||
note_table_btn_delete: 'Excluir',
|
||||
|
||||
about_page_title_one: 'Sobre o',
|
||||
about_page_title_two: 'App TaskNote',
|
||||
about_page_title_three: 'Tarefas e notas de forma',
|
||||
about_page_title_four: 'Fácil',
|
||||
about_page_subtitle: 'Saiba mais sobre nós e sobre o app',
|
||||
about_app_title: 'Sobre o TaskNote',
|
||||
about_app_description: `TaskNote é sua aplicação ideal para gerenciamento
|
||||
de tarefas e notas em apenas um lugar. Esteja você buscando registrar sua
|
||||
@@ -147,13 +152,26 @@ const ptBrTranslations = {
|
||||
Sou apaixonado por criar aplicações que fazem a vida mais fácil e organizada.
|
||||
Você pode me contactar em `,
|
||||
about_dev_description_two: ' para qualquer dúvida ou comentário.',
|
||||
about_buy_coffee_one: 'Você também pode ',
|
||||
about_buy_coffee_link: 'Me pagar um café',
|
||||
|
||||
account_my_account_title: 'Minha conta',
|
||||
account_header_my: 'Minha',
|
||||
account_header_account: 'Conta',
|
||||
account_header_update_manage: 'Atualize e Gerencie, Seus',
|
||||
account_header_data: 'Dados',
|
||||
account_data_update_header: 'Atualize apenas o que você precisa. Campos em branco não serão atualizados',
|
||||
account_form_first_name_label: 'Primeiro nome',
|
||||
account_form_first_name_placeholder: 'Seu nome',
|
||||
account_form_save: 'Salvar',
|
||||
account_form_gravatar_one: 'Se você quer adicionar sua foto, nós suportamos Gravatar. Por favor acesse ',
|
||||
account_form_gravatar_two: ' para se registrar ou atualizar seu perfil. Uma vez atualizado, por favor aguarde alguns segundos para vê-la aqui.',
|
||||
account_my_account_hello: 'Olá! Aqui é onde você pode gerenciar suas preferências.',
|
||||
account_my_account_logged: 'Você está logado como: ',
|
||||
account_app_lang_title: 'Idioma do App',
|
||||
account_app_lang_title: 'Mude o idioma do app',
|
||||
account_app_lang_description: 'Você pode escolher um destes idiomas:',
|
||||
account_app_lang_available: 'Idiomas disponívels',
|
||||
account_privacy_little: 'Sua Privacidade Importa',
|
||||
account_privacy_subtitle: 'Você decide quanto excluir seus dados',
|
||||
account_privacy_text: `Estamos comprometidos em proteger sua privacidade e te dar
|
||||
controle total sobre seus dados. Você pode solicitar a remoção completa da sua conta
|
||||
a qualquer hora. Uma vez processado, todos os seus dados pessoais serão excluídos
|
||||
|
||||
@@ -114,6 +114,11 @@ const ruTranslations = {
|
||||
note_table_btn_edit: 'Редактировать',
|
||||
note_table_btn_delete: 'Удалить',
|
||||
|
||||
about_page_title_one: 'около',
|
||||
about_page_title_two: 'TaskNote App',
|
||||
about_page_title_three: 'Задачи и заметки стали',
|
||||
about_page_title_four: 'проще',
|
||||
about_page_subtitle: ' Узнайте больше о нас и приложении',
|
||||
about_app_title: 'О TaskNote',
|
||||
about_app_description: `TaskNote — это ваше приложение для управления
|
||||
задачами и заметками в одном удобном месте. Независимо от того, отслеживаете ли вы
|
||||
@@ -146,13 +151,26 @@ const ruTranslations = {
|
||||
которые делают жизнь проще и более
|
||||
организованной. Вы можете связаться со мной по адресу `,
|
||||
about_dev_description_two: ' для любых вопросов или отзывов.',
|
||||
about_buy_coffee_one: 'Ты также можешь ',
|
||||
about_buy_coffee_link: 'купить мне кофе.',
|
||||
|
||||
account_my_account_title: 'Мой аккаунт',
|
||||
account_header_my: 'Мой',
|
||||
account_header_account: 'аккаунт',
|
||||
account_header_update_manage: 'Обновление и управление',
|
||||
account_header_data: 'данные',
|
||||
account_data_update_header: 'Обновляйте только то, что вам нужно. Пустые поля не будут обновлены',
|
||||
account_form_first_name_label: 'имя',
|
||||
account_form_first_name_placeholder: 'Ваше имя',
|
||||
account_form_save: 'Сохранить',
|
||||
account_form_gravatar_one: 'Если вы хотите показать свою фотографию, мы поддерживаем Gravatar. Пожалуйста, перейдите на ',
|
||||
account_form_gravatar_two: ' для регистрации или обновления вашего фото профиля. После обновления, пожалуйста, подождите несколько секунд, чтобы увидеть его здесь.',
|
||||
account_my_account_hello: 'Привет! Здесь вы можете управлять своими предпочтениями.',
|
||||
account_my_account_logged: 'Вы вошли как:',
|
||||
account_app_lang_title: 'Язык приложения',
|
||||
account_app_lang_title: 'Изменить язык приложения',
|
||||
account_app_lang_description: 'Вы можете выбрать один из этих языков:',
|
||||
account_app_lang_available: 'Доступные языки',
|
||||
account_privacy_little: 'Ваша конфиденциальность имеет значение',
|
||||
account_privacy_subtitle: 'Вы сами решаете, когда удалять свои данные',
|
||||
account_privacy_text: `Мы стремимся защищать вашу конфиденциальность и
|
||||
предоставлять вам полный контроль над вашими данными. Вы можете запросить полное удаление
|
||||
аккаунта в любое время. После обработки вся ваша личная информация будет
|
||||
|
||||
@@ -18,5 +18,6 @@ export const serverResponses: Record<string, string> = {
|
||||
'Unknown error': 'UNKNOWN',
|
||||
'Wrong or missing identification': 'WRONG_IDENTIFICATION',
|
||||
'Wrong or missing information!': 'WRONG_OR_MISSING_INFO',
|
||||
'Invalid credentials': 'INVALID_CREDENTIALS'
|
||||
'Invalid credentials': 'INVALID_CREDENTIALS',
|
||||
'Nothing to update!': 'NOTHING_TO_UPDATE'
|
||||
};
|
||||
|
||||
@@ -114,6 +114,11 @@ const esTranslations = {
|
||||
note_table_btn_edit: 'Editar',
|
||||
note_table_btn_delete: 'Eliminar',
|
||||
|
||||
about_page_title_one: 'Acerca de',
|
||||
about_page_title_two: 'TaskNote App',
|
||||
about_page_title_three: 'Tareas y notas',
|
||||
about_page_title_four: 'Simplificadas',
|
||||
about_page_subtitle: 'Encuentre más información sobre nosotros y la aplicación.',
|
||||
about_app_title: 'Acerca de TaskNote',
|
||||
about_app_description: `TaskNote es tu aplicación ideal para gestionar
|
||||
tareas y notas en un solo lugar. Ya sea que estés organizando tareas diarias
|
||||
@@ -146,13 +151,26 @@ const esTranslations = {
|
||||
Me apasiona crear aplicaciones que faciliten la vida y ayuden a organizarse.
|
||||
Puedes contactarme en `,
|
||||
about_dev_description_two: ' para cualquier duda o comentario.',
|
||||
about_buy_coffee_one: 'También puedes ',
|
||||
about_buy_coffee_link: 'Comprarme un café',
|
||||
|
||||
account_my_account_title: 'Mi Cuenta',
|
||||
account_header_my: 'Mi',
|
||||
account_header_account: 'Cuenta',
|
||||
account_header_update_manage: 'Actualize y Administre, Sus',
|
||||
account_header_data: 'Datos',
|
||||
account_data_update_header: 'Actualice solo lo necesario. Los campos en blanco no se actualizarán',
|
||||
account_form_first_name_label: 'Nombre de pila',
|
||||
account_form_first_name_placeholder: 'Su nombre',
|
||||
account_form_save: 'Guardar',
|
||||
account_form_gravatar_one: 'Si quieres mostrar tu foto, admitimos Gravatar. Visita ',
|
||||
account_form_gravatar_two: ' Para registrarte o actualizar tu foto de perfil. Una vez actualizada, espera unos segundos para verla aquí.',
|
||||
account_my_account_hello: '¡Hola! Aquí puedes gestionar tus preferencias.',
|
||||
account_my_account_logged: 'Has iniciado sesión como: ',
|
||||
account_app_lang_title: 'Idioma del App',
|
||||
account_app_lang_title: 'Cambia el idioma del App',
|
||||
account_app_lang_description: 'Puedes elegir uno de estos idiomas:',
|
||||
account_app_lang_available: 'Idiomas disponíbles',
|
||||
account_privacy_little: 'Su Privacidad es Importante',
|
||||
account_privacy_subtitle: 'Tú decides cuándo eliminar tus datos',
|
||||
account_privacy_text: `Nos comprometemos a proteger su privacidad y a brindarle
|
||||
control total sobre sus datos. Puedes solicitar la eliminación completa de su
|
||||
cuenta en cualquier momento. Una vez procesada, toda su información personal
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
import { UserResponse } from '../types/UserResponse';
|
||||
import { UserRegistration } from '../types/UserRegistration';
|
||||
|
||||
export interface AuthContextData {
|
||||
signed: boolean;
|
||||
@@ -7,7 +8,7 @@ export interface AuthContextData {
|
||||
checkCurrentAuthUser: (pathname: string) => Promise<void>;
|
||||
signIn: (email: string, password: string) => Promise<string>;
|
||||
signOut: () => void;
|
||||
register: (email: string, password: string, passwordAgain: string) => Promise<string>;
|
||||
register: (payload: UserRegistration) => Promise<string>;
|
||||
isAdmin: boolean;
|
||||
updateUser: (userUpdated: UserResponse) => void;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SigninResponse } from '../types/SigninResponse';
|
||||
import api from '../api-service/api';
|
||||
import ApiConfig from '../api-service/apiConfig';
|
||||
import { UserResponse } from '../types/UserResponse';
|
||||
import { UserRegistration } from '../types/UserRegistration';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
@@ -72,9 +73,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (email: string, password: string, passwordAgain: string): Promise<string> => {
|
||||
const register = async (payload: UserRegistration): Promise<string> => {
|
||||
try {
|
||||
const payload = { email, password, passwordAgain };
|
||||
await api.putJSON(ApiConfig.registerUrl, payload);
|
||||
return Promise.resolve('OK');
|
||||
}
|
||||
@@ -96,7 +96,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
email: registerResponse.email,
|
||||
admin: registerResponse.admin,
|
||||
createdAt: new Date(registerResponse.createdAt),
|
||||
gravatarImageUrl: registerResponse.gravatarImageUrl
|
||||
gravatarImageUrl: registerResponse.gravatarImageUrl,
|
||||
lang: registerResponse.lang
|
||||
};
|
||||
|
||||
setSigned(true);
|
||||
|
||||
@@ -11,11 +11,12 @@ function handleLanguage(lang: string) {
|
||||
setDefaultLang(lang);
|
||||
}
|
||||
|
||||
const handleDefaultLang = () => {
|
||||
const lang = getDefaultLang();
|
||||
const handleDefaultLang = (langFromServer?: string): string => {
|
||||
const lang = langFromServer ?? getDefaultLang();
|
||||
if (lang !== 'en') {
|
||||
handleLanguage(lang);
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
export { handleDefaultLang };
|
||||
|
||||
@@ -6,4 +6,5 @@ export type SigninResponse = {
|
||||
createdAt: Date;
|
||||
token: string;
|
||||
gravatarImageUrl: string;
|
||||
lang: string;
|
||||
};
|
||||
|
||||
@@ -3,4 +3,5 @@ export type UserPatchRequest = {
|
||||
email: string | null;
|
||||
password: string | null;
|
||||
passwordAgain: string | null;
|
||||
lang: string | null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type UserRegistration = {
|
||||
email: string;
|
||||
password: string;
|
||||
passwordAgain: string;
|
||||
lang: string;
|
||||
};
|
||||
@@ -5,4 +5,5 @@ export type UserResponse = {
|
||||
admin: boolean;
|
||||
createdAt: Date;
|
||||
gravatarImageUrl: string;
|
||||
lang: string;
|
||||
};
|
||||
|
||||
@@ -17,11 +17,11 @@ function About(): React.ReactNode {
|
||||
return (
|
||||
<Container fluid>
|
||||
<ContentHeader
|
||||
h1TextRegular="About the"
|
||||
h1TextBold="TaskNote App"
|
||||
subtitle="Find more information about us and the app"
|
||||
h2BlackText="Tasks and notes made"
|
||||
h2GreenText="Easy"
|
||||
h1TextRegular={t('about_page_title_one')}
|
||||
h1TextBold={t('about_page_title_two')}
|
||||
subtitle={t('about_page_subtitle')}
|
||||
h2BlackText={t('about_page_title_three')}
|
||||
h2GreenText={t('about_page_title_four')}
|
||||
/>
|
||||
|
||||
<Row className="justify-content-center mb-4">
|
||||
@@ -67,20 +67,19 @@ function About(): React.ReactNode {
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_dev_title')}</h2>
|
||||
<p className="poppins-light">
|
||||
{t('about_dev_description')}
|
||||
<a href="mailto:ricardompcampos@gmail.com" className="text-decoration-none">
|
||||
ricardompcampos@gmail.com
|
||||
<a href="https://gravatar.com/ricardormcampos" className="text-decoration-none">
|
||||
gravatar.com/ricardormcampos
|
||||
</a>
|
||||
{t('about_dev_description_two')}
|
||||
</p>
|
||||
<p className="poppins-light">
|
||||
You can also
|
||||
{' '}
|
||||
{t('about_buy_coffee_one')}
|
||||
<a
|
||||
href="https://buy-me-a-coffee-two-nu.vercel.app/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Buy me a coffee
|
||||
{t('about_buy_coffee_link')}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Body>
|
||||
|
||||
@@ -40,6 +40,7 @@ function Account(): React.ReactNode {
|
||||
const handleLanguage = (lang: string): void => {
|
||||
i18n.changeLanguage(lang);
|
||||
setDefaultLang(lang);
|
||||
patchLanguage(lang);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,31 +93,48 @@ function Account(): React.ReactNode {
|
||||
setValidated(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const patchLanguage = async (lang: string): Promise<void> => {
|
||||
const patchPayload: UserPatchRequest = {
|
||||
name: null,
|
||||
email: '',
|
||||
password: '',
|
||||
passwordAgain: '',
|
||||
lang: lang
|
||||
};
|
||||
|
||||
await patchUserInfo(patchPayload);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
setValidated(true);
|
||||
setErrorMessage('');
|
||||
|
||||
const form = event.currentTarget;
|
||||
const form = event?.currentTarget;
|
||||
|
||||
const patchPayload: UserPatchRequest = {
|
||||
name: userName ? DOMPurify.sanitize(userName) : null,
|
||||
email: userEmail,
|
||||
password: userPassword,
|
||||
passwordAgain: userPasswordAgain
|
||||
passwordAgain: userPasswordAgain,
|
||||
lang: ''
|
||||
};
|
||||
|
||||
const sizeOfPayload = JSON.stringify(patchPayload).length;
|
||||
if (sizeOfPayload === 57) {
|
||||
setErrorMessage('Nothing to update!');
|
||||
if (sizeOfPayload === 67) {
|
||||
setErrorMessage(translateServerResponse('Nothing to update!', i18n.language));
|
||||
return;
|
||||
}
|
||||
|
||||
const updated: UserResponse | undefined = await patchUserInfo(patchPayload);
|
||||
if (updated) {
|
||||
form.reset();
|
||||
form?.reset();
|
||||
resetInputs(updated);
|
||||
if (userEmail !== '') {
|
||||
signOut();
|
||||
clearStorage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,11 +143,11 @@ function Account(): React.ReactNode {
|
||||
return (
|
||||
<Container fluid>
|
||||
<ContentHeader
|
||||
h1TextRegular="My"
|
||||
h1TextBold="Account"
|
||||
h1TextRegular={t('account_header_my')}
|
||||
h1TextBold={t('account_header_account')}
|
||||
subtitle={t('account_my_account_hello')}
|
||||
h2BlackText="Update and Manage, Your"
|
||||
h2GreenText="Data"
|
||||
h2BlackText={t('account_header_update_manage')}
|
||||
h2GreenText={t('account_header_data')}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
@@ -137,7 +155,7 @@ function Account(): React.ReactNode {
|
||||
<Card className="p-4">
|
||||
<Card.Body>
|
||||
<Card.Title>
|
||||
Update only what you need. Blank fields will not be updated
|
||||
{t('account_data_update_header')}
|
||||
</Card.Title>
|
||||
|
||||
<AlertError
|
||||
@@ -148,11 +166,11 @@ function Account(): React.ReactNode {
|
||||
<Form noValidate validated={validated} onSubmit={handleSubmit} className="mt-4">
|
||||
{/* User name */}
|
||||
<FormInput
|
||||
labelText="First name"
|
||||
labelText={t('account_form_first_name_label')}
|
||||
iconName="Person"
|
||||
required={false}
|
||||
name="name"
|
||||
placeholder={user?.name ?? 'Your name'}
|
||||
placeholder={user?.name ?? t('account_form_first_name_placeholder')}
|
||||
value={userName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserName(e.target.value);
|
||||
@@ -161,7 +179,7 @@ function Account(): React.ReactNode {
|
||||
|
||||
{/* User email */}
|
||||
<FormInput
|
||||
labelText="Email"
|
||||
labelText={t('login_email_label')}
|
||||
iconName="At"
|
||||
required={false}
|
||||
name="email"
|
||||
@@ -174,12 +192,16 @@ function Account(): React.ReactNode {
|
||||
|
||||
{/* User password */}
|
||||
<FormInput
|
||||
labelText="Password"
|
||||
labelText={t('login_password_label')}
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="password"
|
||||
value={userPassword}
|
||||
placeholder={t('login_password_placeholder')}
|
||||
pwdShowText={t('password_show_txt')}
|
||||
pwdHideText={t('password_hide_txt')}
|
||||
pwdHelperTxt={t('password_helper')}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPassword(e.target.value);
|
||||
}}
|
||||
@@ -188,12 +210,16 @@ function Account(): React.ReactNode {
|
||||
|
||||
{/* User password again */}
|
||||
<FormInput
|
||||
labelText="Repeat password"
|
||||
labelText={t('register_password_repeat_label')}
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="passwordAgain"
|
||||
value={userPasswordAgain}
|
||||
placeholder={t('login_password_placeholder')}
|
||||
pwdShowText={t('password_show_txt')}
|
||||
pwdHideText={t('password_hide_txt')}
|
||||
pwdHelperTxt={t('password_helper')}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPasswordAgain(e.target.value);
|
||||
}}
|
||||
@@ -204,19 +230,15 @@ function Account(): React.ReactNode {
|
||||
type="submit"
|
||||
className="home-new-item task-note-btn"
|
||||
>
|
||||
Save
|
||||
{t('account_form_save')}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
<hr />
|
||||
<p>
|
||||
If you want to display your picture, we support Gravatar.
|
||||
Please head to
|
||||
{' '}
|
||||
{t('account_form_gravatar_one')}
|
||||
<a href="https://gravatar.com" target="_blank" rel="noreferrer">Gravatar</a>
|
||||
{' '}
|
||||
to register or update your profile picture. Once updated, please wait a few
|
||||
minutes to see it here.
|
||||
{t('account_form_gravatar_two')}
|
||||
</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
@@ -224,9 +246,9 @@ function Account(): React.ReactNode {
|
||||
<Col xs={12} lg={6} className="mt-4 mt-lg-0">
|
||||
<Card className="p-4">
|
||||
<Card.Body>
|
||||
<Card.Title>Change the app language</Card.Title>
|
||||
<Card.Title>{t('account_app_lang_title')}</Card.Title>
|
||||
<span className="description">{t('account_app_lang_description')}</span>
|
||||
<div className="mt-4 mb-2">Available languages</div>
|
||||
<div className="mt-4 mb-2">{t('account_app_lang_available')}</div>
|
||||
<div>
|
||||
{languages.map((lang: LangAvailable) => (
|
||||
<Button
|
||||
@@ -258,9 +280,9 @@ function Account(): React.ReactNode {
|
||||
|
||||
<Card className="mt-4 p-4">
|
||||
<Card.Body>
|
||||
<Card.Title>Your Privacy matters</Card.Title>
|
||||
<Card.Title>{t('account_privacy_little')}</Card.Title>
|
||||
<span className="description">
|
||||
You decide when to delete your data
|
||||
{t('account_privacy_subtitle')}
|
||||
</span>
|
||||
|
||||
<p className="mt-4 mb-2">{t('account_privacy_text')}</p>
|
||||
|
||||
@@ -263,7 +263,7 @@ function Home(): React.ReactNode {
|
||||
const handleCloseModal = () => setShowMarkdownView(false);
|
||||
|
||||
useEffect(() => {
|
||||
handleDefaultLang();
|
||||
handleDefaultLang(user?.lang);
|
||||
setName(user?.name ?? 'User');
|
||||
loadTags();
|
||||
loadAllTasks();
|
||||
|
||||
@@ -55,9 +55,12 @@ public class UserEntity implements UserDetails {
|
||||
@Column(name = "reset_password_expiration", nullable = true)
|
||||
private LocalDateTime resetPasswordExpiration;
|
||||
|
||||
@Column(name = "reset_token", nullable = true)
|
||||
@Column(name = "reset_token", nullable = true, length = 35)
|
||||
private String resetToken;
|
||||
|
||||
@Column(name = "lang", nullable = true, length = 6)
|
||||
private String lang;
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package br.com.tasknoteapp.server.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/** This class represents a Bad Language exception. */
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public class BadLanguageException extends ResponseStatusException {
|
||||
|
||||
public BadLanguageException() {
|
||||
super(HttpStatus.BAD_REQUEST, "Invalid language");
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ public class LoginRequest {
|
||||
@Schema(description = "User password again.")
|
||||
String passwordAgain;
|
||||
|
||||
@Schema(description = "User language. (Optional, default English)")
|
||||
String lang;
|
||||
|
||||
public String email() {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
@@ -39,4 +42,8 @@ public class LoginRequest {
|
||||
public String passwordAgain() {
|
||||
return passwordAgain;
|
||||
}
|
||||
|
||||
public String lang() {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ 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) {}
|
||||
@Schema(description = "User password again. Optional.") String passwordAgain,
|
||||
@Schema(description = "User lang. Optional.") String lang) {}
|
||||
|
||||
@@ -19,7 +19,8 @@ public record UserResponseWithToken(
|
||||
example = "2023-01-01T00:00:00")
|
||||
LocalDateTime inactivatedAt,
|
||||
@Schema(description = "The gravatar image URL, if any") String gravatarImageUrl,
|
||||
@Schema(description = "The token created upon login") String token) {
|
||||
@Schema(description = "The token created upon login") String token,
|
||||
@Schema(description = "The language selected upon login") String lang) {
|
||||
|
||||
/**
|
||||
* Create a {@link UserResponseWithToken} instance from a {@link UserEntity}.
|
||||
@@ -38,6 +39,7 @@ public record UserResponseWithToken(
|
||||
user.getCreatedAt(),
|
||||
user.getInactivatedAt(),
|
||||
gravatarUrl.orElse(null),
|
||||
token);
|
||||
token,
|
||||
user.getLang());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package br.com.tasknoteapp.server.service;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import br.com.tasknoteapp.server.entity.UserPwdLimitEntity;
|
||||
import br.com.tasknoteapp.server.exception.BadLanguageException;
|
||||
import br.com.tasknoteapp.server.exception.BadPasswordException;
|
||||
import br.com.tasknoteapp.server.exception.BadUuidException;
|
||||
import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException;
|
||||
@@ -27,6 +28,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -65,40 +67,47 @@ public class AuthService {
|
||||
/**
|
||||
* Create a new user in the app.
|
||||
*
|
||||
* @param login User details with email and password.
|
||||
* @param newUser User details with email and password.
|
||||
* @return Token
|
||||
*/
|
||||
@Transactional
|
||||
public UserResponseWithToken signUpNewUser(LoginRequest login) {
|
||||
log.info("Signing up new user! {}", login.email());
|
||||
public UserResponseWithToken signUpNewUser(LoginRequest newUser) {
|
||||
log.info("Signing up new user! {}", newUser.email());
|
||||
|
||||
if (findByEmail(login.email()).isPresent()) {
|
||||
if (findByEmail(newUser.email()).isPresent()) {
|
||||
throw new EmailAlreadyExistsException();
|
||||
}
|
||||
|
||||
Optional<String> passwordValidation = authUtil.validatePassword(login.password());
|
||||
Optional<String> passwordValidation = authUtil.validatePassword(newUser.password());
|
||||
if (passwordValidation.isPresent()) {
|
||||
throw new BadPasswordException(passwordValidation.get());
|
||||
}
|
||||
|
||||
if (Objects.isNull(login.passwordAgain()) || !login.password().equals(login.passwordAgain())) {
|
||||
if (Objects.isNull(newUser.passwordAgain())
|
||||
|| !newUser.password().equals(newUser.passwordAgain())) {
|
||||
throw new BadPasswordException("The passwords should match");
|
||||
}
|
||||
|
||||
UUID emailUuid = new UuidUtil().generateEmailUuid(login.email());
|
||||
String[] validLangs = new String[] {"en", "es", "pt_br", "ru"};
|
||||
if (!Arrays.asList(validLangs).contains(newUser.lang())) {
|
||||
throw new BadLanguageException();
|
||||
}
|
||||
|
||||
UUID emailUuid = new UuidUtil().generateEmailUuid(newUser.email());
|
||||
|
||||
UserEntity user = new UserEntity();
|
||||
user.setEmail(login.email());
|
||||
user.setPassword(passwordEncoder.encode(login.password()));
|
||||
user.setAdmin(login.email().equals("ricardompcampos@gmail.com"));
|
||||
user.setEmail(newUser.email());
|
||||
user.setPassword(passwordEncoder.encode(newUser.password()));
|
||||
user.setAdmin(false);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setEmailUuid(emailUuid);
|
||||
user.setLang(newUser.lang());
|
||||
userRepository.save(user);
|
||||
|
||||
mailgunEmailService.sendNewUser(user);
|
||||
|
||||
log.info("User created! ID {}", user.getId());
|
||||
return UserResponseWithToken.fromEntity(user, null, getGravatarImageUrl(login.email()));
|
||||
return UserResponseWithToken.fromEntity(user, null, getGravatarImageUrl(newUser.email()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,6 +265,7 @@ public class AuthService {
|
||||
String email = currentUserEmail.orElseThrow();
|
||||
UserEntity currentUser = findByEmail(email).orElseThrow();
|
||||
boolean shouldUpdate = false;
|
||||
boolean emailChanged = false;
|
||||
|
||||
if (!Objects.isNull(patchRequest.name()) && !patchRequest.name().isBlank()) {
|
||||
currentUser.setName(patchRequest.name().trim());
|
||||
@@ -264,6 +274,11 @@ public class AuthService {
|
||||
if (!Objects.isNull(patchRequest.email()) && !patchRequest.email().isBlank()) {
|
||||
currentUser.setEmail(patchRequest.email().trim());
|
||||
shouldUpdate = true;
|
||||
emailChanged = true;
|
||||
}
|
||||
if (!Objects.isNull(patchRequest.lang()) && !patchRequest.lang().isBlank()) {
|
||||
currentUser.setLang(patchRequest.lang());
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
boolean updatePassword =
|
||||
@@ -290,6 +305,12 @@ public class AuthService {
|
||||
userRepository.save(currentUser);
|
||||
}
|
||||
|
||||
if (emailChanged) {
|
||||
// send email to older and new account
|
||||
log.info("Email changed from {} to {}", email, patchRequest.email());
|
||||
mailgunEmailService.sendEmailChangedNotification(currentUser, email);
|
||||
}
|
||||
|
||||
return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package br.com.tasknoteapp.server.service;
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import br.com.tasknoteapp.server.exception.MailServiceException;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplate;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplateEmailChanged;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwd;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwdConfirm;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplateSignUp;
|
||||
@@ -106,6 +107,25 @@ public class MailgunEmailService {
|
||||
sendEmail(to, subject, resetTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification about the changed email for both previous and new email.
|
||||
*
|
||||
* @param user The user that should be addressed the message.
|
||||
* @param oldEmail The user previous email
|
||||
*/
|
||||
public void sendEmailChangedNotification(UserEntity user, String oldEmail) {
|
||||
log.info("Sending message with changed email notification");
|
||||
|
||||
MailgunTemplateEmailChanged emailChanged = new MailgunTemplateEmailChanged();
|
||||
emailChanged.setEmailFrom(oldEmail);
|
||||
emailChanged.setEmailTo(user.getEmail());
|
||||
emailChanged.setCarbonCopy(oldEmail);
|
||||
|
||||
String subject = "TaskNote App email changed notification";
|
||||
|
||||
sendEmail(user.getEmail(), subject, emailChanged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email message.
|
||||
*
|
||||
@@ -124,6 +144,9 @@ public class MailgunEmailService {
|
||||
MultiValueMap<String, String> mailData = new LinkedMultiValueMap<>();
|
||||
mailData.add("from", from);
|
||||
mailData.add("to", to);
|
||||
if (template.getCarbonCopy().isPresent()) {
|
||||
mailData.add("cc", template.getCarbonCopy().get());
|
||||
}
|
||||
mailData.add("subject", subject);
|
||||
mailData.add("template", template.getName());
|
||||
if (!template.getVariables().isEmpty()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package br.com.tasknoteapp.server.templates;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** This interface represents a mailgun template structure. */
|
||||
public interface MailgunTemplate {
|
||||
@@ -13,6 +14,10 @@ public interface MailgunTemplate {
|
||||
|
||||
Map<String, Object> getVariables();
|
||||
|
||||
default Optional<String> getCarbonCopy() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default method to get variables in JSON format.
|
||||
*
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package br.com.tasknoteapp.server.templates;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** This class represents a template for the email change workflow. */
|
||||
public class MailgunTemplateEmailChanged implements MailgunTemplate {
|
||||
|
||||
private String templateName = "email changed";
|
||||
private String carbonCopy;
|
||||
private final Map<String, Object> props;
|
||||
|
||||
public MailgunTemplateEmailChanged() {
|
||||
this.props = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setEmailFrom(String emailFrom) {
|
||||
props.put("EMAIL_FROM", emailFrom);
|
||||
}
|
||||
|
||||
public void setEmailTo(String emailTo) {
|
||||
props.put("EMAIL_TO", emailTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return templateName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getVariables() {
|
||||
return props;
|
||||
}
|
||||
|
||||
public void setCarbonCopy(String carbonCopy) {
|
||||
this.carbonCopy = carbonCopy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getCarbonCopy() {
|
||||
return Optional.ofNullable(carbonCopy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE tasknote.users
|
||||
ADD lang VARCHAR(35) NULL;
|
||||
+12
-10
@@ -32,12 +32,12 @@ class AuthenticationControllerTest {
|
||||
@Test
|
||||
@DisplayName("Sign up happy path should succeed")
|
||||
void signup_happyPath_shouldSucceed() throws Exception {
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456");
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
|
||||
final String token = "xaxbxcxdx1x2x3A@";
|
||||
|
||||
UserResponseWithToken response =
|
||||
new UserResponseWithToken(
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, null, token);
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, null, token, "en");
|
||||
when(authService.signUpNewUser(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
@@ -63,12 +63,12 @@ class AuthenticationControllerTest {
|
||||
@Test
|
||||
@DisplayName("Sign up bad email request should fail")
|
||||
void signup_badEmailRequest_shouldFail() throws Exception {
|
||||
LoginRequest request = new LoginRequest("user@domain..com", "abcde123456", "abcde123456");
|
||||
LoginRequest request = new LoginRequest("user@domain..com", "abcde123456", "abcde123456", "en");
|
||||
final String token = "xaxbxcxdx1x2x3@A";
|
||||
|
||||
UserResponseWithToken response =
|
||||
new UserResponseWithToken(
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, null, token);
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, null, token, "en");
|
||||
when(authService.signUpNewUser(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
@@ -94,7 +94,7 @@ class AuthenticationControllerTest {
|
||||
@Test
|
||||
@DisplayName("Sign up email already exists should fail")
|
||||
void signup_userAlreadyExists_shouldFail() throws Exception {
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456");
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
|
||||
|
||||
when(authService.signUpNewUser(request)).thenThrow(new EmailAlreadyExistsException());
|
||||
|
||||
@@ -103,7 +103,8 @@ class AuthenticationControllerTest {
|
||||
{
|
||||
"email": "user@domain.com",
|
||||
"password": "abcde123456",
|
||||
"passwordAgain": "abcde123456"
|
||||
"passwordAgain": "abcde123456",
|
||||
"lang": "en"
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -121,12 +122,12 @@ class AuthenticationControllerTest {
|
||||
@Test
|
||||
@DisplayName("Sign in happy path should succeed")
|
||||
void signin_happyPath_shouldSucceed() throws Exception {
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456");
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
|
||||
final String token = "xaxbxcxdx1x2x3A@";
|
||||
|
||||
UserResponseWithToken response =
|
||||
new UserResponseWithToken(
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, null, token);
|
||||
123L, null, request.email(), false, LocalDateTime.now(), null, null, token, "en");
|
||||
when(authService.signInUser(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
@@ -134,7 +135,8 @@ class AuthenticationControllerTest {
|
||||
{
|
||||
"email": "user@domain.com",
|
||||
"password": "abcde123456",
|
||||
"passwordAgain": "abcde123456"
|
||||
"passwordAgain": "abcde123456",
|
||||
"lang": "en"
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -156,7 +158,7 @@ class AuthenticationControllerTest {
|
||||
@Test
|
||||
@DisplayName("Sign in invalid credentials should fail")
|
||||
void signIn_invalidCredentials_shouldFail() throws Exception {
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456");
|
||||
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
|
||||
|
||||
when(authService.signInUser(request)).thenReturn(null);
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class UserControllerTest {
|
||||
void patchUserInfo_happyPath_shouldSucceed() throws Exception {
|
||||
UserResponse response =
|
||||
new UserResponse(1L, "John", "email@example.com", false, null, null, null);
|
||||
UserPatchRequest request = new UserPatchRequest("John Doe", response.email(), null, null);
|
||||
UserPatchRequest request = new UserPatchRequest("John Doe", response.email(), null, null, null);
|
||||
when(authService.patchUserInfo(request)).thenReturn(response);
|
||||
|
||||
String jsonString =
|
||||
|
||||
@@ -77,7 +77,8 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignUp new user happy path should succeed")
|
||||
void signUpNewUser_happyPath_shouldSucceed() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456@abcde!", "123456@abcde!");
|
||||
LoginRequest request =
|
||||
new LoginRequest("email@domain.com", "123456@abcde!", "123456@abcde!", "en");
|
||||
|
||||
when(userRepository.findByEmail(request.email())).thenReturn(Optional.empty());
|
||||
when(authUtil.validatePassword(request.password())).thenReturn(Optional.empty());
|
||||
@@ -103,7 +104,8 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignUp new user with existing email should fail")
|
||||
void signUpNewUser_emailExists_shouldFail() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456@abcde!", "123456@abcde!");
|
||||
LoginRequest request =
|
||||
new LoginRequest("email@domain.com", "123456@abcde!", "123456@abcde!", "en");
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing));
|
||||
@@ -118,7 +120,7 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignUp new user with bad password should fail")
|
||||
void signUpNewUser_badPassword_shouldFail() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456");
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456", "en");
|
||||
|
||||
when(userRepository.findByEmail(request.email())).thenReturn(Optional.empty());
|
||||
when(authUtil.validatePassword(request.password())).thenReturn(Optional.of("Bad password"));
|
||||
@@ -190,7 +192,7 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignIn user happy path should succeed")
|
||||
void signInUser_happyPath_shouldSucceed() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456");
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456", "en");
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
existing.setId(919L);
|
||||
@@ -213,7 +215,7 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignIn wrong user or password should fail")
|
||||
void signInUser_wrongUserOrPassword_shouldFail() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456");
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456", "en");
|
||||
|
||||
when(userRepository.findByEmail(request.email())).thenReturn(Optional.empty());
|
||||
|
||||
@@ -227,7 +229,7 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignIn max login attempt should fail")
|
||||
void signInUser_maxLoginAttempt_shouldFail() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456");
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456", "en");
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
existing.setId(919L);
|
||||
@@ -251,7 +253,7 @@ class AuthServiceTest {
|
||||
@Test
|
||||
@DisplayName("SignIn bad credentials should fail")
|
||||
void signInUser_badCredentials_shouldFail() {
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456");
|
||||
LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456", "en");
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
existing.setId(919L);
|
||||
@@ -391,7 +393,8 @@ class AuthServiceTest {
|
||||
|
||||
when(userRepository.save(any())).thenReturn(existing);
|
||||
|
||||
UserPatchRequest patchRequest = new UserPatchRequest("Kong", "newemail@domain.com", null, null);
|
||||
UserPatchRequest patchRequest =
|
||||
new UserPatchRequest("Kong", "newemail@domain.com", null, null, null);
|
||||
UserResponse response = authService.patchUserInfo(patchRequest);
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
@@ -416,7 +419,7 @@ class AuthServiceTest {
|
||||
|
||||
String newPassword = "TestHackedPw@difficult!#:)";
|
||||
UserPatchRequest patchRequest =
|
||||
new UserPatchRequest("Kong", "newemail@domain.com", newPassword, newPassword);
|
||||
new UserPatchRequest("Kong", "newemail@domain.com", newPassword, newPassword, "en");
|
||||
|
||||
when(authUtil.validatePassword(patchRequest.password())).thenReturn(Optional.empty());
|
||||
|
||||
|
||||
@@ -96,4 +96,19 @@ class MailgunEmailServiceTest {
|
||||
|
||||
verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendEmailChanged() {
|
||||
UserEntity user = new UserEntity();
|
||||
user.setEmail("test@example.com");
|
||||
|
||||
when(restTemplate.postForEntity(anyString(), any(), eq(String.class)))
|
||||
.thenReturn(ResponseEntity.ok("Success"));
|
||||
|
||||
String oldEmail = "old@example.com";
|
||||
|
||||
mailgunEmailService.sendEmailChangedNotification(user, oldEmail);
|
||||
|
||||
verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,17 @@ class MailgunTemplateTest {
|
||||
Assertions.assertNotNull(signUp.getVariableValuesJson());
|
||||
Assertions.assertFalse(signUp.getVariableValuesJson().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mailgunTemplateEmailChangedTest() {
|
||||
// MailgunTemplateEmailChanged
|
||||
|
||||
MailgunTemplateEmailChanged emailChanged = new MailgunTemplateEmailChanged();
|
||||
|
||||
Assertions.assertNotNull(emailChanged.getName());
|
||||
Assertions.assertEquals("email changed", emailChanged.getName());
|
||||
Assertions.assertNotNull(emailChanged.getVariables());
|
||||
Assertions.assertNotNull(emailChanged.getVariableValuesJson());
|
||||
Assertions.assertFalse(emailChanged.getVariableValuesJson().isBlank());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user