Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

ALLOWED_HOSTS = ["*"]

# Application definition

Expand All @@ -37,9 +36,18 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'rest_framework',
'corsheaders',
'django_extensions',

'users',
]

AUTH_USER_MODEL = 'users.User'

MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
Expand All @@ -49,6 +57,23 @@
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ALLOW_CREDENTIALS = True

CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # frontend
]

CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000",
]

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
}


ROOT_URLCONF = 'config.urls'

TEMPLATES = [
Expand Down
5 changes: 4 additions & 1 deletion backend/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path("api/auth/", include("users.auth_urls")),
path("api/user/", include("users.profile_urls")),
]

Binary file modified backend/requirements.txt
Binary file not shown.
17 changes: 17 additions & 0 deletions backend/users/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

@admin.register(User)
class CustomUserAdmin(UserAdmin):
model = User

list_display = ("username", "email", "avatar_url", "created_at", "is_staff")

fieldsets = UserAdmin.fieldsets + (
("Profile info", {"fields": ("avatar_url", "bio")}),
)

add_fieldsets = UserAdmin.add_fieldsets + (
("Profile info", {"fields": ("avatar_url", "bio")}),
)
10 changes: 10 additions & 0 deletions backend/users/auth_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path
from .views.auth_views import csrf_view, login_view, register_view, me_view, logout_view

urlpatterns = [
path("csrf/", csrf_view),
path("login/", login_view),
path("register/", register_view),
path("me/", me_view),
path("logout/", logout_view),
]
47 changes: 47 additions & 0 deletions backend/users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 6.0.3 on 2026-03-11 18:15

import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]

operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('avatar_url', models.URLField(blank=True, null=True)),
('bio', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
Empty file.
8 changes: 8 additions & 0 deletions backend/users/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
email = models.EmailField(unique=True)
avatar_url = models.URLField(blank=True, null=True)
bio = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
7 changes: 7 additions & 0 deletions backend/users/profile_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path
from .views.profile_views import delete_account, change_password

urlpatterns = [
path("delete/", delete_account),
path("change-password/", change_password),
]
13 changes: 13 additions & 0 deletions backend/users/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from rest_framework import serializers
from .models import User

class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)

class Meta:
model = User
fields = ["username", "email", "password"]

def create(self, validated_data):
user = User.objects.create_user(**validated_data)
return user
86 changes: 86 additions & 0 deletions backend/users/views/auth_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# LOGIN
from django.contrib.auth import authenticate, login, logout
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_protect

@api_view(["POST"])
@csrf_protect
def login_view(request):
username = request.data.get("username")
password = request.data.get("password")

user = authenticate(username=username, password=password)

if user is not None:
login(request, user)
return Response({
"user": {
"username": user.username,
"email": user.email,
"avatar_url": user.avatar_url,
"bio": user.bio,
"created_at": user.created_at,
}
})

return Response({"error": "Invalid credentials"}, status=400)


# REGISTER
from ..serializers import RegisterSerializer

@api_view(["POST"])
@csrf_protect
def register_view(request):
serializer = RegisterSerializer(data=request.data)

if serializer.is_valid():
user = serializer.save()
return Response({
"user": {
"username": user.username,
"email": user.email,
"avatar_url": user.avatar_url,
"bio": user.bio,
"created_at": user.created_at,
}
}, status=201)

return Response({"error": serializer.errors}, status=400)


# CURRENT USER/ME
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import permission_classes

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def me_view(request):
user = request.user
return Response({
"user": {
"username": user.username,
"email": user.email,
"avatar_url": user.avatar_url,
"bio": user.bio,
"created_at": user.created_at,
}
})


# LOGOUT
@api_view(["POST"])
def logout_view(request):
logout(request)
return Response({"message": "Logged out"})


# CSRF
from django.middleware.csrf import get_token
from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(["GET"])
def csrf_view(request):
return Response({"csrfToken": get_token(request)})
31 changes: 31 additions & 0 deletions backend/users/views/profile_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(["DELETE"])
@permission_classes([IsAuthenticated])
def delete_account(request):
user = request.user
user.delete()
return Response({"message": "Account deleted"})

from django.contrib.auth import update_session_auth_hash

@api_view(["POST"])
@permission_classes([IsAuthenticated])
def change_password(request):
user = request.user
old_password = request.data.get("old_password")
new_password = request.data.get("new_password")

if not user.check_password(old_password):
return Response({"error": "Incorrect current password"}, status=400)

user.set_password(new_password)
user.save()

# ważne: nie wylogowuje użytkownika
update_session_auth_hash(request, user)

return Response({"message": "Password updated"})

29 changes: 29 additions & 0 deletions frontend/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { getMe, ApiResponse, User } from "@/lib/api";
import FullscreenLoader from "@/components/dashboard/FullscreenLoader";

export default function AuthLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const [checking, setChecking] = useState(true);

useEffect(() => {
const check = async () => {
const data: ApiResponse<User> = await getMe();

if (data.user) {
router.push("/dashboard");
} else {
setChecking(false);
}
};

check();
}, [router]);

if (checking) return <FullscreenLoader />;

return <>{children}</>;
}
27 changes: 27 additions & 0 deletions frontend/app/api/user/delete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function DELETE() {
const cookieStore = await cookies();

const session = cookieStore.get("sessionid");
const csrf = cookieStore.get("csrftoken");

const cookieHeader = [
session ? `sessionid=${session.value}` : "",
csrf ? `csrftoken=${csrf.value}` : "",
]
.filter(Boolean)
.join("; ");

const res = await fetch("http://localhost:8000/api/user/delete/", {
method: "DELETE",
headers: {
Cookie: cookieHeader,
"X-CSRFToken": csrf?.value ?? "",
},
});

const data = await res.json();
return NextResponse.json(data, { status: res.status });
}
30 changes: 30 additions & 0 deletions frontend/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { getMe } from "@/lib/api";
import FullscreenLoader from "@/components/dashboard/FullscreenLoader";
import Sidebar from "@/components/dashboard/Sidebar";

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchUser = async () => {
const data = await getMe();
if (!data.user) router.push("/login");
else setLoading(false);
};
fetchUser();
}, []);

if (loading) return <FullscreenLoader />;

return (
<div className="min-h-screen bg-background text-foreground md:pl-20">
<Sidebar />
<main className="p-6 md:p-10">{children}</main>
</div>
);
}
Loading