diff --git a/backend/core/migrations/0037_appliedcontrol_priority.py b/backend/core/migrations/0037_appliedcontrol_priority.py deleted file mode 100644 index 3f8a97035..000000000 --- a/backend/core/migrations/0037_appliedcontrol_priority.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-19 15:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("core", "0036_asset_owner"), - ] - - operations = [ - migrations.AddField( - model_name="appliedcontrol", - name="priority", - field=models.PositiveSmallIntegerField( - blank=True, - choices=[(1, "P1"), (2, "P2"), (3, "P3"), (4, "P4")], - null=True, - verbose_name="Priority", - ), - ), - ] diff --git a/backend/core/migrations/0037_asset_disaster_recovery_objectives_and_more.py b/backend/core/migrations/0037_asset_disaster_recovery_objectives_and_more.py index 52f795352..20657d346 100644 --- a/backend/core/migrations/0037_asset_disaster_recovery_objectives_and_more.py +++ b/backend/core/migrations/0037_asset_disaster_recovery_objectives_and_more.py @@ -96,4 +96,14 @@ class Migration(migrations.Migration): verbose_name="Security objectives", ), ), + migrations.AddField( + model_name="appliedcontrol", + name="priority", + field=models.PositiveSmallIntegerField( + blank=True, + choices=[(1, "P1"), (2, "P2"), (3, "P3"), (4, "P4")], + null=True, + verbose_name="Priority", + ), + ), ] diff --git a/backend/core/urls.py b/backend/core/urls.py index 5c5c5ddec..7d88f694c 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -83,6 +83,7 @@ path("iam/", include("iam.urls")), path("serdes/", include("serdes.urls")), path("settings/", include("global_settings.urls")), + path("user-preferences/", UserPreferencesView.as_view(), name="user-preferences"), path("csrf/", get_csrf_token, name="get_csrf_token"), path("build/", get_build, name="get_build"), path("evidences//upload/", UploadAttachmentView.as_view(), name="upload"), diff --git a/backend/core/views.py b/backend/core/views.py index 846345235..49ffb1ad0 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -48,7 +48,6 @@ from rest_framework.renderers import JSONRenderer from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN from rest_framework.utils.serializer_helpers import ReturnDict from rest_framework.views import APIView @@ -268,7 +267,7 @@ def quality_check_detail(self, request, pk): } return Response(res) else: - return Response(status=HTTP_403_FORBIDDEN) + return Response(status=status.HTTP_403_FORBIDDEN) class ThreatViewSet(BaseModelViewSet): @@ -541,7 +540,7 @@ def quality_check_detail(self, request, pk): risk_assessment = self.get_object() return Response(risk_assessment.quality_check()) else: - return Response(status=HTTP_403_FORBIDDEN) + return Response(status=status.HTTP_403_FORBIDDEN) @action(detail=True, methods=["get"], name="Get treatment plan data") def plan(self, request, pk): @@ -574,7 +573,7 @@ def plan(self, request, pk): return Response(risk_assessment) else: - return Response(status=HTTP_403_FORBIDDEN) + return Response(status=status.HTTP_403_FORBIDDEN) @action(detail=True, name="Get treatment plan CSV") def treatment_plan_csv(self, request, pk): @@ -634,7 +633,9 @@ def treatment_plan_csv(self, request, pk): return response else: - return Response({"error": "Permission denied"}, status=HTTP_403_FORBIDDEN) + return Response( + {"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN + ) @action(detail=True, name="Get risk assessment CSV") def risk_assessment_csv(self, request, pk): @@ -680,7 +681,9 @@ def risk_assessment_csv(self, request, pk): return response else: - return Response({"error": "Permission denied"}, status=HTTP_403_FORBIDDEN) + return Response( + {"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN + ) @action(detail=True, name="Get risk assessment PDF") def risk_assessment_pdf(self, request, pk): @@ -1193,7 +1196,7 @@ def update(self, request, *args, **kwargs): _data = { "non_field_errors": "The justification can only be edited by the approver" } - return Response(data=_data, status=HTTP_400_BAD_REQUEST) + return Response(data=_data, status=status.HTTP_400_BAD_REQUEST) else: return super().update(request, *args, **kwargs) @@ -1305,7 +1308,7 @@ def update(self, request: Request, *args, **kwargs) -> Response: if str(admin_group.pk) not in new_user_groups: return Response( {"error": "attemptToRemoveOnlyAdminUserGroup"}, - status=HTTP_403_FORBIDDEN, + status=status.HTTP_403_FORBIDDEN, ) return super().update(request, *args, **kwargs) @@ -1317,7 +1320,7 @@ def destroy(self, request, *args, **kwargs): if number_of_admin_users == 1: return Response( {"error": "attemptToDeleteOnlyAdminAccountError"}, - status=HTTP_403_FORBIDDEN, + status=status.HTTP_403_FORBIDDEN, ) return super().destroy(request, *args, **kwargs) @@ -1532,6 +1535,26 @@ def my_assignments(self, request): ) +class UserPreferencesView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request) -> Response: + return Response(request.user.preferences, status=status.HTTP_200_OK) + + def patch(self, request) -> Response: + new_language = request.data.get("lang") + if new_language is None or new_language not in ( + lang[0] for lang in settings.LANGUAGES + ): + return Response( + {"error": "This language doesn't exist."}, + status=status.HTTP_400_BAD_REQUEST, + ) + request.user.preferences["lang"] = new_language + request.user.save() + return Response({}, status=status.HTTP_200_OK) + + @cache_page(60 * SHORT_CACHE_TTL) @vary_on_cookie @api_view(["GET"]) diff --git a/backend/iam/migrations/0010_user_preferences.py b/backend/iam/migrations/0010_user_preferences.py new file mode 100644 index 000000000..eed321d6b --- /dev/null +++ b/backend/iam/migrations/0010_user_preferences.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.1 on 2024-11-22 01:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("iam", "0009_create_allauth_emailaddress_objects"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="preferences", + field=models.JSONField(default=dict), + ), + ] diff --git a/backend/iam/models.py b/backend/iam/models.py index 61f4c2404..9a8e5f5bc 100644 --- a/backend/iam/models.py +++ b/backend/iam/models.py @@ -342,6 +342,7 @@ class User(AbstractBaseUser, AbstractBaseModel, FolderMixin): first_name = models.CharField(_("first name"), max_length=150, blank=True) email = models.CharField(max_length=100, unique=True) first_login = models.BooleanField(default=True) + preferences = models.JSONField(default=dict) is_sso = models.BooleanField(default=False) is_third_party = models.BooleanField(default=False) is_active = models.BooleanField( diff --git a/backend/iam/views.py b/backend/iam/views.py index db7f7d5e8..ee057dea4 100644 --- a/backend/iam/views.py +++ b/backend/iam/views.py @@ -61,6 +61,7 @@ def post(self, request) -> Response: class CurrentUserView(views.APIView): + # Is this condition really necessary if we have permission_classes = [permissions.IsAuthenticated] ? permission_classes = [permissions.IsAuthenticated] def get(self, request) -> Response: diff --git a/frontend/src/lib/components/SideBar/SideBarFooter.svelte b/frontend/src/lib/components/SideBar/SideBarFooter.svelte index 222db555d..7f65e4adb 100644 --- a/frontend/src/lib/components/SideBar/SideBarFooter.svelte +++ b/frontend/src/lib/components/SideBar/SideBarFooter.svelte @@ -50,6 +50,12 @@ event.preventDefault(); value = event?.target?.value; setLanguageTag(value); + fetch('/api/user-preferences', { + method: 'PATCH', + body: JSON.stringify({ + lang: value + }) + }); // sessionStorage.setItem('lang', value); setCookie('ciso_lang', value); window.location.reload(); diff --git a/frontend/src/lib/utils/helpers.ts b/frontend/src/lib/utils/helpers.ts index 57a134484..b0d5b91aa 100644 --- a/frontend/src/lib/utils/helpers.ts +++ b/frontend/src/lib/utils/helpers.ts @@ -60,7 +60,7 @@ export function formatScoreValue(value: number, max_score: number, fullDonut = f } export function getSecureRedirect(url: any): string { - const SECURE_REDIRECT_URL_REGEX = /^\/\w+/; + const SECURE_REDIRECT_URL_REGEX = /^\/\w*/; return typeof url === 'string' && SECURE_REDIRECT_URL_REGEX.test(url) ? url : ''; } diff --git a/frontend/src/routes/(app)/+page.server.ts b/frontend/src/routes/(app)/+page.server.ts index d139b4b57..93ebc0f16 100644 --- a/frontend/src/routes/(app)/+page.server.ts +++ b/frontend/src/routes/(app)/+page.server.ts @@ -2,5 +2,5 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { - redirect(301, '/analytics'); + redirect(301, '/analytics?refresh=1'); }; diff --git a/frontend/src/routes/(authentication)/login/+page.server.ts b/frontend/src/routes/(authentication)/login/+page.server.ts index 0c5ab5392..0ea51b837 100644 --- a/frontend/src/routes/(authentication)/login/+page.server.ts +++ b/frontend/src/routes/(authentication)/login/+page.server.ts @@ -1,7 +1,6 @@ import { getSecureRedirect } from '$lib/utils/helpers'; import { ALLAUTH_API_URL, BASE_API_URL } from '$lib/utils/constants'; -import { csrfToken } from '$lib/utils/csrf'; import { loginSchema } from '$lib/utils/schemas'; import type { LoginRequestBody } from '$lib/utils/types'; import { fail, redirect, type Actions } from '@sveltejs/kit'; @@ -9,7 +8,6 @@ import { setError, superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import type { PageServerLoad } from './$types'; import { mfaAuthenticateSchema } from './mfa/utils/schemas'; -import { setFlash } from 'sveltekit-flash-message/server'; interface AuthenticationFlow { id: @@ -117,8 +115,25 @@ export const actions: Actions = { secure: true }); + const preferencesRes = await fetch(`${BASE_API_URL}/user-preferences/`); + const preferences = await preferencesRes.json(); + + const currentLang = cookies.get('ciso_lang') || 'en'; + const preferedLang = preferences.lang; + + if (preferedLang && currentLang !== preferedLang) { + cookies.set('ciso_lang', preferedLang, { + httpOnly: false, + sameSite: 'lax', + path: '/', + secure: true + }); + } + const next = url.searchParams.get('next') || '/'; - redirect(302, getSecureRedirect(next)); + const redirectURL = getSecureRedirect(next) + '?refresh=1'; + [0, 0, 0, 0, 0].forEach(() => console.log(redirectURL)); + redirect(302, redirectURL); }, mfaAuthenticate: async (event) => { const formData = await event.request.formData(); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index fc51d8241..51b1a7cc2 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -2,7 +2,7 @@ // Most of your app wide CSS should be put in this file import '../app.postcss'; import '@fortawesome/fontawesome-free/css/all.min.css'; - import ParaglideSvelte from './ParaglideJsProvider.svelte'; + import { browser } from '$app/environment'; import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom'; @@ -77,6 +77,11 @@ createModal: { ref: CreateModal }, deleteConfirmModal: { ref: DeleteConfirmModal } }; + + $: if (browser && $page.url.searchParams.has('refresh')) { + $page.url.searchParams.delete('refresh'); + window.location.href = $page.url.href; + } diff --git a/frontend/src/routes/api/user-preferences/+server.ts b/frontend/src/routes/api/user-preferences/+server.ts new file mode 100644 index 000000000..fbbf7ade2 --- /dev/null +++ b/frontend/src/routes/api/user-preferences/+server.ts @@ -0,0 +1,36 @@ +import { BASE_API_URL } from '$lib/utils/constants'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ fetch, request }) => { + const endpoint = `${BASE_API_URL}/user-preferences/`; + const req = await fetch(endpoint); + const status = await req.status; + const responseData = await req.json(); + + return new Response(JSON.stringify(responseData), { + status: status, + headers: { + 'Content-Type': 'application/json' + } + }); +}; + +export const PATCH: RequestHandler = async ({ fetch, request }) => { + const newPreferences = await request.json(); + const requestInitOptions: RequestInit = { + method: 'PATCH', + body: JSON.stringify(newPreferences) + }; + + const endpoint = `${BASE_API_URL}/user-preferences/`; + const req = await fetch(endpoint, requestInitOptions); + const status = await req.status; + const responseData = await req.text(); + + return new Response(responseData, { + status: status, + headers: { + 'Content-Type': 'application/json' + } + }); +};