├── .env.template ├── alembic.ini ├── alembic ├── README ├── __pycache__ │ └── env.cpython-310.pyc ├── env.py ├── script.py.mako └── versions │ ├── __pycache__ │ └── e2119dd03e66_new_migration.cpython-310.pyc │ └── e2119dd03e66_new_migration.py ├── auth.py ├── main.py ├── models.py └── schema.py /.env.template: -------------------------------------------------------------------------------- 1 | DATABASE_URL='' 2 | JWT_SECRET=myjwtsecret 3 | JWT_ALGORITHM=HS256 4 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python-dateutil library that can be 20 | # installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to dateutil.tz.gettz() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # the output encoding used when revision files 55 | # are written from script.py.mako 56 | # output_encoding = utf-8 57 | 58 | sqlalchemy.url = driver://user:pass@localhost/dbname 59 | 60 | 61 | [post_write_hooks] 62 | # post_write_hooks defines scripts or Python functions that are run 63 | # on newly generated revision scripts. See the documentation for further 64 | # detail and examples 65 | 66 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 67 | # hooks = black 68 | # black.type = console_scripts 69 | # black.entrypoint = black 70 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 71 | 72 | # Logging configuration 73 | [loggers] 74 | keys = root,sqlalchemy,alembic 75 | 76 | [handlers] 77 | keys = console 78 | 79 | [formatters] 80 | keys = generic 81 | 82 | [logger_root] 83 | level = WARN 84 | handlers = console 85 | qualname = 86 | 87 | [logger_sqlalchemy] 88 | level = WARN 89 | handlers = 90 | qualname = sqlalchemy.engine 91 | 92 | [logger_alembic] 93 | level = INFO 94 | handlers = 95 | qualname = alembic 96 | 97 | [handler_console] 98 | class = StreamHandler 99 | args = (sys.stderr,) 100 | level = NOTSET 101 | formatter = generic 102 | 103 | [formatter_generic] 104 | format = %(levelname)-5.5s [%(name)s] %(message)s 105 | datefmt = %H:%M:%S 106 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/__pycache__/env.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J0/fastapi-supabase-auth/ba814b2131ebf70340abbba7dd3a7f2f4d8d6ff4/alembic/__pycache__/env.cpython-310.pyc -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | # My code 9 | import os,sys 10 | from dotenv import load_dotenv 11 | 12 | BASE_DIR= os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 13 | load_dotenv(os.path.join(BASE_DIR, '.env')) 14 | sys.path.append(BASE_DIR) 15 | 16 | 17 | # This is the Alembic Config object, which provides 18 | # Access to the values within the .ini file in use. 19 | config = context.config 20 | 21 | # Making a connection 22 | config.set_main_option('sqlalchemy.url', os.environ['DATABASE_URL']) 23 | 24 | # Interpret the config file for Python logging. 25 | # This line sets up loggers basically. 26 | fileConfig(config.config_file_name) 27 | 28 | 29 | import models 30 | # add your model's MetaData object here 31 | # for 'autogenerate' support 32 | # from myapp import mymodel 33 | # target_metadata = mymodel.Base.metadata 34 | target_metadata = models.Base.metadata 35 | 36 | # other values from the config, defined by the needs of env.py, 37 | # can be acquired: 38 | # my_important_option = config.get_main_option("my_important_option") 39 | # ... etc. 40 | 41 | 42 | def run_migrations_offline(): 43 | """Run migrations in 'offline' mode. 44 | 45 | This configures the context with just a URL 46 | and not an Engine, though an Engine is acceptable 47 | here as well. By skipping the Engine creation 48 | we don't even need a DBAPI to be available. 49 | 50 | Calls to context.execute() here emit the given string to the 51 | script output. 52 | 53 | """ 54 | url = config.get_main_option("sqlalchemy.url") 55 | context.configure( 56 | url=url, 57 | target_metadata=target_metadata, 58 | literal_binds=True, 59 | dialect_opts={"paramstyle": "named"}, 60 | ) 61 | 62 | with context.begin_transaction(): 63 | context.run_migrations() 64 | 65 | 66 | def run_migrations_online(): 67 | """Run migrations in 'online' mode. 68 | 69 | In this scenario we need to create an Engine 70 | and associate a connection with the context. 71 | 72 | """ 73 | connectable = engine_from_config( 74 | config.get_section(config.config_ini_section), 75 | prefix="sqlalchemy.", 76 | poolclass=pool.NullPool, 77 | ) 78 | 79 | with connectable.connect() as connection: 80 | context.configure( 81 | connection=connection, target_metadata=target_metadata 82 | ) 83 | 84 | with context.begin_transaction(): 85 | context.run_migrations() 86 | 87 | 88 | if context.is_offline_mode(): 89 | run_migrations_offline() 90 | else: 91 | run_migrations_online() 92 | 93 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/__pycache__/e2119dd03e66_new_migration.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J0/fastapi-supabase-auth/ba814b2131ebf70340abbba7dd3a7f2f4d8d6ff4/alembic/versions/__pycache__/e2119dd03e66_new_migration.cpython-310.pyc -------------------------------------------------------------------------------- /alembic/versions/e2119dd03e66_new_migration.py: -------------------------------------------------------------------------------- 1 | """New Migration 2 | 3 | Revision ID: e2119dd03e66 4 | Revises: 5 | Create Date: 2022-12-11 19:59:48.278734 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e2119dd03e66' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('author', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(), nullable=True), 24 | sa.Column('age', sa.Integer(), nullable=True), 25 | sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), 26 | sa.Column('time_updated', sa.DateTime(timezone=True), nullable=True), 27 | sa.PrimaryKeyConstraint('id') 28 | ) 29 | op.create_table('book', 30 | sa.Column('id', sa.Integer(), nullable=False), 31 | sa.Column('title', sa.String(), nullable=True), 32 | sa.Column('rating', sa.Float(), nullable=True), 33 | sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), 34 | sa.Column('time_updated', sa.DateTime(timezone=True), nullable=True), 35 | sa.Column('author_id', sa.Integer(), nullable=True), 36 | sa.ForeignKeyConstraint(['author_id'], ['author.id'], ), 37 | sa.PrimaryKeyConstraint('id') 38 | ) 39 | op.create_index(op.f('ix_book_id'), 'book', ['id'], unique=False) 40 | # ### end Alembic commands ### 41 | 42 | 43 | def downgrade() -> None: 44 | # ### commands auto generated by Alembic - please adjust! ### 45 | op.drop_index(op.f('ix_book_id'), table_name='book') 46 | op.drop_table('book') 47 | op.drop_table('author') 48 | # ### end Alembic commands ### 49 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from fastapi import Request, HTTPException 3 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 4 | from decouple import config 5 | import time 6 | 7 | JWT_SECRET = config("JWT_SECRET") 8 | JWT_ALGORITHM = config("JWT_ALGORITHM") 9 | 10 | def decodeJWT(token: str) -> dict: 11 | try: 12 | decoded_token = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) 13 | return decoded_token if decoded_token["expires"] >= time.time() else None 14 | except Exception as e: 15 | print(e) 16 | return {} 17 | 18 | class JWTBearer(HTTPBearer): 19 | def __init__(self, auto_error: bool = True): 20 | super(JWTBearer, self).__init__(auto_error=auto_error) 21 | 22 | async def __call__(self, request: Request): 23 | credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request) 24 | if credentials: 25 | if not credentials.scheme == "Bearer": 26 | raise HTTPException(status_code=403, detail="Invalid authentication scheme.") 27 | if not self.verify_jwt(credentials.credentials): 28 | raise HTTPException(status_code=403, detail="Invalid token or expired token.") 29 | return credentials.credentials 30 | else: 31 | raise HTTPException(status_code=403, detail="Invalid authorization code.") 32 | 33 | def verify_jwt(self, jwtoken: str) -> bool: 34 | isTokenValid: bool = False 35 | 36 | try: 37 | payload = decodeJWT(jwtoken) 38 | except: 39 | payload = None 40 | if payload: 41 | isTokenValid = True 42 | return isTokenValid 43 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI, Depends 3 | from fastapi_sqlalchemy import DBSessionMiddleware, db 4 | 5 | from schema import Book as SchemaBook 6 | from schema import Author as SchemaAuthor 7 | 8 | from schema import Book 9 | from schema import Author 10 | 11 | from models import Book as ModelBook 12 | from models import Author as ModelAuthor 13 | from auth import JWTBearer 14 | 15 | import os 16 | from dotenv import load_dotenv 17 | 18 | load_dotenv('.env') 19 | 20 | 21 | app = FastAPI() 22 | 23 | # to avoid csrftokenError 24 | app.add_middleware(DBSessionMiddleware, db_url=os.environ['DATABASE_URL']) 25 | 26 | @app.get("/") 27 | async def root(): 28 | return {"message": "hello world"} 29 | 30 | 31 | @app.post('/book/',dependencies=[Depends(JWTBearer())], response_model=SchemaBook) 32 | async def book(book: SchemaBook): 33 | db_book = ModelBook(title=book.title, rating=book.rating, author_id = book.author_id) 34 | db.session.add(db_book) 35 | db.session.commit() 36 | return db_book 37 | 38 | @app.get('/book/') 39 | async def book(): 40 | book = db.session.query(ModelBook).all() 41 | return book 42 | 43 | 44 | 45 | @app.post('/author/', dependencies=[Depends(JWTBearer())], response_model=SchemaAuthor) 46 | async def author(author:SchemaAuthor): 47 | db_author = ModelAuthor(name=author.name, age=author.age) 48 | db.session.add(db_author) 49 | db.session.commit() 50 | return db_author 51 | 52 | @app.get('/author/') 53 | async def author(): 54 | author = db.session.query(ModelAuthor).all() 55 | return author 56 | 57 | 58 | # To run locally 59 | if __name__ == '__main__': 60 | uvicorn.run(app, host='0.0.0.0', port=8000) 61 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Float 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import relationship 4 | from sqlalchemy.sql import func 5 | 6 | Base = declarative_base() 7 | 8 | class Book(Base): 9 | __tablename__ = 'book' 10 | id = Column(Integer, primary_key=True, index=True) 11 | title = Column(String) 12 | rating = Column(Float) 13 | time_created = Column(DateTime(timezone=True), server_default=func.now()) 14 | time_updated = Column(DateTime(timezone=True), onupdate=func.now()) 15 | author_id = Column(Integer, ForeignKey('author.id')) 16 | 17 | author = relationship('Author') 18 | 19 | 20 | class Author(Base): 21 | __tablename__ = 'author' 22 | id = Column(Integer, primary_key=True) 23 | name = Column(String) 24 | age = Column(Integer) 25 | time_created = Column(DateTime(timezone=True), server_default=func.now()) 26 | time_updated = Column(DateTime(timezone=True), onupdate=func.now()) 27 | 28 | 29 | -------------------------------------------------------------------------------- /schema.py: -------------------------------------------------------------------------------- 1 | # build a schema using pydantic 2 | from pydantic import BaseModel 3 | 4 | class Book(BaseModel): 5 | title: str 6 | rating: int 7 | author_id: int 8 | 9 | class Config: 10 | orm_mode = True 11 | 12 | class Author(BaseModel): 13 | name:str 14 | age:int 15 | 16 | class Config: 17 | orm_mode = True 18 | 19 | --------------------------------------------------------------------------------