Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
marveldo committed Aug 13, 2024
2 parents 4c31090 + 700ef58 commit bd5e43e
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 104 deletions.
43 changes: 43 additions & 0 deletions api/core/dependencies/email/templates/signin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends 'base.html' %}

{% block title %}
Magic Link
{% endblock %}

{% block content %}
<div class="container"
style="
width: 100%;
max-width: 600px;
margin: auto;
padding: 20px;
font-family: Arial, sans-serif;
color: #333;
">
<div class="header"
style="
text-align: center;
padding-bottom: 20px;
">
<h1>Welcome to HNG Boilerplates! {{ first_name }}</h1>
</div>
<div class="content"
style="
margin-bottom: 20px;
">
<p>Click the link below to sign in:</p>
<a href="{{ link }}" class="btn"
style="
display: inline-block;
padding: 10px 200px;
background-color: orangered;
color: #fff;
text-decoration: none;
border-radius: 10px;
">Please click here to sign in!</a>
</div>
<p>If the button doesn't work, copy and paste the following link into your browser:</p>
<p>{{ link }}</p>
</div>
{% endblock %}

23 changes: 9 additions & 14 deletions api/utils/send_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,25 @@
from email.mime.multipart import MIMEMultipart
from api.utils.settings import settings

url = "deployment.api-python.boilerplate.hng.tech"


def send_magic_link(email: str, token: str):
async def send_magic_link(context: dict):
"""Sends magic-kink to user email"""
from main import email_templates
sender_email = settings.MAIL_USERNAME
receiver_email = email
receiver_email = context.get('email')
password = settings.MAIL_PASSWORD

html = email_templates.get_template("signin.html").render(context)

message = MIMEMultipart("alternative")
message["Subject"] = "Your Magic Link"
message["From"] = sender_email
message["To"] = receiver_email

part = MIMEText(html, "html")

text = f"Use the following link to log in: http://{url}/magic-link?token={token}"
html = f"<html><body><p>Use the following link to log in: <a href='http://{url}/verify-magic-link?token={token}'>Magic Link</a></p></body></html>"

part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")

message.attach(part1)
message.attach(part2)
message.attach(part)

with smtplib.SMTP(settings.MAIL_SERVER, settings.MAIL_PORT) as server:
server.starttls()
with smtplib.SMTP_SSL(settings.MAIL_SERVER, settings.MAIL_PORT) as server:
server.login(sender_email, password)
server.sendmail(sender_email, receiver_email, message.as_string())
5 changes: 3 additions & 2 deletions api/v1/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from api.v1.routes.team import team
from fastapi import APIRouter
from api.v1.routes.auth import auth
from api.v1.routes.newsletter import newsletter
from api.v1.routes.newsletter import newsletter, news_sub
from api.v1.routes.user import user_router
from api.v1.routes.product import product, non_organisation_product
from api.v1.routes.product_comment import product_comment
Expand Down Expand Up @@ -73,6 +73,7 @@
api_version_one.include_router(contact_us)
api_version_one.include_router(waitlist_router)
api_version_one.include_router(newsletter)
api_version_one.include_router(news_sub)
api_version_one.include_router(testimonial)
api_version_one.include_router(test_rout)
api_version_one.include_router(email_sender)
Expand All @@ -90,4 +91,4 @@
api_version_one.include_router(team)
api_version_one.include_router(terms_and_conditions)
api_version_one.include_router(product_comment)
api_version_one.include_router(subscription_)
api_version_one.include_router(subscription_)
52 changes: 43 additions & 9 deletions api/v1/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from api.v1.schemas.user import Token
from api.v1.schemas.user import LoginRequest, UserCreate, EmailRequest
from api.v1.schemas.token import TokenRequest
from api.v1.schemas.user import UserCreate, MagicLinkRequest
from api.v1.schemas.user import UserCreate, MagicLinkRequest, ChangePasswordSchema
from api.v1.services.organisation import organisation_service
from api.v1.schemas.organisation import CreateUpdateOrganisation
from api.db.database import get_db
Expand All @@ -33,11 +33,13 @@ def register(background_tasks: BackgroundTasks, response: Response, user_schema:
name=f"{user.email}'s Organisation",
email=user.email
)
user_org = organisation_service.create(db=db, schema=org, user=user)
organisation_service.create(db=db, schema=org, user=user)
user_organizations = organisation_service.retrieve_user_organizations(user, db)

# Create access and refresh tokens
access_token = user_service.create_access_token(user_id=user.id)
refresh_token = user_service.create_refresh_token(user_id=user.id)
cta_link = 'https://anchor-python.teams.hng.tech/about-us'

