From 3770e12d2098cf272316804e6f8e7a5afeddb5d0 Mon Sep 17 00:00:00 2001 From: smanylov Date: Fri, 22 May 2026 14:59:59 +0700 Subject: [PATCH] add change password modal window --- src/app/actions/auth.ts | 147 ++++++++--- src/app/actions/session.ts | 2 +- src/app/actions/statisticActions.ts | 32 ++- src/app/actions/stuffActions.ts | 25 +- src/app/actions/usersActions.ts | 8 +- src/app/styles/animation.scss | 70 +++++ src/app/styles/global-styles.scss | 1 + src/app/styles/module/login.module.scss | 41 +++ src/app/ui/forms/login-form.tsx | 334 ++++++++++++++++++------ src/i18n/messages/en.json | 15 +- src/i18n/messages/ru.json | 15 +- 11 files changed, 544 insertions(+), 146 deletions(-) create mode 100644 src/app/styles/animation.scss diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index dd4eee6..73a4790 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -9,19 +9,14 @@ export async function authorization( previousState: { email: string; } - error: Record + error: Record; + requirePasswordChange?: boolean; + userData?: { email: string; id: number }; } | undefined, formData: FormData, ) { const email = formData.get('email') as string || ''; - const password = formData.get('password'); - - /* для теста */ - if (email === 'test' && password === 'test') { - /* await createSession('1111', 'test', 0); */ - /* redirect('/pages/'); */ - } - /* для теста */ + const password = formData.get('password') as string || ''; const validatedFields = loginFormSchema.safeParse({ email, @@ -67,9 +62,27 @@ export async function authorization( 'Accept': 'application/json' } }); + if (response.ok) { let parsed = await response.json(); if (parsed.message_code === 0) { + if (!parsed.message_body.admin.active_password) { + const errors: Record = {} + return { + previousState: { + email + }, + error: errors, + requirePasswordChange: true, + userData: { + email: parsed.message_body.admin.email, + id: parsed.message_body.admin.id, + password: password, + token: parsed.message_body.token + } + }; + } + await createSession(parsed.message_body.token, email, parsed.message_body.admin.id); } else { throw (`${parsed.message_code}`); @@ -95,16 +108,80 @@ export async function authorization( break; } - if (error) { + return { + previousState: { + email, + password + }, + error: errors + }; + } + + redirect('/pages/'); +} + +export async function updatePasswordAction( + id: number, + oldPassword: string | null, + newPassword: string, + email: string, + token: string +) { + try { + const requestBody: any = { + version: 1, + msg_id: 100, + message_body: { + action: "update", + admin_id: id, + new_password: newPassword + } + }; + + if (oldPassword) { + requestBody.message_body.old_password = oldPassword; + } + + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify(requestBody), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const parsed = await response.json(); + + if (parsed.message_code === 0) { + console.log(parsed); + if (parsed.message_desc === 'Success') { + console.log('Success'); + await createSession(token, email, id); + return { success: true }; + } else { + return { success: false }; + } + } else { + return { + success: false, + error: `password-update-failed-code-${parsed.message_code}` + }; + } + } else { return { - previousState: { - email, - password - }, - error: errors + success: false, + error: 'request-ended-with-an-error' }; } - throw error; + } catch (error) { + console.error('Password update error:', error); + return { + success: false, + error: 'password-update-failed' + }; } redirect('/pages/'); @@ -114,25 +191,25 @@ export async function logout() { const token = await getSessionData('token'); try { - const response = await fetch(`${API_BASE_URL}/api/admin`, { - method: 'POST', - body: JSON.stringify({ - version: 1, - msg_id: 100, - token: token, - message_body: { - action: "logout" - } - }), - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - }); - - if (response.ok) { - let parsed = await response.json(); - } + /* const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 100, + token: token, + message_body: { + action: "logout" + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + } */ } catch (error) { throw error; } finally { diff --git a/src/app/actions/session.ts b/src/app/actions/session.ts index 4677a09..b200295 100644 --- a/src/app/actions/session.ts +++ b/src/app/actions/session.ts @@ -121,7 +121,7 @@ export async function getSessionData(type: 'token' | 'email' | 'employeId'): Pro export async function deleteSession() { - console.log('delete session') + try { const cookieStore = await cookies() cookieStore.delete('session_nc_ap'); diff --git a/src/app/actions/statisticActions.ts b/src/app/actions/statisticActions.ts index c4f8f40..eea0fa0 100644 --- a/src/app/actions/statisticActions.ts +++ b/src/app/actions/statisticActions.ts @@ -4,6 +4,7 @@ import { API_BASE_URL } from '@/app/actions/definitions'; import { getSessionData } from '@/app/actions/session'; export async function fetchTariffStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -17,7 +18,8 @@ export async function fetchTariffStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -39,6 +41,7 @@ export async function fetchTariffStatistic() { } export async function fetchUserFilesTopStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -52,7 +55,8 @@ export async function fetchUserFilesTopStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -74,6 +78,7 @@ export async function fetchUserFilesTopStatistic() { } export async function fetchUserGeneralStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -87,7 +92,8 @@ export async function fetchUserGeneralStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -109,6 +115,7 @@ export async function fetchUserGeneralStatistic() { } export async function fetchSubscribeGeneralStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -122,7 +129,8 @@ export async function fetchSubscribeGeneralStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -144,6 +152,7 @@ export async function fetchSubscribeGeneralStatistic() { } export async function fetchProtectedContentGeneralStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -157,7 +166,8 @@ export async function fetchProtectedContentGeneralStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -179,6 +189,7 @@ export async function fetchProtectedContentGeneralStatistic() { } export async function fetchViolationGeneralStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -192,7 +203,8 @@ export async function fetchViolationGeneralStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -214,6 +226,7 @@ export async function fetchViolationGeneralStatistic() { } export async function fetchTokensGeneralStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -227,7 +240,8 @@ export async function fetchTokensGeneralStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -249,6 +263,7 @@ export async function fetchTokensGeneralStatistic() { } export async function fetchIncomeGeneralStatistic() { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -262,7 +277,8 @@ export async function fetchIncomeGeneralStatistic() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); diff --git a/src/app/actions/stuffActions.ts b/src/app/actions/stuffActions.ts index 2cc7f99..22b8146 100644 --- a/src/app/actions/stuffActions.ts +++ b/src/app/actions/stuffActions.ts @@ -12,7 +12,6 @@ export async function fetchStuffList(page?: number, size?: number, sortBy?: stri body: JSON.stringify({ version: 1, msg_id: 100, - token: token, message_body: { action: 'get_all', page: page, @@ -24,7 +23,8 @@ export async function fetchStuffList(page?: number, size?: number, sortBy?: stri }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -83,7 +83,8 @@ export async function createStuff(fullName: string, email: string, password: str }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -124,7 +125,8 @@ export async function removeStuff(id: number) { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -166,7 +168,8 @@ export async function updateStuffPermissions(id: number, permissions: Permission }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -204,7 +207,8 @@ export async function fetchEmployeInfo() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -240,7 +244,8 @@ export async function fetchStuffTemplates() { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -279,7 +284,8 @@ export async function createStuffTemplates(name: string, changePassword: boolean }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -316,7 +322,8 @@ export async function removeStuffTemplates(id: number,) { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); diff --git a/src/app/actions/usersActions.ts b/src/app/actions/usersActions.ts index 32b6c82..138a4cd 100644 --- a/src/app/actions/usersActions.ts +++ b/src/app/actions/usersActions.ts @@ -13,7 +13,6 @@ export async function fetchUsesData(page: number, size: number, sortBy?: string, version: 1, msg_id: 30014, message_body: { - /* token: token, */ action: 'getAllWithPagination', page: page, size: size, @@ -24,7 +23,8 @@ export async function fetchUsesData(page: number, size: number, sortBy?: string, }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); @@ -46,6 +46,7 @@ export async function fetchUsesData(page: number, size: number, sortBy?: string, } export async function fetchUsesInfo(userInfo: number) { + const token = await getSessionData('token'); try { const response = await fetch(`${API_BASE_URL}/api/admin/info`, { @@ -60,7 +61,8 @@ export async function fetchUsesInfo(userInfo: number) { }), headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` } }); diff --git a/src/app/styles/animation.scss b/src/app/styles/animation.scss new file mode 100644 index 0000000..dfa9943 --- /dev/null +++ b/src/app/styles/animation.scss @@ -0,0 +1,70 @@ +@use './variable.scss' as v; + +.loading-animation { + display: block; + text-align: center; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + .global-spinner { + display: block; + width: 24px; + height: 24px; + margin: 0 auto; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 4px solid #e5e7eb; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + &.large { + width: 80px; + height: 80px; + + &::before { + border: 10px solid #e5e7eb; + border-top: 10px solid #667eea; + } + } + + } +} + +.loading-placeholder { + display: flex; + align-items: center; + justify-content: center; +/* min-width: 80px; + min-height: 80px; */ + + &.start { + justify-content: start; + } + + .loading-animation { + display: block; + text-align: center; + position: relative; + left: auto; + top: auto; + transform: none; + } + + .global-spinner { + display: inline-block; + width: 24px; + height: 24px; + position: relative; + } +} \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index 41f3b94..12078d5 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -1,5 +1,6 @@ @use './variable.scss' as v; @use './edit-permissions-modal.scss'; +@use './animation.scss'; :root { --primary-color: #2563eb; diff --git a/src/app/styles/module/login.module.scss b/src/app/styles/module/login.module.scss index c71ea81..d9da97a 100644 --- a/src/app/styles/module/login.module.scss +++ b/src/app/styles/module/login.module.scss @@ -294,4 +294,45 @@ .checkbox-label { display: flex; gap: 10px; +} + +.change-password-modal { + padding: 24px; + min-width: 400px; + + h3 { + margin-bottom: 16px; + } + + .modal-buttons { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 24px; + } + + .btn-secondary { + padding: 8px 16px; + background: #6c757d; + color: white; + cursor: pointer; + margin: 20 0 0; + + &:hover { + background: #5a6268; + } + } +} + +.change-password-btn { + background: none; + border: none; + color: #007bff; + text-decoration: underline; + cursor: pointer; + margin-left: 8px; + + &:hover { + color: #0056b3; + } } \ No newline at end of file diff --git a/src/app/ui/forms/login-form.tsx b/src/app/ui/forms/login-form.tsx index 99bd41e..877736f 100644 --- a/src/app/ui/forms/login-form.tsx +++ b/src/app/ui/forms/login-form.tsx @@ -2,10 +2,11 @@ import styles from '@/app/styles/module/login.module.scss' import { useActionState, useState, useRef, useEffect } from 'react'; -import { authorization } from '@/app/actions/auth'; +import { authorization, updatePasswordAction } from '@/app/actions/auth'; import { useTranslations } from 'next-intl'; import { IconEye } from '@/app/ui/icons/icons'; import { useQueryClient } from '@tanstack/react-query'; +import ModalWindow from '@/app/components/modalWindow'; export default function LoginForm() { const [state, formAction, isPending] = useActionState( @@ -15,8 +16,23 @@ export default function LoginForm() { const t = useTranslations('Login-register-form'); const [showPassword, setShowPassword] = useState(false); + const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); + const [pendingPasswordUpdate, setPendingPasswordUpdate] = useState(false); + const [pendingUserData, setPendingUserData] = useState<{ + email: string; + id: number; + oldPassword: string; + token: string; + } | null>(null); + const [passwordUpdateError, setPasswordUpdateError] = useState(null); - function showPassowrd() { + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); + + function toggleShowPassword() { setShowPassword(!showPassword); } @@ -27,92 +43,238 @@ export default function LoginForm() { queryClient.clear(); }, []) - return ( -
-
- - { - e.target.value = e.target.value.toLocaleLowerCase(); - }} - /> - {state?.error?.email && ( -

- {t(state?.error?.email)} -

- )} -
-
- -
- - -
- {state?.error?.password && ( -

- {t(state?.error?.password)} -

- )} -
- {state?.error.server && ( -

- {t(state?.error.server)} -

- )} - {/*
-
- - -
-
*/} - + {state?.error?.password && ( +

+ {t(state?.error?.password)} +

+ )} + + {state?.error?.server && ( +

+ {t(state?.error?.server)} +

)} - -
+ {state?.requirePasswordChange && state?.userData && ( +

+ {t('password-change-required')} + +

+ )} + + + + { + setShowChangePasswordModal(false); + setPendingUserData(null); + setNewPassword(''); + setConfirmPassword(''); + setPasswordUpdateError(null); + }} + > +
+

{t('change-password')}

+

{t('password-change-required-message')}

+ +
+ +
+ setNewPassword(e.target.value)} + className={styles['form-input']} + placeholder={t('enter-new-password')} + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + className={styles['form-input']} + placeholder={t('confirm-new-password')} + /> + +
+
+ + {passwordUpdateError && ( +

{passwordUpdateError}

+ )} + +
+ + +
+
+
+ ) } \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index d21e114..0b7b028 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -228,7 +228,6 @@ "already-have-an-account": "Already have an account", "company": "Company", "company-placeholder": "Your company", - "confirm-password": "Confirm password", "copyright-protection": "Copyright protection", "create-an-account": "Create an account", "email-adress": "Email adress", @@ -291,7 +290,19 @@ "confirmation-code": "Confirmation code", "enter-confirmation-code": "Enter the confirmation code", "full-name-too-short": "Full name must be at least 2 characters", - "full-name-too-long": "Full name must not exceed 100 characters" + "full-name-too-long": "Full name must not exceed 100 characters", + "passwords-do-not-match": "Passwords do not match", + "password-too-short": "Password must be at least 6 characters", + "password-change-required": "Password change required", + "change-password": "Change password", + "password-change-required-message": "You must set a new password to continue", + "new-password": "New password", + "confirm-password": "Confirm password", + "enter-new-password": "Enter new password", + "confirm-new-password": "Confirm new password", + "update-password": "Update password", + "password-update-failed": "Failed to update password. Please try again later", + "cancel": "Cancel" }, "Permissions": { "edit-permissions": "Edit Permissions", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index ac335fa..1ff12e2 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -228,7 +228,6 @@ "already-have-an-account": "Уже есть аккаунт", "company": "Компания", "company-placeholder": "ООО «Ваша компания»", - "confirm-password": "Подтвердите пароль", "copyright-protection": "Защита авторских прав", "create-an-account": "Создать аккаунт", "email-adress": "Email адрес", @@ -291,7 +290,19 @@ "confirmation-code": "Код подтверждения", "enter-confirmation-code": "Введите код подтверждения", "full-name-too-short": "Полное имя должно состоять как минимум из 2 символов", - "full-name-too-long": "Полное имя не должно превышать 100 символов" + "full-name-too-long": "Полное имя не должно превышать 100 символов", + "passwords-do-not-match": "Пароли не совпадают", + "password-too-short": "Пароль должен содержать не менее 6 символов", + "password-change-required": "Требуется смена пароля", + "change-password": "Сменить пароль", + "password-change-required-message": "Для продолжения работы необходимо установить новый пароль", + "new-password": "Новый пароль", + "confirm-password": "Подтверждение пароля", + "enter-new-password": "Введите новый пароль", + "confirm-new-password": "Подтвердите новый пароль", + "update-password": "Обновить пароль", + "password-update-failed": "Не удалось обновить пароль. Попробуйте позже", + "cancel": "Отмена" }, "Permissions": { "edit-permissions": "Редактирование прав доступа",