Skip to content

Commit

Permalink
feat: Implement user verification after registration
Browse files Browse the repository at this point in the history
  • Loading branch information
chetat committed May 20, 2023
1 parent 7c943e6 commit ee08aaf
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 23 deletions.
39 changes: 32 additions & 7 deletions app/api/v1/users/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,61 @@
from app.core.database import get_session
from app.schemas.users import UserRequest
from app.services import users as user_service
from app.services import messages as message_service
from app.logger.kaolog import get_logger

logger = get_logger(__name__)
router = APIRouter()


@router.post("")
async def create_user(
user_request: UserRequest, session: Session = Depends(get_session)
):
def create_user(user_request: UserRequest, session: Session = Depends(get_session)):
password = user_request.password
user_request_dict = user_request.dict(exclude={"password"})
try:
user_service.create_user(session, password, user_request_dict)
otp = message_service.generate_otp()
user_service.create_user(session, password, user_request_dict, otp)
message_service.send_otp_sms(user_request.phone_number, otp)
return {"message": "User created"}
except Exception as e:
logger.error(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
detail={
"message": "Something went wrong when creating user",
}
},
)


@router.post("/verify")
def verify_user(otp: str, session: Session = Depends(get_session)):
try:
is_verified = user_service.verify_user(session, otp)
except Exception as e:
logger.error(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"message": "Something went wrong when verifying user",
},
)

if not is_verified:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"message": "Invalid OTP",
},
)

return {"message": "User verified"}


@router.get("/access-token")
async def authenticate_user(user_id: str, session: Session = Depends(get_session)):
return {"message": f"Hello {user_id}"}



@router.post("/logout")
async def logout_user(session: Session = Depends(get_session)):
return {"message": "Logged Out"}
3 changes: 2 additions & 1 deletion app/core/settings.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import os
from pydantic import BaseSettings
from functools import lru_cache

from dotenv import load_dotenv

load_dotenv()


class Settings(BaseSettings):
SQLALCHEMY_DATABASE_URI: str = os.getenv("SQLALCHEMY_DATABASE_URI")
TWILIO_ACCOUNT_SID: str = os.getenv("TWILIO_ACCOUNT_SID")
TWILIO_AUTH_TOKEN: str = os.getenv("TWILIO_AUTH_TOKEN")


@lru_cache
Expand Down
3 changes: 3 additions & 0 deletions app/helpers/password.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from passlib.context import CryptContext
from uuid import uuid4

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def generate_public_id() -> str:
uid_striped = str(uuid4()).split("-")
return "".join(uid_striped)[:30]


def generate_password_hash(password: str) -> str:
return pwd_context.hash(password)

Expand Down
13 changes: 7 additions & 6 deletions app/logger/kaolog.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import logging
import json


class JsonFormatter(logging.Formatter):
def format(self, record):
log_object = {
'level': record.levelname,
'message': record.getMessage(),
'time': self.formatTime(record),
'source': record.pathname,
'line': record.lineno,
'function': record.funcName
"level": record.levelname,
"message": record.getMessage(),
"time": self.formatTime(record),
"source": record.pathname,
"line": record.lineno,
"function": record.funcName,
}
return json.dumps(log_object)