# Send email in the background
background_tasks.add_task(
Expand All @@ -47,7 +49,8 @@ def register(background_tasks: BackgroundTasks, response: Response, user_schema:
subject='Welcome to HNG Boilerplate',
context={
'first_name': user.first_name,
'last_name': user.last_name
'last_name': user.last_name,
'cta_link': cta_link
}
)

Expand All @@ -59,7 +62,8 @@ def register(background_tasks: BackgroundTasks, response: Response, user_schema:
'user': jsonable_encoder(
user,
exclude=['password', 'is_deleted', 'is_verified', 'updated_at']
)
),
'organizations': user_organizations
}
)

Expand Down Expand Up @@ -119,6 +123,7 @@ def login(login_request: LoginRequest, db: Session = Depends(get_db)):
user = user_service.authenticate_user(
db=db, email=login_request.email, password=login_request.password
)
user_organizations = organisation_service.retrieve_user_organizations(user, db)

# Generate access and refresh tokens
access_token = user_service.create_access_token(user_id=user.id)
Expand All @@ -132,7 +137,8 @@ def login(login_request: LoginRequest, db: Session = Depends(get_db)):
'user': jsonable_encoder(
user,
exclude=['password', 'is_deleted', 'is_verified', 'updated_at']
)
),
'organizations': user_organizations
}
)

Expand Down Expand Up @@ -259,11 +265,22 @@ async def verify_signin_token(
# TODO: Fix magic link authentication
@auth.post("/magic-link", status_code=status.HTTP_200_OK)
def request_magic_link(
request: MagicLinkRequest, response: Response, db: Session = Depends(get_db)
request: MagicLinkRequest, background_tasks: BackgroundTasks,
response: Response, db: Session = Depends(get_db)
):
user = user_service.fetch_by_email(db=db, email=request.email)
access_token = user_service.create_access_token(user_id=user.id)
send_magic_link(user.email, access_token)
magic_link_token = user_service.create_access_token(user_id=user.id)
magic_link = f"https://anchor-python.teams.hng.tech/magic-link/verify?token={magic_link_token}"

background_tasks.add_task(
send_magic_link,
context={
'first_name': user.first_name,
'last_name': user.last_name,
'link': magic_link,
'email': user.email
}
)

response = success_response(
status_code=200, message=f"Magic link sent to {user.email}"
Expand All @@ -274,6 +291,7 @@ def request_magic_link(
@auth.post("/magic-link/verify")
async def verify_magic_link(token_schema: Token, db: Session = Depends(get_db)):
user, access_token = AuthService.verify_magic_token(token_schema.access_token, db)
user_organizations = organisation_service.retrieve_user_organizations(user, db)

refresh_token = user_service.create_refresh_token(user_id=user.id)

Expand All @@ -285,7 +303,8 @@ async def verify_magic_link(token_schema: Token, db: Session = Depends(get_db)):
'user': jsonable_encoder(
user,
exclude=['password', 'is_deleted', 'is_verified', 'updated_at']
)
),
'organizations': user_organizations
}
)

Expand All @@ -300,3 +319,18 @@ async def verify_magic_link(token_schema: Token, db: Session = Depends(get_db)):
)

return response


@auth.patch("/change-password", status_code=200)
async def change_password(
schema: ChangePasswordSchema,
db: Session = Depends(get_db),
user: User = Depends(user_service.get_current_user),
):
"""Endpoint to change the user's password"""
user_service.change_password(new_password=schema.new_password,
user=user,
db=db,
old_password=schema.old_password)

return success_response(status_code=200, message="Password changed successfully")
51 changes: 31 additions & 20 deletions api/v1/routes/newsletter.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
from fastapi import APIRouter, Depends, status , Query
from fastapi import APIRouter, Depends, status, Query
from typing import Annotated
from sqlalchemy.orm import Session
from api.utils.success_response import success_response
from api.v1.schemas.newsletter import EmailSchema, EmailRetrieveSchema, SingleNewsletterResponse, UpdateNewsletter
from api.v1.schemas.newsletter import (
EmailSchema,
EmailRetrieveSchema,
SingleNewsletterResponse,
UpdateNewsletter,
)
from api.db.database import get_db
from api.v1.services.newsletter import NewsletterService, Newsletter
from fastapi.encoders import jsonable_encoder
from api.v1.models.user import User
from api.v1.services.user import user_service

newsletter = APIRouter(prefix="/newsletters", tags=["Newsletter"])
news_sub = APIRouter(prefix="/newsletter-subscription", tags=["Newsletter"])
from api.utils.pagination import paginated_response



