Skip to content

Commit 79721da

Browse files
Merge pull request #19 from HE-Arc/6-login-registration-logic-users-rights
6 login registration logic users rights
2 parents c1a6ebd + 8132c95 commit 79721da

23 files changed

+396
-185
lines changed

api/fantasyforge/settings.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,18 @@
3232
ALLOWED_HOSTS = [
3333
"localhost",
3434
"api-fantasy-forge.k8s.ing.he-arc.ch",
35+
"127.0.0.1"
3536
]
3637

38+
ALLOWED_METHODS = [
39+
"*",
40+
"GET",
41+
"POST",
42+
"PUT",
43+
"DELETE",
44+
"OPTIONS",
45+
"HEAD",
46+
]
3747

3848
# Application definition
3949

@@ -46,6 +56,7 @@
4656
'django.contrib.staticfiles',
4757
'fantasyforgeapp',
4858
'rest_framework',
59+
'rest_framework_simplejwt', # for authentification
4960
'corsheaders',
5061
]
5162
MIDDLEWARE = [
@@ -90,7 +101,7 @@
90101
'USER': os.getenv('DB_USER'),
91102
'PASSWORD': os.getenv('DB_PASSWORD'),
92103
'HOST': os.getenv('DB_HOST'),
93-
'PORT': os.getenv('DB_PORT', '5432'), # Default to 5432
104+
'PORT': os.getenv('DB_PORT'),
94105
}
95106
}
96107

@@ -142,3 +153,16 @@
142153
]
143154

144155
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
156+
157+
REST_FRAMEWORK = {
158+
'DEFAULT_AUTHENTICATION_CLASSES': (
159+
'rest_framework_simplejwt.authentication.JWTAuthentication',
160+
),
161+
}
162+
163+
from datetime import timedelta
164+
165+
SIMPLE_JWT = {
166+
'ACCESS_TOKEN_LIFETIME': timedelta(days=1), # Tokens expire after 1 day
167+
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh token expires after 1 week
168+
}

api/fantasyforge/urls.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
from django.urls import path
1919
from django.urls import include
2020
from rest_framework.routers import DefaultRouter
21+
from fantasyforgeapp import views
2122

2223
router = DefaultRouter()
23-
# Add your viewsets to the router here, for example:
24-
# router.register(r'myviewset', views.MyViewSet)
24+
25+
router.register(r"auth", views.AuthViewSet, basename="auth")
2526

2627
urlpatterns = [
2728
path('admin/', admin.site.urls),
2829
path("api/", include("fantasyforgeapp.urls")),
30+
path("", include(router.urls)),
2931
]

api/fantasyforgeapp/models.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
from django.db import models
22

3-
# Create your models here.
3+
from django.contrib.auth.models import User
44

5-
# for intermediate eval enough:
6-
7-
#User
8-
#Character (for now name enough)
9-
#Class(?)
105

116
class Character(models.Model):
127
name = models.CharField(max_length=100)
8+
#owner = models.ForeignKey(User, on_delete=models.CASCADE)

api/fantasyforgeapp/serializers.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from rest_framework import serializers
22
from .models import Character
3+
from django.contrib.auth.models import User
4+
5+
from django.contrib.auth.password_validation import validate_password
6+
from django.core.exceptions import ValidationError
37

48
class CharacterSerializer(serializers.ModelSerializer):
59
class Meta:
@@ -15,4 +19,28 @@ def create(self, validated_data):
1519
def update(self, instance, validated_data):
1620
instance.name = validated_data.get("name", instance.name)
1721
instance.save()
18-
return instance
22+
return instance
23+
24+
25+
class UserSerializer(serializers.ModelSerializer):
26+
class Meta:
27+
model = User
28+
fields = ['id', 'username', 'password']
29+
extra_kwargs = {'password': {'write_only': True}}
30+
31+
def validate_password(self, value):
32+
"""
33+
Validate password against Django's password validators.
34+
"""
35+
try:
36+
validate_password(value)
37+
except ValidationError as e:
38+
raise serializers.ValidationError(e.messages)
39+
return value
40+
41+
def create(self, validated_data):
42+
password = validated_data.pop('password')
43+
user = User(**validated_data)
44+
user.set_password(password)
45+
user.save()
46+
return user