Expand Down
14 changes: 13 additions & 1 deletion app/models/users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import String, DateTime, BigInteger, Integer
from sqlalchemy import String, DateTime, BigInteger, Integer, Boolean
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
Expand All @@ -22,6 +22,7 @@ class User(Base):
phone_number: Mapped[str] = mapped_column(String(30), unique=True, index=True)
email: Mapped[str] = mapped_column(String(30), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255))
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
addresses: Mapped[List["Address"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
Expand Down Expand Up @@ -131,3 +132,14 @@ class Book(Base):
updated_at: Mapped[DateTime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)


class AccountVerification(Base):
__tablename__ = "account_verification"
id: Mapped[int] = mapped_column(primary_key=True)
token: Mapped[str] = mapped_column(String(30))
phone_number: Mapped[str] = mapped_column(String(30))
created_at: Mapped[DateTime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[DateTime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
22 changes: 21 additions & 1 deletion app/repository/user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
from sqlalchemy.orm import Session
from app.models.users import User
from app.models.users import User, AccountVerification
from typing import Dict, Any


def add_user(session: Session, user_info: Dict[str, Any]):
user_item = User(**user_info)
session.add(user_item)


def add_account_verification(session: Session, verification_info: Dict[str, Any]):
verification_item = AccountVerification(**verification_info)
session.add(verification_item)


def update_verified_status(session: Session, phone_number: str, status: bool):
session.query(User).filter(User.phone_number == phone_number).update(
{"is_verified": status}
)


def get_user_verification(session: Session, otp: str) -> AccountVerification:
acc_verification = (
session.query(AccountVerification)
.filter(AccountVerification.token == otp)
.first()
)
return acc_verification
30 changes: 30 additions & 0 deletions app/services/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
from twilio.rest import Client
from app.core.settings import Settings, get_settings
import random

settings: Settings = get_settings()


def get_twilio_client():
account_sid = settings.TWILIO_ACCOUNT_SID
auth_token = settings.TWILIO_AUTH_TOKEN
client = Client(account_sid, auth_token)
return client


def send_sms(phone_number: str, message: str, client: Client):
message = client.messages.create(
body=message, from_="+12543293270", to=phone_number
)


def generate_otp():
verification_code = random.randint(100000, 999999)
return str(verification_code)


def send_otp_sms(phone_number: str, otp: str):
client = get_twilio_client()
message = f"Your OTP is {otp}"
send_sms(phone_number, message, client)
32 changes: 29 additions & 3 deletions app/services/users.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
from sqlalchemy.orm import Session
from typing import Dict, Any
from app.helpers.password import generate_password_hash, generate_public_id
from app.repository import user as user_repo
from app.repository import user as user_repo
from datetime import datetime, timedelta

def create_user(session: Session, password, user_info: Dict[str, Any]):

def create_user(session: Session, password, user_info: Dict[str, Any], otp: str):
user_info["password_hash"] = generate_password_hash(password)
user_info["public_id"] = generate_public_id()
user_repo.add_user(session, user_info)
verification_info = {"token": otp, "phone_number": user_info["phone_number"]}
user_repo.add_account_verification(session, verification_info)
session.commit()


def create_account_verification(session: Session, verification_info: Dict[str, Any]):
user_repo.add_account_verification(session, verification_info)
session.commit()


def verify_user(session: Session, otp: str):
is_verified = True
acc_verifcation = user_repo.get_user_verification(session, otp)
is_expired = acc_verifcation.created_at + timedelta(minutes=5) < datetime.utcnow()
if not acc_verifcation or is_expired:
return False

update_verification_status(session, acc_verifcation.phone_number, is_verified)
session.commit()
return True


def update_verification_status(session: Session, phone_number: str, status: bool):
user_repo.update_verified_status(session, phone_number, status)
session.commit()


Expand All @@ -22,7 +48,7 @@ def authenticate_user(user_id: str):
return {"message": f"Hello {user_id}"}


def update_user_info(user_id: str):
def update_user_info(session: Session, user_id: str, user_info: Dict[str, Any]):
return {"message": f"Hello {user_id}"}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
"""initial database migration
"""Reset initial migration
Revision ID: 5aa436db1feb
Revision ID: 5c2f90b45a61
Revises:
Create Date: 2023-05-19 22:11:25.149220
Create Date: 2023-05-20 15:39:53.164979
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "5aa436db1feb"
revision = "5c2f90b45a61"
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"account_verification",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("token", sa.String(length=30), nullable=False),
sa.Column("phone_number", sa.String(length=30), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"books",
sa.Column("id", sa.Integer(), nullable=False),
Expand All @@ -38,6 +47,7 @@ def upgrade():
sa.Column("phone_number", sa.String(length=30), nullable=False),
sa.Column("email", sa.String(length=30), nullable=False),
sa.Column("password_hash", sa.String(length=255), nullable=False),
sa.Column("is_verified", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
Expand Down Expand Up @@ -142,4 +152,5 @@ def downgrade():
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_table("users")
op.drop_table("books")
op.drop_table("account_verification")
# ### end Alembic commands ###
Empty file added test.py
Empty file.

0 comments on commit ee08aaf

Please sign in to comment.