From 565ef346e3585420ecf9c12cbadee898b7756521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Wed, 17 Jun 2026 07:43:34 +0300 Subject: [PATCH] feat: implement password change functionality in settings --- src/app/api/user/user.api.ts | 56 ++++--- .../common/constants/api-routes.constants.ts | 3 +- .../constants/frontend-routes.constants.ts | 2 + src/app/router/Routes.tsx | 2 + src/app/stores/user-login-store.ts | 30 ++-- src/features/AdminPage/AdminBar.component.tsx | 4 + .../SettingsPage/SettingsPage.component.tsx | 149 ++++++++++++++++++ .../SettingsPage/SettingsPage.styles.scss | 30 ++++ src/models/user/user.model.ts | 6 + 9 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 src/features/AdminPage/SettingsPage/SettingsPage.component.tsx create mode 100644 src/features/AdminPage/SettingsPage/SettingsPage.styles.scss diff --git a/src/app/api/user/user.api.ts b/src/app/api/user/user.api.ts index aba34d5..012f641 100644 --- a/src/app/api/user/user.api.ts +++ b/src/app/api/user/user.api.ts @@ -1,4 +1,5 @@ import Agent from '@api/agent.api'; +import ChangePasswordDto from '@models/user/user.model'; import { API_ROUTES } from '@/app/common/constants/api-routes.constants'; import { @@ -6,33 +7,36 @@ import { UserLoginRequest, UserLoginResponce, } from '@/models/user/user.model'; - const UserApi = { - login: (loginParams: UserLoginRequest) => - Agent.post( - API_ROUTES.USERS.LOGIN, - loginParams, - ), +const UserApi = { + login: (loginParams: UserLoginRequest) => + Agent.post( + API_ROUTES.USERS.LOGIN, + loginParams, + ), - refreshToken: (token: RefreshTokenRequest) => - Agent.post( - API_ROUTES.USERS.REFRESH_TOKEN, - token, - ), + refreshToken: (token: RefreshTokenRequest) => + Agent.post( + API_ROUTES.USERS.REFRESH_TOKEN, + token, + ), - adminLogin: (loginParams: UserLoginRequest) => - Agent.post( - API_ROUTES.ADMIN_AUTHORIZATION.LOGIN, - loginParams, - ), + adminLogin: (loginParams: UserLoginRequest) => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.LOGIN, + loginParams, + ), - adminRefreshToken: (token: RefreshTokenRequest) => - Agent.post( - API_ROUTES.ADMIN_AUTHORIZATION.REFRESH_TOKEN, - token, - ), - adminLogout: () => - Agent.post( - API_ROUTES.ADMIN_AUTHORIZATION.LOGOUT, {}, - ), - }; + adminRefreshToken: (token: RefreshTokenRequest) => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.REFRESH_TOKEN, + token, + ), + adminLogout: () => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.LOGOUT, {}, + ), + + changePassword: (data: ChangePasswordDto) => + Agent.post(API_ROUTES.ADMIN_AUTHORIZATION.CHANGE_PASSWORD, data), +}; export default UserApi; diff --git a/src/app/common/constants/api-routes.constants.ts b/src/app/common/constants/api-routes.constants.ts index 3de91c9..9ae4fe8 100644 --- a/src/app/common/constants/api-routes.constants.ts +++ b/src/app/common/constants/api-routes.constants.ts @@ -228,6 +228,7 @@ export const API_ROUTES = { ADMIN_AUTHORIZATION: { LOGIN: 'auth/login', REFRESH_TOKEN: 'auth/refresh-token', - LOGOUT: 'auth/logout' + LOGOUT: 'auth/logout', + CHANGE_PASSWORD: 'auth/change-password', }, }; diff --git a/src/app/common/constants/frontend-routes.constants.ts b/src/app/common/constants/frontend-routes.constants.ts index a979636..09c9f8b 100644 --- a/src/app/common/constants/frontend-routes.constants.ts +++ b/src/app/common/constants/frontend-routes.constants.ts @@ -15,6 +15,7 @@ const FRONTEND_ROUTES = { EDITOR: '/admin-panel/editor', CALENDAR: '/admin-panel/calendar', VACANCIES: '/admin-panel/vacancies', + SETTINGS: '/admin-panel/settings', }, ADMIN_RELATIVE: { PARTNERS: 'partners', @@ -26,6 +27,7 @@ const FRONTEND_ROUTES = { CALENDAR: 'calendar', NEWS: 'news', VACANCIES: 'vacancies', + SETTINGS: 'settings', }, OTHER_PAGES: { CATALOG: '/catalog', diff --git a/src/app/router/Routes.tsx b/src/app/router/Routes.tsx index 963ef57..a109a63 100644 --- a/src/app/router/Routes.tsx +++ b/src/app/router/Routes.tsx @@ -20,6 +20,7 @@ import Partners from "@/features/AdminPage/PartnersPage/Partners.component"; import Streetcodes from "@/features/AdminPage/StreetcodesPage/Streetcodes.component"; import TeamPage from "@/features/AdminPage/TeamPage/TeamPage.component"; import VacanciesPage from "@/features/AdminPage/VacanciesPage/VacanciesPage.component"; +import SettingsPage from "@/features/AdminPage/SettingsPage/SettingsPage.component"; import StreetcodeCatalog from "@/features/StreetcodeCatalogPage/StreetcodeCatalog.component"; const router = createBrowserRouter( @@ -38,6 +39,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> } /> diff --git a/src/app/stores/user-login-store.ts b/src/app/stores/user-login-store.ts index dc3baca..cbbb847 100644 --- a/src/app/stores/user-login-store.ts +++ b/src/app/stores/user-login-store.ts @@ -1,5 +1,6 @@ import { makeAutoObservable } from 'mobx'; import UserApi from '@api/user/user.api'; +import {ChangePasswordDto }from '@models/user/user.model'; import { RefreshTokenResponce, UserLoginResponce } from '@/models/user/user.model'; @@ -14,17 +15,17 @@ export default class UserLoginStore { public userLoginResponce?: UserLoginResponce; - private callback?:()=>void; + private callback?: () => void; public constructor() { makeAutoObservable(this); } - private static getExpiredDate():number { + private static getExpiredDate(): number { return Number(localStorage.getItem(UserLoginStore.dateStorageName)!); } - private static setExpiredDate(date: string):void { + private static setExpiredDate(date: string): void { localStorage.setItem(UserLoginStore.dateStorageName, date); } @@ -32,7 +33,7 @@ export default class UserLoginStore { return localStorage.getItem(UserLoginStore.tokenStorageName); } - public static setToken(newToken:string) { + public static setToken(newToken: string) { return localStorage.setItem(UserLoginStore.tokenStorageName, newToken); } @@ -52,11 +53,11 @@ export default class UserLoginStore { localStorage.removeItem(UserLoginStore.refreshTokenStorageName); } - public setCallback(func:()=>void) { + public setCallback(func: () => void) { this.callback = func; } - public static get isLoggedIn():boolean { + public static get isLoggedIn(): boolean { return UserLoginStore.getExpiredDate() > new Date(Date.now()).getTime(); } @@ -67,14 +68,14 @@ export default class UserLoginStore { } public logout() { - if (this.timeoutHandler) { + if (this.timeoutHandler) { clearTimeout(this.timeoutHandler); } UserLoginStore.clearUserData(); } - public setUserLoginResponce(user:UserLoginResponce, func:()=>void) { + public setUserLoginResponce(user: UserLoginResponce, func: () => void) { try { const timeNumber = (new Date(user.expireAt)).getTime(); UserLoginStore.setExpiredDate(timeNumber.toString()); @@ -95,8 +96,8 @@ export default class UserLoginStore { } } - public refreshToken = ():Promise => ( - UserApi.refreshToken({ + public refreshToken = (): Promise => ( + UserApi.refreshToken({ token: UserLoginStore.getToken() ?? '', refreshToken: UserLoginStore.getRefreshToken() ?? '' }) @@ -112,4 +113,13 @@ export default class UserLoginStore { UserLoginStore.setRefreshToken(refreshToken.refreshToken); return refreshToken; })); + + public changePassword = async (data: ChangePasswordDto) => { + try { + await UserApi.changePassword(data); + return { success: true }; + } catch (error) { + return { success: false, error }; + } + }; } diff --git a/src/features/AdminPage/AdminBar.component.tsx b/src/features/AdminPage/AdminBar.component.tsx index efcf0c6..972b4fa 100644 --- a/src/features/AdminPage/AdminBar.component.tsx +++ b/src/features/AdminPage/AdminBar.component.tsx @@ -49,6 +49,10 @@ const adminNavItems = [ title: 'Словник', to: FRONTEND_ROUTES.ADMIN.DICTIONARY, }, + { + title: 'Налаштування', + to: FRONTEND_ROUTES.ADMIN.SETTINGS, + }, ]; const AdminBar = () => { diff --git a/src/features/AdminPage/SettingsPage/SettingsPage.component.tsx b/src/features/AdminPage/SettingsPage/SettingsPage.component.tsx new file mode 100644 index 0000000..9ad32a5 --- /dev/null +++ b/src/features/AdminPage/SettingsPage/SettingsPage.component.tsx @@ -0,0 +1,149 @@ +import { useState } from 'react'; +import { Tabs, Form, Input, Button, message } from 'antd'; +import useMobx from '@stores/root-store'; +import { ChangePasswordDto } from '@models/user/user.model'; +import './SettingsPage.styles.scss'; + +const SettingsPage = () => { + const { userLoginStore } = useMobx(); + const [passwordForm] = Form.useForm(); + const [profileForm] = Form.useForm(); + + const [loading, setLoading] = useState(false); + const [isEditing, setIsEditing] = useState(false); + + const loginResponse = userLoginStore.userLoginResponce; + + const userData = loginResponse?.user; + + const initialValues = { + username: userData?.name ? `${userData.name} ${userData.surname}` : 'Адміністратор', + email: userData?.email || 'admin@streetcode.com', + }; + + const handlePasswordChange = async (data: ChangePasswordDto) => { + setLoading(true); + + try { + const result = await userLoginStore.changePassword(data); + + if (!result.success) { + message.error('Помилка зміни пароля: перевірте поточний пароль'); + return; + } + + message.success('Пароль успішно змінено'); + passwordForm.resetFields(); + } catch (error) { + console.error('Password change error:', error); + message.error('Сталася помилка'); + } finally { + setLoading(false); + } + }; + + const onFinishProfile = (values: any) => { + console.log('Profile update:', values); + message.success('Дані профілю оновлено'); + setIsEditing(false); + }; + + const items = [ + { + key: '1', + label: 'Профіль', + children: ( +
+ + + + + + + + {!isEditing ? ( + + ) : ( +
+ + +
+ )} +
+ ), + }, + { + key: '2', + label: 'Безпека', + children: ( +
+ + + + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('Паролі не співпадають!')); + }, + }), + ]} + > + + + + +
+ ), + }, + ]; + + return ( +
+
+

Налаштування

+
+ +
+ ); +}; + +export default SettingsPage; \ No newline at end of file diff --git a/src/features/AdminPage/SettingsPage/SettingsPage.styles.scss b/src/features/AdminPage/SettingsPage/SettingsPage.styles.scss new file mode 100644 index 0000000..d58807d --- /dev/null +++ b/src/features/AdminPage/SettingsPage/SettingsPage.styles.scss @@ -0,0 +1,30 @@ +@use "@sass/variables/_variables.colors.scss" as c; + +.settings-page { + width: 100%; + padding: 24px; + background: #fff; + border-radius: 8px; + + .settings-page-header { + margin-bottom: 24px; + border-bottom: 1px solid #f0f0f0; + padding-bottom: 16px; + } + + .settings-form { + max-width: 400px; + margin-top: 20px; + } + + .settings-tabs { + .ant-tabs-tab-active { + .ant-tabs-tab-btn { + color: c.$dark-red-color; + } + } + .ant-tabs-ink-bar { + background: c.$dark-red-color; + } + } +} \ No newline at end of file diff --git a/src/models/user/user.model.ts b/src/models/user/user.model.ts index bdaedd1..8cbb888 100644 --- a/src/models/user/user.model.ts +++ b/src/models/user/user.model.ts @@ -35,3 +35,9 @@ export enum UserRole { Administrator, Moderator, } + +export interface ChangePasswordDto { + currentPassword: string; + newPassword: string; + confirmNewPassword: string; +}