api/fantasyforgeapp/views.py

+57-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,62 @@
11

22
from .models import Character
3-
from .serializers import CharacterSerializer
4-
from rest_framework import viewsets
3+
from .serializers import CharacterSerializer, UserSerializer
4+
from rest_framework import viewsets, status
5+
from rest_framework.views import APIView
6+
from rest_framework.decorators import action
7+
from rest_framework.permissions import IsAuthenticated
8+
from rest_framework.response import Response
9+
from django.contrib.auth.models import User
10+
from rest_framework_simplejwt.tokens import RefreshToken
11+
from django.contrib.auth import authenticate
12+
from django.contrib.auth import authenticate, login, logout
513

14+
15+
# list of characters
616
class CharacterViewSet(viewsets.ModelViewSet):
17+
#permission_classes = [IsAuthenticated]
18+
719
queryset = Character.objects.all()
8-
serializer_class = CharacterSerializer
20+
serializer_class = CharacterSerializer
21+
22+
23+
# credit : https://github.com/HE-Arc/Instagenda/
24+
25+
class AuthViewSet(viewsets.ViewSet):
26+
27+
@action(detail=False, methods=['post'])
28+
def login(self, request):
29+
username = request.data.get('username')
30+
password = request.data.get('password')
31+
32+
user = authenticate(request, username=username, password=password)
33+
if user is not None:
34+
login(request, user)
35+
36+
return Response({"message": "User logged in"}, status=status.HTTP_200_OK)
37+
return Response({"error": "Invalid credentials"}, status=status.HTTP_400_BAD_REQUEST)
38+
39+
@action(detail=False, methods=['post'])
40+
def register(self, request):
41+
serializer = UserSerializer(data=request.data)
42+
if serializer.is_valid():
43+
user = serializer.save()
44+
login(request, user)
45+
return Response({"message": "User registered"}, status=status.HTTP_201_CREATED)
46+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
47+
48+
@action(detail=False, methods=['post'])
49+
def logout(self, request):
50+
if request.user.is_authenticated:
51+
logout(request)
52+
return Response({"message": "User logged out"}, status=status.HTTP_200_OK)
53+
return Response({"error": "User not logged in"}, status=status.HTTP_204_NO_CONTENT)
54+
55+
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
56+
def profile(self, request):
57+
if not request.user or request.user.is_anonymous:
58+
return Response({"error": "User not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
59+
60+
serializer = UserSerializer(request.user)
61+
return Response(serializer.data, status=status.HTTP_200_OK)
62+

api/requirements.txt

126 Bytes
Binary file not shown.

frontend/src/api.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import axios from 'axios';
2+
3+
// Create API instance
4+
const api = axios.create({
5+
headers: {
6+
"Content-Type": "application/json",
7+
},
8+
});
9+
10+
// Function to set Authorization token
11+
export function setAuthToken(token) {
12+
if (token) {
13+
api.defaults.headers["Authorization"] = `Bearer ${token}`;
14+
} else {
15+
delete api.defaults.headers["Authorization"];
16+
}
17+
}
18+
19+
export default api;

frontend/src/assets/main.css

+15-20
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,31 @@
88
font-weight: normal;
99
}
1010

11-
a,
12-
.green {
13-
text-decoration: none;
14-
color: hsla(160, 100%, 37%, 1);
15-
transition: 0.4s;
11+
.ff-input {
12+
@apply shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight;
1613
}
1714

18-
@media (hover: hover) {
19-
a:hover {
20-
background-color: hsla(160, 100%, 37%, 0.2);
21-
}
15+
.ff-input:focus {
16+
@apply outline-none ring;
2217
}
2318

24-
#app {
25-
max-width: 1280px;
26-
margin: 0 auto;
19+
.ff-button {
20+
@apply bg-red-500 text-white px-4 py-2 rounded;
21+
}
2722

28-
font-weight: normal;
23+
.ff-button:dark {
24+
@apply text-gray-900;
2925
}
3026

31-
a,
32-
.green {
33-
text-decoration: none;
34-
color: hsla(160, 100%, 37%, 1);
35-
transition: 0.4s;
27+
.ff-button:hover {
28+
@apply bg-red-800;
29+
cursor: pointer;
3630
}
3731

3832
@media (hover: hover) {
3933
a:hover {
40-
background-color: hsla(160, 100%, 37%, 0.2);
34+
background-color: hsla(0, 100%, 50%, 0.874);
35+
@apply rounded;
4136
}
4237
}
4338

@@ -52,4 +47,4 @@ a,
5247
grid-template-columns: 1fr;
5348
padding: 0 2rem;
5449
}
55-
}
50+
}

frontend/src/auth.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import api, { setAuthToken } from "./api";
2+
3+
// Register function
4+
export async function register(username, email, password, password2) {
5+
try {
6+
const response = await api.post("/auth/register/", {
7+
username,
8+
email,
9+
password,
10+
password2,
11+
});
12+
13+
return response.data;
14+
} catch (error) {
15+
console.error("Registration failed:", error.response.data);
16+
throw error.response.data;
17+
}
18+
}
19+
20+
// Login function
21+
export async function login(username, password) {
22+
try {
23+
const response = await api.post("/auth/login/", {
24+
username,
25+
password,
26+
});
27+
28+
const tokens = response.data; // {access, refresh}
29+
localStorage.setItem("access", tokens.access); // Save tokens
30+
localStorage.setItem("refresh", tokens.refresh);
31+
32+
console.log(localStorage.getItem("access"));
33+
34+
setAuthToken(tokens.access); // Set token for future requests
35+
return tokens;
36+
} catch (error) {
37+
console.error("Login failed:", error.response.data);
38+
throw error.response.data;
39+
}
40+
}
41+
42+
// Logout function
43+
export function logout() {
44+
localStorage.removeItem("access");
45+
localStorage.removeItem("refresh");
46+
setAuthToken(null); // Remove token from API
47+
}

frontend/src/components/CreateCharacter.vue

-1
This file was deleted.

frontend/src/components/HelloWorld.vue

-44
This file was deleted.

frontend/src/components/NavBar.vue

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import { ref } from 'vue'
99
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
1010
<ul class="flex justify-center items-center space-x-8 h-full">
1111
<li>
12-
<router-link :to="{ name: 'home' }">
1312
<img src="@/assets/logo-app.svg" alt="App Logo" class="h-12 w-auto block dark:hidden" />
1413
<img src="@/assets/logo-app-dark.svg" alt="App Logo Dark" class="h-12 w-auto hidden dark:block" />
15-
</router-link>
1614
</li>
1715
<li>
1816
<router-link :to="{ name: 'characters' }" class="text-gray-900 dark:text-gray-100 inline-block py-2 px-4 text-sm font-medium">
@@ -24,6 +22,12 @@ import { ref } from 'vue'
2422
About
2523
</router-link>
2624
</li>
25+
<li>
26+
<!-- TODO if logged in - log out -->
27+
<router-link :to="{ name: 'login' }" class="text-gray-400 dark:text-white-400 inline-block py-2 px-4 text-sm font-medium">
28+
Login
29+
</router-link>
30+
</li>
2731
</ul>
2832
</div>
2933
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template>
2+
<svg class="text-pink-500 w-10 h-10"
3+
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z"/> <path d="M9 7 h-3a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-3" /> <path d="M9 15h3l8.5 -8.5a1.5 1.5 0 0 0 -3 -3l-8.5 8.5v3" /> <line x1="16" y1="5" x2="19" y2="8" /></svg>
4+
</template>

0 commit comments

Comments
 (0)