From a4853f3333a3eb9ea1e4c47e4ccf07c1f6877774 Mon Sep 17 00:00:00 2001 From: robines Date: Wed, 20 Nov 2024 20:48:49 +0100 Subject: [PATCH 1/9] Add frontend for changing password --- .../ChangePasswordForm.module.scss | 17 ++++ .../ChangePasswordForm.tsx | 86 +++++++++++++++++++ .../UserChangePasswordPage.tsx | 17 ++++ .../src/Pages/UserChangePasswordPage/index.ts | 1 + frontend/src/Pages/index.ts | 1 + frontend/src/i18n/constants.ts | 3 + frontend/src/i18n/translations.ts | 6 ++ frontend/src/router/router.tsx | 7 ++ frontend/src/routes/frontend.ts | 6 ++ 9 files changed, 144 insertions(+) create mode 100644 frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.module.scss create mode 100644 frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.tsx create mode 100644 frontend/src/Pages/UserChangePasswordPage/UserChangePasswordPage.tsx create mode 100644 frontend/src/Pages/UserChangePasswordPage/index.ts diff --git a/frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.module.scss b/frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.module.scss new file mode 100644 index 000000000..849d932b7 --- /dev/null +++ b/frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.module.scss @@ -0,0 +1,17 @@ +@import 'src/constants'; + +@import 'src/mixins'; + +.form { + width: 100%; + + @include for-desktop-up { + width: 400px; + } +} + +.action_row { + display: flex; + justify-content: flex-end; + margin: 1rem 0; +} diff --git a/frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.tsx b/frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.tsx new file mode 100644 index 000000000..3fcde0c2d --- /dev/null +++ b/frontend/src/Pages/UserChangePasswordPage/ChangePasswordForm.tsx @@ -0,0 +1,86 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from '~/Components'; +import { KEY } from '~/i18n/constants'; +import { PASSWORD } from '~/schema/user'; +import { lowerCapitalize } from '~/utils'; +import styles from './ChangePasswordForm.module.scss'; + +const schema = z.object({ + current_password: PASSWORD, + new_password: PASSWORD, + repeat_password: PASSWORD, +}); + +type SchemaType = z.infer; + +export function ChangePasswordForm() { + const { t } = useTranslation(); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + current_password: '', + new_password: '', + repeat_password: '', + }, + }); + + function onSubmit(values: SchemaType) { + console.log('Values:', values); + } + + return ( +
+ + ( + + {lowerCapitalize(`${t(KEY.common_current)} ${t(KEY.common_password)}`)} + + + + + + )} + /> + ( + + {lowerCapitalize(t(KEY.new_password))} + + + + + + )} + /> + ( + + {lowerCapitalize(`${t(KEY.common_repeat)} ${t(KEY.new_password)}`)} + + + + + + )} + /> + +
+ +
+ + + ); +} diff --git a/frontend/src/Pages/UserChangePasswordPage/UserChangePasswordPage.tsx b/frontend/src/Pages/UserChangePasswordPage/UserChangePasswordPage.tsx new file mode 100644 index 000000000..9f85b87ea --- /dev/null +++ b/frontend/src/Pages/UserChangePasswordPage/UserChangePasswordPage.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from 'react-i18next'; +import { AdminPageLayout } from '~/PagesAdmin/AdminPageLayout/AdminPageLayout'; +import { useTitle } from '~/hooks'; +import { KEY } from '~/i18n/constants'; +import { ChangePasswordForm } from './ChangePasswordForm'; + +export function UserChangePasswordPage() { + const { t } = useTranslation(); + const title = t(KEY.change_password); + useTitle(title); + + return ( + + + + ); +} diff --git a/frontend/src/Pages/UserChangePasswordPage/index.ts b/frontend/src/Pages/UserChangePasswordPage/index.ts new file mode 100644 index 000000000..06f99c31f --- /dev/null +++ b/frontend/src/Pages/UserChangePasswordPage/index.ts @@ -0,0 +1 @@ +export { UserChangePasswordPage } from './UserChangePasswordPage'; diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 1dd719188..89625e6d2 100644 --- a/frontend/src/Pages/index.ts +++ b/frontend/src/Pages/index.ts @@ -24,5 +24,6 @@ export { RecruitmentPage } from './RecruitmentPage'; export { RouteOverviewPage } from './RouteOverviewPage'; export { SaksdokumenterPage } from './SaksdokumenterPage'; export { SignUpPage } from './SignUpPage'; +export { UserChangePasswordPage } from './UserChangePasswordPage'; export { VenuePage } from './VenuePage'; export { OrganizationRecruitmentPage } from './OrganizationRecruitmentPage'; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index e8642e9c8..3243bcf1e 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -131,6 +131,7 @@ export const KEY = { common_occasion: 'common_occasion', common_phonenumber: 'common_phonenumber', common_password: 'common_password', + common_current: 'common_current', common_about_us: 'common_about_us', common_overview: 'common_overview', common_recruitmentposition: 'common_recruitmentposition', @@ -221,6 +222,8 @@ export const KEY = { // ==================== // role_content_type: 'role_content_type', + change_password: 'change_password', + new_password: 'new_password', // LoginPage: loginpage_register: 'loginpage_register', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 8a4d5ce4c..5d1d32b2f 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -112,6 +112,7 @@ export const nb = prepareTranslations({ [KEY.common_phonenumber]: 'Telefonnummer', [KEY.common_register]: 'Registrer', [KEY.common_password]: 'passord', + [KEY.common_current]: 'Nåværende', [KEY.common_about_us]: 'Om oss', [KEY.common_previous]: 'Forrige', [KEY.common_required]: 'Påkrevd', @@ -204,6 +205,8 @@ export const nb = prepareTranslations({ // Others // // ==================== // [KEY.role_content_type]: 'Hierarkinivå', + [KEY.change_password]: 'Bytt passord', + [KEY.new_password]: 'Nytt passord', [KEY.admin_impersonate]: 'Stjel identitet', [KEY.admin_stop_impersonate]: 'Stopp identitetstyveri', @@ -599,6 +602,7 @@ export const en = prepareTranslations({ [KEY.common_phonenumber]: 'Phone number', [KEY.common_lastname]: 'Last name', [KEY.common_password]: 'password', + [KEY.common_current]: 'Current', [KEY.common_select_all]: 'Select all', [KEY.common_unselect_all]: 'Unselect all', [KEY.common_overview]: 'Overview', @@ -692,6 +696,8 @@ export const en = prepareTranslations({ // Others // // ==================== // [KEY.role_content_type]: 'Hierarchical level', + [KEY.change_password]: 'Change password', + [KEY.new_password]: 'New password', [KEY.admin_impersonate]: 'Impersonate', [KEY.admin_stop_impersonate]: 'Stop impersonation', diff --git a/frontend/src/router/router.tsx b/frontend/src/router/router.tsx index 662f7098f..fd29dc9e0 100644 --- a/frontend/src/router/router.tsx +++ b/frontend/src/router/router.tsx @@ -28,6 +28,7 @@ import { RouteOverviewPage, SaksdokumenterPage, SignUpPage, + UserChangePasswordPage, VenuePage, } from '~/Pages'; import { @@ -148,6 +149,12 @@ export const router = createBrowserRouter( path={ROUTES.frontend.admin} element={} />} /> + {/* User pages */} + } + handle={{ crumb: ({ pathname }: UIMatch) => {t(KEY.change_password)} }} + /> {/* Gangs */} } diff --git a/frontend/src/routes/frontend.ts b/frontend/src/routes/frontend.ts index 4bf28443b..7f390957c 100644 --- a/frontend/src/routes/frontend.ts +++ b/frontend/src/routes/frontend.ts @@ -35,6 +35,12 @@ export const ROUTES_FRONTEND = { sulten_reservation: '/lyche/reservation/', sulten_about: '/lyche/about/', sulten_contact: 'lyche/contact/', + + // ==================== // + // User control panel // + // ==================== // + user_change_password: '/control-panel/password/', + // ==================== // // Admin pages // // ==================== // From dc3e95352e27ce581685ffd871ab59b4a8cc294e Mon Sep 17 00:00:00 2001 From: robines Date: Wed, 20 Nov 2024 20:49:00 +0100 Subject: [PATCH 2/9] Add to admin sidebar. Improve current route matching logic --- .../PagesAdmin/AdminLayout/AdminLayout.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx index 5960969da..4477f079d 100644 --- a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx +++ b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx @@ -2,7 +2,7 @@ import { Icon } from '@iconify/react'; import classNames from 'classnames'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Outlet, useLocation, useResolvedPath } from 'react-router-dom'; import { Button, Link, Navbar } from '~/Components'; import type { Applet } from '~/Components/AdminBox/types'; import { appletCategories } from '~/Pages/AdminPage/applets'; @@ -31,7 +31,7 @@ export function AdminLayout() { if (applet.url === undefined) return <>; // Create panel item - const selected = location.pathname.toLowerCase().indexOf(applet.url) !== -1; + const selected = location.pathname === applet.url; return ( { if (!isMobile) { setPanelOpen(true); } }, [isMobile]); + const userApplets: Applet[] = [ + { url: ROUTES_FRONTEND.admin, icon: 'mdi:person', title_nb: 'Profil', title_en: 'Profile' }, + { + url: ROUTES_FRONTEND.user_change_password, + icon: 'mdi:password', + title_nb: 'Bytt passord', + title_en: 'Change password', + }, + ]; + const panel = (
diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 7f9ef7854..e77eb2fb9 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -86,6 +86,11 @@ export async function register(data: RegistrationDto): Promise { return response.status; } +export async function changePassword(current_password: string, new_password: string): Promise { + const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__change_password; + return await axios.post(url, { current_password, new_password }, { withCredentials: true }); +} + export async function getUser(): Promise { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__user; const response = await axios.get(url, { withCredentials: true }); From eeb4b342bae67c3f43d2793ddeb222b97027fc9f Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 22 Nov 2024 18:58:58 +0100 Subject: [PATCH 7/9] Undo unintentional formatting --- backend/samfundet/serializers.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index ffd97b2c2..d8e3447d0 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -347,8 +347,7 @@ def validate(self, attrs: dict) -> dict: # noqa: C901 if username and password: # Try to authenticate the user using Django auth framework. user = User.objects.create_user( - first_name=firstname, last_name=lastname, username=username, email=email, phone_number=phone_number, - password=password + first_name=firstname, last_name=lastname, username=username, email=email, phone_number=phone_number, password=password ) user = authenticate(request=self.context.get('request'), username=username, password=password) else: @@ -733,8 +732,7 @@ class RecruitmentUpdateUserPrioritySerializer(serializers.Serializer): class UserForRecruitmentSerializer(serializers.ModelSerializer): applications = serializers.SerializerMethodField(method_name='get_applications', read_only=True) - applications_without_interview = serializers.SerializerMethodField( - method_name='get_applications_without_interviews_for_recruitment', read_only=True) + applications_without_interview = serializers.SerializerMethodField(method_name='get_applications_without_interviews_for_recruitment', read_only=True) top_application = serializers.SerializerMethodField(method_name='get_top_application', read_only=True) campus = CampusSerializer() @@ -917,8 +915,7 @@ def get_processed_applicants(self, recruitment_position: RecruitmentPosition) -> def get_accepted_applicants(self, recruitment_position: RecruitmentPosition) -> int: return RecruitmentApplication.objects.filter( - recruitment_position=recruitment_position, withdrawn=False, - recruiter_status=RecruitmentStatusChoices.CALLED_AND_ACCEPTED + recruitment_position=recruitment_position, withdrawn=False, recruiter_status=RecruitmentStatusChoices.CALLED_AND_ACCEPTED ).count() @@ -1156,8 +1153,7 @@ def update(self, instance: RecruitmentApplication, validated_data: dict) -> Recr interview_data = validated_data.pop('interview', {}) interview_instance = instance.interview - interview_instance.interview_location = interview_data.get('interview_location', - interview_instance.interview_location) + interview_instance.interview_location = interview_data.get('interview_location', interview_instance.interview_location) interview_instance.interview_time = interview_data.get('interview_time', interview_instance.interview_time) interviewers_data = validated_data.pop('interviewers', []) interview_instance.interviewers.set(interviewers_data) @@ -1220,8 +1216,7 @@ def create(self, validated_data: dict) -> PurchaseFeedbackModel: purchase_feedback = PurchaseFeedbackModel.objects.create(event=event, **validated_data) for alternative, selected in alternatives_data.items(): - PurchaseFeedbackAlternative.objects.create(form=purchase_feedback, alternative=alternative, - selected=selected) + PurchaseFeedbackAlternative.objects.create(form=purchase_feedback, alternative=alternative, selected=selected) for question, answer in responses_data.items(): PurchaseFeedbackQuestion.objects.create(form=purchase_feedback, question=question, answer=answer) From 8de5b8c540f7bd5c4662769395928d6652860e0e Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 22 Nov 2024 19:00:14 +0100 Subject: [PATCH 8/9] Remove unused import --- frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx index 4477f079d..6331157fd 100644 --- a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx +++ b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx @@ -2,7 +2,7 @@ import { Icon } from '@iconify/react'; import classNames from 'classnames'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useLocation, useResolvedPath } from 'react-router-dom'; +import { Outlet, useLocation } from 'react-router-dom'; import { Button, Link, Navbar } from '~/Components'; import type { Applet } from '~/Components/AdminBox/types'; import { appletCategories } from '~/Pages/AdminPage/applets'; From 3f79f1fd3adf8900d6fa6dc93b86f585ba9152b3 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 22 Nov 2024 19:20:46 +0100 Subject: [PATCH 9/9] ruff --- backend/samfundet/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index d8e3447d0..d19f08ce7 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -241,13 +241,13 @@ class ChangePasswordSerializer(serializers.Serializer): new_password = serializers.CharField(required=True) def validate_current_password(self, value: str) -> str: - user = self.context["request"].user + user = self.context['request'].user if not user.check_password(value): raise serializers.ValidationError('Incorrect current password') return value def validate_new_password(self, value: str) -> str: - user = self.context["request"].user + user = self.context['request'].user validate_password(value, user) return value