Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make language choice persistent for users #1075

Merged
merged 46 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6daa48d
Make language choice peristent for users
monsieurswag Nov 22, 2024
29aaf52
Attempt to fix migration issues
monsieurswag Nov 22, 2024
26e7669
Formatter
monsieurswag Nov 22, 2024
4068c0b
Copy conflicts solution from main branch
monsieurswag Nov 22, 2024
061a4e8
Merge branch 'main' into CA-629-Persist-language-in-database
monsieurswag Nov 22, 2024
3b24a84
Attempt to fix startup tests
monsieurswag Nov 22, 2024
acdbba6
Formatter
monsieurswag Nov 22, 2024
18f898b
Attempt to fix startup tests
monsieurswag Nov 23, 2024
168b56c
Log language error in preferenc modification
monsieurswag Nov 25, 2024
9f48981
Remove useless json deserialization
monsieurswag Nov 25, 2024
bcf7c68
Fix wrong language code
monsieurswag Nov 25, 2024
8cd2d31
Limit functional tests for faster debug
monsieurswag Nov 29, 2024
fadda69
Sleep before populating database
monsieurswag Nov 29, 2024
f3dd0f9
Add requestfailed listener
monsieurswag Nov 29, 2024
ea8580b
Fix misplaced requestfailed listener
monsieurswag Nov 29, 2024
c097a0e
Remove redirect query paramete
monsieurswag Nov 29, 2024
7e6a240
Remove redirection query parameter for CI tests
monsieurswag Nov 29, 2024
f89ed6c
Add some debug logging
monsieurswag Nov 29, 2024
f6c62ed
Fix non-working environment variable import
monsieurswag Nov 29, 2024
900f1bb
Remove debug logging
monsieurswag Nov 29, 2024
7e52bd1
Precise env_constants utility
monsieurswag Nov 29, 2024
abaf58c
Restablish functional tests
monsieurswag Nov 29, 2024
22613b9
Also fix enterprise functional tests
monsieurswag Nov 29, 2024
04ddb26
Fix startup tests
monsieurswag Nov 29, 2024
3fb43d9
Fix startup tests again
monsieurswag Nov 29, 2024
9441864
Add logging to debug startup tests
monsieurswag Nov 29, 2024
c877852
Lazy fix for startup tests
monsieurswag Nov 29, 2024
adeca33
Improve startup test verbosity
monsieurswag Dec 3, 2024
a8b25bd
Fix broken getSecureRedirect
monsieurswag Dec 3, 2024
eaee0b5
Formatter
monsieurswag Dec 3, 2024
818624a
First fix attempt debug test
monsieurswag Dec 3, 2024
30cf467
Fix attempt 2: Setup the .env file before make (debug commit only run…
monsieurswag Dec 3, 2024
dc904cd
Fix tests
monsieurswag Dec 3, 2024
0920bb5
Slightly improve startup test verbosity
monsieurswag Dec 3, 2024
4a54027
Attempt to fix codefactor complexity test
monsieurswag Dec 3, 2024
e9b9c88
Attempt 2 to fix codefactor complexity test
monsieurswag Dec 3, 2024
f8e421a
Fix startup tests
monsieurswag Dec 3, 2024
081dbee
Add function test debug line
monsieurswag Dec 3, 2024
bac1b37
Debug: Only execute a single enterprise test
monsieurswag Dec 3, 2024
69f580d
Write debug output inside stderr in an attempt to keep them in the logs
monsieurswag Dec 3, 2024
61edd8b
Recheck that window.location.href page refresh is indeed the cause of…
monsieurswag Dec 3, 2024
e79c87c
Attempt to fix functional tests
monsieurswag Dec 3, 2024
144a800
Formatter
monsieurswag Dec 3, 2024
ea8e76a
Merge branch 'main' into CA-629-Persist-language-in-database
monsieurswag Dec 4, 2024
20134ac
Remove useless environment variable
monsieurswag Dec 4, 2024
d0e017d
Fix conflicts
monsieurswag Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/functional-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ jobs:
run: |
touch .env
echo PUBLIC_BACKEND_API_URL=http://localhost:8000/api >> .env

- name: Create backend environment variables file
working-directory: ${{ env.backend-directory }}
run: |
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/startup-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ jobs:
working-directory: ${{ env.frontend-directory }}
run: |
response=$(curl -d "[email protected]&password=1234" -H "Origin: https://localhost:8443" https://localhost:8443/login\?/login -k)
server_reponse='{"type":"redirect","status":302,"location":""}'
server_reponse='{"type":"redirect","status":302,"location":"/"}'
echo "[SERVER_RESPONSE] $response"
echo "[EXPECTED_RESPONSE] $server_reponse"
if [[ "$response" == "$server_reponse" ]]; then
echo "Success"
exit 0
Expand Down Expand Up @@ -265,7 +267,9 @@ jobs:
working-directory: ${{ env.frontend-directory }}
run: |
response=$(curl -d "[email protected]&password=1234" -H "Origin: https://localhost:8443" https://localhost:8443/login\?/login -k)
server_reponse='{"type":"redirect","status":302,"location":""}'
server_reponse='{"type":"redirect","status":302,"location":"/"}'
echo "[SERVER_RESPONSE] $response"
echo "[EXPECTED_RESPONSE] $server_reponse"
if [[ "$response" == "$server_reponse" ]]; then
echo "Success"
exit 0
Expand Down
2 changes: 1 addition & 1 deletion backend/ciso_assistant/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def set_ciso_assistant_url(_, __, event_dict):
("es", "Spanish"),
("de", "German"),
("it", "Italian"),
("nd", "Dutch"),
("nl", "Dutch"),
("pl", "Polish"),
("pt", "Portuguese"),
("ar", "Arabic"),
Expand Down
1 change: 1 addition & 0 deletions backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("ebios-rm/", include("ebios_rm.urls")),
path("csrf/", get_csrf_token, name="get_csrf_token"),
path("build/", get_build, name="get_build"),
Expand Down
44 changes: 35 additions & 9 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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
from rest_framework.permissions import AllowAny
Expand Down Expand Up @@ -299,7 +298,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)

@action(detail=False, methods=["get"])
def ids(self, request):
Expand Down Expand Up @@ -606,7 +605,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):
Expand Down Expand Up @@ -639,7 +638,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):
Expand Down Expand Up @@ -699,7 +698,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):
Expand Down Expand Up @@ -761,7 +762,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):
Expand Down Expand Up @@ -1324,7 +1327,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)

