Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions backend/backend/managers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.contrib.auth.models import BaseUserManager
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Count, F, Manager, Q
from django.utils import timezone

Expand Down Expand Up @@ -28,8 +30,12 @@ def create_user(
):
if not email:
raise ValueError("The Email field must be set")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")
if not password:
raise ValueError("Password must be set")
try:
validate_password(password)
except ValidationError as err:
raise ValueError(" ".join(err.messages))

unicore = unicoremember()
data = unicore.get_user_data(ssn)
Expand Down
13 changes: 10 additions & 3 deletions backend/backend/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
Expand Down Expand Up @@ -85,12 +87,17 @@ class Meta:
"verified_email": {"read_only": True},
}

def validate_password(self, value):
try:
validate_password(value)
except ValidationError as err:
raise serializers.ValidationError(err.messages)
return value

def create(self, validated_data):
password = validated_data.pop("password", None)
if password is None:
raise ValueError("Password must be set")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")
raise serializers.ValidationError({"password": ["Password must be set"]})
user = Member(**validated_data)
user.set_password(password)
user.save()
Expand Down
26 changes: 26 additions & 0 deletions backend/backend/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,29 @@ def __init__(self):
"YYYYMMDDXXXX, YYMMDDXXXX for your ssn."
),
)


class NumberValidator:
def validate(self, password, user=None):
if not any(char.isdigit() for char in password):
raise validators.ValidationError(
_("This password must contain at least one number."),
code="password_no_number",
)

def get_help_text(self):
return _("Your password must contain at least one number.")


class SpecialCharacterValidator:
SPECIAL_CHARACTERS = r"!@#$%^&*()_+-=[]{};':\"\\|,.<>/?`~"

def validate(self, password, user=None):
if not any(char in self.SPECIAL_CHARACTERS for char in password):
raise validators.ValidationError(
_("This password must contain at least one special character."),
code="password_no_special_character",
)

def get_help_text(self):
return _("Your password must contain at least one special character.")
26 changes: 22 additions & 4 deletions backend/backend/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.contrib.auth import authenticate, get_user_model, login, logout
from django.contrib.auth.hashers import check_password
from django.contrib.auth.password_validation import validate_password
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.middleware.csrf import get_token
from .managers import MemberManager
from rest_framework import status
Expand Down Expand Up @@ -206,9 +208,14 @@ def post(self, request):
token = request.data.get("token")
new_password = request.data.get("new_password")

if len(new_password) < 8:
if not new_password:
return Response({"message": "New password is required"}, status=400)

try:
validate_password(new_password)
except ValidationError as err:
return Response(
{"message": "New password must be at least 8 characters long"},
{"message": " ".join(err.messages)},
status=400,
)

Expand Down Expand Up @@ -252,9 +259,20 @@ def post(self, request):
old_password = request.data.get("old_password")
new_password = request.data.get("new_password")

if len(new_password) < 8:
if not new_password:
return Response({"message": "New password is required"}, status=400)

try:
validate_password(new_password, user=user)
except ValidationError as err:
return Response(
{"message": " ".join(err.messages)},
status=400,
)

if old_password == new_password:
return Response(
{"message": "New password must be at least 8 characters long"},
{"message": "New password cannot be the same as current password"},
status=400,
)

Expand Down
7 changes: 7 additions & 0 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 10},
},
{
"NAME": "backend.utils.validators.NumberValidator",
},
{
"NAME": "backend.utils.validators.SpecialCharacterValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
Expand Down
18 changes: 16 additions & 2 deletions frontend/apply/src/app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,19 @@ export default function Account() {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
const [showSaveMessage, setShowSaveMessage] = useState(false);
const passwordSpecialCharRegex = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/;

const validateNewPassword = (password: string) => {
if (password.length < 8) {
return t("accountPage.accountDeleteError");
if (password.length < 10) {
return t("accountPage.passwordTooShort");
}

if (!/[0-9]/.test(password)) {
return t("accountPage.passwordMustContainNumber");
}

if (!passwordSpecialCharRegex.test(password)) {
return t("accountPage.passwordMustContainSpecialCharacter");
}

return "";
Expand Down Expand Up @@ -172,6 +181,11 @@ export default function Account() {
return;
}

if (newPassword === currentPassword) {
setPasswordError(t("accountPage.passwordCannotBeSame"));
return;
}

const passwordValidationError = validateNewPassword(newPassword);
if (passwordValidationError) {
setNewPasswordError(passwordValidationError);
Expand Down
5 changes: 4 additions & 1 deletion frontend/apply/src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@
"newPassword": "New Password",
"confirmNewPassword": "Confirm New Password",
"passwordsDoNotMatch": "New passwords do not match",
"passwordTooShort": "Password must be at least 8 characters",
"passwordCannotBeSame": "New password cannot be the same as current password",
"passwordTooShort": "Password must be at least 10 characters",
"passwordMustContainNumber": "Password must contain at least one number",
"passwordMustContainSpecialCharacter": "Password must contain at least one special character",
"passwordChanged": "Password changed successfully! You will now be logged out.",
"passwordChangeError": "Failed to change password. Please check your current password.",
"deleteAccount": "Delete Account",
Expand Down
5 changes: 4 additions & 1 deletion frontend/apply/src/app/i18n/locales/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@
"newPassword": "Nytt lösenord",
"confirmNewPassword": "Bekräfta nytt lösenord",
"passwordsDoNotMatch": "De nya lösenorden matchar inte",
"passwordTooShort": "Lösenordet måste vara minst 8 tecken",
"passwordCannotBeSame": "Nytt lösenord kan inte vara detsamma som det nuvarande lösenordet",
"passwordTooShort": "Lösenordet måste vara minst 10 tecken",
"passwordMustContainNumber": "Lösenordet måste innehålla minst en siffra",
"passwordMustContainSpecialCharacter": "Lösenordet måste innehålla minst ett specialtecken",
"passwordChanged": "Lösenordet har ändrats! Du kommer nu att loggas ut.",
"passwordChangeError": "Det gick inte att byta lösenord. Kontrollera ditt nuvarande lösenord.",
"deleteAccount": "Radera konto",
Expand Down
Loading