mirror of
https://github.com/DarrylNixon/ghostforge
synced 2024-04-22 06:27:20 -07:00
Replaced rolled-own/argon2 with fastapi-users, oof
This commit is contained in:
parent
216d2ac42b
commit
6f81ef699d
14 changed files with 310 additions and 142 deletions
13
Dockerfile
13
Dockerfile
|
@ -1,10 +1,5 @@
|
|||
FROM python:3.11-alpine
|
||||
|
||||
# argon2 needs these to build, and we need argon2 for secure password
|
||||
# hashing/salting
|
||||
RUN apk add gcc musl-dev libffi-dev
|
||||
RUN python3 -m pip install -U cffi pip setuptools
|
||||
|
||||
# Enforcement to ensure passwords environment variables are not left blank.
|
||||
# This won't stop bad passwords from being used, but at least won't cause
|
||||
# errors or, worse, weaker crypt.
|
||||
|
@ -26,17 +21,9 @@ RUN mkdir -p "${ENV_GHOSTFORGE_DATA_DIR}"
|
|||
WORKDIR /ghostforge
|
||||
COPY . .
|
||||
|
||||
# argon2 needs these to build, and we need argon2 for secure password
|
||||
# hashing/salting
|
||||
RUN apk add gcc musl-dev libffi-dev
|
||||
RUN python3 -m pip install -U cffi pip setuptools
|
||||
|
||||
# Install ghostforge from the work directory.
|
||||
RUN pip install .
|
||||
|
||||
# Reclaim space from build-time packages
|
||||
RUN apk del gcc musl-dev libffi-dev
|
||||
|
||||
# Expose the web "serve" port specific in the environment variables.
|
||||
ARG GHOSTFORGE_INTERNAL_WEB_PORT
|
||||
ENV ENV_GHOSTFORGE_INTERNAL_WEB_PORT=${GHOSTFORGE_INTERNAL_WEB_PORT}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import os
|
||||
|
||||
from fastapi_users.authentication import AuthenticationBackend
|
||||
from fastapi_users.authentication import BearerTransport
|
||||
from fastapi_users.authentication import JWTStrategy
|
||||
|
||||
SECRET = os.environ.get("GHOSTFORGE_JWT_SECRET")
|
||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||
|
||||
|
||||
def get_jwt_strategy() -> JWTStrategy:
|
||||
return JWTStrategy(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
|
||||
auth_backend = AuthenticationBackend(
|
||||
name="jwt",
|
||||
transport=bearer_transport,
|
||||
get_strategy=get_jwt_strategy,
|
||||
)
|
|
@ -1,9 +1,21 @@
|
|||
import os
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi_users.db import SQLAlchemyBaseUserTableUUID
|
||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
pass
|
||||
|
||||
|
||||
database_url = (
|
||||
f'postgresql+asyncpg://{os.environ.get("POSTGRES_USER")}:'
|
||||
|
@ -24,5 +36,9 @@ async def get_session() -> AsyncSession:
|
|||
async def create_db_and_tables():
|
||||
# TODO: Remove the drop, this is for dev
|
||||
async with engine.begin() as conn:
|
||||
# await conn.run_sync(SQLModel.metadata.drop_all)
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
# await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def get_user_db(session: AsyncSession = Depends(get_session)):
|
||||
yield SQLAlchemyUserDatabase(session, User)
|
||||
|
|
|
@ -1,15 +1,89 @@
|
|||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import FastAPI
|
||||
from fastapi import Request
|
||||
from fastapi import Response
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from ghostforge.db import create_db_and_tables
|
||||
from ghostforge.users import gf as gf_users
|
||||
from ghostforge.db import User
|
||||
from ghostforge.htmljson import HtmlJson
|
||||
from ghostforge.templates import templates
|
||||
from ghostforge.users import fastapi_users
|
||||
from ghostforge.users import get_current_user
|
||||
from ghostforge.users import jwt_backend
|
||||
from ghostforge.users import UserCreate
|
||||
from ghostforge.users import UserRead
|
||||
from ghostforge.users import UserUpdate
|
||||
from ghostforge.users import web_backend
|
||||
|
||||
# from ghostforge.users import gf as gf_users
|
||||
|
||||
# from ghostforge.models import User
|
||||
|
||||
gf = FastAPI()
|
||||
hj = HtmlJson()
|
||||
gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static")
|
||||
|
||||
gf.include_router(gf_users)
|
||||
# gf.include_router(gf_users)
|
||||
|
||||
gf.include_router(fastapi_users.get_auth_router(jwt_backend), prefix="/auth/jwt", tags=["auth"])
|
||||
gf.include_router(fastapi_users.get_auth_router(web_backend), prefix="/auth/cookie", tags=["auth"])
|
||||
gf.include_router(
|
||||
fastapi_users.get_register_router(UserRead, UserCreate),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
gf.include_router(
|
||||
fastapi_users.get_reset_password_router(),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
gf.include_router(
|
||||
fastapi_users.get_verify_router(UserRead),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
gf.include_router(
|
||||
fastapi_users.get_users_router(UserRead, UserUpdate),
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
)
|
||||
|
||||
|
||||
@gf.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request, current_user=Depends(get_current_user())):
|
||||
if current_user:
|
||||
return RedirectResponse(url="/dashboard")
|
||||
return RedirectResponse(url="/login")
|
||||
|
||||
|
||||
@gf.get("/login", response_class=HTMLResponse)
|
||||
async def login_form(request: Request, current_user=Depends(get_current_user(optional=True))):
|
||||
if current_user:
|
||||
return RedirectResponse(url="/dashboard")
|
||||
return templates.TemplateResponse("login.html", {"request": request})
|
||||
|
||||
|
||||
@gf.get("/logout")
|
||||
async def logout(response: Response):
|
||||
response.delete_cookie("ghostforge")
|
||||
response.headers["Location"] = "/login"
|
||||
response.status_code = 302
|
||||
return response
|
||||
|
||||
|
||||
@gf.get("/dashboard")
|
||||
async def authenticated_route(
|
||||
request: Request, current_user: Annotated[User, Depends(get_current_user(optional=True))]
|
||||
):
|
||||
if not current_user:
|
||||
return RedirectResponse(url="/login")
|
||||
crumbs = [("dashboard", False), (current_user.email, False)]
|
||||
return templates.TemplateResponse("dashboard.html", {"request": request, "crumbs": crumbs, "user": current_user})
|
||||
|
||||
|
||||
@gf.on_event("startup")
|
||||
|
|
34
ghostforge/static/js/login.js
Normal file
34
ghostforge/static/js/login.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const loginForm = document.getElementById('auth-form');
|
||||
const loginError = document.getElementById('auth-error');
|
||||
|
||||
loginForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
const encodedData = new URLSearchParams();
|
||||
encodedData.append('username', email);
|
||||
encodedData.append('password', password);
|
||||
|
||||
const response = await fetch(`/auth/cookie/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: encodedData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
document.cookie = 'ghostforge=' + response.headers.get('Set-Cookie');
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
const errorMessage = "Invalid username or password.";
|
||||
let toast = document.getElementById("error-msg");
|
||||
if (!toast) {
|
||||
toast = document.createElement('div');
|
||||
toast.id = "error-msg";
|
||||
toast.classList.add("toast", "toast-error");
|
||||
loginError.appendChild(toast);
|
||||
}
|
||||
toast.innerText = errorMessage;
|
||||
}
|
||||
});
|
|
@ -16,30 +16,30 @@ templates.env.globals["gf_repository_url"] = "https://github.com/DarrylNixon/gho
|
|||
# Since 3.7 (we require >= 3.9), dicts are guaranteed ordered as inserted.
|
||||
templates.env.globals["gf_navbar"] = {
|
||||
"Ghosts": {
|
||||
"Dashboard": "/users/0",
|
||||
"New": "/users/0",
|
||||
"Active": "/users/0",
|
||||
"Archived": "/users/0",
|
||||
"Dashboard": "/dashboard",
|
||||
"New": "/ghosts/new",
|
||||
"Active": "/ghosts",
|
||||
"Archived": "/ghosts/archive",
|
||||
},
|
||||
"Research": {
|
||||
"Guidebook": "/users/0",
|
||||
"Cheat Sheet": "/users/0",
|
||||
"Guidebook": "/guidebook",
|
||||
"Cheat Sheet": "/cheatsheet",
|
||||
},
|
||||
"Settings": {
|
||||
"Your Profile": "/users/0",
|
||||
"Configuration": "/users/0",
|
||||
"Integrations": "/users/0",
|
||||
"Manage Users": "/users/0",
|
||||
"Your Profile": "/profile",
|
||||
"Configuration": "/configuration",
|
||||
"Integrations": "/integrations",
|
||||
"Manage Users": "/manage",
|
||||
},
|
||||
"Meta": {
|
||||
"About GhostForge": "/users/0",
|
||||
"System Logs": "/users/0",
|
||||
"Logout": "/users/0",
|
||||
"About GhostForge": "/about",
|
||||
"System Logs": "/logs",
|
||||
"Logout": "/logout",
|
||||
},
|
||||
}
|
||||
|
||||
templates.env.globals["avatar_menu"] = {
|
||||
"Dashboard": "/users/0",
|
||||
"Your Profile": "/users/0",
|
||||
"Logout": "/users/0",
|
||||
"Dashboard": "/dashboard",
|
||||
"Your Profile": "/profile",
|
||||
"Logout": "/logout",
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div class="off-canvas off-canvas-sidebar-show d-flex">
|
||||
{% include "navigation/side.html" %}
|
||||
<div class="off-canvas-content">
|
||||
<div class="container">
|
||||
<div class="container" style="padding-right: 5em; padding-top: 1em;">
|
||||
{% include "navigation/top.html" %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
|
|
59
ghostforge/templates/dashboard.html
Normal file
59
ghostforge/templates/dashboard.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="columns">
|
||||
<div class="column col-12">
|
||||
<h1>{% block title %}Dashboard{% endblock %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column col-4">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Comments</div>
|
||||
</div>
|
||||
<div class="panel-nav">
|
||||
<!-- navigation components: tabs, breadcrumbs or pagination -->
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<!-- contents -->
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<!-- buttons or inputs -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Comments</div>
|
||||
</div>
|
||||
<div class="panel-nav">
|
||||
<!-- navigation components: tabs, breadcrumbs or pagination -->
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<!-- contents -->
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<!-- buttons or inputs -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Comments</div>
|
||||
</div>
|
||||
<div class="panel-nav">
|
||||
<!-- navigation components: tabs, breadcrumbs or pagination -->
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<!-- contents -->
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<!-- buttons or inputs -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
51
ghostforge/templates/login.html
Normal file
51
ghostforge/templates/login.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='/img/favicon.ico') }}">
|
||||
<title>{% block title %}{{ title }}{% endblock title %}</title>
|
||||
<link href="{{ url_for('static', path='/css/spectre.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/spectre-icons.min.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<html>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column col-4 col-xs-12"></div>
|
||||
<div class="column col-4 col-xs-12">
|
||||
<div class="card" style="margin-top: 5em;">
|
||||
<div class="card-image" style="text-align: center; margin-top: 1em;">
|
||||
<img src="{{ url_for('static', path='/img/ghostforge-sidebar.png') }}" alt="ghostforge">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="auth-error"></div>
|
||||
<form id="auth-form" method="POST">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input class="form-input" type="email" name="email" id="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input class="form-input" type="password" name="password" id="password" required>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" type="submit">Log In</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="text-center my-2">
|
||||
© 2023 ghostforge
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4 col-xs-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="{{ url_for('static', path='/js/login.js') }}"></script>
|
||||
|
||||
</html>
|
|
@ -30,7 +30,7 @@
|
|||
alt="Avatar">
|
||||
</div>
|
||||
<div class="tile-content">
|
||||
Steve Rogers
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -1,109 +1,76 @@
|
|||
from datetime import datetime
|
||||
from typing import List
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import Request
|
||||
from sqlmodel import Field
|
||||
from sqlmodel import select
|
||||
from sqlmodel import Session
|
||||
from sqlmodel import SQLModel
|
||||
from fastapi_users import BaseUserManager
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users import schemas
|
||||
from fastapi_users import UUIDIDMixin
|
||||
from fastapi_users.authentication import AuthenticationBackend
|
||||
from fastapi_users.authentication import BearerTransport
|
||||
from fastapi_users.authentication import CookieTransport
|
||||
from fastapi_users.authentication import JWTStrategy
|
||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
|
||||
from ghostforge.db import get_session
|
||||
from ghostforge.htmljson import HtmlJson
|
||||
from ghostforge.db import get_user_db
|
||||
from ghostforge.db import User
|
||||
|
||||
SECRET = os.environ.get("GHOSTFORGE_JWT_SECRET")
|
||||
|
||||
gf = APIRouter()
|
||||
ph = PasswordHasher()
|
||||
hj = HtmlJson()
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
name: str = Field()
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||
pass
|
||||
|
||||
|
||||
class User(UserBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
creation: datetime = Field(default=datetime.now())
|
||||
password_hash: Optional[str]
|
||||
|
||||
def verify_password(self, password: str):
|
||||
return ph.verify(self.password_hash, password)
|
||||
|
||||
def set_password(self, password: str):
|
||||
self.password_hash = ph.hash(password)
|
||||
class UserCreate(schemas.BaseUserCreate):
|
||||
pass
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
name: str
|
||||
password: str
|
||||
class UserUpdate(schemas.BaseUserUpdate):
|
||||
pass
|
||||
|
||||
|
||||
class UserRead(UserBase):
|
||||
id: int
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
reset_password_token_secret = SECRET
|
||||
verification_token_secret = SECRET
|
||||
|
||||
async def on_after_register(self, user: User, request: Optional[Request] = None):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
async def on_after_forgot_password(self, user: User, token: str, request: Optional[Request] = None):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
async def on_after_request_verify(self, user: User, token: str, request: Optional[Request] = None):
|
||||
print(f"Verification requested for user {user.id}. Verification token: {token}")
|
||||
|
||||
|
||||
class UserUpdate(SQLModel):
|
||||
name: Optional[str] = None
|
||||
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
|
||||
yield UserManager(user_db)
|
||||
|
||||
|
||||
@gf.get("/users", response_model=List[UserRead])
|
||||
async def read_users(
|
||||
offset: int = 0, limit: int = Query(default=100, lte=100), session: Session = Depends(get_session)
|
||||
):
|
||||
users = await session.execute(select(User).offset(offset).limit(limit))
|
||||
return [UserRead.from_orm(user[0]) for user in users.all()]
|
||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||
cookie_transport = CookieTransport(cookie_httponly=True, cookie_name="ghostforge", cookie_samesite="strict")
|
||||
|
||||
|
||||
@gf.post("/users", response_model=UserRead)
|
||||
async def create_hero(user: UserCreate, session: Session = Depends(get_session)):
|
||||
new_user = User.from_orm(user)
|
||||
if len(user.password) < 12:
|
||||
raise HTTPException(status_code=442, detail="Password must be at least 12 characters")
|
||||
new_user.set_password(user.password)
|
||||
session.add(new_user)
|
||||
await session.commit()
|
||||
await session.refresh(new_user)
|
||||
return new_user
|
||||
def get_jwt_strategy() -> JWTStrategy:
|
||||
return JWTStrategy(secret=SECRET, lifetime_seconds=604800)
|
||||
|
||||
|
||||
@gf.get("/users/{user_id}", response_model=UserRead)
|
||||
@hj.html_or_json("user.html")
|
||||
async def read_user(user_id: int, session: Session = Depends(get_session), request: Request = None):
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
jwt_backend = AuthenticationBackend(
|
||||
name="jwt",
|
||||
transport=bearer_transport,
|
||||
get_strategy=get_jwt_strategy,
|
||||
)
|
||||
|
||||
data = {"crumbs": [("settings", False), ("users", "/users"), (user_id, False)]}
|
||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||
return user
|
||||
web_backend = AuthenticationBackend(name="cookie", transport=cookie_transport, get_strategy=get_jwt_strategy)
|
||||
|
||||
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [web_backend, jwt_backend])
|
||||
|
||||
|
||||
@gf.patch("/users/{user_id}", response_model=UserRead)
|
||||
async def update_user(user_id: int, user: UserUpdate, session: Session = Depends(get_session), request: Request = None):
|
||||
edit_user = await session.get(User, user_id)
|
||||
if not edit_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
data = user.dict(exclude_unset=True)
|
||||
for key, value in data.items():
|
||||
setattr(edit_user, key, value)
|
||||
session.add(edit_user)
|
||||
await session.commit()
|
||||
await session.refresh(edit_user)
|
||||
return edit_user
|
||||
|
||||
|
||||
@gf.delete("/users/{user_id}")
|
||||
async def delete_user(user_id: int, session: Session = Depends(get_session), request: Request = None):
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
session.delete(user)
|
||||
await session.commit()
|
||||
return {"ok": True}
|
||||
def get_current_user(active: bool = True, optional: bool = False) -> User:
|
||||
return fastapi_users.current_user(active=active, optional=optional)
|
||||
|
|
|
@ -6,7 +6,8 @@ from alembic import context
|
|||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from ghostforge.db import Base
|
||||
|
||||
# from ghostforge.models import User
|
||||
|
||||
|
@ -32,7 +33,7 @@ if config.config_file_name is not None:
|
|||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = SQLModel.metadata
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
|
|
|
@ -7,7 +7,6 @@ Create Date: ${create_date}
|
|||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
|
|
@ -18,12 +18,11 @@ dependencies = [
|
|||
"pydantic==1.10.8",
|
||||
"alembic==1.11.1",
|
||||
"asyncpg==0.27.0",
|
||||
"sqlmodel==0.0.8",
|
||||
"greenlet==2.0.2",
|
||||
"jinja2==3.1.2",
|
||||
"argon2-cffi==21.3.0",
|
||||
"fastapi-users==11.0.0",
|
||||
"fastapi-users-db-sqlalchemy==5.0.0",
|
||||
"httpx==0.24.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
|
Loading…
Reference in a new issue