mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-06-28 05:26:07 +00:00
Initial commit
This commit is contained in:
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
71
api/admin.py
Normal file
71
api/admin.py
Normal file
@ -0,0 +1,71 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from api.models import User, Atoll, Island
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
|
||||
# Define a new User admin
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
list_display = (
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"mobile",
|
||||
"address",
|
||||
"acc_no",
|
||||
"id_card",
|
||||
"dob",
|
||||
"atoll",
|
||||
"island",
|
||||
"terms_accepted",
|
||||
"policy_accepted",
|
||||
) # Add custom fields here
|
||||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(
|
||||
"Personal info",
|
||||
{
|
||||
"fields": (
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"mobile",
|
||||
"address",
|
||||
"acc_no",
|
||||
"id_card",
|
||||
"dob",
|
||||
"atoll",
|
||||
"island",
|
||||
"terms_accepted",
|
||||
"policy_accepted",
|
||||
)
|
||||
},
|
||||
), # Add custom fields here
|
||||
(
|
||||
"Permissions",
|
||||
{
|
||||
"fields": (
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"user_permissions",
|
||||
)
|
||||
},
|
||||
),
|
||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
|
||||
|
||||
# Re-register UserAdmin
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(Permission)
|
||||
admin.site.register(Atoll)
|
||||
admin.site.register(Island)
|
||||
|
||||
|
||||
# TokenAdmin.raw_id_fields = ["user"]
|
9
api/apps.py
Normal file
9
api/apps.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api"
|
||||
|
||||
def ready(self):
|
||||
import api.signals # Add this line to import the signals.py
|
5
api/authentication.py
Normal file
5
api/authentication.py
Normal file
@ -0,0 +1,5 @@
|
||||
from rest_framework.authentication import TokenAuthentication as BaseTokenAuth
|
||||
|
||||
|
||||
class TokenAuthentication(BaseTokenAuth):
|
||||
keyword = "Token"
|
17
api/backends.py
Normal file
17
api/backends.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class EmailBackend(ModelBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
UserModel = get_user_model()
|
||||
if not password:
|
||||
return None
|
||||
try:
|
||||
user = UserModel.objects.get(email=username)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
else:
|
||||
if user.check_password(password):
|
||||
return user
|
||||
return None
|
18
api/exceptions.py
Normal file
18
api/exceptions.py
Normal file
@ -0,0 +1,18 @@
|
||||
from rest_framework.views import exception_handler
|
||||
from rest_framework.exceptions import Throttled
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
# Call REST framework's default exception handler first,
|
||||
# to get the standard error response.
|
||||
response = exception_handler(exc, context)
|
||||
|
||||
if isinstance(exc, Throttled): # check that a Throttled exception is raised
|
||||
custom_response_data = { # prepare custom response data
|
||||
"message": "Too many attemps. Please Try again in %d seconds." % exc.wait,
|
||||
}
|
||||
response.data = (
|
||||
custom_response_data # set the custom response data on response object
|
||||
)
|
||||
|
||||
return response
|
16
api/filters.py
Normal file
16
api/filters.py
Normal file
@ -0,0 +1,16 @@
|
||||
import django_filters
|
||||
from api.models import User
|
||||
|
||||
|
||||
class UserFilter(django_filters.FilterSet):
|
||||
last_name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
first_name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
email = django_filters.CharFilter(lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"username",
|
||||
"last_name",
|
||||
"first_name",
|
||||
]
|
20
api/managers.py
Normal file
20
api/managers.py
Normal file
@ -0,0 +1,20 @@
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
def create_user(self, username, password=None, **extra_fields):
|
||||
"""Create and return a user with an email and password."""
|
||||
if not username:
|
||||
raise ValueError("The Username field must be set")
|
||||
|
||||
user = self.model(username=username, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username, password=None, **extra_fields):
|
||||
"""Create and return a superuser with an email and password."""
|
||||
extra_fields.setdefault('is_staff', True)
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
|
||||
return self.create_user(username, password, **extra_fields)
|
190
api/migrations/0001_initial.py
Normal file
190
api/migrations/0001_initial.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Generated by Django 5.1.2 on 2025-01-20 03:29
|
||||
|
||||
import api.managers
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
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="Atoll",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.CharField(max_length=255, primary_key=True, serialize=False),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Island",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.CharField(max_length=255, primary_key=True, serialize=False),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"atoll",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="islands",
|
||||
to="api.atoll",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
("address", models.CharField(blank=True, max_length=255)),
|
||||
("mobile", models.CharField(blank=True, max_length=255)),
|
||||
("designation", models.CharField(blank=True, max_length=255)),
|
||||
("acc_no", models.CharField(blank=True, max_length=255)),
|
||||
("id_card", models.CharField(blank=True, max_length=255)),
|
||||
("verified", models.BooleanField(default=False)),
|
||||
("dob", models.DateField(blank=True, null=True)),
|
||||
("terms_accepted", models.BooleanField(default=False)),
|
||||
("policy_accepted", models.BooleanField(default=False)),
|
||||
("wallet_balance", models.FloatField(default=0.0)),
|
||||
("ninja_user_id", models.CharField(blank=True, max_length=255)),
|
||||
(
|
||||
"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",
|
||||
),
|
||||
),
|
||||
(
|
||||
"atoll",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="users",
|
||||
to="api.atoll",
|
||||
),
|
||||
),
|
||||
(
|
||||
"island",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="users",
|
||||
to="api.island",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", api.managers.CustomUserManager()),
|
||||
],
|
||||
),
|
||||
]
|
17
api/migrations/0002_alter_user_email.py
Normal file
17
api/migrations/0002_alter_user_email.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.2 on 2025-01-20 03:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="email",
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, unique=True),
|
||||
),
|
||||
]
|
16
api/migrations/0003_alter_user_managers.py
Normal file
16
api/migrations/0003_alter_user_managers.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.1.2 on 2025-01-20 04:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0002_alter_user_email"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[],
|
||||
),
|
||||
]
|
26
api/migrations/0004_alter_atoll_id_alter_island_id.py
Normal file
26
api/migrations/0004_alter_atoll_id_alter_island_id.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.1.2 on 2025-01-20 04:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0003_alter_user_managers"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="atoll",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="island",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
0
api/migrations/__init__.py
Normal file
0
api/migrations/__init__.py
Normal file
14
api/mixins.py
Normal file
14
api/mixins.py
Normal file
@ -0,0 +1,14 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from .permissions import IsStaffEditorPermission
|
||||
|
||||
# from knox.auth import TokenAuthentication
|
||||
|
||||
|
||||
class StaffEditorPermissionMixin:
|
||||
permission_classes = [
|
||||
# permissions.IsAdminUser,
|
||||
permissions.IsAuthenticated,
|
||||
IsStaffEditorPermission,
|
||||
# TokenAuthentication,
|
||||
]
|
46
api/models.py
Normal file
46
api/models.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""
|
||||
This is the models module for api.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from .managers import CustomUserManager
|
||||
from django.utils import timezone
|
||||
|
||||
class User(AbstractUser):
|
||||
email = models.EmailField(unique=True, blank=True, null=True)
|
||||
address = models.CharField(max_length=255, blank=True)
|
||||
mobile = models.CharField(max_length=255, blank=True)
|
||||
designation = models.CharField(max_length=255, blank=True)
|
||||
acc_no = models.CharField(max_length=255, blank=True)
|
||||
id_card = models.CharField(max_length=255, blank=True)
|
||||
verified = models.BooleanField(default=False)
|
||||
dob = models.DateField(blank=True, null=True)
|
||||
terms_accepted = models.BooleanField(default=False)
|
||||
policy_accepted = models.BooleanField(default=False)
|
||||
wallet_balance = models.FloatField(default=0.0)
|
||||
ninja_user_id = models.CharField(max_length=255, blank=True)
|
||||
atoll = models.ForeignKey('Atoll', on_delete=models.SET_NULL, null=True, blank=True, related_name='users')
|
||||
island = models.ForeignKey('Island', on_delete=models.SET_NULL, null=True, blank=True, related_name='users')
|
||||
def get_all_fields(self, instance):
|
||||
return [field.name for field in instance.get_fields()]
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
|
||||
class Atoll(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Island(models.Model):
|
||||
atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name='islands')
|
||||
name = models.CharField(max_length=255)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
21
api/pagination.py
Normal file
21
api/pagination.py
Normal file
@ -0,0 +1,21 @@
|
||||
from rest_framework import pagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
class CustomPagination(pagination.LimitOffsetPagination):
|
||||
def get_paginated_response(self, data):
|
||||
return Response(
|
||||
{
|
||||
"meta": {
|
||||
"total": self.count,
|
||||
"per_page": self.limit,
|
||||
"current_page": int(self.offset / self.limit) + 1,
|
||||
"last_page": int((self.count - 1) / self.limit) + 1,
|
||||
},
|
||||
"links": {
|
||||
"next_page": self.get_next_link(),
|
||||
"previous_page": self.get_previous_link(),
|
||||
},
|
||||
"data": data,
|
||||
}
|
||||
)
|
32
api/permissions.py
Normal file
32
api/permissions.py
Normal file
@ -0,0 +1,32 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class IsStaffEditorPermission(permissions.DjangoModelPermissions):
|
||||
perms_map = {
|
||||
"GET": ["%(app_label)s.view_%(model_name)s"],
|
||||
"OPTIONS": [],
|
||||
"HEAD": [],
|
||||
"POST": ["%(app_label)s.add_%(model_name)s"],
|
||||
"PUT": ["%(app_label)s.change_%(model_name)s"],
|
||||
"PATCH": ["%(app_label)s.change_%(model_name)s"],
|
||||
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
|
||||
}
|
||||
|
||||
message = {
|
||||
"message": "You do not have permission to perform this action.",
|
||||
}
|
||||
|
||||
def has_permission(self, request, view):
|
||||
# Ensure the user is authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Get the model name from the view
|
||||
model_name = view.queryset.model._meta.model_name
|
||||
app_label = view.queryset.model._meta.app_label
|
||||
|
||||
# Check permissions based on the request method
|
||||
perms = self.perms_map.get(request.method, [])
|
||||
perms = [perm % {'app_label': app_label, 'model_name': model_name} for perm in perms]
|
||||
|
||||
return request.user.has_perms(perms)
|
94
api/serializers.py
Normal file
94
api/serializers.py
Normal file
@ -0,0 +1,94 @@
|
||||
from knox.models import AuthToken
|
||||
from django.contrib.auth import authenticate
|
||||
from api.models import User
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class CustomUserSerializer(serializers.ModelSerializer):
|
||||
"""serializer for the user object"""
|
||||
|
||||
user_permissions = serializers.SerializerMethodField()
|
||||
|
||||
def get_user_permissions(self, instance):
|
||||
permission_ids = instance.user_permissions.all()
|
||||
return [
|
||||
{"id": permission.id, "name": permission.name}
|
||||
for permission in permission_ids
|
||||
]
|
||||
|
||||
class Meta: # type: ignore
|
||||
model = User
|
||||
fields = (
|
||||
"id",
|
||||
"username",
|
||||
"email",
|
||||
"user_permissions",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
"is_superuser",
|
||||
)
|
||||
|
||||
|
||||
class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
|
||||
"""serializer for the user object"""
|
||||
|
||||
class Meta: # type: ignore
|
||||
model = User
|
||||
# fields = "__all__"
|
||||
fields = (
|
||||
"id",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"username",
|
||||
"mobile",
|
||||
"address",
|
||||
|
||||
)
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""serializer for the user object"""
|
||||
|
||||
class Meta: # type: ignore
|
||||
model = User
|
||||
fields = ("username", "password")
|
||||
extra_kwargs = {"password": {"write_only": True, "min_length": 5}}
|
||||
|
||||
def create(self, validated_data):
|
||||
return User.objects.create_user(**validated_data)
|
||||
|
||||
|
||||
class AuthSerializer(serializers.Serializer):
|
||||
"""serializer for the user authentication object"""
|
||||
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField(
|
||||
style={"input_type": "password"}, trim_whitespace=False
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get("username")
|
||||
password = attrs.get("password")
|
||||
|
||||
user = authenticate(
|
||||
request=self.context.get("request"), username=username, password=password
|
||||
)
|
||||
|
||||
if not user:
|
||||
msg = "Unable to authenticate with provided credentials"
|
||||
raise serializers.ValidationError(msg, code="authentication")
|
||||
|
||||
attrs["user"] = user
|
||||
return user
|
||||
|
||||
|
||||
class KnoxTokenSerializer(serializers.ModelSerializer):
|
||||
"""serializer for the auth token object"""
|
||||
|
||||
class Meta: # type: ignore
|
||||
model = AuthToken
|
||||
fields = "__all__"
|
61
api/signals.py
Normal file
61
api/signals.py
Normal file
@ -0,0 +1,61 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import render_to_string
|
||||
from decouple import config
|
||||
from django_rest_passwordreset.signals import reset_password_token_created
|
||||
from django.db.models.signals import post_save
|
||||
from api.models import User
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def assign_device_permissions(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
# Assign all permissions for devices and read permission for atoll and island
|
||||
device_permissions = Permission.objects.filter(content_type__model='device')
|
||||
atoll_read_permission = Permission.objects.get(codename='view_atoll')
|
||||
island_read_permission = Permission.objects.get(codename='view_island')
|
||||
|
||||
for permission in device_permissions:
|
||||
instance.user_permissions.add(permission)
|
||||
instance.user_permissions.add(atoll_read_permission, island_read_permission)
|
||||
|
||||
|
||||
@receiver(reset_password_token_created)
|
||||
def password_reset_token_created(
|
||||
sender, instance, reset_password_token, *args, **kwargs
|
||||
):
|
||||
"""
|
||||
Handles password reset tokens
|
||||
When a token is created, an e-mail needs to be sent to the user
|
||||
:param sender: View Class that sent the signal
|
||||
:param instance: View Instance that sent the signal
|
||||
:param reset_password_token: Token Model Object
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
context = {
|
||||
"current_user": reset_password_token.user,
|
||||
"username": reset_password_token.user.username,
|
||||
"email": reset_password_token.user.email,
|
||||
"reset_password_url": f"{config('FRONTEND_URL')}/auth/reset-password-confirm/?token={reset_password_token.key}",
|
||||
}
|
||||
|
||||
# render email text
|
||||
email_html_message = render_to_string("email/password_reset_email.html", context)
|
||||
email_plaintext_message = (
|
||||
f"Here is your password reset link: {context['reset_password_url']}"
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
# title:
|
||||
"Password Reset for {title}".format(title="Sarlink Portal"),
|
||||
# message:
|
||||
email_plaintext_message, # This is the plaintext version
|
||||
# from:
|
||||
"noreply@sarlink.net",
|
||||
# to:
|
||||
[reset_password_token.user.email],
|
||||
)
|
||||
msg.attach_alternative(email_html_message, "text/html")
|
||||
msg.send()
|
102
api/templates/email/password_reset_email.html
Normal file
102
api/templates/email/password_reset_email.html
Normal file
@ -0,0 +1,102 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="Instructions to reset your password." />
|
||||
<meta name="keywords" content="password, reset, email, instructions" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Password Reset Email</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #2c3e50;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #6c757d;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">Password Reset Instructions</div>
|
||||
</div>
|
||||
|
||||
<p class="message">
|
||||
Hello {{ username }},
|
||||
</p>
|
||||
<p class="message">
|
||||
We received a request to reset your password. Click the button below to create a new password:
|
||||
</p>
|
||||
|
||||
<a href="{{ reset_password_url }}" class="button">Reset Password</a>
|
||||
|
||||
<p class="message">
|
||||
If the button doesn't work, you can copy and paste this link into your browser:
|
||||
<br>
|
||||
<a href="{{ reset_password_url }}">{{ reset_password_url }}</a>
|
||||
</p>
|
||||
|
||||
<p class="message">
|
||||
If you did not request this password reset, you can safely ignore
|
||||
this email.
|
||||
</p>
|
||||
|
||||
<p class="footer">Best regards,<br>SARLink</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
1
api/tests.py
Normal file
1
api/tests.py
Normal file
@ -0,0 +1 @@
|
||||
# Create your tests here.
|
5
api/throttle.py
Normal file
5
api/throttle.py
Normal file
@ -0,0 +1,5 @@
|
||||
from rest_framework import throttling
|
||||
|
||||
|
||||
class BurstRateThrottle(throttling.UserRateThrottle):
|
||||
scope = "burst"
|
30
api/urls.py
Normal file
30
api/urls.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from knox import views as knox_views
|
||||
from .views import (
|
||||
LoginView,
|
||||
CreateUserView,
|
||||
ManageUserView,
|
||||
KnoxTokenListApiView,
|
||||
ListUserView,
|
||||
UserDetailAPIView,
|
||||
healthcheck,
|
||||
test_email,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("create/", CreateUserView.as_view(), name="create"),
|
||||
path("profile/", ManageUserView.as_view(), name="profile"),
|
||||
path("login/", LoginView.as_view(), name="knox_login"),
|
||||
path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"),
|
||||
path("logoutall/", knox_views.LogoutAllView.as_view(), name="knox_logoutall"),
|
||||
path("tokens/", KnoxTokenListApiView.as_view(), name="knox_tokens"),
|
||||
# path("auth/", CustomAuthToken.as_view()),
|
||||
path("users/", ListUserView.as_view(), name="users"),
|
||||
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
||||
path("healthcheck/", healthcheck, name="healthcheck"),
|
||||
path("test/", test_email, name="testemail"),
|
||||
|
||||
]
|
29
api/utils.py
Normal file
29
api/utils.py
Normal file
@ -0,0 +1,29 @@
|
||||
def reverse_dhivehi_string(input_str):
|
||||
"""
|
||||
Reverses a Dhivehi string while preserving character composition.
|
||||
|
||||
Args:
|
||||
input_str (str): The Dhivehi string to be reversed
|
||||
|
||||
Returns:
|
||||
str: The reversed Dhivehi string
|
||||
"""
|
||||
# Reverse the string and then normalize the character order
|
||||
reversed_str = input_str[::-1]
|
||||
|
||||
# List to store the corrected characters
|
||||
corrected_chars = []
|
||||
|
||||
# Iterate through the reversed string
|
||||
i = 0
|
||||
while i < len(reversed_str):
|
||||
# Check if current character is a combining character
|
||||
if i + 1 < len(reversed_str) and "\u0300" <= reversed_str[i + 1] <= "\u036F":
|
||||
# If next character is a combining mark, add it before the base character
|
||||
corrected_chars.append(reversed_str[i + 1] + reversed_str[i])
|
||||
i += 2
|
||||
else:
|
||||
corrected_chars.append(reversed_str[i])
|
||||
i += 1
|
||||
|
||||
return "".join(corrected_chars)
|
221
api/views.py
Normal file
221
api/views.py
Normal file
@ -0,0 +1,221 @@
|
||||
# django imports
|
||||
from django.contrib.auth import login
|
||||
|
||||
# rest_framework imports
|
||||
from rest_framework import generics, permissions
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
from api.filters import UserFilter
|
||||
from api.mixins import StaffEditorPermissionMixin
|
||||
from api.models import User, Atoll, Island
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
|
||||
# knox imports
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
from knox.models import AuthToken
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
import re
|
||||
from typing import cast, Dict, Any
|
||||
from django.core.mail import send_mail
|
||||
|
||||
# local apps import
|
||||
from .serializers import (
|
||||
KnoxTokenSerializer,
|
||||
UserSerializer,
|
||||
AuthSerializer,
|
||||
CustomUserSerializer,
|
||||
CustomReadOnlyUserSerializer,
|
||||
)
|
||||
|
||||
|
||||
class CreateUserView(generics.CreateAPIView):
|
||||
# Create user API view
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = User.objects.all()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Extract required fields from request data
|
||||
password = request.data.get("password")
|
||||
username = request.data.get("username") # This can be None
|
||||
address = request.data.get("address")
|
||||
mobile = request.data.get("mobile")
|
||||
acc_no = request.data.get("acc_no")
|
||||
id_card = request.data.get("id_card")
|
||||
dob = request.data.get("dob")
|
||||
atoll_id = request.data.get("atoll") # Get the atoll ID
|
||||
island_id = request.data.get("island") # Get the island ID
|
||||
terms_accepted = request.data.get("terms_accepted")
|
||||
policy_accepted = request.data.get("policy_accepted")
|
||||
firstname = request.data.get("firstname")
|
||||
lastname = request.data.get("lastname")
|
||||
# Validate required fields
|
||||
|
||||
existing_username = User.objects.filter(username=username).first()
|
||||
if existing_username:
|
||||
return Response({"message": "Username already exists."}, status=400)
|
||||
|
||||
if not firstname:
|
||||
return Response({"message": "firstname is required."}, status=400)
|
||||
if not lastname:
|
||||
return Response({"message": "lastname is required."}, status=400)
|
||||
if not password:
|
||||
return Response({"message": "Password is required."}, status=400)
|
||||
if not username:
|
||||
return Response({"message": "Username is required."}, status=400)
|
||||
if not address:
|
||||
return Response({"message": "Address is required."}, status=400)
|
||||
if not mobile:
|
||||
return Response({"message": "Mobile number is required."}, status=400)
|
||||
if not acc_no:
|
||||
return Response({"message": "Account number is required."}, status=400)
|
||||
if not id_card:
|
||||
return Response({"message": "ID card is required."}, status=400)
|
||||
if not dob:
|
||||
return Response({"message": "Date of birth is required."}, status=400)
|
||||
if not atoll_id:
|
||||
return Response({"message": "Atoll is required."}, status=400)
|
||||
if not island_id:
|
||||
return Response({"message": "Island is required."}, status=400)
|
||||
if terms_accepted is None:
|
||||
return Response({"message": "Terms acceptance is required."}, status=400)
|
||||
if policy_accepted is None:
|
||||
return Response({"message": "Policy acceptance is required."}, status=400)
|
||||
|
||||
if not re.match(r"^[A-Z]{1,2}[0-9]{6,7}$", id_card):
|
||||
return Response({"message": "Please enter a valid ID card number."}, status=400)
|
||||
|
||||
if not re.match(r"^[7|9][0-9]{6}$", mobile):
|
||||
return Response({"message": "Please enter a valid mobile number."}, status=400)
|
||||
|
||||
if not re.match(r"^(7\d{12}|9\d{16})$", acc_no):
|
||||
return Response({"message": "Please enter a valid account number."}, status=400)
|
||||
|
||||
# Fetch Atoll and Island instances
|
||||
try:
|
||||
atoll = Atoll.objects.get(id=atoll_id)
|
||||
island = Island.objects.get(id=island_id)
|
||||
except Atoll.DoesNotExist:
|
||||
return Response({"message": "Atoll not found."}, status=404)
|
||||
except Island.DoesNotExist:
|
||||
return Response({"message": "Island not found."}, status=404)
|
||||
|
||||
# Create user without email
|
||||
user = User.objects.create_user(
|
||||
first_name=firstname,
|
||||
last_name=lastname,
|
||||
username=username,
|
||||
password=password,
|
||||
address=address,
|
||||
mobile=str("+960") + str(mobile),
|
||||
acc_no=acc_no,
|
||||
id_card=id_card,
|
||||
dob=dob,
|
||||
atoll=atoll, # Assign the Atoll instance
|
||||
island=island, # Assign the Island instance
|
||||
terms_accepted=terms_accepted,
|
||||
policy_accepted=policy_accepted,
|
||||
)
|
||||
serializer = self.get_serializer(user)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
|
||||
class LoginView(KnoxLoginView):
|
||||
# login view extending KnoxLoginView
|
||||
serializer_class = AuthSerializer
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
throttle_scope = "login"
|
||||
|
||||
def post(self, request, format=None):
|
||||
try:
|
||||
serializer = AuthTokenSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = cast(Dict[str, Any], serializer.validated_data)["user"]
|
||||
login(request, user)
|
||||
response = super(LoginView, self).post(request, format=None)
|
||||
return response
|
||||
except ValidationError as e:
|
||||
message = "Unable to log in with provided credentials."
|
||||
if (
|
||||
hasattr(e, "detail")
|
||||
and isinstance(e.detail, list)
|
||||
and len(e.detail) > 0
|
||||
):
|
||||
message = e.detail[0]
|
||||
return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ManageUserView(generics.RetrieveUpdateAPIView):
|
||||
"""Manage the authenticated user"""
|
||||
|
||||
serializer_class = CustomUserSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get_object(self):
|
||||
"""Retrieve and return authenticated user"""
|
||||
return self.request.user
|
||||
|
||||
|
||||
class KnoxTokenListApiView(
|
||||
StaffEditorPermissionMixin,
|
||||
generics.ListAPIView,
|
||||
):
|
||||
# Create user API view
|
||||
serializer_class = KnoxTokenSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
queryset = AuthToken.objects.all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_id = getattr(request.user, "id", None)
|
||||
if user_id is None:
|
||||
return Response({"error": "User ID not found"}, status=400)
|
||||
queryset = AuthToken.objects.filter(user_id=user_id)
|
||||
data = KnoxTokenSerializer(queryset, many=True).data
|
||||
return Response({"data": data})
|
||||
|
||||
|
||||
class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView):
|
||||
# Create user API view
|
||||
serializer_class = CustomReadOnlyUserSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = "__all__"
|
||||
filterset_class = UserFilter
|
||||
queryset = User.objects.all()
|
||||
|
||||
|
||||
class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = CustomReadOnlyUserSerializer
|
||||
lookup_field = "pk"
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
data = serializer.data
|
||||
|
||||
# Customize the response format
|
||||
|
||||
return Response(data)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def healthcheck(request):
|
||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes((permissions.AllowAny,))
|
||||
def test_email(request):
|
||||
send_mail(
|
||||
"Subject here",
|
||||
"Here is the message.",
|
||||
"noreply@sarlink.net",
|
||||
["shihaam@shihaam.me"],
|
||||
fail_silently=False,
|
||||
)
|
||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
Reference in New Issue
Block a user