Skip to content

Commit

Permalink
feat(auth): implement JWT-based auth flow for register, login & token…
Browse files Browse the repository at this point in the history
… management

- Configure user registration and login with JWT tokens, set tokens in cookies
- Secure API views with IsAuthenticated, enforce permission checks
  • Loading branch information
trisDeveloper committed Oct 30, 2024
1 parent 61915de commit 118ae8b
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 53 deletions.
15 changes: 10 additions & 5 deletions backend/focusty/focusty/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# settings.py
import os
from pathlib import Path
from decouple import config
from decouple import config, Csv

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
Expand All @@ -12,11 +12,9 @@

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config("SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["trisdev.pythonanywhere.com", "127.0.0.1"]

INSTALLED_APPS = [
"django.contrib.admin",
Expand Down Expand Up @@ -123,3 +121,10 @@

# Path where media is stored
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")


REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}
20 changes: 16 additions & 4 deletions backend/focusty/focusty_app/serializers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
#serializers.py
# serializers.py
from rest_framework import serializers
from .models import User, Task, Pomodoro


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = fields = ('id', 'username', 'email', 'password', 'date_joined', 'birthday', 'profile_picture', 'country')
fields = fields = (
"id",
"username",
"email",
"password",
"date_joined",
"birthday",
"profile_picture",
"country",
)


class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
fields = "__all__"


class PomodoroSerializer(serializers.ModelSerializer):
class Meta:
model = Pomodoro
fields = ['date', 'minutes']
fields = ["date", "minutes"]
2 changes: 2 additions & 0 deletions backend/focusty/focusty_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TaskListCreate,
TaskDetail,
login_view,
RegisterView,
PomodoroListCreate,
PomodoroDetail,
tasks_count,
Expand All @@ -18,6 +19,7 @@
path("users/<int:user_id>/tasks/<int:pk>/", TaskDetail.as_view()),
path("users/<int:user_id>/tasks/all/", tasks_count, name="tasks count"),
path("login/", login_view, name="login"),
path("register/", RegisterView.as_view(), name="register"),
path("users/<int:user_id>/pomodoros/", PomodoroListCreate.as_view()),
path("users/<int:user_id>/pomodoros/<int:pk>/", PomodoroDetail.as_view()),
]
121 changes: 82 additions & 39 deletions backend/focusty/focusty_app/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# views.py
import uuid
from datetime import datetime
import uuid, json
from rest_framework import generics
from .models import User, Task, Pomodoro
from .serializers import TaskSerializer, UserSerializer, PomodoroSerializer
Expand All @@ -9,7 +8,10 @@
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
import json
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.tokens import RefreshToken


from django.db.models import Count, Sum
from .task_repeat import repeat_task

Expand All @@ -22,6 +24,83 @@ class UserList(generics.ListCreateAPIView):
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]

def get_object(self):
obj = super().get_object()
if obj != self.request.user:
return JsonResponse(
{"error": "You do not have permission to access this data."}
)
return obj


class RegisterView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer

def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)

user = self.get_user_from_response(response.data)

if user:
token = self.get_token(user)
response.set_cookie(key="jwt", value=str(token), httponly=True)
response.data["token"] = str(token)
return response

def get_token(self, user):

refresh = RefreshToken.for_user(user)
return refresh.access_token

def get_user_from_response(self, data):
try:
user_id = data.get("id")
return User.objects.get(id=user_id)
except User.DoesNotExist:
return None


@csrf_exempt
def login_view(request):
if request.method == "POST":
data = json.loads(request.body)
email = data.get("email")
password = data.get("password")

try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return JsonResponse(
{"success": False, "message": "User does not exist"}, status=404
)

if password == user.password:
# Generate token
refresh = RefreshToken.for_user(user)

return JsonResponse(
{
"success": True,
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
},
"access": str(refresh.access_token),
"refresh": str(refresh),
}
)
else:
return JsonResponse(
{"success": False, "message": "Invalid email or password"}, status=400
)
else:
return JsonResponse(
{"success": False, "message": "Method not allowed"}, status=405
)


