├── .gitignore ├── README.md ├── backend ├── .gitignore ├── Procfile ├── accounts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── exceptions.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_user_is_active.py │ │ └── __init__.py │ ├── models.py │ ├── renderers.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── backend │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py ├── requirements.txt └── runtime.txt └── frontend ├── .env.exemple ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.d.ts ├── app.html ├── global.d.ts ├── lib │ ├── components │ │ ├── Header │ │ │ ├── Header.svelte │ │ │ ├── john.svg │ │ │ └── svelte-logo.svg │ │ └── Loader │ │ │ └── Loader.svelte │ ├── dist │ │ └── css │ │ │ ├── style.min.css │ │ │ └── style.min.css.map │ ├── formats │ │ └── formatString.ts │ ├── helpers │ │ ├── buttonText.ts │ │ └── whitespacesHelper.ts │ ├── interfaces │ │ ├── error.interface.ts │ │ ├── user.interface.ts │ │ └── variables.interface.ts │ ├── store │ │ ├── loadingStore.ts │ │ ├── notificationStore.ts │ │ └── userStore.ts │ └── utils │ │ ├── constants.ts │ │ └── requestUtils.ts ├── routes │ ├── +layout.svelte │ ├── +page.svelte │ └── accounts │ │ ├── login │ │ └── +page.svelte │ │ ├── register │ │ └── +page.svelte │ │ └── user │ │ └── [username]-[id] │ │ ├── +page.svelte │ │ └── +page.ts └── sass │ ├── _about.scss │ ├── _form.scss │ ├── _globals.scss │ ├── _header.scss │ ├── _home.scss │ ├── _variables.scss │ └── style.scss ├── static ├── favicon.png ├── robots.txt ├── svelte-welcome.png └── svelte-welcome.webp ├── svelte.config.js ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .project 3 | .pydevproject 4 | .idea 5 | .vscode 6 | .settings 7 | .DS_Store 8 | 9 | # Random files 10 | *~ 11 | \#* 12 | tags 13 | 14 | venv/ 15 | virtualenv/ 16 | virtaulenv/ 17 | htmlcov/ 18 | src/static_root/* 19 | bvn_images/* 20 | !bvn_images/.gitkeep 21 | 22 | .env 23 | 24 | # Python compiled byte code 25 | __pycache__/ 26 | .pytest_cache/ 27 | *.pyc 28 | 29 | # SQLite3 database 30 | */*.sqlite 31 | 32 | # Docker volume data 33 | *.rdb 34 | *.pid 35 | celerybeat-schedule.* 36 | 37 | # Node modules 38 | node_modules/ 39 | npm-debug.log 40 | 41 | # Coverage 42 | coverage/ 43 | .coverage* 44 | .cache 45 | .coverage.ubuntu.* 46 | 47 | # static validation files 48 | .eslintrc 49 | .sass-lint.yml 50 | .prospector.yml 51 | .coveragerc 52 | .prettierrc 53 | .mypy_cache 54 | 55 | 56 | # Celery files 57 | celerybeat-schedule 58 | celerybeat.pid 59 | 60 | # Xero certificate 61 | src/bloombackend/certificates/*-prod.* 62 | 63 | dev_run.sh 64 | qc_scripts 65 | prod_2.py 66 | todo.txt 67 | 68 | kyc/* 69 | !src/lib/validators/kyc 70 | 71 | .vercel 72 | .vercel_build_output -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django_svelte_jwt_auth 2 | 3 | This is the codebase that follows the series of tutorials on building a [FullStack JWT Authentication and Authorization System with Django and SvelteKit][1]. 4 | 5 | This project was deployed on heroku (backend) and vercel (frontend) and its live version can be accessed [here][2]. 6 | 7 | To run this application locally, you need to run both the `backend` and `frontend` projects. While the latter has some instructions already for spinning it up, the former can be spinned up following the instructions below. 8 | 9 | ## Run locally 10 | 11 | To run locally 12 | 13 | - Clone this repo: 14 | ``` 15 | git clone https://github.com/Sirneij/django_svelte_jwt_auth.git 16 | ``` 17 | - Change directory into the `backend` folder: 18 | ``` 19 | cd backend 20 | ``` 21 | - Create a virtual environment: 22 | 23 | ``` 24 | pipenv shell 25 | ``` 26 | 27 | You might opt for other dependencies management tools such as `virtualenv`, `poetry`, or `venv`. It's up to you. 28 | 29 | - Install the dependencies: 30 | ``` 31 | pipenv install 32 | ``` 33 | - Make migrations and migrate the database: 34 | ``` 35 | python manage.py makemigrations 36 | python manage.py migrate 37 | ``` 38 | - Finally, run the application: 39 | ``` 40 | python manage.py runserver 41 | ``` 42 | 43 | [1]: https://dev.to/sirneij/fullstack-jwt-authentication-and-authorization-system-with-django-and-sveltekit-2ih3 "FullStack JWT Authentication and Authorization System with Django and SvelteKit " 44 | [2]: https://django-sveltekit-jwt-auth.vercel.app/ "FullStack JWT Authentication and Authorization System with Django and SvelteKit Live version." 45 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | db.sqlite3 -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn backend.wsgi --log-file - -------------------------------------------------------------------------------- /backend/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/backend/accounts/__init__.py -------------------------------------------------------------------------------- /backend/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.forms import UserChangeForm, UserCreationForm 4 | from rest_framework_simplejwt import token_blacklist 5 | 6 | from .models import User 7 | 8 | 9 | class CustomUserCreationForm(UserCreationForm): 10 | class Meta: 11 | model = User 12 | fields = ( 13 | 'email', 14 | 'username', 15 | ) 16 | 17 | 18 | class CustomUserChangeForm(UserChangeForm): 19 | class Meta: 20 | model = User 21 | fields = ( 22 | 'email', 23 | 'username', 24 | ) 25 | 26 | 27 | class CustomUserAdmin(UserAdmin): 28 | add_form = CustomUserCreationForm 29 | form = CustomUserChangeForm 30 | model = User 31 | list_display = ( 32 | 'email', 33 | 'username', 34 | 'is_staff', 35 | 'is_active', 36 | ) 37 | list_filter = ( 38 | 'email', 39 | 'username', 40 | 'is_staff', 41 | 'is_active', 42 | ) 43 | fieldsets = ( 44 | ( 45 | None, 46 | { 47 | 'fields': ( 48 | 'email', 49 | 'username', 50 | 'password', 51 | 'bio', 52 | 'full_name', 53 | 'birth_date', 54 | ) 55 | }, 56 | ), 57 | ( 58 | 'Permissions', 59 | { 60 | 'fields': ( 61 | 'is_staff', 62 | 'is_active', 63 | ) 64 | }, 65 | ), 66 | ) 67 | add_fieldsets = ( 68 | ( 69 | None, 70 | {'classes': ('wide',), 'fields': ('email', 'username', 'password1', 'password2', 'is_staff', 'is_active')}, 71 | ), 72 | ) 73 | search_fields = ( 74 | 'email', 75 | 'username', 76 | ) 77 | ordering = ( 78 | 'email', 79 | 'username', 80 | ) 81 | 82 | class OutstandingTokenAdmin(token_blacklist.admin.OutstandingTokenAdmin): 83 | 84 | def has_delete_permission(self, *args, **kwargs): 85 | return True # or whatever logic you want 86 | 87 | admin.site.unregister(token_blacklist.models.OutstandingToken) 88 | admin.site.register(token_blacklist.models.OutstandingToken, OutstandingTokenAdmin) 89 | 90 | 91 | admin.site.register(User, CustomUserAdmin) 92 | -------------------------------------------------------------------------------- /backend/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'accounts' 7 | -------------------------------------------------------------------------------- /backend/accounts/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from rest_framework.response import Response 4 | from rest_framework.views import exception_handler 5 | 6 | 7 | def core_exception_handler(exc: Exception, context: dict[str, Any]) -> Optional[Response]: 8 | """Error handler for the API.""" 9 | response = exception_handler(exc, context) 10 | handlers = {'ValidationError': _handle_generic_error} 11 | 12 | exception_class = exc.__class__.__name__ 13 | 14 | if exception_class in handlers: 15 | 16 | return handlers[exception_class](exc, context, response) 17 | 18 | return response 19 | 20 | 21 | def _handle_generic_error(exc: Exception, context: dict[str, Any], response: Optional[Response]) -> Optional[Response]: 22 | if response: 23 | response.data = {'errors': response.data} 24 | 25 | return response 26 | return None 27 | -------------------------------------------------------------------------------- /backend/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-07 11:31 2 | 3 | import uuid 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0012_alter_user_first_name_max_length'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 22 | ( 23 | 'is_superuser', 24 | models.BooleanField( 25 | default=False, 26 | help_text='Designates that this user has all permissions without explicitly assigning them.', 27 | verbose_name='superuser status', 28 | ), 29 | ), 30 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 31 | ('username', models.CharField(db_index=True, max_length=255, unique=True)), 32 | ('email', models.EmailField(db_index=True, max_length=254, unique=True)), 33 | ('is_active', models.BooleanField(default=False)), 34 | ('is_staff', models.BooleanField(default=False)), 35 | ('created_at', models.DateTimeField(auto_now_add=True)), 36 | ('updated_at', models.DateTimeField(auto_now=True)), 37 | ('bio', models.TextField(null=True)), 38 | ('full_name', models.CharField(max_length=20000, null=True)), 39 | ('birth_date', models.DateField(null=True)), 40 | ( 41 | 'groups', 42 | models.ManyToManyField( 43 | blank=True, 44 | help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', 45 | related_name='user_set', 46 | related_query_name='user', 47 | to='auth.Group', 48 | verbose_name='groups', 49 | ), 50 | ), 51 | ( 52 | 'user_permissions', 53 | models.ManyToManyField( 54 | blank=True, 55 | help_text='Specific permissions for this user.', 56 | related_name='user_set', 57 | related_query_name='user', 58 | to='auth.Permission', 59 | verbose_name='user permissions', 60 | ), 61 | ), 62 | ], 63 | options={ 64 | 'abstract': False, 65 | }, 66 | ), 67 | ] 68 | -------------------------------------------------------------------------------- /backend/accounts/migrations/0002_alter_user_is_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-08 10:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='is_active', 16 | field=models.BooleanField(default=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/backend/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /backend/accounts/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Any, Optional 3 | 4 | from django.contrib.auth.models import ( 5 | AbstractBaseUser, 6 | BaseUserManager, 7 | PermissionsMixin, 8 | ) 9 | from django.db import models 10 | from rest_framework_simplejwt.tokens import RefreshToken 11 | 12 | 13 | class UserManager(BaseUserManager): # type: ignore 14 | """UserManager class.""" 15 | 16 | # type: ignore 17 | def create_user(self, username: str, email: str, password: Optional[str] = None) -> 'User': 18 | """Create and return a `User` with an email, username and password.""" 19 | if username is None: 20 | raise TypeError('Users must have a username.') 21 | 22 | if email is None: 23 | raise TypeError('Users must have an email address.') 24 | 25 | user = self.model(username=username, email=self.normalize_email(email)) 26 | user.set_password(password) 27 | user.save() 28 | 29 | return user 30 | 31 | def create_superuser(self, username: str, email: str, password: str) -> 'User': # type: ignore 32 | """Create and return a `User` with superuser (admin) permissions.""" 33 | if password is None: 34 | raise TypeError('Superusers must have a password.') 35 | 36 | user = self.create_user(username, email, password) 37 | user.is_superuser = True 38 | user.is_staff = True 39 | user.is_active = True 40 | user.save() 41 | 42 | return user 43 | 44 | 45 | class User(AbstractBaseUser, PermissionsMixin): 46 | 47 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 48 | username = models.CharField(db_index=True, max_length=255, unique=True) 49 | email = models.EmailField(db_index=True, unique=True) 50 | is_active = models.BooleanField(default=True) 51 | is_staff = models.BooleanField(default=False) 52 | created_at = models.DateTimeField(auto_now_add=True) 53 | updated_at = models.DateTimeField(auto_now=True) 54 | bio = models.TextField(null=True) 55 | full_name = models.CharField(max_length=20000, null=True) 56 | birth_date = models.DateField(null=True) 57 | 58 | USERNAME_FIELD = 'email' 59 | REQUIRED_FIELDS = ['username'] 60 | 61 | # Tells Django that the UserManager class defined above should manage 62 | # objects of this type. 63 | objects = UserManager() 64 | 65 | def __str__(self) -> str: 66 | """Return a string representation of this `User`.""" 67 | string = self.email if self.email != '' else self.get_full_name() 68 | return f'{self.id} {string}' 69 | 70 | @property 71 | def tokens(self) -> dict[str, str]: 72 | """Allow us to get a user's token by calling `user.token`.""" 73 | refresh = RefreshToken.for_user(self) 74 | return {'refresh': str(refresh), 'access': str(refresh.access_token)} 75 | 76 | def get_full_name(self) -> Optional[str]: 77 | """Return the full name of the user.""" 78 | return self.full_name 79 | 80 | def get_short_name(self) -> str: 81 | """Return user username.""" 82 | return self.username 83 | -------------------------------------------------------------------------------- /backend/accounts/renderers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Mapping, Optional 3 | 4 | from rest_framework.renderers import JSONRenderer 5 | 6 | 7 | class UserJSONRenderer(JSONRenderer): 8 | """Custom method.""" 9 | 10 | charset = 'utf-8' 11 | 12 | def render( 13 | self, 14 | data: dict[str, Any], 15 | media_type: Optional[str] = None, 16 | renderer_context: Optional[Mapping[str, Any]] = None, 17 | ) -> str: 18 | """Return a well formatted user jSON.""" 19 | errors = data.get('errors', None) 20 | token = data.get('token', None) 21 | if errors is not None: 22 | return super(UserJSONRenderer, self).render(data) 23 | 24 | if token is not None and isinstance(token, bytes): 25 | # Also as mentioned above, we will decode `token` if it is of type 26 | # bytes. 27 | data['token'] = token.decode('utf-8') 28 | 29 | # Finally, we can render our data under the "user" namespace. 30 | return json.dumps({'user': data}) 31 | -------------------------------------------------------------------------------- /backend/accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate 2 | from rest_framework import exceptions, serializers 3 | from rest_framework_simplejwt.tokens import RefreshToken, TokenError 4 | 5 | from .models import User 6 | from .utils import validate_email as email_is_valid 7 | 8 | 9 | class RegistrationSerializer(serializers.ModelSerializer[User]): 10 | """Serializers registration requests and creates a new user.""" 11 | 12 | password = serializers.CharField(max_length=128, min_length=8, write_only=True) 13 | 14 | class Meta: 15 | model = User 16 | fields = [ 17 | 'email', 18 | 'username', 19 | 'password', 20 | 'bio', 21 | 'full_name', 22 | ] 23 | 24 | def validate_email(self, value: str) -> str: 25 | """Normalize and validate email address.""" 26 | valid, error_text = email_is_valid(value) 27 | if not valid: 28 | raise serializers.ValidationError(error_text) 29 | try: 30 | email_name, domain_part = value.strip().rsplit('@', 1) 31 | except ValueError: 32 | pass 33 | else: 34 | value = '@'.join([email_name, domain_part.lower()]) 35 | 36 | return value 37 | 38 | def create(self, validated_data): # type: ignore 39 | """Return user after creation.""" 40 | user = User.objects.create_user( 41 | username=validated_data['username'], email=validated_data['email'], password=validated_data['password'] 42 | ) 43 | user.bio = validated_data.get('bio', '') 44 | user.full_name = validated_data.get('full_name', '') 45 | user.save(update_fields=['bio', 'full_name']) 46 | return user 47 | 48 | 49 | class LoginSerializer(serializers.ModelSerializer[User]): 50 | email = serializers.CharField(max_length=255) 51 | username = serializers.CharField(max_length=255, read_only=True) 52 | password = serializers.CharField(max_length=128, write_only=True) 53 | 54 | tokens = serializers.SerializerMethodField() 55 | 56 | def get_tokens(self, obj): # type: ignore 57 | """Get user token.""" 58 | user = User.objects.get(email=obj.email) 59 | 60 | return {'refresh': user.tokens['refresh'], 'access': user.tokens['access']} 61 | 62 | class Meta: 63 | model = User 64 | fields = ['email', 'username', 'password', 'tokens', 'full_name'] 65 | 66 | def validate(self, data): # type: ignore 67 | """Validate and return user login.""" 68 | email = data.get('email', None) 69 | password = data.get('password', None) 70 | if email is None: 71 | raise serializers.ValidationError('An email address is required to log in.') 72 | 73 | if password is None: 74 | raise serializers.ValidationError('A password is required to log in.') 75 | 76 | user = authenticate(username=email, password=password) 77 | 78 | if user is None: 79 | raise serializers.ValidationError('A user with this email and password was not found.') 80 | 81 | if not user.is_active: 82 | raise serializers.ValidationError('This user is not currently activated.') 83 | 84 | return user 85 | 86 | 87 | class UserSerializer(serializers.ModelSerializer[User]): 88 | """Handle serialization and deserialization of User objects.""" 89 | 90 | password = serializers.CharField(max_length=128, min_length=8, write_only=True) 91 | 92 | class Meta: 93 | model = User 94 | fields = ( 95 | 'id', 96 | 'email', 97 | 'username', 98 | 'password', 99 | 'tokens', 100 | 'bio', 101 | 'full_name', 102 | 'birth_date', 103 | 'is_staff', 104 | ) 105 | read_only_fields = ('tokens', 'is_staff') 106 | 107 | def update(self, instance, validated_data): # type: ignore 108 | """Perform an update on a User.""" 109 | 110 | password = validated_data.pop('password', None) 111 | 112 | for (key, value) in validated_data.items(): 113 | setattr(instance, key, value) 114 | 115 | if password is not None: 116 | instance.set_password(password) 117 | 118 | instance.save() 119 | 120 | return instance 121 | 122 | 123 | class LogoutSerializer(serializers.Serializer[User]): 124 | refresh = serializers.CharField() 125 | 126 | def validate(self, attrs): # type: ignore 127 | """Validate token.""" 128 | self.token = attrs['refresh'] 129 | return attrs 130 | 131 | def save(self, **kwargs): # type: ignore 132 | """Validate save backlisted token.""" 133 | 134 | try: 135 | RefreshToken(self.token).blacklist() 136 | 137 | except TokenError as ex: 138 | raise exceptions.AuthenticationFailed(ex) 139 | -------------------------------------------------------------------------------- /backend/accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 3 | 4 | from .views import ( 5 | LoginAPIView, 6 | LogoutAPIView, 7 | RegistrationAPIView, 8 | UserRetrieveUpdateAPIView, 9 | ) 10 | 11 | app_name = 'accounts' 12 | 13 | urlpatterns = [ 14 | path('register/', RegistrationAPIView.as_view(), name='register_user'), 15 | path('login/', LoginAPIView.as_view(), name='login_user'), 16 | path('logout/', LogoutAPIView.as_view(), name="logout_user"), 17 | path('user/', UserRetrieveUpdateAPIView.as_view(), name='user'), # kwargs={'id': None}, 18 | path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 19 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/accounts/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.core.validators import validate_email as django_validate_email 3 | 4 | 5 | def validate_email(value: str) -> tuple[bool, str]: 6 | """Validate a single email.""" 7 | message_invalid = 'Enter a valid email address.' 8 | 9 | if not value: 10 | return False, message_invalid 11 | # Check the regex, using the validate_email from django. 12 | try: 13 | django_validate_email(value) 14 | except ValidationError: 15 | return False, message_invalid 16 | 17 | return True, '' 18 | -------------------------------------------------------------------------------- /backend/accounts/views.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from django.conf import settings 4 | from django.shortcuts import get_object_or_404 5 | from rest_framework import parsers, status 6 | from rest_framework.generics import RetrieveUpdateDestroyAPIView 7 | from rest_framework.permissions import AllowAny, IsAuthenticated 8 | from rest_framework.request import Request 9 | from rest_framework.response import Response 10 | from rest_framework.views import APIView 11 | 12 | from .models import User 13 | from .renderers import UserJSONRenderer 14 | from .serializers import ( 15 | LoginSerializer, 16 | LogoutSerializer, 17 | RegistrationSerializer, 18 | UserSerializer, 19 | ) 20 | 21 | 22 | class RegistrationAPIView(APIView): 23 | permission_classes = (AllowAny,) 24 | renderer_classes = (UserJSONRenderer,) 25 | serializer_class = RegistrationSerializer 26 | 27 | def post(self, request: Request) -> Response: 28 | """Return user response after a successful registration.""" 29 | user_request = request.data.get('user', {}) 30 | serializer = self.serializer_class(data=user_request) 31 | serializer.is_valid(raise_exception=True) 32 | serializer.save() 33 | return Response(serializer.data, status=status.HTTP_201_CREATED) 34 | 35 | 36 | class LoginAPIView(APIView): 37 | permission_classes = (AllowAny,) 38 | renderer_classes = (UserJSONRenderer,) 39 | serializer_class = LoginSerializer 40 | 41 | def post(self, request: Request) -> Response: 42 | """Return user after login.""" 43 | user = request.data.get('user', {}) 44 | 45 | serializer = self.serializer_class(data=user) 46 | if not serializer.is_valid(): 47 | print(serializer.errors) 48 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 49 | 50 | return Response(serializer.data, status=status.HTTP_200_OK) 51 | 52 | 53 | class UserRetrieveUpdateAPIView(RetrieveUpdateDestroyAPIView): 54 | permission_classes = (IsAuthenticated,) 55 | renderer_classes = (UserJSONRenderer,) 56 | serializer_class = UserSerializer 57 | lookup_url_kwarg = 'id' 58 | parser_classes = [ 59 | parsers.JSONParser, 60 | parsers.FormParser, 61 | parsers.MultiPartParser, 62 | ] 63 | 64 | def get(self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any]) -> Response: 65 | """Get request.""" 66 | serializer = self.serializer_class(request.user, context={'request': request}) 67 | return Response(serializer.data, status=status.HTTP_200_OK) 68 | 69 | def patch(self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any]) -> Response: 70 | """Patch method.""" 71 | serializer_data = request.data.get('user', {}) 72 | 73 | serializer = UserSerializer(request.user, data=serializer_data, partial=True) 74 | 75 | if serializer.is_valid(): 76 | 77 | user = serializer.save() 78 | 79 | return Response(UserSerializer(user).data) 80 | 81 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 82 | 83 | 84 | class LogoutAPIView(APIView): 85 | serializer_class = LogoutSerializer 86 | 87 | permission_classes = (IsAuthenticated,) 88 | 89 | def post(self, request: Request) -> Response: 90 | """Validate token and save.""" 91 | serializer = self.serializer_class(data=request.data) 92 | serializer.is_valid(raise_exception=True) 93 | serializer.save() 94 | 95 | return Response(status=status.HTTP_204_NO_CONTENT) 96 | -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | import dj_database_url 16 | from decouple import Csv, config 17 | 18 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 19 | BASE_DIR = Path(__file__).resolve().parent.parent 20 | 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = config("SECRET_KEY", default='django-insecure-&g)%is(f3(e-@33tz)jj$67*tqh3qktgtve56%h47@n10@g1i4') 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = config("DEBUG", default=True, cast=bool) 30 | 31 | ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="127.0.0.1,localhost", cast=Csv()) 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | # Third-Party Apps 44 | 'rest_framework', 45 | 'rest_framework_simplejwt.token_blacklist', 46 | 'corsheaders', 47 | # local apps 48 | 'accounts.apps.AccountsConfig', 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'whitenoise.middleware.WhiteNoiseMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'corsheaders.middleware.CorsMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.middleware.csrf.CsrfViewMiddleware', 58 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 59 | 'django.contrib.messages.middleware.MessageMiddleware', 60 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 61 | ] 62 | 63 | ROOT_URLCONF = 'backend.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [], 69 | 'APP_DIRS': True, 70 | 'OPTIONS': { 71 | 'context_processors': [ 72 | 'django.template.context_processors.debug', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.auth.context_processors.auth', 75 | 'django.contrib.messages.context_processors.messages', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = 'backend.wsgi.application' 82 | 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 86 | 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.sqlite3', 90 | 'NAME': BASE_DIR / 'db.sqlite3', 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'en-us' 118 | 119 | TIME_ZONE = 'UTC' 120 | 121 | USE_I18N = True 122 | 123 | USE_TZ = True 124 | 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 128 | 129 | STATIC_URL = 'static/' 130 | STATICFILES_DIRS = [ 131 | BASE_DIR / 'static', 132 | ] 133 | STATIC_ROOT = BASE_DIR / 'staticfiles' 134 | 135 | # Default primary key field type 136 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 137 | 138 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 139 | # REST_FRAMEWORK 140 | REST_FRAMEWORK = { 141 | 'EXCEPTION_HANDLER': 'accounts.exceptions.core_exception_handler', 142 | 'NON_FIELD_ERRORS_KEY': 'error', 143 | 'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_simplejwt.authentication.JWTAuthentication',), 144 | } 145 | 146 | 147 | # DEFAULT USER MODEL 148 | AUTH_USER_MODEL = 'accounts.User' 149 | 150 | # CORS 151 | CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='http://127.0.0.1:5173,http://localhost:5173', cast=Csv()) 152 | 153 | 154 | db_from_env = dj_database_url.config(conn_max_age=500) 155 | DATABASES['default'].update(db_from_env) 156 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('api/', include('accounts.urls', namespace='accounts')), 7 | ] 8 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/backend/db.sqlite3 -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.5.0 2 | dj-database-url==0.5.0 3 | Django==4.0.2 4 | django-cors-headers==3.11.0 5 | djangorestframework==3.13.1 6 | djangorestframework-simplejwt==5.0.0 7 | gunicorn==20.1.0 8 | psycopg2-binary==2.9.3 9 | PyJWT==2.3.0 10 | python-decouple==3.6 11 | pytz==2021.3 12 | sqlparse==0.4.2 13 | whitenoise==6.0.0 14 | -------------------------------------------------------------------------------- /backend/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.2 -------------------------------------------------------------------------------- /frontend/.env.exemple: -------------------------------------------------------------------------------- 1 | # API Production URL 2 | # 'https://django-sveltekit-jwt-auth.herokuapp.com/api' 3 | VITE_BASE_API_URI_PROD= 4 | # API Dev URL 5 | VITE_BASE_API_URI_DEV='http://localhost:8000/api' 6 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build/ 4 | .svelte-kit/ 5 | .env 6 | .vercel_build_output/ 7 | 8 | /package 9 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm init svelte@next 12 | 13 | # create a new project in my-app 14 | npm init svelte@next my-app 15 | ``` 16 | 17 | > Note: the `@next` is temporary 18 | 19 | ## Developing 20 | 21 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 22 | 23 | ```bash 24 | npm run dev 25 | 26 | # or start the server and open the app in a new browser tab 27 | npm run dev -- --open 28 | ``` 29 | 30 | ## Building 31 | 32 | To create a production version of your app: 33 | 34 | ```bash 35 | npm run build 36 | ``` 37 | 38 | You can preview the production build with `npm run preview`. 39 | 40 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. 41 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.1", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "package": "vite package", 8 | "preview": "vite preview", 9 | "check": "svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", 12 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "next", 16 | "@sveltejs/kit": "^1.0.0-next.511", 17 | "@typescript-eslint/eslint-plugin": "^5.39.0", 18 | "@typescript-eslint/parser": "^5.39.0", 19 | "eslint": "^8.24.0", 20 | "eslint-config-prettier": "^8.5.0", 21 | "eslint-plugin-svelte3": "^4.0.0", 22 | "prettier": "^2.7.1", 23 | "prettier-plugin-svelte": "^2.7.1", 24 | "svelte": "^3.50.1", 25 | "svelte-check": "^2.9.1", 26 | "svelte-preprocess": "^4.10.7", 27 | "tslib": "^2.4.0", 28 | "typescript": "^4.8.4" 29 | }, 30 | "type": "module", 31 | "dependencies": { 32 | "@sveltejs/adapter-vercel": "^1.0.0-next.77" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | declare namespace App { 5 | // interface Locals {} 6 | // interface PageData {} 7 | // interface Error {} 8 | // interface Platform {} 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | readonly VITE_BASE_API_URI_PROD: string, 3 | readonly VITE_BASE_API_URI_DEV: string 4 | } 5 | 6 | interface ImportMeta { 7 | readonly env: ImportMetaEnv 8 | } -------------------------------------------------------------------------------- /frontend/src/lib/components/Header/Header.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | 12 | SvelteKit 13 | 14 |
15 | 16 | 46 | 47 |
48 | 49 | John O. Idogun 50 | 51 |
52 |
53 | -------------------------------------------------------------------------------- /frontend/src/lib/components/Header/john.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.16, written by Peter Selinger 2001-2019 9 | 10 | 12 | 477 | 480 | 493 | 500 | 507 | 511 | 520 | 541 | 546 | 547 | 548 | -------------------------------------------------------------------------------- /frontend/src/lib/components/Header/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /frontend/src/lib/components/Loader/Loader.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if $loading.status === 'LOADING'} 14 |
15 |
16 | {#if $loading.message} 17 |

{$loading.message}

18 | {/if} 19 |
20 | {/if} 21 | 22 | 78 | -------------------------------------------------------------------------------- /frontend/src/lib/dist/css/style.min.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 3 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | } 5 | 6 | body { 7 | min-height: 100vh; 8 | margin: 0; 9 | background-color: #b9c6d2; 10 | background: linear-gradient(180deg, #b9c6d2 0%, #d0dde9 10.45%, #edf0f8 41.35%); 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | body::before { 15 | content: ''; 16 | width: 80vw; 17 | height: 100vh; 18 | position: absolute; 19 | top: 0; 20 | left: 10vw; 21 | z-index: -1; 22 | background: radial-gradient(50% 50% at 50% 50%, #ffffff 0%, rgba(255, 255, 255, 0) 100%); 23 | opacity: 0.05; 24 | } 25 | 26 | h1, 27 | h2, 28 | p { 29 | font-weight: 400; 30 | color: rgba(0, 0, 0, 0.7); 31 | } 32 | 33 | p { 34 | line-height: 1.5; 35 | } 36 | 37 | a { 38 | color: #ff3e00; 39 | text-decoration: none; 40 | } 41 | 42 | a:hover { 43 | text-decoration: underline; 44 | } 45 | 46 | h1 { 47 | font-size: 2rem; 48 | text-align: center; 49 | } 50 | 51 | h2 { 52 | font-size: 1rem; 53 | } 54 | 55 | pre { 56 | font-size: 16px; 57 | font-family: 'Fira Mono', monospace; 58 | background-color: rgba(255, 255, 255, 0.45); 59 | border-radius: 3px; 60 | padding: 0.5em; 61 | overflow-x: auto; 62 | color: #444444; 63 | } 64 | 65 | input, 66 | button { 67 | font-size: inherit; 68 | font-family: inherit; 69 | } 70 | 71 | button:focus:not(:focus-visible) { 72 | outline: none; 73 | } 74 | 75 | @media (min-width: 720px) { 76 | h1 { 77 | font-size: 2.4rem; 78 | } 79 | } 80 | main { 81 | flex: 1; 82 | display: flex; 83 | flex-direction: column; 84 | padding: 1rem; 85 | width: 100%; 86 | max-width: 1024px; 87 | margin: 0 auto; 88 | box-sizing: border-box; 89 | } 90 | 91 | footer { 92 | display: flex; 93 | flex-direction: column; 94 | justify-content: center; 95 | align-items: center; 96 | padding: 40px; 97 | margin-top: auto; 98 | } 99 | footer a { 100 | font-weight: bold; 101 | } 102 | 103 | @media (min-width: 480px) { 104 | footer { 105 | padding: 40px 0; 106 | } 107 | } 108 | header { 109 | display: flex; 110 | justify-content: space-between; 111 | } 112 | 113 | .corner { 114 | width: 3em; 115 | height: 3em; 116 | } 117 | .corner a { 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | width: 100%; 122 | height: 100%; 123 | } 124 | .corner img { 125 | width: 2em; 126 | height: 2em; 127 | -o-object-fit: contain; 128 | object-fit: contain; 129 | } 130 | 131 | nav { 132 | display: flex; 133 | justify-content: center; 134 | --background: rgba(255, 255, 255, 0.7); 135 | } 136 | 137 | svg { 138 | width: 2em; 139 | height: 3em; 140 | display: block; 141 | } 142 | 143 | path { 144 | fill: var(--background); 145 | } 146 | 147 | ul { 148 | position: relative; 149 | padding: 0; 150 | margin: 0; 151 | height: 3em; 152 | display: flex; 153 | justify-content: center; 154 | align-items: center; 155 | list-style: none; 156 | background: var(--background); 157 | background-size: contain; 158 | } 159 | 160 | li { 161 | position: relative; 162 | height: 100%; 163 | display: flex; 164 | align-items: center; 165 | text-transform: uppercase; 166 | font-size: 0.8rem; 167 | } 168 | 169 | li.active::before { 170 | --size: 6px; 171 | content: ''; 172 | width: 0; 173 | height: 0; 174 | position: absolute; 175 | top: 0; 176 | left: calc(50% - var(--size)); 177 | border: var(--size) solid transparent; 178 | border-top: var(--size) solid #ff3e00; 179 | } 180 | 181 | nav a { 182 | display: flex; 183 | height: 100%; 184 | align-items: center; 185 | padding: 0 1em; 186 | color: rgba(0, 0, 0, 0.7); 187 | font-weight: 700; 188 | font-size: 0.8rem; 189 | letter-spacing: 0.1em; 190 | text-decoration: none; 191 | transition: color 0.2s linear; 192 | } 193 | 194 | a:hover { 195 | color: #ff3e00; 196 | } 197 | 198 | .content { 199 | width: 100%; 200 | max-width: 42rem; 201 | margin: 4rem auto 0 auto; 202 | } 203 | 204 | section { 205 | display: flex; 206 | flex-direction: column; 207 | justify-content: center; 208 | align-items: center; 209 | flex: 1; 210 | } 211 | 212 | h1 { 213 | width: 100%; 214 | } 215 | 216 | .welcome { 217 | position: relative; 218 | width: 100%; 219 | height: 0; 220 | padding: 0 0 24.169921875% 0; 221 | } 222 | .welcome img { 223 | position: absolute; 224 | width: 100%; 225 | height: 100%; 226 | top: 0; 227 | display: block; 228 | } 229 | 230 | .container { 231 | width: 100%; 232 | max-width: var(--column-width); 233 | margin: var(--column-margin-top) auto 0 auto; 234 | line-height: 1; 235 | } 236 | .container h1:first-letter { 237 | text-transform: capitalize; 238 | } 239 | 240 | .form { 241 | margin: 0 0 0.5rem 0; 242 | } 243 | 244 | input { 245 | border: 1px solid rgba(0, 0, 0, 0.1); 246 | } 247 | input:focus-visible { 248 | box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1); 249 | border: 1px solid #ff3e00 !important; 250 | outline: none; 251 | } 252 | 253 | .form input { 254 | font-size: 28px; 255 | width: 100%; 256 | padding: 0.5em 1em 0.3em 1em; 257 | box-sizing: border-box; 258 | background: rgba(255, 255, 255, 0.05); 259 | border-radius: 8px; 260 | text-align: center; 261 | } 262 | .form input:not(:last-of-type) { 263 | margin-bottom: 0.5rem; 264 | } 265 | 266 | button { 267 | font: inherit; 268 | border: none; 269 | outline: none; 270 | /* color: $accent-color; */ 271 | cursor: pointer; 272 | } 273 | 274 | .btn { 275 | position: relative; 276 | font-size: 1.2rem; 277 | background: rgba(255, 255, 255, 0.05); 278 | border: 1px solid #ff3e00; 279 | padding: 0.5rem 1rem; 280 | margin-top: 0.5rem; 281 | left: 50%; 282 | transform: translate(-50%, 0); 283 | border-radius: 8px; 284 | transition: all 0.3s; 285 | } 286 | .btn:hover:not(:disabled) { 287 | background: #ff3e00; 288 | color: #fff; 289 | } 290 | .btn:disabled { 291 | border: 1px solid #000; 292 | } 293 | .btn:disabled:hover { 294 | background: #000; 295 | color: #fff; 296 | border: none; 297 | cursor: default; 298 | } 299 | 300 | .notification-container { 301 | display: flex; 302 | flex-direction: column; 303 | } 304 | 305 | .notification { 306 | width: 25%; 307 | background: #70c48f; 308 | color: white; 309 | border-radius: 3px; 310 | font-size: 0.9rem; 311 | padding: 0.5rem 1rem; 312 | display: flex; 313 | justify-content: center; 314 | align-items: center; 315 | position: fixed; 316 | right: 0; 317 | transition: all 0.3s; 318 | } 319 | 320 | .notification.disappear { 321 | visibility: hidden; 322 | } 323 | 324 | @media (max-width: 600px) { 325 | .notification { 326 | width: 60%; 327 | } 328 | } 329 | .user { 330 | align-items: center; 331 | margin: 0 0 0.5rem 0; 332 | padding: 0.5rem; 333 | background-color: white; 334 | border-radius: 8px; 335 | filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1)); 336 | transform: translate(-1px, -1px); 337 | transition: filter 0.2s, transform 0.2s; 338 | } 339 | .user input { 340 | flex: 1; 341 | padding: 0.5em 2em 0.5em 0.8em; 342 | border-radius: 3px; 343 | outline: none; 344 | border: none; 345 | } 346 | .user button { 347 | width: 2em; 348 | height: 2em; 349 | border: none; 350 | background-color: transparent; 351 | background-position: 50% 50%; 352 | background-repeat: no-repeat; 353 | } 354 | 355 | .text { 356 | position: relative; 357 | display: flex; 358 | align-items: center; 359 | flex: 1; 360 | } 361 | 362 | .save { 363 | position: absolute; 364 | right: 0; 365 | opacity: 0; 366 | background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A"); 367 | } 368 | 369 | .user input:focus + .save, 370 | .save:focus { 371 | transition: opacity 0.2s; 372 | opacity: 1; 373 | } 374 | 375 | #svelte { 376 | min-height: 100vh; 377 | display: flex; 378 | flex-direction: column; 379 | } 380 | 381 | .center { 382 | text-align: center; 383 | } 384 | 385 | .error { 386 | color: red; 387 | text-transform: capitalize; 388 | } 389 | 390 | .center.error:not(:first-of-type) { 391 | margin-top: 0.1rem; 392 | } /*# sourceMappingURL=style.min.css.map */ 393 | -------------------------------------------------------------------------------- /frontend/src/lib/dist/css/style.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["_globals.scss","style.css","_variables.scss","_header.scss","_about.scss","_home.scss","_form.scss","style.scss"],"names":[],"mappings":"AAAA;EACC,+IAAA;ACCD;;ADGA;EACC,iBAAA;EACA,SAAA;EACA,yBENe;EFOf,+EAAA;EAMA,aAAA;EACA,sBAAA;ACLD;ADMC;EACC,WAAA;EACA,WAAA;EACA,aAAA;EACA,kBAAA;EACA,MAAA;EACA,UAAA;EACA,WAAA;EACA,wFAAA;EACA,aAAA;ACJF;;ADOA;;;EAGC,gBAAA;EACA,yBE3Be;ADuBhB;;ADOA;EACC,gBAAA;ACJD;;ADOA;EACC,cEpCc;EFqCd,qBAAA;ACJD;;ADOA;EACC,0BAAA;ACJD;;ADOA;EACC,eAAA;EACA,kBAAA;ACJD;;ADOA;EACC,eAAA;ACJD;;ADOA;EACC,eAAA;EACA,mCE5DW;EF6DX,2CAAA;EACA,kBAAA;EAEA,cAAA;EACA,gBAAA;EACA,cE3DY;ADsDb;;ADQA;;EAEC,kBAAA;EACA,oBAAA;ACLD;;ADQA;EACC,aAAA;ACLD;;ADQA;EACC;IACC,iBAAA;ECLA;AACF;ADQA;EACC,OAAA;EACA,aAAA;EACA,sBAAA;EACA,aAAA;EACA,WAAA;EACA,iBAAA;EACA,cAAA;EACA,sBAAA;ACND;;ADSA;EACC,aAAA;EACA,sBAAA;EACA,uBAAA;EACA,mBAAA;EACA,aAAA;EACA,gBAAA;ACND;ADOC;EACC,iBAAA;ACLF;;ADSA;EACC;IACC,eAAA;ECNA;AACF;AEzGA;EACC,aAAA;EACA,8BAAA;AF2GD;;AExGA;EACC,UAAA;EACA,WAAA;AF2GD;AE1GC;EACC,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;AF4GF;AE1GC;EACC,UAAA;EACA,WAAA;EACA,sBAAA;KAAA,mBAAA;AF4GF;;AExGA;EACC,aAAA;EACA,uBAAA;EACA,sCAAA;AF2GD;;AExGA;EACC,UAAA;EACA,WAAA;EACA,cAAA;AF2GD;;AExGA;EACC,uBAAA;AF2GD;;AExGA;EACC,kBAAA;EACA,UAAA;EACA,SAAA;EACA,WAAA;EACA,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,gBAAA;EACA,6BAAA;EACA,wBAAA;AF2GD;;AExGA;EACC,kBAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;EACA,iBAAA;AF2GD;;AExGA;EACC,WAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,kBAAA;EACA,MAAA;EACA,6BAAA;EACA,qCAAA;EACA,qCAAA;AF2GD;;AEvGC;EACC,aAAA;EACA,YAAA;EACA,mBAAA;EACA,cAAA;EACA,yBDxEc;ECyEd,gBAAA;EACA,iBAAA;EAEA,qBAAA;EACA,qBAAA;EACA,6BAAA;AFyGF;;AErGA;EACC,cDpFc;AD4Lf;;AGjMA;EACE,WAAA;EACA,gBFOa;EENb,wBAAA;AHoMF;;AIvMA;EACE,aAAA;EACA,sBAAA;EACA,uBAAA;EACA,mBAAA;EACA,OAAA;AJ0MF;;AIvMA;EACE,WAAA;AJ0MF;;AIvMA;EACE,kBAAA;EACA,WAAA;EACA,SAAA;EACA,4BAAA;AJ0MF;AIxME;EACE,kBAAA;EACA,WAAA;EACA,YAAA;EACA,MAAA;EACA,cAAA;AJ0MJ;;AKjOA;EACC,WAAA;EACA,8BAAA;EACA,4CAAA;EACA,cAAA;ALoOD;AKlOC;EACC,0BAAA;ALoOF;;AKhOA;EACC,oBAAA;ALmOD;;AKhOA;EACC,oCAAA;ALmOD;AKlOC;EACC,gDAAA;EACA,oCAAA;EACA,aAAA;ALoOF;;AK/NC;EACC,eAAA;EACA,WAAA;EACA,4BAAA;EACA,sBAAA;EACA,qCAAA;EACA,kBAAA;EACA,kBAAA;ALkOF;AKhOE;EACC,qBAAA;ALkOH;;AK9NA;EACC,aAAA;EACA,YAAA;EACA,aAAA;EACA,0BAAA;EACA,eAAA;ALiOD;;AK/NA;EACC,kBAAA;EACA,iBAAA;EACA,qCAAA;EACA,yBAAA;EACA,oBAAA;EACA,kBAAA;EACA,SAAA;EACA,6BAAA;EACA,kBAAA;EACA,oBAAA;ALkOD;AKjOC;EACC,mBAAA;EACA,WAAA;ALmOF;AKjOC;EACC,sBAAA;ALmOF;AKjOC;EACC,gBAAA;EACA,WAAA;EACA,YAAA;EACA,eAAA;ALmOF;;AKhOA;EACC,aAAA;EACA,sBAAA;ALmOD;;AKjOA;EACC,UAAA;EACA,mBAAA;EACA,YAAA;EACA,kBAAA;EACA,iBAAA;EACA,oBAAA;EACA,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,eAAA;EACA,QAAA;EACA,oBAAA;ALoOD;;AKlOA;EACC,kBAAA;ALqOD;;AKnOA;EACC;IACC,UAAA;ELsOA;AACF;AKnOA;EACC,mBAAA;EACA,oBAAA;EACA,eAAA;EACA,uBAAA;EACA,kBAAA;EACA,mDAAA;EACA,gCAAA;EACA,uCAAA;ALqOD;AKnOC;EACC,OAAA;EACA,8BAAA;EACA,kBAAA;EACA,aAAA;EACA,YAAA;ALqOF;AKnOC;EACC,UAAA;EACA,WAAA;EACA,YAAA;EACA,6BAAA;EACA,4BAAA;EACA,4BAAA;ALqOF;;AKjOA;EACC,kBAAA;EACA,aAAA;EACA,mBAAA;EACA,OAAA;ALoOD;;AKlOA;EACC,kBAAA;EACA,QAAA;EACA,UAAA;EACA,uqBAAA;ALqOD;;AKlOA;;EAEC,wBAAA;EACA,UAAA;ALqOD;;AM3WA;EACC,iBAAA;EACA,aAAA;EACA,sBAAA;AN8WD;;AM5WA;EACC,kBAAA;AN+WD;;AM7WA;EACC,UAAA;EACA,0BAAA;ANgXD;;AM9WA;EACC,kBAAA;ANiXD","file":"style.css"} -------------------------------------------------------------------------------- /frontend/src/lib/formats/formatString.ts: -------------------------------------------------------------------------------- 1 | export const formatText = (text: string): string => { 2 | const replacedText: string = text.replace(/_/g, ' '); 3 | return replacedText.charAt(0).toUpperCase() + replacedText.slice(1); 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/lib/helpers/buttonText.ts: -------------------------------------------------------------------------------- 1 | export const changeText = (e: Event, text: string) => { 2 | (e.target).textContent = text; 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/lib/helpers/whitespacesHelper.ts: -------------------------------------------------------------------------------- 1 | function isAllWhitespaces(nod: CharacterData): boolean { 2 | // Use ECMA-262 Edition 3 String and RegExp features 3 | return !/[^\t\n\r ]/.test(nod.textContent); 4 | } 5 | function isIgnorable(nod: Node): boolean { 6 | return ( 7 | nod.nodeType == 8 || // A comment node 8 | (nod.nodeType == 3 && isAllWhitespaces(nod as CharacterData)) 9 | ); // a text node, all ws 10 | } 11 | export function nodeBefore(sib: Node): HTMLInputElement | undefined { 12 | while ((sib = sib.previousSibling)) { 13 | if (!isIgnorable(sib)) return sib as HTMLInputElement; 14 | } 15 | return null; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/lib/interfaces/error.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CustomError { 2 | error: string; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/lib/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Token { 2 | refresh?: string; 3 | access?: string; 4 | } 5 | export interface User { 6 | id?: string; 7 | email?: string; 8 | username?: string; 9 | password?: string; 10 | tokens?: Token; 11 | bio?: string; 12 | full_name?: string; 13 | birth_date?: string; 14 | is_staff?: boolean; 15 | } 16 | 17 | export interface UserResponse { 18 | user?: User; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/lib/interfaces/variables.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Variables { 2 | readonly BASE_API_URI: string; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/lib/store/loadingStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | const newLoading = () => { 4 | const { subscribe, update, set } = writable({ 5 | status: 'IDLE', // IDLE, LOADING, NAVIGATING 6 | message: '' 7 | }); 8 | 9 | function setNavigate(isNavigating: boolean) { 10 | update(() => { 11 | return { 12 | status: isNavigating ? 'NAVIGATING' : 'IDLE', 13 | message: '' 14 | }; 15 | }); 16 | } 17 | 18 | function setLoading(isLoading: boolean, message = '') { 19 | update(() => { 20 | return { 21 | status: isLoading ? 'LOADING' : 'IDLE', 22 | message: isLoading ? message : '' 23 | }; 24 | }); 25 | } 26 | 27 | return { subscribe, update, set, setNavigate, setLoading }; 28 | }; 29 | 30 | export const loading = newLoading(); 31 | -------------------------------------------------------------------------------- /frontend/src/lib/store/notificationStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const notificationData = writable(''); -------------------------------------------------------------------------------- /frontend/src/lib/store/userStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | import type { User } from '$lib/interfaces/user.interface'; 4 | 5 | export const userData = writable({}); 6 | -------------------------------------------------------------------------------- /frontend/src/lib/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Variables } from '$lib/interfaces/variables.interface'; 2 | 3 | const BASE_API_URI: string = import.meta.env.DEV 4 | ? import.meta.env.VITE_BASE_API_URI_DEV 5 | : import.meta.env.VITE_BASE_API_URI_PROD; 6 | 7 | export const variables: Variables = { BASE_API_URI: BASE_API_URI }; 8 | 9 | //'http://localhost:8000/api' //'https://django-sveltekit-jwt-auth.herokuapp.com/api' 10 | -------------------------------------------------------------------------------- /frontend/src/lib/utils/requestUtils.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | import { goto } from '$app/navigation'; 3 | import type { Token, UserResponse } from '$lib/interfaces/user.interface'; 4 | import type { CustomError } from '$lib/interfaces/error.interface'; 5 | import { notificationData } from '$lib/store/notificationStore'; 6 | import { userData } from '$lib/store/userStore'; 7 | 8 | import { variables } from '$lib/utils/constants'; 9 | import { formatText } from '$lib/formats/formatString'; 10 | 11 | export const browserGet = (key: string): string | undefined => { 12 | if (browser) { 13 | const item = localStorage.getItem(key); 14 | if (item) { 15 | return item; 16 | } 17 | } 18 | return undefined; 19 | }; 20 | 21 | export const browserSet = (key: string, value: string): void => { 22 | if (browser) { 23 | localStorage.setItem(key, value); 24 | } 25 | }; 26 | 27 | export const post = async ( 28 | fetch: any, 29 | url: string, 30 | body: unknown 31 | ): Promise<[object, Array]> => { 32 | try { 33 | const headers = {'Content-Type': 'application/octet-stream', 'Authorization': ''}; 34 | if (!(body instanceof FormData)) { 35 | headers['Content-Type'] = 'application/json'; 36 | body = JSON.stringify(body); 37 | const token = browserGet('refreshToken'); 38 | if (token) { 39 | headers['Authorization'] = `Bearer ${token}`; 40 | } 41 | const res = await fetch(url, { 42 | method: 'POST', 43 | body, 44 | headers 45 | }); 46 | const response = await res.json(); 47 | if (response.errors) { 48 | const errors: Array = []; 49 | for (const p in response.errors) { 50 | errors.push({ error: response.errors[p] }); 51 | } 52 | return [{}, errors]; 53 | } 54 | return [response, []]; 55 | } 56 | } catch (error) { 57 | const errors: Array = [{ error: 'An unknown error occurred.' }, { error: `${error} `}]; 58 | return [{}, errors]; 59 | } 60 | }; 61 | 62 | export const getCurrentUser = async ( 63 | fetch: any, 64 | refreshUrl: string, 65 | userUrl: string 66 | ): Promise<[object, Array]> => { 67 | const jsonRes = await fetch(refreshUrl, { 68 | method: 'POST', 69 | mode: 'cors', 70 | headers: { 71 | 'Content-Type': 'application/json' 72 | }, 73 | body: JSON.stringify({ 74 | refresh: `${browserGet('refreshToken')}` 75 | }) 76 | }); 77 | const accessRefresh: Token = await jsonRes.json(); 78 | if (accessRefresh.access) { 79 | const res = await fetch(userUrl, { 80 | headers: { 81 | Authorization: `Bearer ${accessRefresh.access}` 82 | } 83 | }); 84 | if (res.status === 400) { 85 | const data = await res.json(); 86 | const error = data.user.error[0]; 87 | return [{}, error]; 88 | } 89 | const response = await res.json(); 90 | return [response.user, []]; 91 | } else { 92 | return [{}, [{ error: 'Refresh token is invalid...' }]]; 93 | } 94 | }; 95 | 96 | export const logOutUser = async (): Promise => { 97 | const res = await fetch(`${variables.BASE_API_URI}/token/refresh/`, { 98 | method: 'POST', 99 | mode: 'cors', 100 | headers: { 101 | 'Content-Type': 'application/json' 102 | }, 103 | body: JSON.stringify({ 104 | refresh: `${browserGet('refreshToken')}` 105 | }) 106 | }); 107 | const accessRefresh = await res.json(); 108 | const jres = await fetch(`${variables.BASE_API_URI}/logout/`, { 109 | method: 'POST', 110 | mode: 'cors', 111 | headers: { 112 | Authorization: `Bearer ${accessRefresh.access}`, 113 | 'Content-Type': 'application/json' 114 | }, 115 | body: JSON.stringify({ 116 | refresh: `${browserGet('refreshToken')}` 117 | }) 118 | }); 119 | if (jres.status !== 204) { 120 | const data = await jres.json(); 121 | const error = data.user.error[0]; 122 | throw { id: error.id, message: error }; 123 | } 124 | localStorage.removeItem('refreshToken'); 125 | userData.set({}); 126 | notificationData.update(() => 'You have successfully logged out...'); 127 | await goto('/accounts/login'); 128 | }; 129 | 130 | export const handlePostRequestsWithPermissions = async ( 131 | fetch: any, 132 | targetUrl: string, 133 | body: unknown, 134 | method = 'POST' 135 | ): Promise<[object, Array]> => { 136 | const res = await fetch(`${variables.BASE_API_URI}/token/refresh/`, { 137 | method: 'POST', 138 | mode: 'cors', 139 | headers: { 140 | 'Content-Type': 'application/json' 141 | }, 142 | body: JSON.stringify({ 143 | refresh: `${browserGet('refreshToken')}` 144 | }) 145 | }); 146 | const accessRefresh = await res.json(); 147 | const jres = await fetch(targetUrl, { 148 | method: method, 149 | mode: 'cors', 150 | headers: { 151 | Authorization: `Bearer ${accessRefresh.access}`, 152 | 'Content-Type': 'application/json' 153 | }, 154 | body: JSON.stringify(body) 155 | }); 156 | 157 | if (method === 'PATCH') { 158 | if (jres.status !== 200) { 159 | const data = await jres.json(); 160 | console.error(`Data: ${data}`); 161 | const errs = data.errors; 162 | console.error(errs); 163 | return [{}, errs]; 164 | } 165 | return [await jres.json(), []]; 166 | } else if (method === 'POST') { 167 | if (jres.status !== 201) { 168 | const data = await jres.json(); 169 | console.error(`Data: ${data}`); 170 | const errs = data.errors; 171 | console.error(errs); 172 | return [{}, errs]; 173 | } 174 | return [jres.json(), []]; 175 | } 176 | }; 177 | 178 | export const UpdateField = async ( 179 | fieldName: string, 180 | fieldValue: string, 181 | url: string 182 | ): Promise<[object, Array]> => { 183 | const userObject: UserResponse = { user: {} }; 184 | let formData: UserResponse | any; 185 | if (url.includes('/user/')) { 186 | formData = userObject; 187 | formData['user'][`${fieldName}`] = fieldValue; 188 | } else { 189 | formData[`${fieldName}`] = fieldValue; 190 | } 191 | 192 | const [response, err] = await handlePostRequestsWithPermissions(fetch, url, formData, 'PATCH'); 193 | if (err.length > 0) { 194 | return [{}, err]; 195 | } 196 | notificationData.update(() => `${formatText(fieldName)} has been updated successfully.`); 197 | return [response, []]; 198 | }; 199 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 52 | 53 |
54 | 55 | {#if $notificationData} 56 |

62 | {$notificationData} 63 |

64 | {/if} 65 | 66 |
67 | 68 | 69 |
70 | 71 | 77 | -------------------------------------------------------------------------------- /frontend/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Home | FullStack Django & SvelteKit 9 | 10 | 11 |
12 |

13 |
14 | 15 | 16 | Welcome 17 | 18 |
19 | {#if $userData.username} 20 | {$userData.username}, 21 | {/if} 22 | 23 | to our new
SvelteKit app 24 |

25 |
26 | -------------------------------------------------------------------------------- /frontend/src/routes/accounts/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | Login | FullStack Django & SvelteKit 42 | 43 | 44 |
49 |

Login

50 | {#if errors} 51 | {#each errors as error} 52 |

{error.error}

53 | {/each} 54 | {/if} 55 |
56 | 64 | 72 | 74 |

No account yet? Get started.

75 |
76 |
77 | -------------------------------------------------------------------------------- /frontend/src/routes/accounts/register/+page.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | Register | FullStack Django & SvelteKit 42 | 43 | 44 |
49 |

Register

50 | {#if errors} 51 | {#each errors as error} 52 |

{error.error}

53 | {/each} 54 | {/if} 55 |
56 | 63 | 70 | 77 | 84 | 92 | 100 | {#if confirmPassword} 101 | 104 | {:else} 105 | 106 | {/if} 107 |
108 |
109 | -------------------------------------------------------------------------------- /frontend/src/routes/accounts/user/[username]-[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | {#if currentUserData.id} 34 |

35 | {currentUserData.full_name ? currentUserData.full_name : currentUserData.username} profile 36 |

37 | {/if} 38 | 39 |
40 |
41 | 48 |
50 |
51 |
52 |
53 | 60 |
62 |
63 |
64 |
65 | 72 |
74 |
75 |
76 |
77 | 84 |
86 |
87 |
88 |
89 | 96 |
102 |
103 |
104 | -------------------------------------------------------------------------------- /frontend/src/routes/accounts/user/[username]-[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { variables } from '$lib/utils/constants'; 3 | import { getCurrentUser } from '$lib/utils/requestUtils'; 4 | import type { PageLoad } from '.svelte-kit/types/src/routes/$types'; 5 | import type { User } from '$lib/interfaces/user.interface'; 6 | 7 | export const load: PageLoad = async ({ fetch }) => { 8 | const [userRes, errs] = await getCurrentUser( 9 | fetch, 10 | `${variables.BASE_API_URI}/token/refresh/`, 11 | `${variables.BASE_API_URI}/user/` 12 | ); 13 | 14 | const userResponse: User = userRes; 15 | 16 | if (errs.length > 0 && !userResponse.id) { 17 | throw redirect(302, '/accounts/login'); 18 | } 19 | 20 | return { userResponse }; 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/sass/_about.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 100%; 3 | max-width: $column-width; 4 | margin: $column-margin-top auto 0 auto; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/sass/_form.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | max-width: var(--column-width); 4 | margin: var(--column-margin-top) auto 0 auto; 5 | line-height: 1; 6 | 7 | h1:first-letter { 8 | text-transform: capitalize; 9 | } 10 | } 11 | 12 | .form { 13 | margin: 0 0 0.5rem 0; 14 | } 15 | 16 | input { 17 | border: 1px solid rgba(0, 0, 0, 0.1); 18 | &:focus-visible { 19 | box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1); 20 | border: 1px solid #ff3e00 !important; 21 | outline: none; 22 | } 23 | } 24 | 25 | .form { 26 | input { 27 | font-size: 28px; 28 | width: 100%; 29 | padding: 0.5em 1em 0.3em 1em; 30 | box-sizing: border-box; 31 | background: rgba(255, 255, 255, 0.05); 32 | border-radius: 8px; 33 | text-align: center; 34 | 35 | &:not(:last-of-type) { 36 | margin-bottom: 0.5rem; 37 | } 38 | } 39 | } 40 | button { 41 | font: inherit; 42 | border: none; 43 | outline: none; 44 | /* color: $accent-color; */ 45 | cursor: pointer; 46 | } 47 | .btn { 48 | position: relative; 49 | font-size: 1.2rem; 50 | background: rgba(255, 255, 255, 0.05); 51 | border: 1px solid #ff3e00; 52 | padding: 0.5rem 1rem; 53 | margin-top: 0.5rem; 54 | left: 50%; 55 | transform: translate(-50%, 0); 56 | border-radius: 8px; 57 | transition: all 0.3s; 58 | &:hover:not(:disabled) { 59 | background: #ff3e00; 60 | color: #fff; 61 | } 62 | &:disabled { 63 | border: 1px solid #000; 64 | } 65 | &:disabled:hover { 66 | background: #000; 67 | color: #fff; 68 | border: none; 69 | cursor: default; 70 | } 71 | } 72 | .notification-container { 73 | display: flex; 74 | flex-direction: column; 75 | } 76 | .notification { 77 | width: 25%; 78 | background: #70c48f; 79 | color: white; 80 | border-radius: 3px; 81 | font-size: 0.9rem; 82 | padding: 0.5rem 1rem; 83 | display: flex; 84 | justify-content: center; 85 | align-items: center; 86 | position: fixed; 87 | right: 0; 88 | transition: all 0.3s; 89 | } 90 | .notification.disappear { 91 | visibility: hidden; 92 | } 93 | @media (max-width: 600px) { 94 | .notification { 95 | width: 60%; 96 | } 97 | } 98 | 99 | .user { 100 | align-items: center; 101 | margin: 0 0 0.5rem 0; 102 | padding: 0.5rem; 103 | background-color: white; 104 | border-radius: 8px; 105 | filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1)); 106 | transform: translate(-1px, -1px); 107 | transition: filter 0.2s, transform 0.2s; 108 | 109 | input { 110 | flex: 1; 111 | padding: 0.5em 2em 0.5em 0.8em; 112 | border-radius: 3px; 113 | outline: none; 114 | border: none; 115 | } 116 | button { 117 | width: 2em; 118 | height: 2em; 119 | border: none; 120 | background-color: transparent; 121 | background-position: 50% 50%; 122 | background-repeat: no-repeat; 123 | } 124 | } 125 | 126 | .text { 127 | position: relative; 128 | display: flex; 129 | align-items: center; 130 | flex: 1; 131 | } 132 | .save { 133 | position: absolute; 134 | right: 0; 135 | opacity: 0; 136 | background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A"); 137 | } 138 | 139 | .user input:focus + .save, 140 | .save:focus { 141 | transition: opacity 0.2s; 142 | opacity: 1; 143 | } 144 | -------------------------------------------------------------------------------- /frontend/src/sass/_globals.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 3 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | } 5 | 6 | body { 7 | min-height: 100vh; 8 | margin: 0; 9 | background-color: $primary-color; 10 | background: linear-gradient( 11 | 180deg, 12 | $primary-color 0%, 13 | $secondary-color 10.45%, 14 | $tertiary-color 41.35% 15 | ); 16 | display: flex; 17 | flex-direction: column; 18 | &::before { 19 | content: ''; 20 | width: 80vw; 21 | height: 100vh; 22 | position: absolute; 23 | top: 0; 24 | left: 10vw; 25 | z-index: -1; 26 | background: radial-gradient(50% 50% at 50% 50%, $pure-white 0%, rgba(255, 255, 255, 0) 100%); 27 | opacity: 0.05; 28 | } 29 | } 30 | h1, 31 | h2, 32 | p { 33 | font-weight: 400; 34 | color: $heading-color; 35 | } 36 | 37 | p { 38 | line-height: 1.5; 39 | } 40 | 41 | a { 42 | color: $accent-color; 43 | text-decoration: none; 44 | } 45 | 46 | a:hover { 47 | text-decoration: underline; 48 | } 49 | 50 | h1 { 51 | font-size: 2rem; 52 | text-align: center; 53 | } 54 | 55 | h2 { 56 | font-size: 1rem; 57 | } 58 | 59 | pre { 60 | font-size: 16px; 61 | font-family: $font-mono; 62 | background-color: rgba(255, 255, 255, 0.45); 63 | border-radius: 3px; 64 | // box-shadow: 2px 2px 6px rgba(255, 255, 255 / 25%); 65 | padding: 0.5em; 66 | overflow-x: auto; 67 | color: $text-color; 68 | } 69 | 70 | input, 71 | button { 72 | font-size: inherit; 73 | font-family: inherit; 74 | } 75 | 76 | button:focus:not(:focus-visible) { 77 | outline: none; 78 | } 79 | 80 | @media (min-width: 720px) { 81 | h1 { 82 | font-size: 2.4rem; 83 | } 84 | } 85 | 86 | main { 87 | flex: 1; 88 | display: flex; 89 | flex-direction: column; 90 | padding: 1rem; 91 | width: 100%; 92 | max-width: 1024px; 93 | margin: 0 auto; 94 | box-sizing: border-box; 95 | } 96 | 97 | footer { 98 | display: flex; 99 | flex-direction: column; 100 | justify-content: center; 101 | align-items: center; 102 | padding: 40px; 103 | margin-top: auto; 104 | a { 105 | font-weight: bold; 106 | } 107 | } 108 | 109 | @media (min-width: 480px) { 110 | footer { 111 | padding: 40px 0; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /frontend/src/sass/_header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .corner { 7 | width: 3em; 8 | height: 3em; 9 | a { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | width: 100%; 14 | height: 100%; 15 | } 16 | img { 17 | width: 2em; 18 | height: 2em; 19 | object-fit: contain; 20 | } 21 | } 22 | 23 | nav { 24 | display: flex; 25 | justify-content: center; 26 | --background: rgba(255, 255, 255, 0.7); 27 | } 28 | 29 | svg { 30 | width: 2em; 31 | height: 3em; 32 | display: block; 33 | } 34 | 35 | path { 36 | fill: var(--background); 37 | } 38 | 39 | ul { 40 | position: relative; 41 | padding: 0; 42 | margin: 0; 43 | height: 3em; 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | list-style: none; 48 | background: var(--background); 49 | background-size: contain; 50 | } 51 | 52 | li { 53 | position: relative; 54 | height: 100%; 55 | display: flex; 56 | align-items: center; 57 | text-transform: uppercase; 58 | font-size: 0.8rem; 59 | } 60 | 61 | li.active::before { 62 | --size: 6px; 63 | content: ''; 64 | width: 0; 65 | height: 0; 66 | position: absolute; 67 | top: 0; 68 | left: calc(50% - var(--size)); 69 | border: var(--size) solid transparent; 70 | border-top: var(--size) solid $accent-color; 71 | } 72 | 73 | nav { 74 | a { 75 | display: flex; 76 | height: 100%; 77 | align-items: center; 78 | padding: 0 1em; 79 | color: $heading-color; 80 | font-weight: 700; 81 | font-size: 0.8rem; 82 | 83 | letter-spacing: 0.1em; 84 | text-decoration: none; 85 | transition: color 0.2s linear; 86 | } 87 | } 88 | 89 | a:hover { 90 | color: $accent-color; 91 | } 92 | -------------------------------------------------------------------------------- /frontend/src/sass/_home.scss: -------------------------------------------------------------------------------- 1 | section { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | flex: 1; 7 | } 8 | 9 | h1 { 10 | width: 100%; 11 | } 12 | 13 | .welcome { 14 | position: relative; 15 | width: 100%; 16 | height: 0; 17 | padding: 0 0 calc(100% * 495 / 2048) 0; 18 | 19 | img { 20 | position: absolute; 21 | width: 100%; 22 | height: 100%; 23 | top: 0; 24 | display: block; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $font-mono: "Fira Mono", monospace; 2 | $pure-white: #ffffff; 3 | $primary-color: #b9c6d2; 4 | $secondary-color: #d0dde9; 5 | $tertiary-color: #edf0f8; 6 | $accent-color: #ff3e00; 7 | $heading-color: rgba(0, 0, 0, 0.7); 8 | $text-color: #444444; 9 | $background-without-opacity: rgba(255, 255, 255, 0.7); 10 | $column-width: 42rem; 11 | $column-margin-top: 4rem; 12 | -------------------------------------------------------------------------------- /frontend/src/sass/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'globals'; 3 | @import 'header'; 4 | @import 'about'; 5 | @import 'home'; 6 | @import 'form'; 7 | 8 | #svelte { 9 | min-height: 100vh; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | .center { 14 | text-align: center; 15 | } 16 | .error { 17 | color: red; 18 | text-transform: capitalize; 19 | } 20 | .center.error:not(:first-of-type) { 21 | margin-top: 0.1rem; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/frontend/static/favicon.png -------------------------------------------------------------------------------- /frontend/static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/static/svelte-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/frontend/static/svelte-welcome.png -------------------------------------------------------------------------------- /frontend/static/svelte-welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_svelte_jwt_auth/13d687ac00bde4c34e009cb93686d9219f2ac1ba/frontend/static/svelte-welcome.webp -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import vercel from '@sveltejs/adapter-vercel'; 2 | import preprocess from 'svelte-preprocess'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://github.com/sveltejs/svelte-preprocess 7 | // for more information about preprocessors 8 | preprocess: preprocess(), 9 | 10 | kit: { 11 | adapter: vercel() 12 | } 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import type { UserConfig } from 'vite'; 3 | 4 | const config: UserConfig = { 5 | plugins: [sveltekit()] 6 | }; 7 | 8 | export default config; 9 | --------------------------------------------------------------------------------