Skip to content

feat(backend): Initial account creation endpoints #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
2,504 changes: 1,326 additions & 1,178 deletions backend-services/poetry.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions backend-services/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ readme = "README.md"
packages = [
{include = "common", from = "src"},
{include = "data_service", from = "src"},
{include = "user_auth_service", from = "src"},
{include = "web_asgi", from = "src"},
{include = "tests"},
]
Expand All @@ -20,6 +21,8 @@ python-dotenv = "^0.21.0"
uvicorn = {extras = ["standard"], version = "^0.20.0"}
gunicorn = "^20.1.0"
odmantic = "^0.9.1"
passlib = {extras = ["argon2"], version = "^1.7.4"}
python-jose = {extras = ["cryptography"], version = "^3.3.0"}

[tool.poetry.group.test.dependencies]
pytest = "^7.2.0"
Expand Down
14 changes: 13 additions & 1 deletion backend-services/src/common/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
from typing import NewType
from typing import NewType, TypeAlias

from odmantic import AIOEngine

WalletAddress = NewType("WalletAddress", str)
"""
A type for vault wallet addresses.
"""

Engine: TypeAlias = AIOEngine
"""
A database engine instance.
"""

HashString = NewType("HashString", str)
"""
HashString
"""
3 changes: 1 addition & 2 deletions backend-services/src/data_service/operations/dataschema.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from fastapi import HTTPException
from odmantic import ObjectId

from common.types import WalletAddress
from data_service.schema.actions import CreateDataschema, DeleteDataschema
from data_service.schema.entities import Dataschema
from data_service.schema.types import Engine
Expand Down Expand Up @@ -32,7 +31,7 @@ async def delete_dataschema(engine: Engine, params: DeleteDataschema) -> None:
await engine.delete(existing_dataschema)


async def find_by_pool(engine: Engine, data_schema_id: str) -> DataschemaList:
async def find_by_pool(engine: Engine, data_schema_id: str) -> list[Dataschema]:
"""
Retrieve a list of all dataschemas for a given user from the database.
"""
Expand Down
3 changes: 2 additions & 1 deletion backend-services/src/data_service/schema/actions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import datetime

from pydantic import BaseModel, validator

from common.types import WalletAddress
from datetime import datetime


class CreateDataset(BaseModel):
Expand Down
3 changes: 1 addition & 2 deletions backend-services/src/data_service/schema/entities.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from datetime import datetime
from typing import TypeAlias

from odmantic import Model

from common.types import WalletAddress

from datetime import datetime


class Dataset(Model):
"""
Expand Down
3 changes: 3 additions & 0 deletions backend-services/src/user_auth_service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
User Authentication service for the Nautilus Vault backend.
"""
3 changes: 3 additions & 0 deletions backend-services/src/user_auth_service/operations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
The collection of operations provided by the user authentication service.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from fastapi import HTTPException

from common.types import Engine
from datetime import datetime, timedelta
from user_auth_service.schema.actions import (
AuthenticateUser,
AuthenticateUserResult,
AuthenticateUserSuccess,
)
from user_auth_service.schema.entities import UserDetailsStorable, UserDisplay

from .create_new_user import argon2_context

from jose import jwt

SECRET_KEY = ""
ALGORITHM = "HS256"


def create_access_token(email_address: str,
user_id: str,
owner_name: str,
phone_number: str,
expires_delta: timedelta | None = None) -> str:
encode = {"email": email_address, "user_id": user_id, "owner_name": owner_name, "phone_number": phone_number}
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
encode.update({"exp": expire})
return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM)

def verify_password(password_attempt: str, hashed_password: str) -> bool:
return argon2_context.verify(password_attempt, hashed_password)

async def authenticate_user(engine: Engine, params: AuthenticateUser) -> AuthenticateUserResult:
"""
Authenticate User
"""
existing_user = await engine.find_one(UserDetailsStorable, UserDetailsStorable.email_address == params.email_address)
if existing_user is None:
raise HTTPException(status_code=404, detail="This email address does not exist.")

if not verify_password(params.password, existing_user.password_hash_string):
raise HTTPException(status_code=401, detail="Incorrect Password.")

user_display = UserDisplay(
user_id=str(existing_user.id),
email_address=existing_user.email_address,
owner_name=existing_user.full_name,
phone_number=existing_user.phone_number
)

token_expires = timedelta(minutes=15)
token = create_access_token(user_display.email_address, user_display.user_id,
user_display.owner_name, user_display.phone_number, token_expires)
return AuthenticateUserSuccess(Opened=token)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from fastapi import HTTPException
from passlib.context import CryptContext

from data_service.schema.types import Engine
from user_auth_service.schema.actions import (
CreateNewUser,
CreateNewUserResult,
CreateNewUserSuccess,
)
from user_auth_service.schema.entities import UserDetailsStorable, UserDisplay

argon2_context = CryptContext(schemes=["argon2"], deprecated="auto")