Expand Down Expand Up @@ -1436,7 +1439,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)
Expand All @@ -1448,7 +1451,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)
Expand Down Expand Up @@ -1677,6 +1680,29 @@ 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
):
logger.error(
f"Error in UserPreferencesView: new_language={new_language} available languages={[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"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

def create_emailaddress_objects(apps, schema_editor):
try:
from allauth.account.models import EmailAddress
from iam.models import User
EmailAddress = apps.get_model("account", "EmailAddress")
User = apps.get_model("iam", "User")

for user in User.objects.all():
EmailAddress.objects.create(
Expand Down
17 changes: 17 additions & 0 deletions backend/iam/migrations/0010_user_preferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.1 on 2024-12-04 10:42

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),
),
]
1 change: 1 addition & 0 deletions backend/iam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,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(
Expand Down
1 change: 1 addition & 0 deletions backend/iam/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions enterprise/frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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';

Expand Down Expand Up @@ -97,6 +98,11 @@
? `data:${$faviconB64.mimeType};base64, ${$faviconB64.data}`
: favicon;
});

$: if (browser && $page.url.searchParams.has('refresh')) {
$page.url.searchParams.delete('refresh');
window.location.href = $page.url.href;
}
</script>

<svelte:head>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/lib/components/SideBar/SideBarFooter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/routes/(app)/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
redirect(301, '/analytics');
export const load: PageServerLoad = async ({ url }) => {
const queryParams = url.searchParams.has('refresh') ? '?refresh=1' : '';
redirect(301, `/analytics${queryParams}`);
};
71 changes: 45 additions & 26 deletions frontend/src/routes/(authentication)/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
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';
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:
Expand All @@ -27,6 +25,15 @@ interface AuthenticationFlow {
types: 'totp' | 'recovery_codes';
}

function makeRedirectURL(currentLang: string, preferedLang: string, url: URL): string {
const next = url.searchParams.get('next');
const secureNext = getSecureRedirect(next) || '/';
if (currentLang === preferedLang) {
return secureNext;
}
return secureNext ? `${secureNext}?refresh=1` : `/?refresh=1`;
}

export const load: PageServerLoad = async ({ fetch, request, locals }) => {
// redirect user if already logged in
if (locals.user) {
Expand Down Expand Up @@ -74,29 +81,27 @@ export const actions: Actions = {
});
return fail(res.status, { form });
}
if (res.status === 401) {
if (res.status === 401 && res.data) {
// User is not authenticated
if (res.data) {
const flows: AuthenticationFlow[] = res.data.flows;
if (flows.length > 0) {
const mfaFlow = flows.find((flow) => flow.id === 'mfa_authenticate');
const sessionToken = res.meta.session_token;
if (sessionToken) {
cookies.set('allauth_session_token', sessionToken, {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true
});
}

if (mfaFlow) {
return {
form,
mfa: true,
mfaFlow
};
}
const flows: AuthenticationFlow[] = res.data.flows;
if (flows.length > 0) {
const mfaFlow = flows.find((flow) => flow.id === 'mfa_authenticate');
const sessionToken = res.meta.session_token;
if (sessionToken) {
cookies.set('allauth_session_token', sessionToken, {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true
});
}

if (mfaFlow) {
return {
form,
mfa: true,
mfaFlow
};
}
}
}
Expand All @@ -117,8 +122,22 @@ export const actions: Actions = {
secure: true
});

const next = url.searchParams.get('next') || '/';
redirect(302, getSecureRedirect(next));
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 || 'en';

if (currentLang !== preferedLang) {
cookies.set('ciso_lang', preferedLang, {
httpOnly: false,
sameSite: 'lax',
path: '/',
secure: true
});
}

redirect(302, makeRedirectURL(currentLang, preferedLang, url));
},
mfaAuthenticate: async (event) => {
const formData = await event.request.formData();
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
</script>

<svelte:head><link rel="icon" href="/favicon.ico" /></svelte:head>
Expand Down
Loading
Loading