Fixed user manager script after db changes from a while back. Fixed some object router code that was broken.
This commit is contained in:
@@ -9,6 +9,8 @@ from persistent.mapping import PersistentMapping
|
|||||||
from persistent.list import PersistentList
|
from persistent.list import PersistentList
|
||||||
from packetserver.common.util import is_valid_ax25_callsign
|
from packetserver.common.util import is_valid_ax25_callsign
|
||||||
from .database import DbDependency
|
from .database import DbDependency
|
||||||
|
from typing import Union
|
||||||
|
from ZODB.Connection import Connection
|
||||||
|
|
||||||
ph = PasswordHasher()
|
ph = PasswordHasher()
|
||||||
|
|
||||||
@@ -52,11 +54,15 @@ class HttpUser(Persistent):
|
|||||||
# rf enabled checks..
|
# rf enabled checks..
|
||||||
#
|
#
|
||||||
|
|
||||||
def is_rf_enabled(self, db: DbDependency) -> bool:
|
def is_rf_enabled(self, db: Union[DbDependency,Connection]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
||||||
Requires an open ZODB connection.
|
Requires an open ZODB connection.
|
||||||
"""
|
"""
|
||||||
|
if type(db) is Connection:
|
||||||
|
root = db.root()
|
||||||
|
blacklist = root.get('config', {}).get('blacklist', [])
|
||||||
|
return self.username not in blacklist
|
||||||
with db.transaction() as conn:
|
with db.transaction() as conn:
|
||||||
root = conn.root()
|
root = conn.root()
|
||||||
blacklist = root.get('config', {}).get('blacklist', [])
|
blacklist = root.get('config', {}).get('blacklist', [])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Header
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Header, Request
|
||||||
from fastapi.responses import PlainTextResponse, Response, JSONResponse, StreamingResponse
|
from fastapi.responses import PlainTextResponse, Response, JSONResponse, StreamingResponse, RedirectResponse
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -137,23 +137,54 @@ class TextObjectCreate(BaseModel):
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
private: bool = True
|
private: bool = True
|
||||||
|
|
||||||
@router.post("/objects/text", response_model=ObjectSummary)
|
@router.post("/objects/text", response_model=None) # Remove response_model to allow mixed returns
|
||||||
async def create_text_object(
|
async def create_text_object(
|
||||||
payload: TextObjectCreate,
|
request: Request,
|
||||||
db: DbDependency,
|
db: DbDependency,
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
):
|
):
|
||||||
username = current_user.username
|
username = current_user.username
|
||||||
|
|
||||||
if not payload.text:
|
# Determine content type and parse accordingly
|
||||||
|
content_type = request.headers.get("content-type", "").lower()
|
||||||
|
|
||||||
|
if "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
|
||||||
|
form = await request.form()
|
||||||
|
name = form.get("name")
|
||||||
|
text = form.get("text")
|
||||||
|
private_str = form.get("private") # "on" if checked, None otherwise
|
||||||
|
is_form = True
|
||||||
|
elif "application/json" in content_type:
|
||||||
|
try:
|
||||||
|
json_data = await request.json()
|
||||||
|
name = json_data.get("name")
|
||||||
|
text = json_data.get("text")
|
||||||
|
private_str = json_data.get("private")
|
||||||
|
is_form = False
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=415, detail="Unsupported Media Type")
|
||||||
|
|
||||||
|
# Validate text
|
||||||
|
if not text:
|
||||||
raise HTTPException(status_code=400, detail="Text content cannot be empty")
|
raise HTTPException(status_code=400, detail="Text content cannot be empty")
|
||||||
|
|
||||||
obj_name = (payload.name or "text_object.txt").strip()
|
# Normalize name (optional, default like original)
|
||||||
|
obj_name = (name or "text_object.txt").strip()
|
||||||
if len(obj_name) > 300:
|
if len(obj_name) > 300:
|
||||||
raise HTTPException(status_code=400, detail="Object name too long (max 300 chars)")
|
raise HTTPException(status_code=400, detail="Object name too long (max 300 chars)")
|
||||||
if not obj_name:
|
if not obj_name:
|
||||||
raise HTTPException(status_code=400, detail="Invalid object name")
|
raise HTTPException(status_code=400, detail="Invalid object name")
|
||||||
|
|
||||||
|
# Normalize private to bool (handles form "on"/None, JSON bool, or string)
|
||||||
|
if isinstance(private_str, bool):
|
||||||
|
private = private_str
|
||||||
|
elif isinstance(private_str, str):
|
||||||
|
private = private_str.lower() in ("true", "on", "1", "yes")
|
||||||
|
else:
|
||||||
|
private = False # Default to False if invalid/missing
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db.transaction() as conn:
|
with db.transaction() as conn:
|
||||||
root = conn.root()
|
root = conn.root()
|
||||||
@@ -161,13 +192,13 @@ async def create_text_object(
|
|||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
# Create object with str data → forces binary=False
|
# Create object with str data → forces binary=False
|
||||||
new_object = Object(name=obj_name, data=payload.text)
|
new_object = Object(name=obj_name, data=text)
|
||||||
new_object.private = payload.private
|
new_object.private = private
|
||||||
|
|
||||||
obj_uuid = new_object.write_new(db, username=username)
|
obj_uuid = new_object.write_new(db, username=username)
|
||||||
|
|
||||||
logging.info(f"User {username} created text object {obj_uuid} ({obj_name}, {len(payload.text)} chars)")
|
logging.info(f"User {username} created text object {obj_uuid} ({obj_name}, {len(text)} chars)")
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -175,22 +206,28 @@ async def create_text_object(
|
|||||||
logging.error(f"Text object creation failed for {username}: {e}\n{format_exc()}")
|
logging.error(f"Text object creation failed for {username}: {e}\n{format_exc()}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to create text object")
|
raise HTTPException(status_code=500, detail="Failed to create text object")
|
||||||
|
|
||||||
# Build summary
|
# Build summary (for JSON responses)
|
||||||
content_type, _ = mimetypes.guess_type(new_object.name)
|
content_type_guess, _ = mimetypes.guess_type(new_object.name)
|
||||||
if content_type is None:
|
if content_type_guess is None:
|
||||||
content_type = "text/plain" # always text here
|
content_type_guess = "text/plain" # always text here
|
||||||
|
|
||||||
return ObjectSummary(
|
summary = ObjectSummary(
|
||||||
uuid=obj_uuid,
|
uuid=obj_uuid,
|
||||||
name=new_object.name,
|
name=new_object.name,
|
||||||
binary=new_object.binary, # should be False
|
binary=new_object.binary, # should be False
|
||||||
size=new_object.size,
|
size=new_object.size,
|
||||||
content_type=content_type,
|
content_type=content_type_guess,
|
||||||
private=new_object.private,
|
private=new_object.private,
|
||||||
created_at=new_object.created_at,
|
created_at=new_object.created_at,
|
||||||
modified_at=new_object.modified_at
|
modified_at=new_object.modified_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return based on input type
|
||||||
|
if is_form:
|
||||||
|
return RedirectResponse(url="/objects", status_code=303) # Back to HTML list
|
||||||
|
else:
|
||||||
|
return JSONResponse(content=summary.model_dump(), status_code=201)
|
||||||
|
|
||||||
class BinaryObjectCreate(BaseModel):
|
class BinaryObjectCreate(BaseModel):
|
||||||
data_base64: str
|
data_base64: str
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
import ax25
|
import ax25
|
||||||
|
import os.path
|
||||||
|
import os
|
||||||
|
|
||||||
import ZODB.FileStorage
|
import ZODB.FileStorage
|
||||||
import ZODB.DB
|
import ZODB.DB
|
||||||
import transaction
|
import transaction
|
||||||
from persistent.mapping import PersistentMapping
|
from persistent.mapping import PersistentMapping
|
||||||
|
|
||||||
|
os.environ['PS_APP_ZEO_FILE'] = "N/A"
|
||||||
|
|
||||||
# Import our HTTP package internals
|
# Import our HTTP package internals
|
||||||
from packetserver.http.auth import HttpUser, ph # ph = PasswordHasher
|
from packetserver.http.auth import HttpUser, ph # ph = PasswordHasher
|
||||||
|
|
||||||
@@ -42,6 +46,13 @@ def open_database(db_arg: str) -> ZODB.DB:
|
|||||||
return ZODB.DB(storage)
|
return ZODB.DB(storage)
|
||||||
|
|
||||||
|
|
||||||
|
def open_database_zeo_file(filename: str) -> ZODB.DB:
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return open_database(open(filename,'r').read().strip())
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError("Must provide a filename to a zeo address.")
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_http_users(root):
|
def get_or_create_http_users(root):
|
||||||
if HTTP_USERS_KEY not in root:
|
if HTTP_USERS_KEY not in root:
|
||||||
root[HTTP_USERS_KEY] = PersistentMapping()
|
root[HTTP_USERS_KEY] = PersistentMapping()
|
||||||
@@ -55,7 +66,8 @@ def confirm(prompt: str) -> bool:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Manage PacketServer HTTP API users")
|
parser = argparse.ArgumentParser(description="Manage PacketServer HTTP API users")
|
||||||
parser.add_argument("--db", required=True, help="DB path (local /path/to/Data.fs) or ZEO (host:port)")
|
parser.add_argument("--db", required=False, help="DB path (local /path/to/Data.fs) or ZEO (host:port)")
|
||||||
|
parser.add_argument("--zeo-file", required=False, help="zeo address file")
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
# add
|
# add
|
||||||
@@ -99,12 +111,16 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Open the database
|
# Open the database
|
||||||
db = open_database(args.db)
|
if args.db:
|
||||||
connection = db.open()
|
db = open_database(args.db)
|
||||||
root = connection.root()
|
else:
|
||||||
|
db = open_database_zeo_file(args.zeo_file)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
users_mapping = get_or_create_http_users(root)
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
http_users_list = list(get_or_create_http_users(root).keys())
|
||||||
|
|
||||||
upper_callsign = lambda c: c.upper()
|
upper_callsign = lambda c: c.upper()
|
||||||
|
|
||||||
@@ -117,7 +133,7 @@ def main():
|
|||||||
print(f"Error: Trying to add valid callsign + ssid. Remove -<num> and add again.")
|
print(f"Error: Trying to add valid callsign + ssid. Remove -<num> and add again.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if callsign in users_mapping:
|
if callsign in http_users_list:
|
||||||
print(f"Error: HTTP user {callsign} already exists")
|
print(f"Error: HTTP user {callsign} already exists")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -127,79 +143,90 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Create the HTTP-specific user
|
# Create the HTTP-specific user
|
||||||
http_user = HttpUser(args.callsign, password)
|
with db.transaction() as conn:
|
||||||
users_mapping[callsign] = http_user
|
root = conn.root()
|
||||||
|
http_user = HttpUser(args.callsign, password)
|
||||||
|
users_mapping = get_or_create_http_users(conn.root())
|
||||||
|
users_mapping[callsign] = http_user
|
||||||
|
# Sync: create corresponding regular BBS user using proper write_new for UUID/uniqueness
|
||||||
|
from packetserver.server.users import User
|
||||||
|
|
||||||
# Sync: create corresponding regular BBS user using proper write_new for UUID/uniqueness
|
main_users = root.setdefault('users', PersistentMapping())
|
||||||
from packetserver.server.users import User
|
if callsign not in main_users:
|
||||||
|
new_user = User(args.callsign)
|
||||||
main_users = root.setdefault('users', PersistentMapping())
|
new_user.write_new(conn.root())
|
||||||
if callsign not in main_users:
|
print(f" → Also created regular BBS user {callsign}")
|
||||||
User.write_new(main_users, args.callsign) # correct: pass mapping + callsign
|
else:
|
||||||
print(f" → Also created regular BBS user {callsign} (with UUID)")
|
print(f" → Regular BBS user {callsign} already exists")
|
||||||
else:
|
print(f"Created HTTP user {callsign}")
|
||||||
print(f" → Regular BBS user {callsign} already exists")
|
|
||||||
|
|
||||||
transaction.commit()
|
|
||||||
print(f"Created HTTP user {callsign}")
|
|
||||||
|
|
||||||
elif args.command == "delete":
|
elif args.command == "delete":
|
||||||
callsign = upper_callsign(args.callsign)
|
callsign = upper_callsign(args.callsign)
|
||||||
if callsign not in users_mapping:
|
if callsign not in http_users_list:
|
||||||
print(f"Error: User {callsign} not found")
|
print(f"Error: User {callsign} not found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not confirm(f"Delete HTTP user {callsign}?"):
|
if not confirm(f"Delete HTTP user {callsign}?"):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
del users_mapping[callsign]
|
with db.transaction() as conn:
|
||||||
transaction.commit()
|
root = conn.root()
|
||||||
print(f"Deleted HTTP user {callsign}")
|
users_mapping = get_or_create_http_users(root)
|
||||||
|
del users_mapping[callsign]
|
||||||
|
print(f"Deleted HTTP user {callsign}")
|
||||||
|
|
||||||
elif args.command == "set-password":
|
elif args.command == "set-password":
|
||||||
callsign = upper_callsign(args.callsign)
|
callsign = upper_callsign(args.callsign)
|
||||||
user = users_mapping.get(callsign)
|
with db.transaction() as conn:
|
||||||
if not user:
|
root = conn.root()
|
||||||
print(f"Error: User {callsign} not found")
|
users_mapping = get_or_create_http_users(root)
|
||||||
sys.exit(1)
|
user = users_mapping.get(callsign)
|
||||||
newpass = args.newpassword or getpass("New password: ")
|
if not user:
|
||||||
if not newpass:
|
print(f"Error: User {callsign} not found")
|
||||||
print("Error: No password provided")
|
sys.exit(1)
|
||||||
sys.exit(1)
|
newpass = args.newpassword or getpass("New password: ")
|
||||||
user.password_hash = ph.hash(newpass)
|
if not newpass:
|
||||||
user._p_changed = True
|
print("Error: No password provided")
|
||||||
transaction.commit()
|
sys.exit(1)
|
||||||
print(f"Password updated for {callsign}")
|
user.password_hash = ph.hash(newpass)
|
||||||
|
user._p_changed = True
|
||||||
|
print(f"Password updated for {callsign}")
|
||||||
|
|
||||||
elif args.command == "enable":
|
elif args.command == "enable":
|
||||||
callsign = upper_callsign(args.callsign)
|
with db.transaction() as conn:
|
||||||
user = users_mapping.get(callsign)
|
root = conn.root()
|
||||||
if not user:
|
users_mapping = get_or_create_http_users(root)
|
||||||
print(f"Error: User {callsign} not found")
|
callsign = upper_callsign(args.callsign)
|
||||||
sys.exit(1)
|
user = users_mapping.get(callsign)
|
||||||
user.enabled = True
|
if not user:
|
||||||
user._p_changed = True
|
print(f"Error: User {callsign} not found")
|
||||||
transaction.commit()
|
sys.exit(1)
|
||||||
print(f"HTTP access enabled for {callsign}")
|
user.http_enabled = True
|
||||||
|
user._p_changed = True
|
||||||
|
print(f"HTTP access enabled for {callsign}")
|
||||||
|
|
||||||
elif args.command == "disable":
|
elif args.command == "disable":
|
||||||
callsign = upper_callsign(args.callsign)
|
callsign = upper_callsign(args.callsign)
|
||||||
user = users_mapping.get(callsign)
|
with db.transaction() as conn:
|
||||||
if not user:
|
root = conn.root()
|
||||||
print(f"Error: User {callsign} not found")
|
users_mapping = get_or_create_http_users(root)
|
||||||
sys.exit(1)
|
user = users_mapping.get(callsign)
|
||||||
user.enabled = False
|
if not user:
|
||||||
user._p_changed = True
|
print(f"Error: User {callsign} not found")
|
||||||
transaction.commit()
|
sys.exit(1)
|
||||||
print(f"HTTP access disabled for {callsign}")
|
user.http_enabled = False
|
||||||
|
user._p_changed = True
|
||||||
|
print(f"HTTP access disabled for {callsign}")
|
||||||
|
|
||||||
elif args.command == "rf-enable":
|
elif args.command == "rf-enable":
|
||||||
callsign = upper_callsign(args.callsign)
|
callsign = upper_callsign(args.callsign)
|
||||||
user = users_mapping.get(callsign)
|
with db.transaction() as conn:
|
||||||
if not user:
|
root = conn.root()
|
||||||
print(f"Error: User {callsign} not found")
|
users_mapping = get_or_create_http_users(root)
|
||||||
sys.exit(1)
|
user = users_mapping.get(callsign)
|
||||||
|
if not user:
|
||||||
|
print(f"Error: User {callsign} not found")
|
||||||
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
user.set_rf_enabled(connection, True)
|
user.set_rf_enabled(db, True)
|
||||||
transaction.commit()
|
|
||||||
print(f"RF gateway enabled for {callsign}")
|
print(f"RF gateway enabled for {callsign}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
@@ -207,67 +234,75 @@ def main():
|
|||||||
|
|
||||||
elif args.command == "rf-disable":
|
elif args.command == "rf-disable":
|
||||||
callsign = upper_callsign(args.callsign)
|
callsign = upper_callsign(args.callsign)
|
||||||
user = users_mapping.get(callsign)
|
with db.transaction() as conn:
|
||||||
if not user:
|
root = conn.root()
|
||||||
print(f"Error: User {callsign} not found")
|
users_mapping = get_or_create_http_users(root)
|
||||||
sys.exit(1)
|
user = users_mapping.get(callsign)
|
||||||
user.set_rf_enabled(connection, False)
|
if not user:
|
||||||
transaction.commit()
|
print(f"Error: User {callsign} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
user.set_rf_enabled(db, False)
|
||||||
print(f"RF gateway disabled for {callsign}")
|
print(f"RF gateway disabled for {callsign}")
|
||||||
|
|
||||||
elif args.command == "list":
|
elif args.command == "list":
|
||||||
if not users_mapping:
|
if not http_users_list:
|
||||||
print("No HTTP users configured")
|
print("No HTTP users configured")
|
||||||
else:
|
else:
|
||||||
print(f"{'Callsign':<12} {'HTTP Enabled':<13} {'RF Enabled':<11} {'Created':<20} Last Login")
|
with db.transaction() as conn:
|
||||||
print("-" * 75)
|
root = conn.root()
|
||||||
for user in sorted(users_mapping.values(), key=lambda u: u.username):
|
users_mapping = get_or_create_http_users(root)
|
||||||
created = time.strftime("%Y-%m-%d %H:%M", time.localtime(user.created_at))
|
print(f"{'Callsign':<12} {'HTTP Enabled':<13} {'RF Enabled':<11} {'Created':<20} Last Login")
|
||||||
last = (time.strftime("%Y-%m-%d %H:%M", time.localtime(user.last_login))
|
print("-" * 75)
|
||||||
if user.last_login else "-")
|
for user in sorted(users_mapping.values(), key=lambda u: u.username):
|
||||||
rf_status = "True" if user.is_rf_enabled(connection) else "False"
|
created = time.strftime("%Y-%m-%d %H:%M", time.localtime(user.created_at))
|
||||||
print(f"{user.username:<12} {str(user.http_enabled):<13} {rf_status:<11} {created:<20} {last}")
|
last = (time.strftime("%Y-%m-%d %H:%M", time.localtime(user.last_login))
|
||||||
|
if user.last_login else "-")
|
||||||
|
rf_status = "True" if user.is_rf_enabled(conn) else "False"
|
||||||
|
print(f"{user.username:<12} {str(user.http_enabled):<13} {rf_status:<11} {created:<20} {last}")
|
||||||
|
|
||||||
elif args.command == "dump":
|
elif args.command == "dump":
|
||||||
import json
|
import json
|
||||||
|
|
||||||
callsign = upper_callsign(args.callsign)
|
callsign = upper_callsign(args.callsign)
|
||||||
http_user = users_mapping.get(callsign)
|
with db.transaction() as conn:
|
||||||
if not http_user:
|
root = conn.root()
|
||||||
print(f"Error: No HTTP user {callsign} found")
|
users_mapping = get_or_create_http_users(root)
|
||||||
sys.exit(1)
|
http_user = users_mapping.get(callsign)
|
||||||
|
if not http_user:
|
||||||
|
print(f"Error: No HTTP user {callsign} found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
main_users = root.get('users', {})
|
main_users = root.get('users', {})
|
||||||
bbs_user = main_users.get(callsign)
|
bbs_user = main_users.get(callsign)
|
||||||
if not bbs_user:
|
if not bbs_user:
|
||||||
print(f"Error: No corresponding BBS user {callsign} found")
|
print(f"Error: No corresponding BBS user {callsign} found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
dump_data = {
|
dump_data = {
|
||||||
"http_user": {
|
"http_user": {
|
||||||
"username": http_user.username,
|
"username": http_user.username,
|
||||||
"http_enabled": http_user.http_enabled,
|
"http_enabled": http_user.http_enabled,
|
||||||
"rf_enabled": http_user.is_rf_enabled(connection),
|
"rf_enabled": http_user.is_rf_enabled(conn),
|
||||||
"blacklisted": not http_user.is_rf_enabled(connection), # explicit inverse
|
"blacklisted": not http_user.is_rf_enabled(conn), # explicit inverse
|
||||||
"created_at": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(http_user.created_at)),
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(http_user.created_at)),
|
||||||
"failed_attempts": http_user.failed_attempts,
|
"failed_attempts": http_user.failed_attempts,
|
||||||
},
|
},
|
||||||
"bbs_user": {
|
"bbs_user": {
|
||||||
"username": bbs_user.username,
|
"username": bbs_user.username,
|
||||||
"uuid": str(bbs_user.uuid) if hasattr(bbs_user, 'uuid') and bbs_user.uuid else None,
|
"uuid": str(bbs_user.uuid) if hasattr(bbs_user, 'uuid') and bbs_user.uuid else None,
|
||||||
"hidden": bbs_user.hidden,
|
"hidden": bbs_user.hidden,
|
||||||
"enabled": bbs_user.enabled, # BBS enabled flag
|
"enabled": bbs_user.enabled, # BBS enabled flag
|
||||||
"created_at": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(bbs_user.created_at.timestamp())) if hasattr(bbs_user.created_at, "timestamp") else str(bbs_user.created_at),
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(bbs_user.created_at.timestamp())) if hasattr(bbs_user.created_at, "timestamp") else str(bbs_user.created_at),
|
||||||
"last_seen": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(bbs_user.last_seen.timestamp())) if hasattr(bbs_user.last_seen, "timestamp") else str(bbs_user.last_seen),
|
"last_seen": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(bbs_user.last_seen.timestamp())) if hasattr(bbs_user.last_seen, "timestamp") else str(bbs_user.last_seen),
|
||||||
"bio": bbs_user.bio.strip() or None,
|
"bio": bbs_user.bio.strip() or None,
|
||||||
"status": bbs_user.status.strip() or None,
|
"status": bbs_user.status.strip() or None,
|
||||||
"email": bbs_user.email.strip() if bbs_user.email != " " else None,
|
"email": bbs_user.email.strip() if bbs_user.email != " " else None,
|
||||||
"location": bbs_user.location.strip() if bbs_user.location != " " else None,
|
"location": bbs_user.location.strip() if bbs_user.location != " " else None,
|
||||||
"socials": bbs_user.socials,
|
"socials": bbs_user.socials,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
print(json.dumps(dump_data, indent=4))
|
print(json.dumps(dump_data, indent=4))
|
||||||
|
|
||||||
elif args.command == "sync-missing":
|
elif args.command == "sync-missing":
|
||||||
import secrets
|
import secrets
|
||||||
@@ -277,38 +312,39 @@ def main():
|
|||||||
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
bbs_users = root.get('users', {})
|
with db.transaction() as conn:
|
||||||
http_users = get_or_create_http_users(root)
|
root = conn.root()
|
||||||
|
bbs_users = root.get('users', {})
|
||||||
|
http_users = get_or_create_http_users(root)
|
||||||
|
|
||||||
missing = [call for call in bbs_users if call not in http_users and call != "SYSTEM"]
|
missing = [call for call in bbs_users if call not in http_users and call != "SYSTEM"]
|
||||||
if not missing:
|
if not missing:
|
||||||
print("No missing HTTP users—all BBS users have HttpUser objects")
|
print("No missing HTTP users—all BBS users have HttpUser objects")
|
||||||
else:
|
|
||||||
print(f"Found {len(missing)} BBS users without HTTP accounts:")
|
|
||||||
for call in sorted(missing):
|
|
||||||
print(f" - {call}")
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
print("\n--dry-run: No changes made")
|
|
||||||
else:
|
else:
|
||||||
confirm_msg = f"Create {len(missing)} new HttpUser objects (http_enabled={'True' if args.enable else 'False'})?"
|
print(f"Found {len(missing)} BBS users without HTTP accounts:")
|
||||||
if not confirm(confirm_msg):
|
for call in sorted(missing):
|
||||||
print("Aborted")
|
print(f" - {call}")
|
||||||
else:
|
|
||||||
created_count = 0
|
|
||||||
for call in missing:
|
|
||||||
password = generate_password() # strong random, not printed
|
|
||||||
new_http = HttpUser(call, password)
|
|
||||||
new_http.http_enabled = args.enable
|
|
||||||
http_users[call] = new_http
|
|
||||||
created_count += 1
|
|
||||||
|
|
||||||
transaction.commit()
|
if args.dry_run:
|
||||||
print(f"\nSync complete: {created_count} HTTP users added (passwords random & hidden)")
|
print("\n--dry-run: No changes made")
|
||||||
print("Use 'set-password <call>' to set a known password before enabling login")
|
else:
|
||||||
|
confirm_msg = f"Create {len(missing)} new HttpUser objects (http_enabled={'True' if args.enable else 'False'})?"
|
||||||
|
if not confirm(confirm_msg):
|
||||||
|
print("Aborted")
|
||||||
|
else:
|
||||||
|
created_count = 0
|
||||||
|
for call in missing:
|
||||||
|
password = generate_password() # strong random, not printed
|
||||||
|
new_http = HttpUser(call, password)
|
||||||
|
new_http.http_enabled = args.enable
|
||||||
|
http_users[call] = new_http
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
transaction.commit()
|
||||||
|
print(f"\nSync complete: {created_count} HTTP users added (passwords random & hidden)")
|
||||||
|
print("Use 'set-password <call>' to set a known password before enabling login")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
connection.close()
|
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user