mirror of
https://github.com/DarrylNixon/ghostforge
synced 2024-04-22 06:27:20 -07:00
Add user views and some fixes.
This commit is contained in:
parent
d41af49512
commit
11679795ab
9 changed files with 319 additions and 40 deletions
|
@ -27,7 +27,7 @@ cp .env.sample .env && \
|
|||
PW=$(/usr/bin/env python3 -c "import secrets; print(secrets.token_urlsafe(32))") /bin/bash -c 'sed -i "" "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$PW/" .env' && \
|
||||
JWT=$(/usr/bin/env python3 -c "import secrets; print(secrets.token_urlsafe(32))") /bin/bash -c 'sed -i "" "s/^GHOSTFORGE_JWT_SECRET=.*/GHOSTFORGE_JWT_SECRET=$JWT/" .env';
|
||||
docker-compose up --detach --build;
|
||||
docker exec --interactive --tty ghostforge ghostforge_adduser;
|
||||
docker exec --interactive --tty ghostforge ghostforge_adduser <username> <email> --superuser;
|
||||
```
|
||||
|
||||
Follow the prompts to create an administrator user. Assuming you didn't change the default port, browse to [http://localhost:1337/](http://localhost:1337/) to begin using `ghostforge` with your new credentials.
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
import argparse
|
||||
import asyncio
|
||||
import contextlib
|
||||
import getpass
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
from fastapi_users.exceptions import UserAlreadyExists
|
||||
|
||||
from ghostforge.db import get_session
|
||||
from ghostforge.db import get_user_db
|
||||
from ghostforge.users import get_user_manager
|
||||
from ghostforge.users import get_async_session_context
|
||||
from ghostforge.users import get_user_db_context
|
||||
from ghostforge.users import get_user_manager_context
|
||||
from ghostforge.users import UserCreate
|
||||
|
||||
|
||||
get_async_session_context = contextlib.asynccontextmanager(get_session)
|
||||
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
|
||||
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
|
||||
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||
return bool(re.match(email_regex, email))
|
||||
|
|
|
@ -19,7 +19,6 @@ from sqlalchemy import or_
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import and_
|
||||
from sqlmodel import AutoString
|
||||
from sqlmodel import Column
|
||||
|
@ -116,7 +115,7 @@ async def read_ghost(
|
|||
if not can_view:
|
||||
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||
result = await session.execute(
|
||||
select(Ghost, User.username.label("owner_username"))
|
||||
select(Ghost, User.username.label("owner_username"), User.id.label("owner_guid"))
|
||||
.join(User, Ghost.owner_id == User.id)
|
||||
.where(Ghost.id == ghost_id)
|
||||
)
|
||||
|
@ -189,7 +188,10 @@ async def get_ghosts(
|
|||
|
||||
# Retrieve filtered ghosts from database
|
||||
query = (
|
||||
select(Ghost, User.email.label("owner_email")).offset(offset).limit(limit).join(User, Ghost.owner_id == User.id)
|
||||
select(Ghost, User.username.label("owner_username"), User.id.label("owner_guid"))
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.join(User, Ghost.owner_id == User.id)
|
||||
)
|
||||
permission_filter = or_(
|
||||
Ghost.owner_id == current_user.id,
|
||||
|
@ -231,7 +233,7 @@ async def read_users(
|
|||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
session: Session = Depends(get_session),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
request: Request = None,
|
||||
):
|
||||
subquery = (
|
||||
|
@ -242,6 +244,6 @@ async def read_users(
|
|||
)
|
||||
query = await session.execute(select(Ghost).where(Ghost.id.in_(subquery)).offset(offset).limit(limit))
|
||||
ghosts = query.scalars().all()
|
||||
data = {"ghosts": ghosts, "user": current_user, "crumbs": [("ghosts", "/ghosts")]}
|
||||
data = {"ghosts": ghosts, "user": current_user, "crumbs": [("ghosts", False)]}
|
||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||
return ghosts
|
||||
|
|
|
@ -33,7 +33,7 @@ class HtmlJson:
|
|||
|
||||
params = list(sig.parameters.values())
|
||||
request_param = Parameter("request", _ParameterKind.POSITIONAL_OR_KEYWORD, annotation=Request)
|
||||
params.append(request_param)
|
||||
params.insert(0, request_param)
|
||||
wrapper.__signature__ = sig.replace(parameters=params)
|
||||
|
||||
def render_template(self, template: str, request: Request, result):
|
||||
|
|
|
@ -16,6 +16,7 @@ from ghostforge.pages import gf as gf_pages
|
|||
from ghostforge.templates import templates
|
||||
from ghostforge.users import fastapi_users
|
||||
from ghostforge.users import get_current_user
|
||||
from ghostforge.users import gf as gf_users
|
||||
from ghostforge.users import jwt_backend
|
||||
from ghostforge.users import UserCreate
|
||||
from ghostforge.users import UserRead
|
||||
|
@ -25,6 +26,7 @@ from ghostforge.users import web_backend
|
|||
|
||||
gf = FastAPI()
|
||||
gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static")
|
||||
gf.include_router(gf_users)
|
||||
gf.include_router(gf_ghosts)
|
||||
gf.include_router(gf_pages)
|
||||
gf.include_router(gf_ethnicities)
|
||||
|
|
|
@ -88,7 +88,12 @@
|
|||
return month + '/' + day + '/' + year;
|
||||
}
|
||||
},
|
||||
{ "data": "owner_email" },
|
||||
{
|
||||
"data": "owner_username",
|
||||
"render": function (data, type, row, meta) {
|
||||
return '<a href="/manage/' + row.owner_guid + '">' + data + '</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,55 +1,66 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ target_user.username }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="columns">
|
||||
<div class="column col-12">
|
||||
<h1>Words!</h1>
|
||||
<h1>Manage User</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column col-3"></div>
|
||||
<div class="column col-6">
|
||||
<div class="column col-4"></div>
|
||||
<div class="column col-4">
|
||||
<div class="panel">
|
||||
<div class="panel-header text-center">
|
||||
<figure class="avatar avatar-lg"><img src="{{ url_for('static', path='/img/default-avatar.png') }}"
|
||||
alt="Avatar"></figure>
|
||||
<div class="panel-title h5 mt-10">Bruce Banner</div>
|
||||
<div class="panel-subtitle">THE HULK</div>
|
||||
<div class="panel-title h5 mt-10">{{ target_user.username }}</div>
|
||||
<div class="panel-subtitle">
|
||||
{% if target_user.is_superuser %}
|
||||
<kbd>Administrator</kbd>
|
||||
{% else %}
|
||||
Normal User
|
||||
{% endif %}
|
||||
{% if not target_user.is_active %}
|
||||
<s>Deactivated</s>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav class="panel-nav">
|
||||
<ul class="tab tab-block">
|
||||
<li class="tab-item active"><a href="#panels">Profile</a></li>
|
||||
<li class="tab-item"><a href="#panels">Files</a></li>
|
||||
<li class="tab-item"><a href="#panels">Tasks</a></li>
|
||||
<li class="tab-item active">Profile</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="panel-body">
|
||||
<div class="tile tile-centered">
|
||||
<div class="tile-content">
|
||||
<div class="tile-title text-bold">E-mail</div>
|
||||
<div class="tile-subtitle">bruce.banner@hulk.com</div>
|
||||
<div class="tile-title text-bold">Username</div>
|
||||
<div class="tile-subtitle">{{ target_user.username }}</div>
|
||||
</div>
|
||||
<div class="tile-action">
|
||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left"
|
||||
data-tooltip="Edit E-mail"><i class="icon icon-edit"></i></button>
|
||||
data-tooltip="Change Username" id="edit_username"><i class="icon icon-edit"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile tile-centered">
|
||||
<div class="tile-content">
|
||||
<div class="tile-title text-bold">Skype</div>
|
||||
<div class="tile-subtitle">bruce.banner</div>
|
||||
<div class="tile-title text-bold">E-mail</div>
|
||||
<div class="tile-subtitle">{{ target_user.email }}</div>
|
||||
</div>
|
||||
<div class="tile-action">
|
||||
<button class="btn btn-link btn-action btn-lg"><i class="icon icon-edit"></i></button>
|
||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left" data-tooltip="Change E-mail"
|
||||
id="edit_email"><i class="icon icon-edit"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile tile-centered">
|
||||
<div class="tile-content">
|
||||
<div class="tile-title text-bold">Location</div>
|
||||
<div class="tile-subtitle">Dayton, Ohio</div>
|
||||
<div class="tile-title text-bold">New Password</div>
|
||||
<div class="tile-subtitle">blocked</div>
|
||||
</div>
|
||||
<div class="tile-action">
|
||||
<button class="btn btn-link btn-action btn-lg"><i class="icon icon-edit"></i></button>
|
||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left"
|
||||
data-tooltip="Change Password" id="edit_password"><i class="icon icon-edit"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,6 +69,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-3"></div>
|
||||
<div class="column col-4"></div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
99
ghostforge/templates/users.html
Normal file
99
ghostforge/templates/users.html
Normal file
|
@ -0,0 +1,99 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Browse Ghosts{% endblock title %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/colReorder.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.searchPanes.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.colReorder.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.stateRestore.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/responsive.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/fixedHeader.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/searchBuilder.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.fixedHeader.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/searchPanes.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.responsive.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/stateRestore.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/dataTables.searchBuilder.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='/js/jquery.dataTables.min.js') }}"></script>
|
||||
{% endblock js %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ url_for('static', path='/css/datatables.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/colReorder.datatables.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/dataTables.datatables.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/fixedHeader.datatables.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/jquery.datatables.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/responsive.datatables.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/colReorder.datatables.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='/css/stateRestore.datatables.min.css') }}" rel="stylesheet">
|
||||
{% endblock css %}
|
||||
|
||||
{% block content %}
|
||||
<div class="columns">
|
||||
<div class="column col-12">
|
||||
<h1>Manage Users</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column col-12">
|
||||
<table id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Active</th>
|
||||
<th>Administrator</th>
|
||||
<th>E-mail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block bottomjs %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#users-table').DataTable({
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": {
|
||||
"url": "/manage",
|
||||
"type": "POST",
|
||||
"dataType": "json"
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
"data": "User.id",
|
||||
"render": function (data, type, row, meta) {
|
||||
return '<a href="/manage/' + data + '">' + data + '</a>';
|
||||
}
|
||||
},
|
||||
{ "data": "User.username" },
|
||||
{
|
||||
"data": "User.is_active",
|
||||
"render": function (data, type, row, meta) {
|
||||
var icon = data ? '\u2714' : '\u2718';
|
||||
var color = data ? '32b643' : 'e85600';
|
||||
return '<span style="color:#' + color + '">' + icon + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "User.is_superuser",
|
||||
"render": function (data, type, row, meta) {
|
||||
var icon = data ? '\u2714' : '\u2718';
|
||||
var color = data ? '32b643' : 'e85600';
|
||||
return '<span style="color:#' + color + ';">' + icon + '</span>';
|
||||
}
|
||||
},
|
||||
{ "data": "User.email" },
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock bottomjs %}
|
|
@ -1,10 +1,18 @@
|
|||
import contextlib
|
||||
import os
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import Form
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi_users import BaseUserManager
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users import schemas
|
||||
|
@ -13,15 +21,37 @@ 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.exceptions import UserAlreadyExists
|
||||
from fastapi_users_db_sqlmodel import SQLModelUserDatabase
|
||||
from sqlalchemy import asc
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import AutoString
|
||||
|
||||
from ghostforge.db import get_session
|
||||
from ghostforge.db import get_user_db
|
||||
from ghostforge.db import User
|
||||
from ghostforge.htmljson import HtmlJson
|
||||
|
||||
|
||||
SECRET = os.environ.get("GHOSTFORGE_JWT_SECRET")
|
||||
|
||||
|
||||
async def get_user_manager(user_db: SQLModelUserDatabase = Depends(get_user_db)):
|
||||
yield UserManager(user_db)
|
||||
|
||||
|
||||
get_async_session_context = contextlib.asynccontextmanager(get_session)
|
||||
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
|
||||
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
|
||||
|
||||
gf = APIRouter()
|
||||
hj = HtmlJson()
|
||||
|
||||
|
||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||
|
@ -30,10 +60,11 @@ class UserRead(schemas.BaseUser[uuid.UUID]):
|
|||
|
||||
class UserCreate(schemas.BaseUserCreate):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
class UserUpdate(schemas.BaseUserUpdate):
|
||||
username: str
|
||||
pass
|
||||
|
||||
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
|
@ -50,10 +81,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
|||
print(f"Verification requested for user {user.id}. Verification token: {token}")
|
||||
|
||||
|
||||
async def get_user_manager(user_db: SQLModelUserDatabase = Depends(get_user_db)):
|
||||
yield UserManager(user_db)
|
||||
|
||||
|
||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||
cookie_transport = CookieTransport(cookie_httponly=True, cookie_name="ghostforge", cookie_samesite="strict")
|
||||
|
||||
|
@ -75,3 +102,142 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [web_backend, jw
|
|||
|
||||
def get_current_user(active: bool = True, optional: bool = False) -> User:
|
||||
return fastapi_users.current_user(active=active, optional=optional)
|
||||
|
||||
|
||||
@gf.get("/manage", response_model=List[UserRead])
|
||||
@hj.html_or_json("users.html")
|
||||
async def read_users(
|
||||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
request: Request = None,
|
||||
):
|
||||
query = await session.execute(select(User).offset(offset).limit(limit))
|
||||
users = query.scalars().all()
|
||||
data = {"users": users, "user": current_user, "crumbs": [("settings", False), ("users", False)]}
|
||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||
return users
|
||||
|
||||
|
||||
@gf.post("/manage/new", response_model=UserRead)
|
||||
async def create_user(
|
||||
user: UserCreate,
|
||||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
async with get_async_session_context() as session:
|
||||
async with get_user_db_context(session) as user_db:
|
||||
async with get_user_manager_context(user_db) as user_manager:
|
||||
if not current_user.is_superuser:
|
||||
user.is_superuser = False
|
||||
user = await user_manager.create(
|
||||
UserCreate(
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
password=user.password,
|
||||
is_superuser=user.is_superuser,
|
||||
)
|
||||
)
|
||||
return UserRead(user)
|
||||
except UserAlreadyExists:
|
||||
return {"error": "User exists"}
|
||||
|
||||
|
||||
@gf.get("/manage/{user_guid}", response_model=UserRead)
|
||||
@hj.html_or_json("user.html")
|
||||
async def get_user(
|
||||
user_guid: str,
|
||||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
user_manager: UserManager = Depends(get_user_manager),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
request: Request = None,
|
||||
):
|
||||
async with get_user_db_context(session) as user_db:
|
||||
async with get_user_manager_context(user_db) as user_manager:
|
||||
user = await user_manager.get(user_guid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
data = {
|
||||
"target_user": user,
|
||||
"user": current_user,
|
||||
"crumbs": [("settings", False), ("users", "/manage"), (user.id, False)],
|
||||
}
|
||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||
return user
|
||||
|
||||
|
||||
@gf.put("/manage/{user_guid}", response_model=UserRead)
|
||||
async def update_user(
|
||||
user_guid: str,
|
||||
user_update: UserUpdate,
|
||||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
user_manager: UserManager = Depends(get_user_manager),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
if not current_user.is_superuser and current_user.id != user_guid:
|
||||
raise HTTPException(status_code=403, detail="No permission to edit this user")
|
||||
db_user = await user_manager.get(user_guid, session)
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
update_data = user_update.dict(exclude_unset=True)
|
||||
if "is_superuser" in update_data and not current_user.is_superuser:
|
||||
del update_data["is_superuser"]
|
||||
if "password" in update_data:
|
||||
update_data["hashed_password"] = await user_manager.hash_password(update_data["password"])
|
||||
del update_data["password"]
|
||||
await user_manager.update(db_user, session, **update_data)
|
||||
|
||||
await session.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
|
||||
@gf.get("/profile", response_class=HTMLResponse)
|
||||
async def home(request: Request, current_user=Depends(get_current_user())):
|
||||
return RedirectResponse(url=f"/manage/{current_user.id}")
|
||||
|
||||
|
||||
@gf.post("/manage")
|
||||
async def get_users(
|
||||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
start: int = Form(None, alias="start"),
|
||||
length: int = Form(None, alias="length"),
|
||||
search: Optional[str] = Form(None, alias="search[value]"),
|
||||
order_col: int = Form(0, alias="order[0][column]"),
|
||||
order_dir: str = Form("asc", alias="order[0][dir]"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
request: Request = None,
|
||||
):
|
||||
offset = start
|
||||
limit = offset + length
|
||||
|
||||
# Get the column name based on the order_col integer value
|
||||
user_columns = [c.key for c in inspect(User).c]
|
||||
order_col_name = user_columns[order_col]
|
||||
|
||||
# Retrieve filtered users from database
|
||||
count = select(func.count(User.id))
|
||||
total_users = (await session.execute(count)).scalar()
|
||||
|
||||
query = select(User).offset(offset).limit(limit)
|
||||
|
||||
if search:
|
||||
conditions = []
|
||||
for col in user_columns:
|
||||
column_attr = getattr(User, col)
|
||||
if isinstance(column_attr.type, (AutoString, Text)):
|
||||
conditions.append(column_attr.ilike(f"%{search}%"))
|
||||
query = query.where(or_(*conditions))
|
||||
if order_dir == "asc":
|
||||
query = query.order_by(asc(getattr(User, order_col_name)))
|
||||
else:
|
||||
query = query.order_by(desc(getattr(User, order_col_name)))
|
||||
users = (await session.execute(query)).all()
|
||||
|
||||
return {
|
||||
"recordsTotal": total_users,
|
||||
"recordsFiltered": total_users,
|
||||
"data": users,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue