Skip to content

Commit

Permalink
added user login for local authentication (#107)
Browse files Browse the repository at this point in the history
* added user login for local authentication

* password feild changed, username changed

* added login button in landing page

* updated readme

* login page styling added

* updated authentication service to update new password

* removed sessions from tenant applications list

* reverted STATIC_URL to it's old state from taking varible

* Update backend/README.md

Co-authored-by: Hari John Kuriakose <[email protected]>
Signed-off-by: ali <[email protected]>

---------

Signed-off-by: ali <[email protected]>
Co-authored-by: vishnuszipstack <[email protected]>
Co-authored-by: Hari John Kuriakose <[email protected]>
  • Loading branch information
3 people authored Mar 16, 2024
1 parent cc517b6 commit 6f53013
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 165 deletions.
8 changes: 8 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ python manage.py runserver localhost:8000
```

- Server will start and run at port 8000. (<http://localhost:8000>)

## Default Username and Password

The default username is `unstract` and the default password is `unstract`.

To customize your password, simply navigate to the `.env` file and update the `DEFAULT_AUTH_PASSWORD` config before launching the server. Then use your new password to log in.

To update the password after it's been set, first change it in the .env file, restart the server for it to take effect, then log in using the new password.

## Asynchronous execution/pipeline execution

Expand Down
19 changes: 1 addition & 18 deletions backend/account/authentication_helper.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import logging
from typing import Any

from account.constants import DefaultOrg
from account.dto import CallbackData, MemberData, OrganizationData
from account.dto import MemberData
from account.models import Organization, User
from platform_settings.platform_auth_service import (
PlatformAuthenticationService,
)
from rest_framework.request import Request

logger = logging.getLogger(__name__)

Expand All @@ -16,21 +14,6 @@ class AuthenticationHelper:
def __init__(self) -> None:
pass

def get_organizations_by_user_id(self) -> list[OrganizationData]:
organizationData: OrganizationData = OrganizationData(
id=DefaultOrg.MOCK_ORG,
display_name=DefaultOrg.MOCK_ORG,
name=DefaultOrg.MOCK_ORG,
)
return [organizationData]

def get_authorize_token(rself, equest: Request) -> CallbackData:
return CallbackData(
user_id=DefaultOrg.MOCK_USER_ID,
email=DefaultOrg.MOCK_USER_EMAIL,
token="",
)

def list_of_members_from_user_model(
self, model_data: list[Any]
) -> list[MemberData]:
Expand Down
212 changes: 135 additions & 77 deletions backend/account/authentication_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Any, Optional

from account.authentication_helper import AuthenticationHelper
from account.constants import Common, DefaultOrg
from account.constants import DefaultOrg, ErrorMessage, UserLoginTemplate
from account.custom_exceptions import Forbidden, MethodNotImplemented
from account.dto import (
CallbackData,
Expand All @@ -13,38 +13,117 @@
ResetUserPasswordDto,
UserInfo,
UserRoleData,
UserSessionInfo,
)
from account.enums import UserRole
from account.models import Organization, User
from account.organization import OrganizationService
from account.serializer import LoginRequestSerializer
from django.conf import settings
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.hashers import make_password
from django.http import HttpRequest
from django.shortcuts import redirect, render
from rest_framework.request import Request
from rest_framework.response import Response
from tenant_account.models import OrganizationMember as OrganizationMember
from utils.cache_service import CacheService

Logger = logging.getLogger(__name__)


class AuthenticationService:
def __init__(self) -> None:
self.authentication_helper = AuthenticationHelper()
self.default_user: User = self.get_user()
self.default_organization: Organization = self.user_organization()
self.user_session_info = self.get_user_session_info()

def get_current_organization(self) -> Organization:
return self.default_organization
def user_login(self, request: Request) -> Any:
"""Authenticate and log in a user.
def get_current_user(self) -> User:
return self.default_user
Args:
request (Request): The HTTP request object.
def get_current_user_session(self) -> UserSessionInfo:
return self.user_session_info
Returns:
Any: The response object.
def user_login(self, request: HttpRequest) -> Any:
raise MethodNotImplemented()
Raises:
ValueError: If there is an error in the login credentials.
"""
if request.method == "GET":
return self.render_login_page(request)
try:
validated_data = self.validate_login_credentials(request)
username = validated_data.get("username")
password = validated_data.get("password")
except ValueError as e:
return render(
request,
UserLoginTemplate.TEMPLATE,
{UserLoginTemplate.ERROR_PLACE_HOLDER: str(e)},
)
if self.authenticate_and_login(request, username, password):
return redirect(settings.WEB_APP_ORIGIN_URL)

return self.render_login_page_with_error(
request, ErrorMessage.USER_LOGIN_ERROR
)

def authenticate_and_login(
self, request: Request, username: str, password: str
) -> bool:
"""Authenticate and log in a user.
Args:
request (Request): The HTTP request object.
username (str): The username of the user.
password (str): The password of the user.
Returns:
bool: True if the user is successfully authenticated and logged in,
False otherwise.
"""
user = authenticate(request, username=username, password=password)
if user:
login(request, user)
return True
# Attempt to initiate default user and authenticate again
if self.set_default_user(username, password):
user = authenticate(request, username=username, password=password)
if user:
login(request, user)
return True
return False

def render_login_page(self, request: Request) -> Any:
return render(request, UserLoginTemplate.TEMPLATE)

def render_login_page_with_error(
self, request: Request, error_message: str
) -> Any:
return render(
request,
UserLoginTemplate.TEMPLATE,
{UserLoginTemplate.ERROR_PLACE_HOLDER: error_message},
)

def validate_login_credentials(self, request: Request) -> Any:
"""Validate the login credentials.
Args:
request (Request): The HTTP request object.
Returns:
dict: The validated login credentials.
Raises:
ValueError: If the login credentials are invalid.
"""
serializer = LoginRequestSerializer(data=request.POST)
if not serializer.is_valid():
error_messages = {
field: errors[0] for field, errors in serializer.errors.items()
}
first_error_message = list(error_messages.values())[0]
raise ValueError(first_error_message)
return serializer.validated_data

def user_signup(self, request: HttpRequest) -> Any:
raise MethodNotImplemented()
Expand All @@ -65,8 +144,8 @@ def is_admin_by_role(self, role: str) -> bool:

def get_callback_data(self, request: Request) -> CallbackData:
return CallbackData(
user_id=DefaultOrg.MOCK_USER_ID,
email=DefaultOrg.MOCK_USER_EMAIL,
user_id=request.user.user_id,
email=request.user.email,
token="",
)

Expand All @@ -82,7 +161,7 @@ def handle_invited_user_while_callback(
self, request: Request, user: User
) -> MemberData:
member_data: MemberData = MemberData(
user_id=self.default_user.user_id,
user_id=user.user_id,
organization_id=self.default_organization.organization_id,
role=[UserRole.ADMIN.value],
)
Expand All @@ -101,7 +180,7 @@ def add_to_organization(
data: Optional[dict[str, Any]] = None,
) -> MemberData:
member_data: MemberData = MemberData(
user_id=self.default_user.user_id,
user_id=user.user_id,
organization_id=self.default_organization.organization_id,
)

Expand Down Expand Up @@ -204,38 +283,38 @@ def get_organization_by_org_id(self, id: str) -> OrganizationData:
)
return organizationData

def get_user(self) -> User:
user = CacheService.get_user_session_info(DefaultOrg.MOCK_USER_EMAIL)
if not user:
try:
user = User.objects.get(email=DefaultOrg.MOCK_USER_EMAIL)
except User.DoesNotExist:
user = User(
username=DefaultOrg.MOCK_USER,
user_id=DefaultOrg.MOCK_USER_ID,
email=DefaultOrg.MOCK_USER_EMAIL,
)
user.save()
if isinstance(user, User):
id = user.id
user_id = user.user_id
email = user.email
else:
id = user[Common.ID]
user_id = user[Common.USER_ID]
email = user[Common.USER_EMAIL]
def set_default_user(self, username: str, password: str) -> bool:
"""Set the default user for authentication.
This method creates a default user with the provided username and
password if the username and password match the default values defined
in the 'DefaultOrg' class. The default user is saved in the database.
Args:
username (str): The username of the default user.
password (str): The password of the default user.
current_org = Common.PUBLIC_SCHEMA_NAME
Returns:
bool: True if the default user is successfully created and saved,
False otherwise.
"""
if (
username != DefaultOrg.MOCK_USER
or password != DefaultOrg.MOCK_USER_PASSWORD
):
return False

user_session_info: UserSessionInfo = UserSessionInfo(
id=id,
user_id=user_id,
email=email,
current_org=current_org,
user, created = User.objects.get_or_create(
username=DefaultOrg.MOCK_USER
)
CacheService.set_user_session_info(user_session_info)
user_info = User(id=id, user_id=user_id, username=email, email=email)
return user_info
if created:
user.password = make_password(DefaultOrg.MOCK_USER_PASSWORD)
else:
user.user_id = DefaultOrg.MOCK_USER_ID
user.email = DefaultOrg.MOCK_USER_EMAIL
user.password = make_password(DefaultOrg.MOCK_USER_PASSWORD)
user.save()
return True

def get_user_info(self, request: Request) -> Optional[UserInfo]:
user: User = request.user
Expand All @@ -248,32 +327,7 @@ def get_user_info(self, request: Request) -> Optional[UserInfo]:
email=user.email,
)
else:
user = self.get_user()
return UserInfo(
id=user.id,
user_id=user.user_id,
name=user.username,
display_name=user.username,
email=user.email,
)

def get_user_session_info(self) -> UserSessionInfo:
user_session_info_dict = CacheService.get_user_session_info(
self.default_user.email
)
if not user_session_info_dict:
user_session_info: UserSessionInfo = UserSessionInfo(
id=self.default_user.id,
user_id=self.default_user.user_id,
email=self.default_user.email,
current_org=self.default_organization.organization_id,
)
CacheService.set_user_session_info(user_session_info)
else:
user_session_info = UserSessionInfo.from_dict(
user_session_info_dict
)
return user_session_info
return None

def get_organization_info(self, org_id: str) -> Optional[Organization]:
return OrganizationService.get_organization_by_org_id(org_id=org_id)
Expand All @@ -300,12 +354,16 @@ def make_user_organization_display_name(self, user_name: str) -> str:
return f"{name} organization"

def user_logout(self, request: HttpRequest) -> Response:
raise MethodNotImplemented()
"""Log out the user.
def get_user_id_from_token(
self, token: Optional[dict[str, Any]]
) -> Response:
return DefaultOrg.MOCK_USER_ID
Args:
request (HttpRequest): The HTTP request object.
Returns:
Response: The redirect response to the web app origin URL.
"""
logout(request)
return redirect(settings.WEB_APP_ORIGIN_URL)

def get_organization_members_by_org_id(
self, organization_id: str
Expand Down
12 changes: 11 additions & 1 deletion backend/account/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from django.conf import settings


class LoginConstant:
INVITATION = "invitation"
ORGANIZATION = "organization"
Expand Down Expand Up @@ -35,14 +38,21 @@ class Cookie:
class ErrorMessage:
ORGANIZATION_EXIST = "Organization already exists"
DUPLICATE_API = "It appears that a duplicate call may have been made."
USER_LOGIN_ERROR = "Invalid username or password. Please try again."


class DefaultOrg:
ORGANIZATION_NAME = "mock_org"
MOCK_ORG = "mock_org"
MOCK_USER = "mock_user"
MOCK_USER = "unstract"
MOCK_USER_ID = "mock_user_id"
MOCK_USER_EMAIL = "[email protected]"
MOCK_USER_PASSWORD = settings.DEFAULT_AUTH_PASSWORD


class UserLoginTemplate:
TEMPLATE = "login.html"
ERROR_PLACE_HOLDER = "error_message"


class PluginConfig:
Expand Down
Loading

0 comments on commit 6f53013

Please sign in to comment.