mirror of
https://github.com/aykhans/portfolio-blog.git
synced 2025-04-18 19:39:43 +00:00
Added send-email feature
This commit is contained in:
parent
6d45d1c604
commit
a1b3d23c37
@ -1 +1,7 @@
|
|||||||
SECRET_KEY="SECRET_KEY"
|
SECRET_KEY="SECRET_KEY"
|
||||||
|
SMTP_HOST="smtp.gmail.com"
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SSL_TLS=False
|
||||||
|
SMTP_USER="SMTP_USER"
|
||||||
|
SMTP_PASSWORD="SMTP_PASSWORD"
|
||||||
|
EMAIL_RECIPIENTS=["EMAIL_RECIPIENT", "EMAIL_RECIPIENT"]
|
@ -1,7 +1,12 @@
|
|||||||
from pydantic import PostgresDsn
|
from typing import Optional
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from pydantic import (
|
||||||
|
EmailStr,
|
||||||
|
PostgresDsn
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
PROJECT_NAME: str = 'FastAPI Portfolio & Blog'
|
PROJECT_NAME: str = 'FastAPI Portfolio & Blog'
|
||||||
@ -36,5 +41,14 @@ class Settings(BaseSettings):
|
|||||||
path=self.POSTGRES_DB
|
path=self.POSTGRES_DB
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SMTP_SSL_TLS: bool = True
|
||||||
|
SMTP_PORT: int = 587
|
||||||
|
SMTP_HOST: str = "smtp.gmail.com"
|
||||||
|
SMTP_USER: EmailStr
|
||||||
|
SMTP_PASSWORD: str
|
||||||
|
|
||||||
|
EMAILS_FROM_NAME: str = PROJECT_NAME
|
||||||
|
EMAIL_RECIPIENTS: list[EmailStr] = []
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
@ -40,30 +40,6 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.include_router(main_router)
|
app.include_router(main_router)
|
||||||
|
|
||||||
# templates = Jinja2Templates(directory=main_path / 'templates')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @app.get("/", response_class=HTMLResponse)
|
|
||||||
# async def read_item(request: Request):
|
|
||||||
# return templates.TemplateResponse("test.html", {"request": request})
|
|
||||||
|
|
||||||
# from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
# class User(BaseModel):
|
|
||||||
# username: str
|
|
||||||
# password: str
|
|
||||||
|
|
||||||
# @app.post("/", response_class=HTMLResponse)
|
|
||||||
# async def read_item(request: Request, user: User):
|
|
||||||
# print(user)
|
|
||||||
# return templates.TemplateResponse("index.html", {"request": request})
|
|
||||||
|
|
||||||
# @app.get("/t", response_class=HTMLResponse)
|
|
||||||
# async def read_item(request: Request):
|
|
||||||
# return templates.TemplateResponse("index.html", {"request": request})
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(ValidationError)
|
@app.exception_handler(ValidationError)
|
||||||
async def validation_exception_handler(request, exc):
|
async def validation_exception_handler(request, exc):
|
||||||
|
14
src/app/schemas/main.py
Normal file
14
src/app/schemas/main.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Form
|
||||||
|
|
||||||
|
from pydantic import EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SendEmail:
|
||||||
|
name: str = Form(..., max_length=50)
|
||||||
|
email: EmailStr = Form(...)
|
||||||
|
phone: Optional[str] = Form(None, max_length=20)
|
||||||
|
message: str = Form(..., max_length=1000)
|
@ -56,18 +56,18 @@
|
|||||||
<div class="col-md-10 col-lg-8 mx-auto">
|
<div class="col-md-10 col-lg-8 mx-auto">
|
||||||
<h1>Contact me</h1>
|
<h1>Contact me</h1>
|
||||||
<p>Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!</p>
|
<p>Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!</p>
|
||||||
<form id="contactForm" name="sentMessage">
|
<form action="/send-email" method="POST" id="contactForm" name="sentMessage">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="form-floating controls mb-3"><input class="form-control" type="text" id="name" required="" placeholder="Name"><label class="form-label" for="name">Name</label><small class="form-text text-danger help-block"></small></div>
|
<div class="form-floating controls mb-3"><input name="name" class="form-control" type="text" id="name" required maxlength="50" placeholder="Name"><label class="form-label" for="name">Name</label><small class="form-text text-danger help-block"></small></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="form-floating controls mb-3"><input class="form-control" type="email" id="email" required="" placeholder="Email Address"><label class="form-label">Email Address</label><small class="form-text text-danger help-block"></small></div>
|
<div class="form-floating controls mb-3"><input class="form-control" name="email" type="email" id="email" required placeholder="Email Address"><label class="form-label">Email Address</label><small class="form-text text-danger help-block"></small></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="form-floating controls mb-3"><input class="form-control" type="tel" id="phone" required="" placeholder="Phone Number"><label class="form-label">Phone Number</label><small class="form-text text-danger help-block"></small></div>
|
<div class="form-floating controls mb-3"><input class="form-control" name="phone" type="tel" id="phone" maxlength="20" placeholder="Phone Number"><label class="form-label">Phone Number</label><small class="form-text text-danger help-block"></small></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="form-floating controls mb-3"><textarea class="form-control" id="message" data-validation-required-message="Please enter a message." required="" placeholder="Message" style="height: 150px;"></textarea><label class="form-label">Message</label><small class="form-text text-danger help-block"></small></div>
|
<div class="form-floating controls mb-3"><textarea class="form-control" name="message" id="message" maxlength="1000" data-validation-required-message="Please enter a message." required="" placeholder="Message" style="height: 150px;"></textarea><label class="form-label">Message</label><small class="form-text text-danger help-block"></small></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="success"></div>
|
<div id="success"></div>
|
||||||
<div class="mb-3"><button class="btn btn-primary" id="sendMessageButton" type="submit">Send</button></div>
|
<div class="mb-3"><button class="btn btn-primary" id="sendMessageButton" type="submit">Send</button></div>
|
||||||
|
37
src/app/utils/email.py
Normal file
37
src/app/utils/email.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from fastapi_mail import (
|
||||||
|
FastMail,
|
||||||
|
MessageSchema,
|
||||||
|
ConnectionConfig,
|
||||||
|
MessageType
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def send_email_notification(
|
||||||
|
subject: str,
|
||||||
|
body: str
|
||||||
|
) -> partial:
|
||||||
|
|
||||||
|
if settings.EMAIL_RECIPIENTS:
|
||||||
|
conf = ConnectionConfig(
|
||||||
|
MAIL_USERNAME = settings.SMTP_USER,
|
||||||
|
MAIL_PASSWORD = settings.SMTP_PASSWORD,
|
||||||
|
MAIL_FROM = settings.SMTP_USER,
|
||||||
|
MAIL_PORT = settings.SMTP_PORT,
|
||||||
|
MAIL_SERVER = settings.SMTP_HOST,
|
||||||
|
MAIL_SSL_TLS = settings.SMTP_SSL_TLS,
|
||||||
|
MAIL_STARTTLS = True
|
||||||
|
)
|
||||||
|
|
||||||
|
message = MessageSchema(
|
||||||
|
subject = subject,
|
||||||
|
recipients = settings.EMAIL_RECIPIENTS,
|
||||||
|
body = body,
|
||||||
|
subtype=MessageType.plain
|
||||||
|
)
|
||||||
|
|
||||||
|
fast_mail = FastMail(conf)
|
||||||
|
return partial(fast_mail.send_message, message)
|
@ -1,12 +1,13 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
Depends
|
Depends,
|
||||||
|
BackgroundTasks
|
||||||
)
|
)
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@ -14,6 +15,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.schemas import ListPostInTemplate
|
from app.schemas import ListPostInTemplate
|
||||||
|
from app.schemas.main import SendEmail
|
||||||
|
from app.utils.email import send_email_notification
|
||||||
from app.views.depends import get_async_db
|
from app.views.depends import get_async_db
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +33,31 @@ async def home(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/send-email')
|
||||||
|
async def send_email(
|
||||||
|
request: Request,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
form_data: SendEmail = Depends()
|
||||||
|
):
|
||||||
|
|
||||||
|
body = f"name: {form_data.name}\n"\
|
||||||
|
f"email: {form_data.email}\n"\
|
||||||
|
f"phone: {form_data.phone}\n"\
|
||||||
|
f"message: {form_data.message}"
|
||||||
|
|
||||||
|
background_tasks.add_task(
|
||||||
|
send_email_notification(
|
||||||
|
subject = f"Portfolio Blog (by {form_data.email})",
|
||||||
|
body = body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
str(request.url_for('home')) + '#contact',
|
||||||
|
status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/blog', response_class=HTMLResponse)
|
@router.get('/blog', response_class=HTMLResponse)
|
||||||
async def blog(
|
async def blog(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
@ -23,9 +23,6 @@ if config.config_file_name is not None:
|
|||||||
# target_metadata = mymodel.Base.metadata
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
|
||||||
########### Custom Config ###########
|
########### Custom Config ###########
|
||||||
from os import getenv
|
|
||||||
print('-'*100)
|
|
||||||
print(getenv('POSTGRES_USER'))
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
|
52
src/poetry.lock
generated
52
src/poetry.lock
generated
@ -11,6 +11,21 @@ files = [
|
|||||||
{file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"},
|
{file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosmtplib"
|
||||||
|
version = "2.0.2"
|
||||||
|
description = "asyncio SMTP client"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7,<4.0"
|
||||||
|
files = [
|
||||||
|
{file = "aiosmtplib-2.0.2-py3-none-any.whl", hash = "sha256:1e631a7a3936d3e11c6a144fb8ffd94bb4a99b714f2cb433e825d88b698e37bc"},
|
||||||
|
{file = "aiosmtplib-2.0.2.tar.gz", hash = "sha256:138599a3227605d29a9081b646415e9e793796ca05322a78f69179f0135016a3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0,<6.0.0)", "sphinx_autodoc_typehints (>=1.7.0,<2.0.0)"]
|
||||||
|
uvloop = ["uvloop (>=0.14,<0.15)", "uvloop (>=0.14,<0.15)", "uvloop (>=0.17,<0.18)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@ -115,6 +130,17 @@ files = [
|
|||||||
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||||
test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"]
|
test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blinker"
|
||||||
|
version = "1.6.2"
|
||||||
|
description = "Fast, simple object-to-object and broadcast signaling"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"},
|
||||||
|
{file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@ -347,6 +373,30 @@ typing-extensions = ">=4.5.0"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi-mail"
|
||||||
|
version = "1.4.1"
|
||||||
|
description = "Simple lightweight mail library for FastApi"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8.1,<4.0"
|
||||||
|
files = [
|
||||||
|
{file = "fastapi_mail-1.4.1-py3-none-any.whl", hash = "sha256:fa5ef23b2dea4d3ba4587f4bbb53f8f15274124998fb4e40629b3b636c76c398"},
|
||||||
|
{file = "fastapi_mail-1.4.1.tar.gz", hash = "sha256:9095b713bd9d3abb02fe6d7abb637502aaf680b52e177d60f96273ef6bc8bb70"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiosmtplib = ">=2.0,<3.0"
|
||||||
|
blinker = ">=1.5,<2.0"
|
||||||
|
email-validator = ">=2.0,<3.0"
|
||||||
|
Jinja2 = ">=3.0,<4.0"
|
||||||
|
pydantic = ">=2.0,<3.0"
|
||||||
|
pydantic_settings = ">=2.0,<3.0"
|
||||||
|
starlette = ">=0.24,<1.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
httpx = ["httpx[httpx] (>=0.23,<0.24)"]
|
||||||
|
redis = ["redis[redis] (>=4.3,<5.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@ -1064,4 +1114,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "98704ea52451d766de4392fa5dc14eaa8eb336758ae4e74a7ecc618439b5af17"
|
content-hash = "333a9be3573863da41b6a2615cd28aa3a6daf2e608a76da1476af72d343dae52"
|
||||||
|
@ -22,6 +22,7 @@ python-slugify = "^8.0.1"
|
|||||||
pillow = "^10.0.0"
|
pillow = "^10.0.0"
|
||||||
aiofiles = "^23.2.1"
|
aiofiles = "^23.2.1"
|
||||||
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
|
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
|
||||||
|
fastapi-mail = "^1.4.1"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user