def password_hash(password: str) -> str:
return argon2_context.hash(password)


async def create_new_user(engine: Engine, params: CreateNewUser) -> CreateNewUserResult:
"""
User Creation.
"""
existing_email = await engine.find_one(UserDetailsStorable,
UserDetailsStorable.email_address == params.email_address)
if existing_email is not None:
raise HTTPException(status_code=400, detail="This email address is already used.")
hash_password = password_hash(params.password)

new_user = UserDetailsStorable(
email_address=params.email_address,
full_name=params.full_name,
phone_number=params.phone_number,
password_hash_string=hash_password
)
await engine.save(new_user)
user_display = UserDisplay(
user_id=str(new_user.id),
email_address=new_user.email_address,
owner_name=new_user.full_name,
phone_number=new_user.phone_number
)
return CreateNewUserSuccess(Created=user_display)
3 changes: 3 additions & 0 deletions backend-services/src/user_auth_service/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Internals and abstractions for the User Authentication service.
"""
54 changes: 54 additions & 0 deletions backend-services/src/user_auth_service/schema/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import TypeAlias

from pydantic import BaseModel

from user_auth_service.schema.entities import UserDisplay


class CreateNewUser(BaseModel):
"""
User creation parameters.
"""

full_name: str
phone_number: str
email_address: str
password: str

class CreateNewUserSuccess(BaseModel):
"""
Return email address, full name, and phone number.
"""

Created: UserDisplay

class CreateNewUserFailure(BaseModel):
"""
Return Failure if user's credentials is not created.
"""
Failed: str

CreateNewUserResult: TypeAlias = CreateNewUserSuccess | CreateNewUserFailure

class AuthenticateUser(BaseModel):
"""
Authentic user parameters.
"""
email_address: str
password: str

class AuthenticateUserSuccess(BaseModel):
"""
Successfully authenticated user.
"""

Opened: str

class AuthenticateUserFailure(BaseModel):
"""
Failed to authenticate user.
"""

Failed: str

AuthenticateUserResult: TypeAlias = AuthenticateUserSuccess | AuthenticateUserFailure
31 changes: 31 additions & 0 deletions backend-services/src/user_auth_service/schema/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pydantic
from odmantic import Model, ObjectId
from pydantic import BaseModel

from common.types import HashString

pydantic.json.ENCODERS_BY_TYPE[ObjectId]=str

class UserDetailsStorable(Model):
"""
Storing new ueser's credentials.
"""

email_address: str
full_name: str
phone_number: str
password_hash_string: HashString

class Config:
collection = "user"

class UserDisplay(BaseModel):
"""
Return User credentials when user is created or opened.
"""

user_id: str
email_address: str
owner_name: str
phone_number: str

45 changes: 39 additions & 6 deletions backend-services/src/web_asgi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@
from odmantic import AIOEngine

from common.types import WalletAddress
from data_service.operations.dataset import datasets, create_dataset
from data_service.operations.datapool import create_datapool, datapools, delete_datapool
from data_service.operations.dataschema import create_dataschema
from data_service.operations.dataset import create_dataset, datasets
from data_service.operations.dataset import delete_dataset as data_delete_dataset
from data_service.schema.actions import CreateDataset, DeleteDataset, CreateDatapool, DeleteDatapool, CreateDataschema
from data_service.schema.entities import Dataset, DatasetList, Datapool, DatapoolList, Dataschema

from data_service.operations.datapool import datapools, create_datapool, delete_datapool

from data_service.schema.actions import (
CreateDatapool,
CreateDataschema,
CreateDataset,
DeleteDatapool,
DeleteDataset,
)
from data_service.schema.entities import (
Datapool,
DatapoolList,
Dataschema,
Dataset,
DatasetList,
)
from user_auth_service.operations.authenticate_user import authenticate_user
from user_auth_service.operations.create_new_user import create_new_user
from user_auth_service.schema.actions import (
AuthenticateUser,
AuthenticateUserResult,
CreateNewUser,
CreateNewUserResult,
)
from web_asgi.settings import AppSettings

app_settings = AppSettings()
Expand All @@ -38,6 +57,20 @@
)


@app.post(
"/auth/create", response_model=CreateNewUserResult, status_code=status.HTTP_201_CREATED
)
async def post_create_new_user(request: CreateNewUser) -> CreateNewUserResult:
return await create_new_user(mongo_engine, request)


@app.post(
"/auth/login", response_model=AuthenticateUserResult, status_code=status.HTTP_200_OK
)
async def post_authenticate_user(request: AuthenticateUser) -> AuthenticateUserResult:
return await authenticate_user(mongo_engine, request)


@app.get("/datasets", response_model=DatasetList, status_code=status.HTTP_200_OK)
async def get_datasets(wallet_id: WalletAddress) -> DatasetList:
return await datasets(mongo_engine, wallet_id)
Expand Down