class TaskListCreate(generics.ListCreateAPIView):
Expand Down Expand Up @@ -177,42 +256,6 @@ def tasks_count(request, user_id):
return Response(tasks_count_by_date)


@csrf_exempt
def login_view(request):
if request.method == "POST":
data = json.loads(request.body)
email = data.get("email")
password = data.get("password")

try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return JsonResponse(
{"success": False, "message": "User does not exist"}, status=404
)
if password == user.password:
print(user)
return JsonResponse(
{
"success": True,
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
},
}
)

else:
return JsonResponse(
{"success": False, "message": "Invalid email or password"}, status=400
)
else:
return JsonResponse(
{"success": False, "message": "Method not allowed"}, status=405
)


class PomodoroDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Pomodoro.objects.all()
serializer_class = PomodoroSerializer
Expand Down
2 changes: 1 addition & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dist
dist-ssr
coverage
*.local

*.env
/cypress/videos/
/cypress/screenshots/

Expand Down
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"axios": "^1.7.7",
"chart.js": "^4.4.5",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"pinia": "^2.2.4",
"sortablejs": "^1.15.3",
"uuid": "^10.0.0",
Expand Down
19 changes: 17 additions & 2 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import './styles.scss'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Cookies from 'js-cookie'
import axios from 'axios'
import App from './App.vue'
import router from './router'
Expand All @@ -9,8 +10,22 @@ import VueDatePicker from '@vuepic/vue-datepicker'
import '@vuepic/vue-datepicker/dist/main.css'
import 'v-calendar/style.css'

//axios.defaults.baseURL = 'https://trisdev.pythonanywhere.com'
axios.defaults.baseURL = 'http://127.0.0.1:8000/'
//axios
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers['Content-Type'] = 'application/json'
axios.interceptors.request.use(
(config) => {
const token = Cookies.get('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)

// font awesome icons
import { library } from '@fortawesome/fontawesome-svg-core'
import {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/views/user/log-in.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import { useStore } from '@/stores'
import Cookies from 'js-cookie'
const store = useStore()
const router = useRouter()
Expand All @@ -17,6 +18,11 @@ const login = async () => {
})
// Handle successful login
localStorage.setItem('userId', response.data.user.id)
const [token, refresh] = [response.data.access, response.data.refresh]
if (token) {
Cookies.set('token', token, { expires: 7, secure: true, sameSite: 'Strict' })
Cookies.set('refresh', refresh, { expires: 7, secure: true, sameSite: 'Strict' })
}
store.setUser({
id: response.data.user.id,
username: response.data.user.username,
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/views/user/sign-up.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup>
import { ref } from 'vue'
import Cookies from 'js-cookie'
import axios from 'axios'
import { useRouter } from 'vue-router'
import { useStore } from '@/stores'
Expand All @@ -12,13 +13,17 @@ const password = ref('')
const errormsg = ref(null)
const signup = async () => {
try {
const response = await axios.post('/api/users/', {
const response = await axios.post('/api/register/', {
username: username.value,
email: email.value,
password: password.value
})
// Handle successful signup
localStorage.setItem('userId', response.data.id)
const token = response.data.token
if (token) {
Cookies.set('token', token, { expires: 7, secure: true, sameSite: 'Strict' })
}
store.setUser({
id: response.data.id,
username: response.data.username,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/views/user/user-profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref } from 'vue'
import axios from 'axios'
import { useStore } from '@/stores'
import { useRouter } from 'vue-router'
import Cookies from 'js-cookie'
const router = useRouter()
const store = useStore()
const password = ref(null)
Expand Down Expand Up @@ -71,6 +72,7 @@ const deleteAccount = () => {
.then(() => {
//window.reload()
localStorage.clear()
Cookies.remove('token')
store.setUser(null)
router.push('/')
window.location.href = '/focusty/'
Expand All @@ -85,6 +87,7 @@ const logout = () => {
if (confirm('Are you sure you want to log out?')) {
store.setUser(null)
localStorage.clear()
Cookies.remove('token')
router.push('/')
window.location.href = '/'
Expand Down

0 comments on commit 118ae8b

Please sign in to comment.