diff --git a/.env.example b/.env.example
index 0892687..41a5811 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,17 @@
POSTGRES_USER=
POSTGRES_PASSWORD=
-POSTGRES_DB=
\ No newline at end of file
+POSTGRES_DB=
+
+UNICORE_USER=
+UNICORE_PASSWORD=
+UNICORE_ORG_ID=
+UNICORE_URL=https://unicorecustomapi.mecenat.com/utn
+
+# Email settings for local testing with MailHog
+EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
+EMAIL_HOST=mailhog
+EMAIL_PORT=1025
+EMAIL_HOST_USER=
+EMAIL_HOST_PASSWORD=
+EMAIL_USE_TLS=False
+EMAIL_USE_SSL=False
diff --git a/.gitignore b/.gitignore
index 4cd9f8b..eac977e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ venv
.DS_Store
node_modules
team_logos
+.venv/
+backend/.venv/
diff --git a/backend/backend/migrations/0008_alter_member_is_superuser.py b/backend/backend/migrations/0008_alter_member_is_superuser.py
new file mode 100644
index 0000000..7006da7
--- /dev/null
+++ b/backend/backend/migrations/0008_alter_member_is_superuser.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.7 on 2026-05-14 12:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('backend', '0007_merge_0006_alter_appointment_member_0006_role_role_description_url'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='member',
+ name='is_superuser',
+ field=models.BooleanField(default=False, help_text='Designates whether the user is a superuser'),
+ ),
+ ]
diff --git a/backend/backend/migrations/0009_add_email_verification_code.py b/backend/backend/migrations/0009_add_email_verification_code.py
new file mode 100644
index 0000000..b8b27b4
--- /dev/null
+++ b/backend/backend/migrations/0009_add_email_verification_code.py
@@ -0,0 +1,50 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("backend", "0008_alter_member_is_superuser"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="member",
+ name="email_verification_code",
+ field=models.CharField(
+ blank=True,
+ default=None,
+ help_text="One-time email verification code",
+ max_length=128,
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="member",
+ name="email_verification_code_expires_at",
+ field=models.DateTimeField(
+ blank=True,
+ default=None,
+ help_text="When the email verification code expires",
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="member",
+ name="email_verification_attempts",
+ field=models.PositiveSmallIntegerField(
+ default=0,
+ help_text="Failed email verification attempts",
+ ),
+ ),
+ migrations.AddField(
+ model_name="member",
+ name="email_verification_sent_at",
+ field=models.DateTimeField(
+ blank=True,
+ default=None,
+ help_text="When the last email verification code was sent",
+ null=True,
+ ),
+ ),
+ ]
diff --git a/backend/backend/models.py b/backend/backend/models.py
index a134981..2f0095e 100644
--- a/backend/backend/models.py
+++ b/backend/backend/models.py
@@ -73,6 +73,29 @@ class Member(AbstractBaseUser, PermissionsMixin):
)
verified_email = models.BooleanField(default=False)
+ email_verification_code = models.CharField(
+ max_length=128,
+ null=True,
+ blank=True,
+ default=None,
+ help_text=_("One-time email verification code"),
+ )
+ email_verification_code_expires_at = models.DateTimeField(
+ null=True,
+ blank=True,
+ default=None,
+ help_text=_("When the email verification code expires"),
+ )
+ email_verification_attempts = models.PositiveSmallIntegerField(
+ default=0,
+ help_text=_("Failed email verification attempts"),
+ )
+ email_verification_sent_at = models.DateTimeField(
+ null=True,
+ blank=True,
+ default=None,
+ help_text=_("When the last email verification code was sent"),
+ )
phone_number = models.CharField(
max_length=20,
@@ -82,7 +105,9 @@ class Member(AbstractBaseUser, PermissionsMixin):
)
is_superuser = models.BooleanField(
- help_text=("Designates whether the user is a superuser"))
+ default=False,
+ help_text=("Designates whether the user is a superuser"),
+ )
is_staff = models.BooleanField(
_("Staff status"),
diff --git a/backend/backend/send_email.py b/backend/backend/send_email.py
index e134a44..688c9e2 100644
--- a/backend/backend/send_email.py
+++ b/backend/backend/send_email.py
@@ -1,23 +1,58 @@
+import secrets
+import string
+from datetime import timedelta
+
+from django.contrib.auth.hashers import make_password
from django.contrib.auth.tokens import default_token_generator
-from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
-from django.template.loader import render_to_string
-from django.utils.html import strip_tags
+from django.utils import timezone
+
+VERIFICATION_CODE_LENGTH = 6
+VERIFICATION_CODE_TTL_MINUTES = 15
+VERIFICATION_RESEND_COOLDOWN_SECONDS = 60
+
+
+def generate_verification_code(length=VERIFICATION_CODE_LENGTH):
+ characters = string.ascii_uppercase + string.digits
+ return "".join(secrets.choice(characters) for _ in range(length))
# This needs rate limiting
-def send_verification_email(user):
+def send_verification_email(user, *, allow_rate_limit=True):
"""
- TEMPORARY FUNCTION
- This function sends an email verification link to the user.
- The link contains a token that the user can use to verify their email address.
-
+ Sends an email verification code to the user.
"""
- token = default_token_generator.make_token(user)
+ now = timezone.now()
+ if (
+ allow_rate_limit
+ and user.email_verification_sent_at
+ and (now - user.email_verification_sent_at).total_seconds()
+ < VERIFICATION_RESEND_COOLDOWN_SECONDS
+ ):
+ return False
+
+ code = generate_verification_code()
+ user.email_verification_code = make_password(code)
+ user.email_verification_code_expires_at = now + timedelta(
+ minutes=VERIFICATION_CODE_TTL_MINUTES
+ )
+ user.email_verification_attempts = 0
+ user.email_verification_sent_at = now
+ user.save(
+ update_fields=[
+ "email_verification_code",
+ "email_verification_code_expires_at",
+ "email_verification_attempts",
+ "email_verification_sent_at",
+ ]
+ )
subject = "Verify your email address"
- message = f"Sign in with the secure link: http://localhost:3000/auth/verify-email?id={user.id}&token={token}"
+ message = (
+ f"Verify your email address for Apply using the verification code {code}\n\n"
+ f"Or you can verify by visiting http://localhost:3000/verify-email?email={user.email}&code={code}"
+ )
send_mail(
subject,
@@ -26,19 +61,22 @@ def send_verification_email(user):
[user.email],
)
+ return True
+
# This needs rate limiting
def send_password_reset_email(user):
"""
- TEMPORARY FUNCTION
This function sends a password reset email to the user.
The email contains a link to reset the user's password.
-
"""
+ if not user.verified_email:
+ return
+
token = default_token_generator.make_token(user)
subject = "Reset your password"
- message = f"Click to reset your password: http://localhost:3000/reset-password?id={user.id}&token={token}"
+ message = f"Follow this link to reset your password:\nhttp://localhost:3000/reset-password?id={user.id}&token={token}"
send_mail(
subject,
message,
diff --git a/backend/backend/serializers.py b/backend/backend/serializers.py
index a00e78c..0314efa 100644
--- a/backend/backend/serializers.py
+++ b/backend/backend/serializers.py
@@ -12,6 +12,7 @@
Application,
Reference,
)
+from .send_email import send_verification_email
def get_language_from_request(request):
@@ -63,6 +64,24 @@ class MemberSerializer(ModelSerializer):
"""
study_program = StudyProgramSerializer(read_only=True)
+ study_program_id = serializers.PrimaryKeyRelatedField(
+ queryset=StudyProgram.objects.all(),
+ source="study_program",
+ write_only=True,
+ required=False,
+ allow_null=True,
+ )
+ section_id = serializers.PrimaryKeyRelatedField(
+ queryset=Section.objects.all(),
+ write_only=True,
+ required=False,
+ allow_null=True,
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.instance is not None:
+ self.fields["email"].read_only = True
class Meta:
model = Member
@@ -70,6 +89,8 @@ class Meta:
"name",
"phone_number",
"study_program",
+ "study_program_id",
+ "section_id",
"registration_year",
"status",
"ssn",
@@ -94,7 +115,19 @@ def validate_password(self, value):
raise serializers.ValidationError(err.messages)
return value
+ def validate(self, attrs):
+ section = attrs.get("section_id")
+ study_program = attrs.get("study_program")
+
+ if section and study_program and study_program.section_id != section.id:
+ raise serializers.ValidationError(
+ {"section_id": ["Section does not match selected program."]}
+ )
+
+ return attrs
+
def create(self, validated_data):
+ validated_data.pop("section_id", None)
password = validated_data.pop("password", None)
if password is None:
raise serializers.ValidationError({"password": ["Password must be set"]})
@@ -102,7 +135,7 @@ def create(self, validated_data):
user.set_password(password)
user.save()
- # TODO: Email verification here
+ send_verification_email(user)
return user
diff --git a/backend/backend/tests.py b/backend/backend/tests.py
index 7ce503c..99da6c0 100644
--- a/backend/backend/tests.py
+++ b/backend/backend/tests.py
@@ -1,3 +1,55 @@
+from django.contrib.auth import get_user_model
+from django.core import mail
+from django.urls import reverse
+from rest_framework.test import APIClient
from django.test import TestCase
-# Create your tests here.
+
+class PasswordResetTests(TestCase):
+ def setUp(self):
+ self.client = APIClient()
+ User = get_user_model()
+
+ self.verified_user = User.objects.create(
+ email="verified@example.com",
+ name="Verified User",
+ phone_number="+4600000000",
+ ssn="199001010001",
+ verified_email=True,
+ is_active=True,
+ )
+ self.verified_user.set_password("securepass123")
+ self.verified_user.save()
+
+ self.unverified_user = User.objects.create(
+ email="unverified@example.com",
+ name="Unverified User",
+ phone_number="+4600000001",
+ ssn="199001010002",
+ verified_email=False,
+ is_active=True,
+ )
+ self.unverified_user.set_password("securepass123")
+ self.unverified_user.save()
+
+ def test_password_reset_email_sent_for_verified_user(self):
+ response = self.client.post(
+ reverse("reset-password"),
+ {"email": self.verified_user.email},
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertIn("reset-password", mail.outbox[0].body)
+ self.assertEqual(mail.outbox[0].to, [self.verified_user.email])
+
+ def test_password_reset_email_not_sent_for_unverified_user(self):
+ response = self.client.post(
+ reverse("reset-password"),
+ {"email": self.unverified_user.email},
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(mail.outbox), 0)
diff --git a/backend/backend/utils/validators.py b/backend/backend/utils/validators.py
index 9dfd6d1..a0e9a95 100644
--- a/backend/backend/utils/validators.py
+++ b/backend/backend/utils/validators.py
@@ -36,7 +36,7 @@ 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."),
+ _("The password must contain at least one number."),
code="password_no_number",
)
@@ -50,7 +50,7 @@ class SpecialCharacterValidator:
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."),
+ _("The password must contain at least one special character."),
code="password_no_special_character",
)
diff --git a/backend/backend/views.py b/backend/backend/views.py
index 29ceb41..ce49dfe 100644
--- a/backend/backend/views.py
+++ b/backend/backend/views.py
@@ -4,6 +4,7 @@
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.middleware.csrf import get_token
+from django.utils import timezone
from .managers import MemberManager
from rest_framework import status
from rest_framework.decorators import action
@@ -180,7 +181,8 @@ def post(self, request):
status=200,
)
- send_password_reset_email(user)
+ if user.verified_email:
+ send_password_reset_email(user)
return Response(
{
@@ -296,7 +298,7 @@ def post(self, request):
class EmailVerificationAPIView(APIView):
"""
EmailVerificationAPIView handles email verification through the API.
- This view processes GET requests to verify user email addresses using a token and ID.
+ This view processes GET requests to verify user email addresses using a code and ID.
DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING
@@ -316,20 +318,54 @@ class EmailVerificationAPIView(APIView):
permission_classes = [AllowAny]
def get(self, request):
- user_id = request.GET.get("id")
- token = request.GET.get("token")
+ code = (request.GET.get("code") or "").strip().upper()
+ email = (request.GET.get("email") or "").strip()
try:
- user = get_user_model().objects.get(pk=user_id)
+ user = get_user_model().objects.get(email=email)
except get_user_model().DoesNotExist:
user = None
- if default_token_generator.check_token(user, token):
- login(request, user)
+ if user is None:
+ return Response({"message": "No account found for this email."}, status=400)
+
+ if not code:
+ return Response({"message": "Verification code is required."}, status=400)
+
+ if not user.email_verification_code or not user.email_verification_code_expires_at:
+ return Response({"message": "No active verification code. Please request a new one."}, status=400)
+
+ if user.email_verification_code_expires_at < timezone.now():
+ return Response({"message": "Verification code has expired. Please request a new one."}, status=400)
- return Response({"message": "Email verified successfully"}, status=200)
+ if user.email_verification_attempts >= 5:
+ return Response({"message": "Too many failed attempts. Please request a new code."}, status=400)
- return Response({"message": "Invalid verification link"}, status=400)
+ if not check_password(code, user.email_verification_code):
+ user.email_verification_attempts += 1
+ user.save(update_fields=["email_verification_attempts"])
+ remaining_attempts = max(0, 5 - user.email_verification_attempts)
+ return Response(
+ {"message": f"Invalid verification code. Attempts remaining: {remaining_attempts}."},
+ status=400,
+ )
+
+ user.verified_email = True
+ user.email_verification_code = None
+ user.email_verification_code_expires_at = None
+ user.email_verification_attempts = 0
+ user.email_verification_sent_at = None
+ user.save(
+ update_fields=[
+ "verified_email",
+ "email_verification_code",
+ "email_verification_code_expires_at",
+ "email_verification_attempts",
+ "email_verification_sent_at",
+ ]
+ )
+ login(request, user)
+ return Response({"message": "Email verified successfully"}, status=200)
class ResendVerificationEmailAPIView(APIView):
@@ -369,7 +405,12 @@ def post(self, request):
if user.verified_email:
return Response({"message": "Email already verified"}, status=400)
- send_verification_email(user)
+ sent = send_verification_email(user, allow_rate_limit=True)
+ if not sent:
+ return Response(
+ {"message": "Verification email sent recently. Try again shortly."},
+ status=429,
+ )
return Response({"message": "Verification email resent"}, status=200)
diff --git a/backend/config/settings.py b/backend/config/settings.py
index 25aef8a..73c3153 100644
--- a/backend/config/settings.py
+++ b/backend/config/settings.py
@@ -29,9 +29,17 @@
if DEBUG:
SECRET_KEY = "django-insecure-ljadjxsm&naahk*kduro)1es8l7#d65msgqdq65o*pd)7hu+m&"
-if DEBUG:
- EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
- ALLOWED_HOSTS = ["localhost", "backend"]
+EMAIL_BACKEND = os.getenv(
+ "EMAIL_BACKEND",
+ "django.core.mail.backends.console.EmailBackend",
+)
+EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")
+EMAIL_PORT = int(os.getenv("EMAIL_PORT", 25))
+EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
+EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
+EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "False").lower() in ("true", "1", "yes")
+EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "False").lower() in ("true", "1", "yes")
+ALLOWED_HOSTS = ["localhost", "backend"]
AUTH_USER_MODEL = "backend.Member"
diff --git a/docker-compose.yml b/docker-compose.yml
index 0260db0..a46507a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,14 @@ services:
volumes:
- pg_data:/var/lib/postgresql/data
+ mailhog:
+ image: mailhog/mailhog:latest
+ container_name: mailhog
+ restart: always
+ ports:
+ - "1025:1025"
+ - "8025:8025"
+
backend:
build:
context: ./backend
@@ -17,6 +25,7 @@ services:
container_name: django_backend
depends_on:
- db
+ - mailhog
ports:
- "8000:8000"
env_file:
diff --git a/frontend/apply/src/app/about/page.tsx b/frontend/apply/src/app/about/page.tsx
index 7109c13..6743f84 100644
--- a/frontend/apply/src/app/about/page.tsx
+++ b/frontend/apply/src/app/about/page.tsx
@@ -1,6 +1,8 @@
"use client";
import { useTranslation } from "react-i18next";
+import Image from "next/image";
+import { getImageUrl } from "@/utils/imageUrl";
export default function About() {
const { t } = useTranslation();
@@ -8,7 +10,11 @@ export default function About() {
{t("aboutPage.title")}
{t("aboutPage.introText")}
-

+
{t("aboutPage.timeRequiredText")}
diff --git a/frontend/apply/src/app/account/page.tsx b/frontend/apply/src/app/account/page.tsx
index e152897..7c4192e 100644
--- a/frontend/apply/src/app/account/page.tsx
+++ b/frontend/apply/src/app/account/page.tsx
@@ -224,7 +224,10 @@ export default function Account() {
window.location.href = "/login";
}, 1500);
} else {
- setPasswordError(t("accountPage.passwordChangeError"));
+ const errorData = await response.json().catch(() => ({}));
+ setPasswordError(
+ errorData?.message || t("accountPage.passwordChangeError"),
+ );
}
} catch {
setPasswordError(t("accountPage.passwordChangeError"));
@@ -577,6 +580,7 @@ export default function Account() {
name="email"
icon={
}
error={errors.email}
+ disabled
/>
diff --git a/frontend/apply/src/app/forgot-password/forgot-password.module.css b/frontend/apply/src/app/forgot-password/forgot-password.module.css
index df2de61..92a0d68 100644
--- a/frontend/apply/src/app/forgot-password/forgot-password.module.css
+++ b/frontend/apply/src/app/forgot-password/forgot-password.module.css
@@ -12,7 +12,7 @@
border: 1px solid rgb(239, 239, 239);
padding: 40px;
width: 100%;
- max-width: 420px;
+ max-width: 450px;
display: flex;
flex-direction: column;
gap: 16px;
diff --git a/frontend/apply/src/app/i18n/locales/en.json b/frontend/apply/src/app/i18n/locales/en.json
index c648c25..e73cd53 100644
--- a/frontend/apply/src/app/i18n/locales/en.json
+++ b/frontend/apply/src/app/i18n/locales/en.json
@@ -81,6 +81,18 @@
"emailPlaceholder": "your.email@example.com",
"passwordPlaceholder": "Enter your password"
},
+ "verifyEmailPage": {
+ "title": "Verify your email",
+ "instructions": "Enter the six-character code you received in your email to verify your email address.",
+ "verificationCode": "Verification code",
+ "codeInvalid": "Enter a valid 6-character code.",
+ "invalidOrExpired": "The verification code is invalid or expired.",
+ "error": "Unable to verify your email. Please try again.",
+ "verifyButton": "Verify",
+ "resendButton": "Resend verification email",
+ "resendSuccess": "Verification email sent successfully.",
+ "resendError": "Failed to resend verification email. Please try again."
+ },
"logoutPage": {
"doYouWantToLogOut": "Do you want to log out?",
"yesLogOut": "Yes, log out",
@@ -88,6 +100,46 @@
"logoutError": "An error occurred during logout",
"networkError": "Network error. Please try again."
},
+ "registerPage": {
+ "registerTitle": "Create Account",
+ "username": "Username",
+ "usernamePlaceholder": "Enter your username",
+ "usernameRequired": "Username is required",
+ "usernameTooShort": "Username must be at least 3 characters",
+ "email": "Email",
+ "emailPlaceholder": "Enter your email",
+ "emailRequired": "Email is required",
+ "emailInvalid": "Please enter a valid email address",
+ "password": "Password",
+ "passwordPlaceholder": "Enter your password",
+ "passwordRequired": "Password is required",
+ "passwordTooShort": "Password must be at least 10 characters",
+ "passwordNeedsUppercase": "Password must contain at least one uppercase letter",
+ "passwordNeedsNumber": "Password must contain at least one number",
+ "passwordConfirmation": "Confirm password",
+ "passwordConfirmationPlaceholder": "Confirm your password",
+ "passwordConfirmationRequired": "Please confirm your password",
+ "personalIdentityNumber": "Personal identity number",
+ "PersonNumberPlaceholder": "YYYYMMDD-XXXX",
+ "PersonNumberRequired": "Personal identity number is required",
+ "PersonNumberInvalid": "Enter a valid personal identity number (YYYYMMDD-XXXX)",
+ "section": "Section",
+ "sectionPlaceholder": "Select your section",
+ "selectSection": "Select a section",
+ "selectProgram": "Select a program",
+ "selectSectionFirst": "Select a section to see its programs",
+ "sectionRequired": "Section is required",
+ "program": "Program",
+ "programRequired": "Please select a program",
+ "phoneNumber": "Phone number",
+ "phoneNumberPlaceholder": "Enter your phone number",
+ "phoneNumberRequired": "Phone number is required",
+ "phoneNumberInvalid": "Enter a valid phone number",
+ "registerAccount": "Create account",
+ "registerError": "Unable to create account. Please check your details and try again.",
+ "networkError": "Network error. Please try again.",
+ "passwordsDoNotMatch": "Passwords do not match"
+ },
"applyPage": {
"failedToLoadPosition": "Failed to load position",
"failedToLoadApplication": "Failed to load application",
@@ -195,4 +247,4 @@
"requestNewLink": "Request a new link",
"error": "Something went wrong. Please try again."
}
-}
+}
\ No newline at end of file
diff --git a/frontend/apply/src/app/i18n/locales/sv.json b/frontend/apply/src/app/i18n/locales/sv.json
index acce13d..6eac0e6 100644
--- a/frontend/apply/src/app/i18n/locales/sv.json
+++ b/frontend/apply/src/app/i18n/locales/sv.json
@@ -64,7 +64,7 @@
"moreInfoWebsite": "hemsidan",
"moreInfoUrl": "https://utn.se/engagera-dig/att-engagera-sig",
"moreInfoContinuationText": ", där vi också har svar på vanliga frågor om att engagera sig.",
- "contactText": "Om du undrar något kan du också maila till"
+ "contactText": "Om du undrar något kan du också mejla till"
},
"loginPage": {
"loginTitle": "Logga in",
@@ -81,6 +81,18 @@
"emailPlaceholder": "din.epost@exempel.se",
"passwordPlaceholder": "Ange ditt lösenord"
},
+ "verifyEmailPage": {
+ "title": "Verifiera din e-post",
+ "instructions": "Ange den sex tecken långa koden du fick till din e-post för att verifiera din e-postaddress.",
+ "verificationCode": "Verifieringskod",
+ "codeInvalid": "Ange en giltig kod med 6 tecken.",
+ "invalidOrExpired": "Verifieringskoden är ogiltig eller har gått ut.",
+ "error": "Kunde inte verifiera din e-post. Försök igen.",
+ "verifyButton": "Verifiera",
+ "resendButton": "Skicka verifieringsmejl igen",
+ "resendSuccess": "Verifieringsmejl skickades.",
+ "resendError": "Misslyckades med att skicka verifieringsmejl igen. Försök igen."
+ },
"logoutPage": {
"doYouWantToLogOut": "Vill du logga ut?",
"yesLogOut": "Ja, logga ut",
@@ -174,6 +186,46 @@
"applicationSubmittedSuccess": "Din ansökan har skickats in.",
"submitApplicationConfirmation": "Är du säker på att du vill skicka in denna ansökan? När den väl är inskickad kommer du inte kunna redigera den längre."
},
+ "registerPage": {
+ "registerTitle": "Registrera",
+ "username": "Användarnamn",
+ "usernamePlaceholder": "Välj ett användarnamn",
+ "usernameRequired": "Användarnamn krävs",
+ "usernameTooShort": "Användarnamnet måste vara minst 3 tecken",
+ "email": "E-post",
+ "emailPlaceholder": "din.epost@exempel.se",
+ "emailRequired": "E-post krävs",
+ "emailInvalid": "Ange en giltig e-postadress",
+ "password": "Lösenord",
+ "passwordPlaceholder": "Ange ett lösenord",
+ "passwordRequired": "Lösenord krävs",
+ "passwordTooShort": "Lösenordet måste vara minst 10 tecken",
+ "passwordNeedsUppercase": "Lösenordet måste innehålla minst en stor bokstav",
+ "passwordNeedsNumber": "Lösenordet måste innehålla minst en siffra",
+ "passwordConfirmation": "Bekräfta lösenord",
+ "passwordConfirmationPlaceholder": "Bekräfta ditt lösenord",
+ "passwordConfirmationRequired": "Bekräftelse av lösenord krävs",
+ "personalIdentityNumber": "Personnummer",
+ "PersonNumberPlaceholder": "ÅÅÅÅMMDD-XXXX",
+ "PersonNumberRequired": "Personnummer krävs",
+ "PersonNumberInvalid": "Ange ett giltigt personnummer (ÅÅÅÅMMDD-XXXX)",
+ "section": "Sektion",
+ "sectionPlaceholder": "Välj din sektion",
+ "selectSection": "Välj en sektion",
+ "selectProgram": "Välj ett program",
+ "selectSectionFirst": "Välj en sektion för att se dess program",
+ "sectionRequired": "Sektion krävs",
+ "program": "Program",
+ "programRequired": "Välj ett program",
+ "phoneNumber": "Telefonnummer",
+ "phoneNumberPlaceholder": "+46 12 345 67 89",
+ "phoneNumberRequired": "Telefonnummer krävs",
+ "phoneNumberInvalid": "Ange ett giltigt telefonnummer",
+ "registerAccount": "Skapa konto",
+ "registerError": "Det gick inte att skapa kontot. Kontrollera dina uppgifter och försök igen.",
+ "networkError": "Nätverksfel. Försök igen.",
+ "passwordsDoNotMatch": "Lösenorden stämmer inte överens"
+ },
"forgotPasswordPage": {
"title": "Glömt lösenord",
"description": "Skriv din mejl så skickar vi en återställningslänk.",
diff --git a/frontend/apply/src/app/login/page.tsx b/frontend/apply/src/app/login/page.tsx
index 1adcdf3..5fd910e 100644
--- a/frontend/apply/src/app/login/page.tsx
+++ b/frontend/apply/src/app/login/page.tsx
@@ -1,26 +1,33 @@
"use client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import TextInput from "@/components/TextInput";
import Button from "@/components/Button";
import styles from "./login.module.css";
-import { logIn } from "@/utils/auth";
+import { logIn, useIsLoggedIn } from "@/utils/auth";
import { useTranslation } from "react-i18next";
import "@/i18n/config";
export default function Login() {
const { t } = useTranslation();
const router = useRouter();
+ const { isLoggedIn, loading } = useIsLoggedIn();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
- const [loading, setLoading] = useState(false);
+ const [loadingForm, setLoadingForm] = useState(false);
+
+ useEffect(() => {
+ if (!loading && isLoggedIn) {
+ router.push("/");
+ }
+ }, [isLoggedIn, loading, router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
- setLoading(true);
+ setLoadingForm(true);
try {
const response = await logIn(email, password);
@@ -39,7 +46,7 @@ export default function Login() {
} catch {
setError(t("loginPage.networkError"));
} finally {
- setLoading(false);
+ setLoadingForm(false);
}
};
@@ -83,8 +90,8 @@ export default function Login() {