@newsletter.post("/subscribers")
@news_sub.post("")
async def sub_newsletter(request: EmailSchema, db: Session = Depends(get_db)):
"""
Newsletter subscription endpoint
Expand Down Expand Up @@ -59,11 +64,17 @@ def retrieve_subscribers(
data=jsonable_encoder(subs_filtered),
)

@newsletter.get('/{id}', response_model=SingleNewsletterResponse, status_code=status.HTTP_200_OK)

@newsletter.get(
"/{id}", response_model=SingleNewsletterResponse, status_code=status.HTTP_200_OK
)
async def get_single_newsletter(id: str, db: Annotated[Session, Depends(get_db)]):
"""Retrieves a single newsletter."""
newsletter = NewsletterService.fetch(db=db, id=id)
return success_response(message="Successfully fetched newsletter", status_code=200, data=newsletter)
return success_response(
message="Successfully fetched newsletter", status_code=200, data=newsletter
)


@newsletter.delete(
"/{id}",
Expand All @@ -79,40 +90,41 @@ def delete_newsletter(
"""Endpoint to delete a newsletter"""
NewsletterService.delete(db=db, id=id)


@newsletter.patch(
"/{id}",
status_code=status.HTTP_200_OK,
status_code=status.HTTP_200_OK,
)
async def update_newsletter(
id: str,
schema: UpdateNewsletter,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_super_admin)
current_user: User = Depends(user_service.get_current_super_admin),
):
newsletter = NewsletterService.update(db, id, schema)
return success_response(
data=jsonable_encoder(newsletter),
message="Successfully updated a newsletter",
status_code=status.HTTP_200_OK
status_code=status.HTTP_200_OK,
)
@newsletter.get('', status_code=200)


@newsletter.get("", status_code=200)
def get_all_newsletters(
db:Session = Depends(get_db),
page_size: Annotated[int, Query(ge=1, description="Number of products per page")] = 10,
db: Session = Depends(get_db),
page_size: Annotated[
int, Query(ge=1, description="Number of products per page")
] = 10,
page: Annotated[int, Query(ge=1, description="Page number (starts from 1)")] = 0,
):
"""
Retrieving all newsletters
"""

return paginated_response(
db=db,
skip=page,
limit = page_size,
model = Newsletter
)
return paginated_response(db=db, skip=page, limit=page_size, model=Newsletter)

@newsletter.post('/unsubscribe')

@newsletter.post("/unsubscribe")
async def unsubscribe_newsletter(request: EmailSchema, db: Session = Depends(get_db)):
"""
Newsletter unsubscription endpoint
Expand All @@ -122,4 +134,3 @@ async def unsubscribe_newsletter(request: EmailSchema, db: Session = Depends(get
message="Unsubscribed successfully.",
status_code=status.HTTP_200_OK,
)

6 changes: 3 additions & 3 deletions api/v1/routes/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ def stripe_payment(
@subscription_.get("/stripe/success")
def success_upgrade():

return success_response(status_code=status.HTTP_201_CREATED, message="Payment intent initiated")
return success_response(status_code=status.HTTP_200_OK, message="Payment intent initiated")

@subscription_.get("/stripe/cancel")
def cancel_upgrade():
return success_response(status_code=status.HTTP_201_CREATED, message="Payment intent canceled")
return success_response(status_code=status.HTTP_200_OK, message="Payment intent canceled")


@subscription_.get("/plans")
Expand Down Expand Up @@ -88,7 +88,7 @@ async def get_organisations_with_users_and_plans(db: Session = Depends(get_db),
if not data:
raise HTTPException(status_code=404, detail="No data found")
return success_response(
status_code=status.HTTP_302_FOUND,
status_code=status.HTTP_200_OK,
message='billing details successfully retrieved',
data=data,
)
Expand Down
18 changes: 1 addition & 17 deletions api/v1/routes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
from api.utils.success_response import success_response
from api.v1.models.user import User
from api.v1.schemas.user import (
DeactivateUserSchema,
ChangePasswordSchema,
ChangePwdRet, AllUsersResponse, UserUpdate,
AllUsersResponse, UserUpdate,
AdminCreateUserResponse, AdminCreateUser
)
from api.db.database import get_db
Expand Down Expand Up @@ -52,20 +50,6 @@ async def delete_account(request: Request, db: Session = Depends(get_db), curren
message='User deleted successfully',
)


@user_router.patch("/me/password", status_code=200)
async def change_password(
schema: ChangePasswordSchema,
db: Session = Depends(get_db),
user: User = Depends(user_service.get_current_user),
):
"""Endpoint to change the user's password"""

user_service.change_password(schema.old_password, schema.new_password, user, db)

return success_response(status_code=200, message="Password changed successfully")


@user_router.patch("/",status_code=status.HTTP_200_OK)
def update_current_user(
current_user : Annotated[User , Depends(user_service.get_current_user)],
Expand Down
Loading

0 comments on commit bd5e43e

Please sign in to comment.