11 Commits

Author SHA1 Message Date
ITQ 50662548f2 [fix] Fixed CI 2024-04-01 18:04:30 +03:00
Data-Name-ID 268a541466 [fix] 2024-04-01 18:00:52 +03:00
Data-Name-ID 1b84b746d1 Merge branch 'fix-auth' into develop 2024-04-01 17:49:10 +03:00
Data-Name-ID 25a47d660c [feat] vacancy 2024-04-01 17:48:02 +03:00
Data-Name-ID 851017847c [fix] auth 2024-04-01 17:31:33 +03:00
ITQ bf0f46428f [chore] Changed url paths 2024-04-01 14:18:06 +03:00
Data-Name-ID c8b070368b [fix] apps names 2024-04-01 14:15:43 +03:00
ITQ 0fa37bf7fc [chore] Improvements in docker structure 2024-04-01 14:06:30 +03:00
ITQ b88ae25d67 [chore] Changed frontend port 2024-04-01 12:21:43 +03:00
ITQ 49a2ef525f [fix] Fixes after refactoring 2024-04-01 12:17:38 +03:00
ITQ c1807ce032 Merge pull request #1 from Central-University-IT-prod/main
Added CI/CD and global project refactoring
2024-04-01 11:46:53 +03:00
29 changed files with 386 additions and 554 deletions
+2
View File
@@ -0,0 +1,2 @@
pyproject.toml
poetry.lock
+1 -1
View File
@@ -3,4 +3,4 @@ from django.apps import AppConfig
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "core" name = "api.core"
+3 -2
View File
@@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from notifications import models
from notifications.forms import ( from api.notifications import models
from api.notifications.forms import (
CreateNotificationAdminForm, CreateNotificationAdminForm,
EditNotificationAdminForm, EditNotificationAdminForm,
) )
+1 -1
View File
@@ -3,4 +3,4 @@ from django.apps import AppConfig
class NotificationsConfig(AppConfig): class NotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "notifications" name = "api.notifications"
+2 -1
View File
@@ -1,5 +1,6 @@
from django import forms from django import forms
from notifications.models import Notification
from api.notifications.models import Notification
class EditNotificationAdminForm(forms.ModelForm): class EditNotificationAdminForm(forms.ModelForm):
@@ -0,0 +1,24 @@
# Generated by Django 5.0.3 on 2024-04-01 14:59
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=150)),
('content', models.TextField(verbose_name='content')),
('read', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.0.3 on 2024-04-01 14:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('notifications', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='notification',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL),
),
]
+1 -1
View File
@@ -3,4 +3,4 @@ from django.apps import AppConfig
class TeamsConfig(AppConfig): class TeamsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "teams" name = "api.teams"
@@ -1,9 +1,7 @@
# Generated by Django 4.2.11 on 2024-03-31 19:06 # Generated by Django 5.0.3 on 2024-04-01 14:59
from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -11,35 +9,28 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('users', '0002_rename_technologies_user_skills'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel(
name='Vacancy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='название вакансии')),
('start_date', models.DateField(blank=True, null=True, verbose_name='дата начала диапазона возраста участников')),
('end_date', models.DateField(blank=True, null=True, verbose_name='дата конец диапазона возраста участников')),
('skills', models.ManyToManyField(blank=True, to='users.skill', verbose_name='Технологии')),
('specialization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.specialization', verbose_name='специализация')),
],
),
migrations.CreateModel( migrations.CreateModel(
name='Team', name='Team',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(verbose_name='описание команды')), ('name', models.CharField(max_length=255)),
('name', models.CharField(max_length=255, verbose_name='название команды')), ('description', models.TextField()),
('avatar', models.ImageField(blank=True, upload_to='teams_avatars', verbose_name='аватарка')), ('avatar', models.ImageField(blank=True, upload_to='teams_avatars')),
('count_of_members', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxLengthValidator(5)], verbose_name='количество участников')), ('count_of_members', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxLengthValidator(5)], verbose_name='количество участников')),
('country', models.CharField(blank=True, max_length=255, verbose_name='страна')), ('country', models.CharField(blank=True, max_length=255, verbose_name='страна')),
('city', models.CharField(blank=True, max_length=255, verbose_name='город')), ('city', models.CharField(blank=True, max_length=255, verbose_name='город')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to=settings.AUTH_USER_MODEL)), ],
('members', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='участники')), ),
('vacancies', models.ManyToManyField(to='teams.vacancy', verbose_name='вакансии')), migrations.CreateModel(
name='Vacancy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('start_date', models.DateField(blank=True, null=True)),
('end_date', models.DateField(blank=True, null=True)),
], ],
), ),
] ]
@@ -0,0 +1,49 @@
# Generated by Django 5.0.3 on 2024-04-01 14:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('teams', '0001_initial'),
('users', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='team',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='team',
name='members',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='vacancy',
name='skills',
field=models.ManyToManyField(blank=True, related_name='vacancies', to='users.skill'),
),
migrations.AddField(
model_name='vacancy',
name='specialization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.specialization'),
),
migrations.AddField(
model_name='vacancy',
name='users',
field=models.ManyToManyField(blank=True, related_name='vacancies', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='team',
name='vacancies',
field=models.ManyToManyField(blank=True, to='teams.vacancy'),
),
]
+12 -5
View File
@@ -8,7 +8,11 @@ class Vacancy(models.Model):
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
) )
age_restriction = models.DateField( start_date = models.DateField(
blank=True,
null=True,
)
end_date = models.DateField(
blank=True, blank=True,
null=True, null=True,
) )
@@ -21,6 +25,12 @@ class Vacancy(models.Model):
skills = models.ManyToManyField( skills = models.ManyToManyField(
Skill, Skill,
blank=True, blank=True,
related_name="vacancies",
)
users = models.ManyToManyField(
User,
blank=True,
related_name="vacancies",
) )
def __str__(self): def __str__(self):
@@ -34,13 +44,11 @@ class Team(models.Model):
members = models.ManyToManyField( members = models.ManyToManyField(
User, User,
blank=True, blank=True,
unique=True,
) )
vacancies = models.ManyToManyField( vacancies = models.ManyToManyField(
Vacancy, Vacancy,
blank=True, blank=True,
unique=True,
) )
avatar = models.ImageField( avatar = models.ImageField(
@@ -70,8 +78,7 @@ class Team(models.Model):
) )
author = models.ForeignKey( author = models.ForeignKey(
User, User, on_delete=models.CASCADE, related_name="teams"
on_delete=models.CASCADE,
) )
def __str__(self): def __str__(self):
+26 -1
View File
@@ -1,8 +1,33 @@
from datetime import datetime, timedelta, timezone
from rest_framework import serializers from rest_framework import serializers
from teams.models import Team
from api.teams.models import Team, Vacancy
class TeamSerializer(serializers.ModelSerializer): class TeamSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Team model = Team
fields = ["id", "name", "description"] fields = ["id", "name", "description"]
class VacancySerializer(serializers.ModelSerializer):
min_age = serializers.IntegerField(write_only=True, required=True)
max_age = serializers.IntegerField(write_only=True, required=True)
class Meta:
model = Vacancy
fields = "__all__"
def create(self, validated_data):
min_age = validated_data.pop("min_age")
max_age = validated_data.pop("max_age")
validated_data["start_date"] = datetime.now(
timezone.utc
).date() - timedelta(days=365 * min_age)
validated_data["end_date"] = datetime.now(
timezone.utc
).date() - timedelta(days=365 * max_age)
return Team.objects.create(**validated_data)
+6 -1
View File
@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from .views import AddUserToTeam from .views import AddUserToTeam, CreateVacancy
urlpatterns = [ urlpatterns = [
path( path(
@@ -8,4 +8,9 @@ urlpatterns = [
AddUserToTeam.as_view(), AddUserToTeam.as_view(),
name="add_user_to_team", name="add_user_to_team",
), ),
path(
"create_vacancy/",
CreateVacancy.as_view(),
name="create_vacancy",
),
] ]
+9 -3
View File
@@ -1,10 +1,11 @@
from backend.project.users.models import User
from rest_framework import status from rest_framework import status
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from teams.models import Team
from .serializers import TeamSerializer from api.teams.models import Team
from api.teams.serializers import TeamSerializer, VacancySerializer
from api.users.models import User
class AddUserToTeam(APIView): class AddUserToTeam(APIView):
@@ -24,3 +25,8 @@ class AddUserToTeam(APIView):
team.members.add(user) team.members.add(user)
team_serializer = TeamSerializer(team) team_serializer = TeamSerializer(team)
return Response(team_serializer.data, status=status.HTTP_200_OK) return Response(team_serializer.data, status=status.HTTP_200_OK)
class CreateVacancy(CreateAPIView):
http_method_names = ("post",)
serializer_class = VacancySerializer
+33
View File
@@ -1,6 +1,39 @@
from django.urls import include, path from django.urls import include, path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(title="SkillHub API", default_version="v1"),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [ urlpatterns = [
path("ping", include("api.ping.urls")), path("ping", include("api.ping.urls")),
path("auth", include("api.users.urls")), path("auth", include("api.users.urls")),
path(
"",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
path("teams", include("api.teams.urls")),
# API documentation
path(
"swagger<format>/",
schema_view.without_ui(cache_timeout=0),
name="schema-json",
),
path(
"swagger/",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
path(
"redoc/",
schema_view.with_ui("redoc", cache_timeout=0),
name="schema-redoc",
),
] ]
@@ -1,51 +0,0 @@
import bcrypt
import jwt
from django.conf import settings
from rest_framework.authentication import (
BaseAuthentication,
)
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated
from rest_framework.permissions import IsAuthenticated
from api.users.models import User
class JWTAuthentication(BaseAuthentication):
def authenticate_header(self, request): # noqa: ARG002
return "Provide a valid token in the 'Authorization' header"
def authenticate(self, request):
if IsAuthenticated not in getattr(
request.resolver_match.func.cls, "permission_classes", []
):
return None
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
if not token:
raise NotAuthenticated
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=["HS256"]
)
user = User.objects.get(id=payload["id"])
if not bcrypt.checkpw(
payload["password"].encode("utf-8"),
user.password.encode("utf-8"),
):
error = "Token has expired"
raise AuthenticationFailed(error)
except User.DoesNotExist:
error = "Invalid token"
raise AuthenticationFailed(error) from None
except jwt.ExpiredSignatureError:
error = "Token has expired"
raise AuthenticationFailed(error) from None
except jwt.InvalidTokenError:
error = "Invalid token"
raise AuthenticationFailed(error) from None
else:
return (user, None)
@@ -1,12 +1,12 @@
# Generated by Django 4.2.11 on 2024-04-01 01:58 # Generated by Django 5.0.3 on 2024-04-01 14:59
import api.users.models import api.users.models
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
import django.core.validators import django.core.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -14,225 +14,72 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("auth", "0012_alter_user_first_name_max_length"), ('auth', '0012_alter_user_first_name_max_length'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Achievements", name='Achievements',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('file', models.FileField(upload_to=api.users.models.get_file_path)),
models.BigAutoField( ('info', models.TextField(max_length=255)),
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
upload_to=api.users.models.Achievements.get_file_path
),
),
("info", models.TextField(max_length=255)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="Skill", name='Skill',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255, unique=True)),
models.BigAutoField( ('level', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
(
"level",
models.IntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(10),
]
),
),
], ],
options={ options={
"abstract": False, 'abstract': False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="Specialization", name='Specialization',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255, unique=True)),
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
], ],
options={ options={
"abstract": False, 'abstract': False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="User", name='User',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('password', models.CharField(max_length=128, verbose_name='password')),
models.BigAutoField( ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
auto_created=True, ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
primary_key=True, ('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')),
serialize=False, ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
verbose_name="ID", ('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')),
("password", models.CharField(max_length=128, verbose_name="password")), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
( ('email', models.EmailField(max_length=254, unique=True)),
"last_login", ('birthday', models.DateField(blank=True, null=True)),
models.DateTimeField( ('avatar', models.ImageField(max_length=200, null=True, upload_to=api.users.models.User.get_file_path)),
blank=True, null=True, verbose_name="last login" ('country', models.TextField(blank=True)),
), ('city', models.TextField(blank=True)),
), ('experience', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MinValueValidator(100)])),
( ('bio', models.TextField(blank=True, validators=[django.core.validators.MaxLengthValidator(512)])),
"is_superuser", ('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')),
models.BooleanField( ('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')),
default=False, ('achievements', models.ManyToManyField(blank=True, to='users.achievements')),
help_text="Designates that this user has all permissions without explicitly assigning them.", ('skills', models.ManyToManyField(blank=True, to='users.skill')),
verbose_name="superuser status", ('specialization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.specialization')),
),
),
(
"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)),
("birthday", models.DateField(blank=True, null=True)),
(
"avatar",
models.ImageField(
max_length=200,
null=True,
upload_to=api.users.models.User.get_file_path,
),
),
("country", models.TextField(blank=True)),
("city", models.TextField(blank=True)),
(
"experience",
models.IntegerField(
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MinValueValidator(100),
],
),
),
(
"bio",
models.TextField(
blank=True,
validators=[django.core.validators.MaxLengthValidator(512)],
),
),
(
"achievements",
models.ManyToManyField(blank=True, to="users.achievements"),
),
(
"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",
),
),
("skills", models.ManyToManyField(blank=True, to="users.skill")),
(
"specialization",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="users.specialization",
),
),
(
"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={ options={
"verbose_name": "user", 'verbose_name': 'user',
"verbose_name_plural": "users", 'verbose_name_plural': 'users',
"abstract": False, 'abstract': False,
}, },
managers=[ managers=[
("objects", django.contrib.auth.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],
), ),
] ]
+6 -8
View File
@@ -7,6 +7,10 @@ from django.db import models
from api.core.models import AbstractTag from api.core.models import AbstractTag
def get_file_path(filename):
return f"achievements/{uuid.uuid4()}/{filename}"
class Skill(AbstractTag): class Skill(AbstractTag):
level = models.IntegerField( level = models.IntegerField(
validators=[ validators=[
@@ -17,18 +21,12 @@ class Skill(AbstractTag):
class Achievements(models.Model): class Achievements(models.Model):
def get_file_path(self, filename): file = models.FileField(upload_to=get_file_path)
folder_name = str(uuid.uuid4())
return f"achievements/{folder_name}/{filename}"
file = models.FileField( # noqa: DJ012
upload_to=get_file_path,
)
info = models.TextField( info = models.TextField(
max_length=255, max_length=255,
) )
def __str__(self): # noqa: DJ012 def __str__(self):
return self.info return self.info
+36 -55
View File
@@ -1,79 +1,60 @@
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.hashers import check_password, make_password
from rest_framework import serializers from rest_framework import serializers
from api.users.models import User from api.users.models import User
class UserRegistrationSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"first_name",
"last_name",
"username",
"email",
"password",
"country",
"city",
]
def validate_password(self, value):
validate_password(value)
return value
class UserLoginSerializer(serializers.Serializer):
remember_me = serializers.BooleanField(default=False, required=False)
username = serializers.CharField()
password = serializers.CharField()
class UserProfileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ( fields = (
"first_name",
"last_name",
"username",
"email", "email",
"birthday", "birthday",
"avatar",
"country", "country",
"city", "city",
"bio", "bio",
"avatar",
"experience", "experience",
"password",
"first_name",
"last_name",
"specialization", "specialization",
"achievements", "achievements",
"username",
"skills", "skills",
) )
extra_kwargs = {"password": {"write_only": True}}
def validate(self, attrs):
if User.objects.filter(username=attrs["username"]).exists():
raise serializers.ValidationError(
{"username": "Username already exists"}
)
return super().validate(attrs)
def create(self, validated_data):
validated_data["password"] = make_password(
validated_data["password"],
)
return super().create(validated_data)
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(write_only=True)
new_password = serializers.CharField(source="password", write_only=True)
class UpdateProfileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = [ fields = ("old_password", "new_password")
"countryCode",
"isPublic",
"phone",
"image",
]
def to_representation(self, instance):
data = super().to_representation(instance)
if data["image"] is None:
del data["image"]
if data["phone"] is None:
del data["phone"]
return data
class PasswordChangeSerializer(serializers.Serializer):
# ruff: noqa: N815
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
def validate_password(self, value):
validate_password(value)
def validate_old_password(self, value):
if not check_password(value, self.instance.password):
msg = "Wrong password"
raise serializers.ValidationError(msg)
return value return value
def update(self, instance, validated_data):
instance.set_password(validated_data["password"])
instance.save()
return instance
+19 -4
View File
@@ -1,25 +1,40 @@
from django.urls import path from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
import api.users.views import api.users.views
urlpatterns = [ urlpatterns = [
path( path(
"/sign-up/", "/signup/",
api.users.views.SignupUserApiView.as_view(), api.users.views.SignupUserApiView.as_view(),
name="sign-up", name="signup",
), ),
path( path(
"/sign-in/", "/sign-in/",
api.users.views.SigninUserApiView.as_view(), TokenObtainPairView.as_view(),
name="sign-in", name="sign-in",
), ),
path(
"api/token/refresh/",
TokenRefreshView.as_view(),
name="token_refresh",
),
path(
"api/token/verify/",
TokenVerifyView.as_view(),
name="token_verify",
),
path( path(
"/me/profile/", "/me/profile/",
api.users.views.ProfileMeApiView.as_view(), api.users.views.ProfileMeApiView.as_view(),
name="profile-me", name="profile-me",
), ),
path( path(
"/me/updatePassword/", "/me/password/",
api.users.views.PasswordChangeApiView.as_view(), api.users.views.PasswordChangeApiView.as_view(),
name="password-change", name="password-change",
), ),
+13 -117
View File
@@ -1,131 +1,27 @@
from datetime import timedelta from rest_framework.generics import CreateAPIView, UpdateAPIView
import bcrypt
import jwt
from django.conf import settings
from django.utils import timezone
from rest_framework import status
from rest_framework.exceptions import (
NotAuthenticated,
PermissionDenied,
ValidationError,
)
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from api.users.models import User
from api.users.serializers import ( from api.users.serializers import (
PasswordChangeSerializer, ChangePasswordSerializer,
UpdateProfileSerializer, UserSerializer,
UserLoginSerializer,
UserProfileSerializer,
UserRegistrationSerializer,
) )
class SignupUserApiView(APIView): class SignupUserApiView(CreateAPIView):
def post(self, request): http_method_names = ("post",)
serializer = UserRegistrationSerializer(data=request.data) serializer_class = UserSerializer
if serializer.is_valid():
password = serializer.validated_data["password"]
password_hash = bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
serializer.validated_data["password"] = password_hash
serializer.save()
return Response("ok", status=status.HTTP_201_CREATED)
raise ValidationError(serializer.errors)
class SigninUserApiView(APIView): class ProfileMeApiView(UpdateAPIView):
def post(self, request):
serializer = UserLoginSerializer(data=request.data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)
username = serializer.validated_data.get("username")
password = serializer.validated_data.get("password")
user = User.objects.filter(username=username).first()
if user is not None:
if not bcrypt.checkpw(
password.encode("utf-8"), user.password.encode("utf-8")
):
raise NotAuthenticated(
{"error": "Invalid credentials"},
)
else:
raise NotAuthenticated(
{"error": "Invalid credentials"},
)
token = jwt.encode(
{
"id": user.id,
"password": password,
"exp": timezone.now() + timedelta(hours=24),
},
settings.SECRET_KEY,
algorithm="HS256",
)
return Response({"token": token})
class ProfileMeApiView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request): def get_object(self):
serializer = UserProfileSerializer(request.user) return self.request.user
return Response(serializer.data)
def patch(self, request):
user = request.user
serializer = UpdateProfileSerializer(
user, data=request.data, partial=True
)
if serializer.is_valid():
errors = User.check_unique(user.id, serializer.validated_data)
if errors:
return Response(
{"reason:": str(errors)}, status=status.HTTP_409_CONFLICT
)
serializer.save()
return Response(self._get_profile_data(user))
raise ValidationError(serializer.errors)
class PasswordChangeApiView(APIView): class PasswordChangeApiView(UpdateAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = ChangePasswordSerializer
def post(self, request): def get_object(self):
serializer = PasswordChangeSerializer(data=request.data) return self.request.user
if serializer.is_valid():
old_password = serializer.validated_data.get("oldPassword")
new_password = serializer.validated_data.get("newPassword")
if bcrypt.checkpw(
old_password.encode("utf-8"),
request.user.password.encode("utf-8"),
):
password_hash = bcrypt.hashpw(
new_password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
request.user.password = password_hash
request.user.save()
return Response({"status": "ok"}, status=status.HTTP_200_OK)
raise PermissionDenied({"error": "Invalid old password"})
raise ValidationError(serializer.errors)
+18 -11
View File
@@ -1,5 +1,6 @@
import pathlib import pathlib
import sys import sys
from datetime import timedelta
import environs import environs
@@ -49,12 +50,14 @@ INSTALLED_APPS = [
# Developed apps # Developed apps
"api.ping.apps.PingConfig", "api.ping.apps.PingConfig",
"api.users.apps.UsersConfig", "api.users.apps.UsersConfig",
"api.teams.apps.TeamsConfig",
"api.core.apps.CoreConfig",
"api.notifications.apps.NotificationsConfig",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
@@ -130,15 +133,12 @@ AUTH_PASSWORD_VALIDATORS = [
] ]
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True USE_I18N = True
STATIC_URL = "static/" USE_TZ = True
TIME_ZONE = "UTC"
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static" STATIC_ROOT = BASE_DIR / "static"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -149,12 +149,19 @@ REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [ "DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend" "django_filters.rest_framework.DjangoFilterBackend"
], ],
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": [
"api.users.authentication.JWTAuthentication", "rest_framework_simplejwt.authentication.JWTAuthentication",
), ],
}
CORS_ORIGIN_ALLOW_ALL = True
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
} }
APPEND_SLASH = False
if DEBUG and not (TESTING or MIGRATING): if DEBUG and not (TESTING or MIGRATING):
INSTALLED_APPS.append("debug_toolbar") INSTALLED_APPS.append("debug_toolbar")
-33
View File
@@ -1,43 +1,10 @@
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(title="SkillHub API", default_version="v1"),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [ urlpatterns = [
# Built-in urls # Built-in urls
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path(
"api-auth/",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
# API documentation
path(
"swagger<format>/",
schema_view.without_ui(cache_timeout=0),
name="schema-json",
),
path(
"swagger/",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
path(
"redoc/",
schema_view.with_ui("redoc", cache_timeout=0),
name="schema-redoc",
),
# API # API
path("api/", include("api.urls")), path("api/", include("api.urls")),
] ]
@@ -1,18 +0,0 @@
# Generated by Django 4.2.11 on 2024-03-31 19:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('notifications', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='content',
field=models.TextField(verbose_name='содержание'),
),
]
+22 -11
View File
@@ -21,12 +21,12 @@ services:
backend: backend:
build: ./backend build: ./backend
container_name: backend container_name: backend
volumes:
- media_volume:/app/project/media/
- static_volume:/app/project/static/
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
restart: unless-stopped
expose:
- 8000
environment: environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
@@ -38,18 +38,28 @@ services:
DJANGO_DEBUG: ${DJANGO_DEBUG:-false} DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-*} DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-*}
DJANGO_INTERNAL_IPS: ${DJANGO_INTERNAL_IPS:-127.0.0.1} DJANGO_INTERNAL_IPS: ${DJANGO_INTERNAL_IPS:-127.0.0.1}
command: ["sh", "-c", "cd project && python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8080"] expose:
ports: - 8080
- 8080:8080 command:
[
"sh",
"-c",
"cd project && python manage.py collectstatic --noinput && python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8080",
]
frontend: nginx:
container_name: frontend container_name: nginx
build: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "3000:3000" - "80:80"
restart: always volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- media_volume:/var/html/media/
- static_volume:/var/html/static/
depends_on:
- backend
pgadmin: pgadmin:
image: dpage/pgadmin4:8.4 image: dpage/pgadmin4:8.4
@@ -68,5 +78,6 @@ services:
volumes: volumes:
postgres_data: postgres_data:
redis_data:
pgadmin_data: pgadmin_data:
media_volume:
static_volume:
-2
View File
@@ -15,5 +15,3 @@ RUN npm run build
FROM nginx:stable-alpine3.17-slim FROM nginx:stable-alpine3.17-slim
COPY --from=builder /app/dist /usr/share/nginx/html COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
-9
View File
@@ -1,9 +0,0 @@
server {
listen 3000;
location / {
root /usr/share/nginx/html/;
include /etc/nginx/mime.types;
try_files $uri $uri/ /index.html;
}
}
+23
View File
@@ -0,0 +1,23 @@
server {
listen 80;
server_tokens off;
location /api/ {
proxy_pass http://backend:8080/api/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location / {
root /usr/share/nginx/html/;
index index.html;
try_files $uri $uri/ /index.html;
}
location /media/ {
root /var/html/;
}
location /static/ {
root /var/html/;
}
}