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
|
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.
|
# 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
|
# This won't stop bad passwords from being used, but at least won't cause
|
||||||
# errors or, worse, weaker crypt.
|
# errors or, worse, weaker crypt.
|
||||||
|
@ -26,17 +21,9 @@ RUN mkdir -p "${ENV_GHOSTFORGE_DATA_DIR}"
|
||||||
WORKDIR /ghostforge
|
WORKDIR /ghostforge
|
||||||
COPY . .
|
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.
|
# Install ghostforge from the work directory.
|
||||||
RUN pip install .
|
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.
|
# Expose the web "serve" port specific in the environment variables.
|
||||||
ARG GHOSTFORGE_INTERNAL_WEB_PORT
|
ARG GHOSTFORGE_INTERNAL_WEB_PORT
|
||||||
ENV ENV_GHOSTFORGE_INTERNAL_WEB_PORT=${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
|
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 AsyncSession
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlmodel import SQLModel
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
database_url = (
|
database_url = (
|
||||||
f'postgresql+asyncpg://{os.environ.get("POSTGRES_USER")}:'
|
f'postgresql+asyncpg://{os.environ.get("POSTGRES_USER")}:'
|
||||||
|
@ -24,5 +36,9 @@ async def get_session() -> AsyncSession:
|
||||||
async def create_db_and_tables():
|
async def create_db_and_tables():
|
||||||
# TODO: Remove the drop, this is for dev
|
# TODO: Remove the drop, this is for dev
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
# await conn.run_sync(SQLModel.metadata.drop_all)
|
# await conn.run_sync(Base.metadata.drop_all)
|
||||||
await conn.run_sync(SQLModel.metadata.create_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 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 fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from ghostforge.db import create_db_and_tables
|
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
|
# from ghostforge.models import User
|
||||||
|
|
||||||
gf = FastAPI()
|
gf = FastAPI()
|
||||||
|
hj = HtmlJson()
|
||||||
gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static")
|
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")
|
@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.
|
# Since 3.7 (we require >= 3.9), dicts are guaranteed ordered as inserted.
|
||||||
templates.env.globals["gf_navbar"] = {
|
templates.env.globals["gf_navbar"] = {
|
||||||
"Ghosts": {
|
"Ghosts": {
|
||||||
"Dashboard": "/users/0",
|
"Dashboard": "/dashboard",
|
||||||
"New": "/users/0",
|
"New": "/ghosts/new",
|
||||||
"Active": "/users/0",
|
"Active": "/ghosts",
|
||||||
"Archived": "/users/0",
|
"Archived": "/ghosts/archive",
|
||||||
},
|
},
|
||||||
"Research": {
|
"Research": {
|
||||||
"Guidebook": "/users/0",
|
"Guidebook": "/guidebook",
|
||||||
"Cheat Sheet": "/users/0",
|
"Cheat Sheet": "/cheatsheet",
|
||||||
},
|
},
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"Your Profile": "/users/0",
|
"Your Profile": "/profile",
|
||||||
"Configuration": "/users/0",
|
"Configuration": "/configuration",
|
||||||
"Integrations": "/users/0",
|
"Integrations": "/integrations",
|
||||||
"Manage Users": "/users/0",
|
"Manage Users": "/manage",
|
||||||
},
|
},
|
||||||
"Meta": {
|
"Meta": {
|
||||||
"About GhostForge": "/users/0",
|
"About GhostForge": "/about",
|
||||||
"System Logs": "/users/0",
|
"System Logs": "/logs",
|
||||||
"Logout": "/users/0",
|
"Logout": "/logout",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
templates.env.globals["avatar_menu"] = {
|
templates.env.globals["avatar_menu"] = {
|
||||||
"Dashboard": "/users/0",
|
"Dashboard": "/dashboard",
|
||||||
"Your Profile": "/users/0",
|
"Your Profile": "/profile",
|
||||||
"Logout": "/users/0",
|
"Logout": "/logout",
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<div class="off-canvas off-canvas-sidebar-show d-flex">
|
<div class="off-canvas off-canvas-sidebar-show d-flex">
|
||||||
{% include "navigation/side.html" %}
|
{% include "navigation/side.html" %}
|
||||||
<div class="off-canvas-content">
|
<div class="off-canvas-content">
|
||||||
<div class="container">
|
<div class="container" style="padding-right: 5em; padding-top: 1em;">
|
||||||
{% include "navigation/top.html" %}
|
{% include "navigation/top.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock 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">
|
alt="Avatar">
|
||||||
</div>
|
</div>
|
||||||
<div class="tile-content">
|
<div class="tile-content">
|
||||||
Steve Rogers
|
{{ user.email }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,109 +1,76 @@
|
||||||
from datetime import datetime
|
import os
|
||||||
from typing import List
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from argon2 import PasswordHasher
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import HTTPException
|
|
||||||
from fastapi import Query
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlmodel import Field
|
from fastapi_users import BaseUserManager
|
||||||
from sqlmodel import select
|
from fastapi_users import FastAPIUsers
|
||||||
from sqlmodel import Session
|
from fastapi_users import schemas
|
||||||
from sqlmodel import SQLModel
|
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.db import get_user_db
|
||||||
from ghostforge.htmljson import HtmlJson
|
from ghostforge.db import User
|
||||||
|
|
||||||
|
SECRET = os.environ.get("GHOSTFORGE_JWT_SECRET")
|
||||||
|
|
||||||
gf = APIRouter()
|
gf = APIRouter()
|
||||||
ph = PasswordHasher()
|
|
||||||
hj = HtmlJson()
|
|
||||||
|
|
||||||
|
|
||||||
class UserBase(SQLModel):
|
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||||
name: str = Field()
|
pass
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserBase, table=True):
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
pass
|
||||||
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(UserBase):
|
class UserUpdate(schemas.BaseUserUpdate):
|
||||||
name: str
|
pass
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
class UserRead(UserBase):
|
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||||
id: int
|
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):
|
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
|
||||||
name: Optional[str] = None
|
yield UserManager(user_db)
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/users", response_model=List[UserRead])
|
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||||
async def read_users(
|
cookie_transport = CookieTransport(cookie_httponly=True, cookie_name="ghostforge", cookie_samesite="strict")
|
||||||
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()]
|
|
||||||
|
|
||||||
|
|
||||||
@gf.post("/users", response_model=UserRead)
|
def get_jwt_strategy() -> JWTStrategy:
|
||||||
async def create_hero(user: UserCreate, session: Session = Depends(get_session)):
|
return JWTStrategy(secret=SECRET, lifetime_seconds=604800)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/users/{user_id}", response_model=UserRead)
|
jwt_backend = AuthenticationBackend(
|
||||||
@hj.html_or_json("user.html")
|
name="jwt",
|
||||||
async def read_user(user_id: int, session: Session = Depends(get_session), request: Request = None):
|
transport=bearer_transport,
|
||||||
user = await session.get(User, user_id)
|
get_strategy=get_jwt_strategy,
|
||||||
if not user:
|
)
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
|
|
||||||
data = {"crumbs": [("settings", False), ("users", "/users"), (user_id, False)]}
|
web_backend = AuthenticationBackend(name="cookie", transport=cookie_transport, get_strategy=get_jwt_strategy)
|
||||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
|
||||||
return user
|
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [web_backend, jwt_backend])
|
||||||
|
|
||||||
|
|
||||||
@gf.patch("/users/{user_id}", response_model=UserRead)
|
def get_current_user(active: bool = True, optional: bool = False) -> User:
|
||||||
async def update_user(user_id: int, user: UserUpdate, session: Session = Depends(get_session), request: Request = None):
|
return fastapi_users.current_user(active=active, optional=optional)
|
||||||
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}
|
|
||||||
|
|
|
@ -6,7 +6,8 @@ from alembic import context
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
from sqlalchemy.engine import Connection
|
from sqlalchemy.engine import Connection
|
||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
from sqlmodel import SQLModel
|
|
||||||
|
from ghostforge.db import Base
|
||||||
|
|
||||||
# from ghostforge.models import User
|
# from ghostforge.models import User
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ if config.config_file_name is not None:
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
# target_metadata = mymodel.Base.metadata
|
# 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,
|
# other values from the config, defined by the needs of env.py,
|
||||||
# can be acquired:
|
# can be acquired:
|
||||||
|
|
|
@ -7,7 +7,6 @@ Create Date: ${create_date}
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|
|
@ -18,12 +18,11 @@ dependencies = [
|
||||||
"pydantic==1.10.8",
|
"pydantic==1.10.8",
|
||||||
"alembic==1.11.1",
|
"alembic==1.11.1",
|
||||||
"asyncpg==0.27.0",
|
"asyncpg==0.27.0",
|
||||||
"sqlmodel==0.0.8",
|
|
||||||
"greenlet==2.0.2",
|
"greenlet==2.0.2",
|
||||||
"jinja2==3.1.2",
|
"jinja2==3.1.2",
|
||||||
"argon2-cffi==21.3.0",
|
|
||||||
"fastapi-users==11.0.0",
|
"fastapi-users==11.0.0",
|
||||||
"fastapi-users-db-sqlalchemy==5.0.0",
|
"fastapi-users-db-sqlalchemy==5.0.0",
|
||||||
|
"httpx==0.24.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|
Loading…
Reference in a new issue