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):
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 notifications import models
from notifications.forms import (
from api.notifications import models
from api.notifications.forms import (
CreateNotificationAdminForm,
EditNotificationAdminForm,
)
+1 -1
View File
@@ -3,4 +3,4 @@ from django.apps import AppConfig
class NotificationsConfig(AppConfig):
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 notifications.models import Notification
from api.notifications.models import Notification
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):
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
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@@ -11,35 +9,28 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('users', '0002_rename_technologies_user_skills'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
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(
name='Team',
fields=[
('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, verbose_name='название команды')),
('avatar', models.ImageField(blank=True, upload_to='teams_avatars', verbose_name='аватарка')),
('name', models.CharField(max_length=255)),
('description', models.TextField()),
('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='количество участников')),
('country', 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(
max_length=255,
)
age_restriction = models.DateField(
start_date = models.DateField(
blank=True,
null=True,
)
end_date = models.DateField(
blank=True,
null=True,
)
@@ -21,6 +25,12 @@ class Vacancy(models.Model):
skills = models.ManyToManyField(
Skill,
blank=True,
related_name="vacancies",
)
users = models.ManyToManyField(
User,
blank=True,
related_name="vacancies",
)
def __str__(self):
@@ -34,13 +44,11 @@ class Team(models.Model):
members = models.ManyToManyField(
User,
blank=True,
unique=True,
)
vacancies = models.ManyToManyField(
Vacancy,
blank=True,
unique=True,
)
avatar = models.ImageField(
@@ -70,8 +78,7 @@ class Team(models.Model):
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
User, on_delete=models.CASCADE, related_name="teams"
)
def __str__(self):
+26 -1
View File
@@ -1,8 +1,33 @@
from datetime import datetime, timedelta, timezone
from rest_framework import serializers
from teams.models import Team
from api.teams.models import Team, Vacancy
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
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 .views import AddUserToTeam
from .views import AddUserToTeam, CreateVacancy
urlpatterns = [
path(
@@ -8,4 +8,9 @@ urlpatterns = [
AddUserToTeam.as_view(),
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.generics import CreateAPIView
from rest_framework.response import Response
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):
@@ -24,3 +25,8 @@ class AddUserToTeam(APIView):
team.members.add(user)
team_serializer = TeamSerializer(team)
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 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 = [
path("ping", include("api.ping.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 django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -14,225 +14,72 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name="Achievements",
name='Achievements',
fields=[
(
"id",
models.BigAutoField(
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)),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=api.users.models.get_file_path)),
('info', models.TextField(max_length=255)),
],
),
migrations.CreateModel(
name="Skill",
name='Skill',
fields=[
(
"id",
models.BigAutoField(
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),
]
),
),
('id', models.BigAutoField(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={
"abstract": False,
'abstract': False,
},
),
migrations.CreateModel(
name="Specialization",
name='Specialization',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
options={
"abstract": False,
'abstract': False,
},
),
migrations.CreateModel(
name="User",
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)),
("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",
),
),
('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)),
('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)])),
('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')),
('achievements', models.ManyToManyField(blank=True, to='users.achievements')),
('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')),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
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
def get_file_path(filename):
return f"achievements/{uuid.uuid4()}/{filename}"
class Skill(AbstractTag):
level = models.IntegerField(
validators=[
@@ -17,18 +21,12 @@ class Skill(AbstractTag):
class Achievements(models.Model):
def get_file_path(self, filename):
folder_name = str(uuid.uuid4())
return f"achievements/{folder_name}/{filename}"
file = models.FileField( # noqa: DJ012
upload_to=get_file_path,
)
file = models.FileField(upload_to=get_file_path)
info = models.TextField(
max_length=255,
)
def __str__(self): # noqa: DJ012
def __str__(self):
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 api.users.models import User
class UserRegistrationSerializer(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 UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"first_name",
"last_name",
"username",
"email",
"birthday",
"avatar",
"country",
"city",
"bio",
"avatar",
"experience",
"password",
"first_name",
"last_name",
"specialization",
"achievements",
"username",
"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:
model = User
fields = [
"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)
fields = ("old_password", "new_password")
def validate_old_password(self, value):
if not check_password(value, self.instance.password):
msg = "Wrong password"
raise serializers.ValidationError(msg)
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 rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
import api.users.views
urlpatterns = [
path(
"/sign-up/",
"/signup/",
api.users.views.SignupUserApiView.as_view(),
name="sign-up",
name="signup",
),
path(
"/sign-in/",
api.users.views.SigninUserApiView.as_view(),
TokenObtainPairView.as_view(),
name="sign-in",
),
path(
"api/token/refresh/",
TokenRefreshView.as_view(),
name="token_refresh",
),
path(
"api/token/verify/",
TokenVerifyView.as_view(),
name="token_verify",
),
path(
"/me/profile/",
api.users.views.ProfileMeApiView.as_view(),
name="profile-me",
),
path(
"/me/updatePassword/",
"/me/password/",
api.users.views.PasswordChangeApiView.as_view(),
name="password-change",
),
+13 -117
View File
@@ -1,131 +1,27 @@
from datetime import timedelta
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.generics import CreateAPIView, UpdateAPIView
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 (
PasswordChangeSerializer,
UpdateProfileSerializer,
UserLoginSerializer,
UserProfileSerializer,
UserRegistrationSerializer,
ChangePasswordSerializer,
UserSerializer,
)
class SignupUserApiView(APIView):
def post(self, request):
serializer = UserRegistrationSerializer(data=request.data)
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 SignupUserApiView(CreateAPIView):
http_method_names = ("post",)
serializer_class = UserSerializer
class SigninUserApiView(APIView):
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):
class ProfileMeApiView(UpdateAPIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserProfileSerializer(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)
def get_object(self):
return self.request.user
class PasswordChangeApiView(APIView):
class PasswordChangeApiView(UpdateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = ChangePasswordSerializer
def post(self, request):
serializer = PasswordChangeSerializer(data=request.data)
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)
def get_object(self):
return self.request.user
+18 -11
View File
@@ -1,5 +1,6 @@
import pathlib
import sys
from datetime import timedelta
import environs
@@ -49,12 +50,14 @@ INSTALLED_APPS = [
# Developed apps
"api.ping.apps.PingConfig",
"api.users.apps.UsersConfig",
"api.teams.apps.TeamsConfig",
"api.core.apps.CoreConfig",
"api.notifications.apps.NotificationsConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
@@ -130,15 +133,12 @@ AUTH_PASSWORD_VALIDATORS = [
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True
STATIC_URL = "static/"
USE_TZ = True
TIME_ZONE = "UTC"
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -149,12 +149,19 @@ REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend"
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"api.users.authentication.JWTAuthentication",
),
"DEFAULT_AUTHENTICATION_CLASSES": [
"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):
INSTALLED_APPS.append("debug_toolbar")
-33
View File
@@ -1,43 +1,10 @@
from django.conf import settings
from django.contrib import admin
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 = [
# Built-in 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
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='содержание'),
),
]
+23 -12
View File
@@ -21,12 +21,12 @@ services:
backend:
build: ./backend
container_name: backend
volumes:
- media_volume:/app/project/media/
- static_volume:/app/project/static/
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
expose:
- 8000
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
@@ -38,18 +38,28 @@ services:
DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-*}
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"]
ports:
- 8080:8080
frontend:
container_name: frontend
expose:
- 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",
]
nginx:
container_name: nginx
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
restart: always
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- media_volume:/var/html/media/
- static_volume:/var/html/static/
depends_on:
- backend
pgadmin:
image: dpage/pgadmin4:8.4
@@ -68,5 +78,6 @@ services:
volumes:
postgres_data:
redis_data:
pgadmin_data:
media_volume:
static_volume:
-2
View File
@@ -15,5 +15,3 @@ RUN npm run build
FROM nginx:stable-alpine3.17-slim
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/;
}
}