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' && \
|
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';
|
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-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.
|
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 argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
|
||||||
import getpass
|
import getpass
|
||||||
import re
|
import re
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from fastapi_users.exceptions import UserAlreadyExists
|
from fastapi_users.exceptions import UserAlreadyExists
|
||||||
|
|
||||||
from ghostforge.db import get_session
|
from ghostforge.users import get_async_session_context
|
||||||
from ghostforge.db import get_user_db
|
from ghostforge.users import get_user_db_context
|
||||||
from ghostforge.users import get_user_manager
|
from ghostforge.users import get_user_manager_context
|
||||||
from ghostforge.users import UserCreate
|
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:
|
def is_valid_email(email: str) -> bool:
|
||||||
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||||
return bool(re.match(email_regex, email))
|
return bool(re.match(email_regex, email))
|
||||||
|
|
|
@ -19,7 +19,6 @@ from sqlalchemy import or_
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy import Text
|
from sqlalchemy import Text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlmodel import and_
|
from sqlmodel import and_
|
||||||
from sqlmodel import AutoString
|
from sqlmodel import AutoString
|
||||||
from sqlmodel import Column
|
from sqlmodel import Column
|
||||||
|
@ -116,7 +115,7 @@ async def read_ghost(
|
||||||
if not can_view:
|
if not can_view:
|
||||||
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
result = await session.execute(
|
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)
|
.join(User, Ghost.owner_id == User.id)
|
||||||
.where(Ghost.id == ghost_id)
|
.where(Ghost.id == ghost_id)
|
||||||
)
|
)
|
||||||
|
@ -189,7 +188,10 @@ async def get_ghosts(
|
||||||
|
|
||||||
# Retrieve filtered ghosts from database
|
# Retrieve filtered ghosts from database
|
||||||
query = (
|
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_(
|
permission_filter = or_(
|
||||||
Ghost.owner_id == current_user.id,
|
Ghost.owner_id == current_user.id,
|
||||||
|
@ -231,7 +233,7 @@ async def read_users(
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int = Query(default=100, lte=100),
|
limit: int = Query(default=100, lte=100),
|
||||||
session: Session = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
):
|
):
|
||||||
subquery = (
|
subquery = (
|
||||||
|
@ -242,6 +244,6 @@ async def read_users(
|
||||||
)
|
)
|
||||||
query = await session.execute(select(Ghost).where(Ghost.id.in_(subquery)).offset(offset).limit(limit))
|
query = await session.execute(select(Ghost).where(Ghost.id.in_(subquery)).offset(offset).limit(limit))
|
||||||
ghosts = query.scalars().all()
|
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", {})
|
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||||
return ghosts
|
return ghosts
|
||||||
|
|
|
@ -33,7 +33,7 @@ class HtmlJson:
|
||||||
|
|
||||||
params = list(sig.parameters.values())
|
params = list(sig.parameters.values())
|
||||||
request_param = Parameter("request", _ParameterKind.POSITIONAL_OR_KEYWORD, annotation=Request)
|
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)
|
wrapper.__signature__ = sig.replace(parameters=params)
|
||||||
|
|
||||||
def render_template(self, template: str, request: Request, result):
|
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.templates import templates
|
||||||
from ghostforge.users import fastapi_users
|
from ghostforge.users import fastapi_users
|
||||||
from ghostforge.users import get_current_user
|
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 jwt_backend
|
||||||
from ghostforge.users import UserCreate
|
from ghostforge.users import UserCreate
|
||||||
from ghostforge.users import UserRead
|
from ghostforge.users import UserRead
|
||||||
|
@ -25,6 +26,7 @@ from ghostforge.users import web_backend
|
||||||
|
|
||||||
gf = FastAPI()
|
gf = FastAPI()
|
||||||
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_ghosts)
|
gf.include_router(gf_ghosts)
|
||||||
gf.include_router(gf_pages)
|
gf.include_router(gf_pages)
|
||||||
gf.include_router(gf_ethnicities)
|
gf.include_router(gf_ethnicities)
|
||||||
|
|
|
@ -88,7 +88,12 @@
|
||||||
return month + '/' + day + '/' + year;
|
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" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ target_user.username }}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-12">
|
<div class="column col-12">
|
||||||
<h1>Words!</h1>
|
<h1>Manage User</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-3"></div>
|
<div class="column col-4"></div>
|
||||||
<div class="column col-6">
|
<div class="column col-4">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header text-center">
|
<div class="panel-header text-center">
|
||||||
<figure class="avatar avatar-lg"><img src="{{ url_for('static', path='/img/default-avatar.png') }}"
|
<figure class="avatar avatar-lg"><img src="{{ url_for('static', path='/img/default-avatar.png') }}"
|
||||||
alt="Avatar"></figure>
|
alt="Avatar"></figure>
|
||||||
<div class="panel-title h5 mt-10">Bruce Banner</div>
|
<div class="panel-title h5 mt-10">{{ target_user.username }}</div>
|
||||||
<div class="panel-subtitle">THE HULK</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>
|
</div>
|
||||||
<nav class="panel-nav">
|
<nav class="panel-nav">
|
||||||
<ul class="tab tab-block">
|
<ul class="tab tab-block">
|
||||||
<li class="tab-item active"><a href="#panels">Profile</a></li>
|
<li class="tab-item active">Profile</li>
|
||||||
<li class="tab-item"><a href="#panels">Files</a></li>
|
|
||||||
<li class="tab-item"><a href="#panels">Tasks</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="tile tile-centered">
|
<div class="tile tile-centered">
|
||||||
<div class="tile-content">
|
<div class="tile-content">
|
||||||
<div class="tile-title text-bold">E-mail</div>
|
<div class="tile-title text-bold">Username</div>
|
||||||
<div class="tile-subtitle">bruce.banner@hulk.com</div>
|
<div class="tile-subtitle">{{ target_user.username }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile-action">
|
<div class="tile-action">
|
||||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left"
|
<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>
|
</div>
|
||||||
<div class="tile tile-centered">
|
<div class="tile tile-centered">
|
||||||
<div class="tile-content">
|
<div class="tile-content">
|
||||||
<div class="tile-title text-bold">Skype</div>
|
<div class="tile-title text-bold">E-mail</div>
|
||||||
<div class="tile-subtitle">bruce.banner</div>
|
<div class="tile-subtitle">{{ target_user.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile-action">
|
<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>
|
</div>
|
||||||
<div class="tile tile-centered">
|
<div class="tile tile-centered">
|
||||||
<div class="tile-content">
|
<div class="tile-content">
|
||||||
<div class="tile-title text-bold">Location</div>
|
<div class="tile-title text-bold">New Password</div>
|
||||||
<div class="tile-subtitle">Dayton, Ohio</div>
|
<div class="tile-subtitle">blocked</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile-action">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,6 +69,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column col-3"></div>
|
<div class="column col-4"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% 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 os
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import Annotated
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
from fastapi import Form
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from fastapi import Query
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi_users import BaseUserManager
|
from fastapi_users import BaseUserManager
|
||||||
from fastapi_users import FastAPIUsers
|
from fastapi_users import FastAPIUsers
|
||||||
from fastapi_users import schemas
|
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 BearerTransport
|
||||||
from fastapi_users.authentication import CookieTransport
|
from fastapi_users.authentication import CookieTransport
|
||||||
from fastapi_users.authentication import JWTStrategy
|
from fastapi_users.authentication import JWTStrategy
|
||||||
|
from fastapi_users.exceptions import UserAlreadyExists
|
||||||
from fastapi_users_db_sqlmodel import SQLModelUserDatabase
|
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 get_user_db
|
||||||
from ghostforge.db import User
|
from ghostforge.db import User
|
||||||
|
from ghostforge.htmljson import HtmlJson
|
||||||
|
|
||||||
|
|
||||||
SECRET = os.environ.get("GHOSTFORGE_JWT_SECRET")
|
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()
|
gf = APIRouter()
|
||||||
|
hj = HtmlJson()
|
||||||
|
|
||||||
|
|
||||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||||
|
@ -30,10 +60,11 @@ class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||||
|
|
||||||
class UserCreate(schemas.BaseUserCreate):
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
username: str
|
username: str
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(schemas.BaseUserUpdate):
|
class UserUpdate(schemas.BaseUserUpdate):
|
||||||
username: str
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
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}")
|
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")
|
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||||
cookie_transport = CookieTransport(cookie_httponly=True, cookie_name="ghostforge", cookie_samesite="strict")
|
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:
|
def get_current_user(active: bool = True, optional: bool = False) -> User:
|
||||||
return fastapi_users.current_user(active=active, optional=optional)
|
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