Reset hesla, doplneni mailu
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ FROM python:3.11-alpine AS app
|
||||
RUN apk update && \
|
||||
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
||||
pip install --upgrade pip
|
||||
\
|
||||
|
||||
# protoze xfs uzivatel ma id=33
|
||||
RUN deluser xfs
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""add password_reset_token table
|
||||
|
||||
Revision ID: a410455fb3bb
|
||||
Revises: 39ebf5a1ab7e
|
||||
Create Date: 2024-02-08 07:59:02.066212
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a410455fb3bb'
|
||||
down_revision = '39ebf5a1ab7e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('password_reset_token',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('token', sa.String(length=64), nullable=False),
|
||||
sa.Column('valid_until', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('password_reset_token')
|
||||
# ### end Alembic commands ###
|
||||
@@ -8,7 +8,7 @@ from logger import log
|
||||
|
||||
async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
||||
async with get_session() as db:
|
||||
airport = await models.Airport.get_one(db, airport_id)
|
||||
airport = await models.Airport.get_one(db, id=airport_id)
|
||||
gps = (airport.gps_latitude, airport.gps_longitude)
|
||||
|
||||
try:
|
||||
@@ -31,7 +31,7 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
|
||||
}
|
||||
|
||||
async with get_session() as db:
|
||||
flight = await models.Flight.get_one(db, flight_id)
|
||||
flight = await models.Flight.get_one(db, id=flight_id)
|
||||
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
|
||||
|
||||
if existing_weather_id:
|
||||
|
||||
+14
-3
@@ -18,8 +18,8 @@ class BaseModel:
|
||||
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
|
||||
|
||||
@classmethod
|
||||
async def get_one(cls, db_session: AsyncSession, id: int):
|
||||
return (await db_session.scalars(select(cls).filter_by(id=id))).one()
|
||||
async def get_one(cls, db_session: AsyncSession, **kwargs):
|
||||
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
|
||||
|
||||
@classmethod
|
||||
async def create(cls, db_session: AsyncSession, data: dict):
|
||||
@@ -35,7 +35,7 @@ class BaseModel:
|
||||
raise ValueError("Provide either obj or id!")
|
||||
|
||||
if not obj:
|
||||
obj = await cls.get_one(db_session, id)
|
||||
obj = await cls.get_one(db_session, id=id)
|
||||
for key, value in data.items():
|
||||
if key in cls._get_column_names() and getattr(obj, key) != value:
|
||||
setattr(obj, key, value)
|
||||
@@ -371,3 +371,14 @@ class User(BaseModel):
|
||||
licences: Mapped[Set['License']] = relationship()
|
||||
flights: Mapped[Set['Flight']] = relationship()
|
||||
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
||||
|
||||
|
||||
class PasswordResetToken(BaseModel):
|
||||
__tablename__ = "password_reset_token"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
token: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
user: Mapped['User'] = relationship()
|
||||
|
||||
@@ -30,9 +30,9 @@ class ContactFormEmail(Email):
|
||||
|
||||
|
||||
class WelcomeEmail(Email):
|
||||
def __init__(self, to: list[EmailStr | str]):
|
||||
def __init__(self, to: EmailStr | str):
|
||||
super().__init__(
|
||||
to=to,
|
||||
to=[to],
|
||||
subject="Vítej",
|
||||
template="welcome.html",
|
||||
template_variables={}
|
||||
@@ -40,10 +40,13 @@ class WelcomeEmail(Email):
|
||||
|
||||
|
||||
class ResetPasswordEmail(Email):
|
||||
def __init__(self, to: list[EmailStr | str], template_variables: dict):
|
||||
def __init__(self, to: EmailStr | str, reset_token: str, validity_hours: int):
|
||||
super().__init__(
|
||||
to=to,
|
||||
subject="Reset hesla",
|
||||
to=[to],
|
||||
subject="Žádost o reset",
|
||||
template="reset_password.html",
|
||||
template_variables=template_variables
|
||||
template_variables={
|
||||
"token": reset_token,
|
||||
"validity_hours": validity_hours,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,10 +11,16 @@ class ContactInput(BaseModel):
|
||||
|
||||
|
||||
class ContactEndpoint():
|
||||
async def on_post(self, input: ContactInput, resp: Response, backgorund_tasks: BackgroundTasks):
|
||||
async def on_post(self, input: ContactInput, backgorund_tasks: BackgroundTasks):
|
||||
backgorund_tasks.add_task(send_email, ContactFormEmail(
|
||||
email_from=input.email_from,
|
||||
content=input.content
|
||||
))
|
||||
|
||||
# todo vratit nejakou smysluplnou
|
||||
return {
|
||||
"status": "success",
|
||||
"email": {
|
||||
"from": input.email_from,
|
||||
"content": input.content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,84 @@
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
import binascii
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import BackgroundTasks
|
||||
from passlib.hash import bcrypt
|
||||
from pydantic import BaseModel
|
||||
from background_jobs.send_email import send_email
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from emails.emails import ResetPasswordEmail
|
||||
|
||||
|
||||
class ForgottenPasswordRequest(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class ChangeForgottenPassword(BaseModel):
|
||||
password: str
|
||||
token: str
|
||||
|
||||
|
||||
class ForgottenPasswordEndpoint():
|
||||
async def on_get(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def generate_random_token(length):
|
||||
characters = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(characters) for i in range(length))
|
||||
|
||||
@staticmethod
|
||||
async def token_info(token: str):
|
||||
async with get_session() as db:
|
||||
reset = await models.PasswordResetToken.get_one(db, token=token)
|
||||
user = await models.User.get_one(db, id=reset.user_id)
|
||||
token_validity = reset.valid_until
|
||||
email, name = user.email, user.name
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"token_validity": token_validity,
|
||||
"user_email": email,
|
||||
"user_name": name
|
||||
}
|
||||
|
||||
async def request(self, input: ForgottenPasswordRequest, backgorund_tasks: BackgroundTasks):
|
||||
validity_hours = 8
|
||||
validity = datetime.now() + timedelta(hours=validity_hours)
|
||||
random_str_token = self.generate_random_token(64)
|
||||
|
||||
async with get_session() as db:
|
||||
user = await models.User.get_one(db, email=input.email)
|
||||
token = await models.PasswordResetToken.create(db, {
|
||||
"user_id": user.id,
|
||||
"token": random_str_token,
|
||||
"valid_until": validity
|
||||
})
|
||||
|
||||
backgorund_tasks.add_task(
|
||||
send_email,
|
||||
email=ResetPasswordEmail(
|
||||
to=input.email,
|
||||
reset_token=token,
|
||||
validity_hours=validity_hours
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def change_password(input: ChangeForgottenPassword):
|
||||
async with get_session() as db:
|
||||
token = await models.PasswordResetToken.get_one(db, token=input.token)
|
||||
user_id = token.user_id
|
||||
|
||||
await models.User.update(
|
||||
db, id=token.user_id,
|
||||
data={"password_hashed": bcrypt.hash(input.password)}
|
||||
)
|
||||
|
||||
await db.delete(token)
|
||||
|
||||
return {
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
from datetime import timedelta
|
||||
from fastapi import FastAPI, Security, Depends, BackgroundTasks, APIRouter
|
||||
from fastapi_jwt import JwtAuthorizationCredentials
|
||||
from fastapi_jwt.jwt import JwtAccessBearerCookie
|
||||
from sqlalchemy import select
|
||||
from starlette.responses import RedirectResponse
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
from config import GRAPHIQL, APP_DEBUG
|
||||
from database import async_session, models
|
||||
from graphql_schema.schema import GraphQLContext, schema
|
||||
|
||||
|
||||
def setup_graphql_endpoint(app: FastAPI, access_security: JwtAccessBearerCookie, api_router: APIRouter):
|
||||
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
||||
user_id = credentials['id'] if credentials else None
|
||||
organization_ids = set()
|
||||
|
||||
if user_id:
|
||||
async with async_session() as db:
|
||||
organization_ids = set((await db.scalars(
|
||||
select(models.user_is_in_organization.c.organization_id)
|
||||
.filter(models.user_is_in_organization.c.user_id == user_id)
|
||||
)).all())
|
||||
|
||||
if APP_DEBUG:
|
||||
@api_router.get("/graphql/autologin")
|
||||
async def autologin():
|
||||
access_token = access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
|
||||
|
||||
response = RedirectResponse(url="/graphql")
|
||||
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
|
||||
|
||||
return response
|
||||
|
||||
return GraphQLContext(
|
||||
user_id=user_id,
|
||||
organization_ids=organization_ids,
|
||||
jwt_auth_credentials=credentials,
|
||||
jwt=access_security,
|
||||
background_tasks=Depends(BackgroundTasks)
|
||||
)
|
||||
|
||||
graphql_app = GraphQLRouter(
|
||||
schema,
|
||||
graphiql=GRAPHIQL,
|
||||
debug=APP_DEBUG,
|
||||
context_getter=setup_graphql_context
|
||||
)
|
||||
app.include_router(graphql_app, prefix="/graphql")
|
||||
@@ -1,11 +1,13 @@
|
||||
import re
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, BackgroundTasks
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from background_jobs.send_email import send_email
|
||||
from database.models import User
|
||||
from database.transaction import get_session
|
||||
from passlib.hash import bcrypt
|
||||
from emails.emails import WelcomeEmail
|
||||
|
||||
|
||||
class RegistrationInput(BaseModel):
|
||||
@@ -24,7 +26,8 @@ class RegistrationInput(BaseModel):
|
||||
|
||||
|
||||
class RegistrationEndpoint:
|
||||
async def on_post(self, user_data: RegistrationInput) -> User:
|
||||
@staticmethod
|
||||
async def on_post(user_data: RegistrationInput, backgorund_tasks: BackgroundTasks) -> User:
|
||||
query = select(User).filter_by(email=user_data.email)
|
||||
async with get_session() as db:
|
||||
existing_user = (await db.scalars(query)).first()
|
||||
@@ -39,6 +42,6 @@ class RegistrationEndpoint:
|
||||
"description": ""
|
||||
})
|
||||
|
||||
# TODO: odeslat WelcomeEmail
|
||||
backgorund_tasks.add_task(send_email, WelcomeEmail(to=user_data.email))
|
||||
|
||||
return model.as_dict()
|
||||
|
||||
@@ -8,6 +8,7 @@ class Mailer:
|
||||
self.conf = ConnectionConfig(
|
||||
MAIL_USERNAME=SMTP_USERNAME,
|
||||
MAIL_PASSWORD=SMTP_PASSWORD,
|
||||
MAIL_FROM="michalkv@gmail.com",
|
||||
MAIL_FROM_NAME="Polétání",
|
||||
MAIL_PORT=SMTP_PORT,
|
||||
MAIL_SERVER=SMTP_HOST,
|
||||
|
||||
+28
-60
@@ -1,25 +1,21 @@
|
||||
import sentry_sdk
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from fastapi import FastAPI, APIRouter, Depends, Security, HTTPException
|
||||
from fastapi import FastAPI, APIRouter, Security, HTTPException
|
||||
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
|
||||
from graphql import GraphQLError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import RedirectResponse, Response
|
||||
from starlette.responses import Response, JSONResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS
|
||||
from database import models, async_session
|
||||
from emails.emails import WelcomeEmail
|
||||
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS
|
||||
from endpoints.contact import ContactEndpoint, ContactInput
|
||||
from endpoints.forgotten_password import ForgottenPasswordEndpoint
|
||||
from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword
|
||||
from endpoints.graphql import setup_graphql_endpoint
|
||||
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
|
||||
from endpoints.photo_editor_preview import PhotoEditorEndpoint
|
||||
from endpoints.registration import RegistrationInput, RegistrationEndpoint
|
||||
from graphql_schema.schema import schema, GraphQLContext
|
||||
from mailer import Mailer
|
||||
|
||||
|
||||
class App:
|
||||
@@ -53,7 +49,9 @@ class App:
|
||||
|
||||
@staticmethod
|
||||
def setup_exception_handlers(app: FastAPI):
|
||||
pass
|
||||
@app.exception_handler(NoResultFound)
|
||||
async def validation_exception_handler(request, exc):
|
||||
return JSONResponse({"message": "Not found"}, status_code=404)
|
||||
|
||||
@staticmethod
|
||||
def setup_middleware(app: FastAPI):
|
||||
@@ -70,46 +68,8 @@ class App:
|
||||
app.mount("/uploads", StaticFiles(directory="/app/uploads"), name="uploads")
|
||||
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
|
||||
|
||||
def setup_graphql_endpoint(self, app: FastAPI):
|
||||
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(self.access_security)):
|
||||
user_id = credentials['id'] if credentials else None
|
||||
organization_ids = set()
|
||||
|
||||
if user_id:
|
||||
async with async_session() as db:
|
||||
organization_ids = set((await db.scalars(
|
||||
select(models.user_is_in_organization.c.organization_id)
|
||||
.filter(models.user_is_in_organization.c.user_id == user_id)
|
||||
)).all())
|
||||
|
||||
return GraphQLContext(
|
||||
user_id=user_id,
|
||||
organization_ids=organization_ids,
|
||||
jwt_auth_credentials=credentials,
|
||||
jwt=self.access_security,
|
||||
background_tasks=Depends(BackgroundTasks)
|
||||
)
|
||||
|
||||
graphql_app = GraphQLRouter(
|
||||
schema,
|
||||
graphiql=GRAPHIQL,
|
||||
debug=APP_DEBUG,
|
||||
context_getter=setup_graphql_context
|
||||
)
|
||||
app.include_router(graphql_app, prefix="/graphql")
|
||||
|
||||
def setup_routes(self, app: FastAPI):
|
||||
self.setup_graphql_endpoint(app)
|
||||
|
||||
if APP_DEBUG:
|
||||
@self.api_router.get("/graphql/autologin")
|
||||
async def autologin():
|
||||
access_token = self.access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
|
||||
|
||||
response = RedirectResponse(url="/graphql")
|
||||
self.access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
|
||||
|
||||
return response
|
||||
setup_graphql_endpoint(app, self.access_security, self.api_router)
|
||||
|
||||
@self.api_router.post("/refresh")
|
||||
async def refresh(
|
||||
@@ -121,6 +81,10 @@ class App:
|
||||
refresh_token=self.refresh_security
|
||||
).on_post(resp, credentials)
|
||||
|
||||
@self.api_router.post("/registration", status_code=201)
|
||||
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
|
||||
return await RegistrationEndpoint().on_post(user, background_tasks)
|
||||
|
||||
@self.api_router.post("/login")
|
||||
async def login(resp: Response, user: LoginInput):
|
||||
return await LoginEndpoint(
|
||||
@@ -128,13 +92,21 @@ class App:
|
||||
refresh_token=self.refresh_security
|
||||
).on_post(user, resp)
|
||||
|
||||
@self.api_router.get("/forgotten-password/token/{token}")
|
||||
async def token_info(token: str):
|
||||
return await ForgottenPasswordEndpoint().token_info(token)
|
||||
|
||||
@self.api_router.post("/forgotten-password/request")
|
||||
async def password_reset(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks):
|
||||
return await ForgottenPasswordEndpoint().request(input, background_tasks)
|
||||
|
||||
@self.api_router.post("/forgotten-password/reset")
|
||||
async def password_reset(input: ChangeForgottenPassword):
|
||||
return await ForgottenPasswordEndpoint().change_password(input)
|
||||
|
||||
@self.api_router.post("/contact")
|
||||
async def contact(resp: Response, input: ContactInput, background_tasks: BackgroundTasks):
|
||||
return await ContactEndpoint().on_post(
|
||||
input,
|
||||
resp,
|
||||
background_tasks
|
||||
)
|
||||
async def contact(input: ContactInput, background_tasks: BackgroundTasks):
|
||||
return await ContactEndpoint().on_post(input, background_tasks)
|
||||
|
||||
@self.api_router.post("/logout")
|
||||
async def logout(resp: Response):
|
||||
@@ -173,9 +145,5 @@ class App:
|
||||
rotate=rotate,
|
||||
)
|
||||
|
||||
@self.api_router.post("/registration", status_code=201)
|
||||
async def registration(user: RegistrationInput):
|
||||
return await RegistrationEndpoint().on_post(user)
|
||||
|
||||
# musi byt na konci
|
||||
app.include_router(self.api_router)
|
||||
|
||||
Reference in New Issue
Block a user