2026-01-26 11:55:42 +00:00
|
|
|
from flask import current_app, jsonify, request
|
|
|
|
|
from helpers import CORS_HEADERS, db_row_to_dict, init_db_connection
|
|
|
|
|
from psycopg2 import IntegrityError
|
|
|
|
|
from pydantic_core import ValidationError
|
|
|
|
|
from schemas import AiUserUpdate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
"""
|
|
|
|
|
```fission
|
|
|
|
|
{
|
|
|
|
|
"name": "ai-admin-update-delete-user",
|
|
|
|
|
"fntimeout": 300,
|
|
|
|
|
"http_triggers": {
|
|
|
|
|
"ai-admin-update-delete-user-http": {
|
2026-01-27 01:23:56 +07:00
|
|
|
"url": "/ailbl/ai/admin/users/{UserID}",
|
2026-01-26 11:55:42 +00:00
|
|
|
"methods": ["DELETE", "PUT"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
if request.method == "DELETE":
|
|
|
|
|
return make_delete_request()
|
|
|
|
|
elif request.method == "PUT":
|
|
|
|
|
return make_update_request()
|
|
|
|
|
else:
|
|
|
|
|
return {"error": "Method not allow"}, 405, CORS_HEADERS
|
|
|
|
|
except Exception as err:
|
|
|
|
|
print(f"ErrorType={type(err)}")
|
|
|
|
|
return {"error": str(err)}, 500, CORS_HEADERS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_update_request():
|
2026-01-27 01:23:56 +07:00
|
|
|
r"""make_update_request() -> tuple[Response, int, dict]
|
|
|
|
|
|
|
|
|
|
Update an existing user by ID.
|
|
|
|
|
|
|
|
|
|
Retrieves the user ID from ``X-Fission-Params-UserID`` header, validates
|
|
|
|
|
the request body using :class:`AiUserUpdate` schema, and performs a
|
|
|
|
|
partial update on the user record.
|
|
|
|
|
|
|
|
|
|
Uses row-level locking (``SELECT ... FOR UPDATE``) to prevent concurrent
|
|
|
|
|
modification conflicts.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple: A tuple containing:
|
|
|
|
|
- JSON response with updated user data or error details
|
|
|
|
|
- HTTP status code (200 on success, 400/404/409 on error)
|
|
|
|
|
- CORS headers dict
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ValidationError: If request body fails Pydantic validation (returns 400).
|
|
|
|
|
IntegrityError: If email conflicts with another user (returns 409).
|
|
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
|
|
>>> # PUT /ai/admin/users/550e8400-e29b-41d4-a716-446655440000
|
|
|
|
|
>>> # Header: X-Fission-Params-UserID: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
|
>>> # Body: {"name": "Jane Doe"}
|
|
|
|
|
>>> # Response: 200 OK
|
|
|
|
|
>>> {
|
|
|
|
|
... "id": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
... "name": "Jane Doe",
|
|
|
|
|
... "email": "john@example.com",
|
|
|
|
|
... "modified": "2024-01-02T10:00:00"
|
|
|
|
|
... }
|
|
|
|
|
"""
|
2026-01-26 11:55:42 +00:00
|
|
|
user_id = request.headers.get("X-Fission-Params-UserID")
|
|
|
|
|
if not user_id:
|
|
|
|
|
return jsonify({"errorCode": "MISSING_USER_ID"}), 400, CORS_HEADERS
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
body = AiUserUpdate(**(request.get_json(silent=True) or {}))
|
|
|
|
|
except ValidationError as e:
|
|
|
|
|
return (
|
|
|
|
|
jsonify({"error": "Validation failed", "details": e.errors()}),
|
|
|
|
|
400,
|
|
|
|
|
CORS_HEADERS,
|
|
|
|
|
)
|
|
|
|
|
conn = None
|
|
|
|
|
try:
|
|
|
|
|
conn = init_db_connection()
|
|
|
|
|
with conn:
|
|
|
|
|
with conn.cursor() as cur:
|
|
|
|
|
cur.execute(
|
|
|
|
|
"SELECT * FROM ai_user WHERE id=%s FOR UPDATE", (user_id,))
|
|
|
|
|
row = cur.fetchone()
|
|
|
|
|
if not row:
|
|
|
|
|
return jsonify({"errorCode": "USER_NOT_FOUND"}), 404, CORS_HEADERS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sets, params = [], {"id": user_id}
|
|
|
|
|
if body.name is not None:
|
|
|
|
|
sets.append("name=%(name)s")
|
|
|
|
|
params["name"] = body.name
|
|
|
|
|
|
|
|
|
|
if body.email is not None:
|
|
|
|
|
sets.append("email=%(email)s")
|
|
|
|
|
params["email"] = body.email
|
|
|
|
|
|
|
|
|
|
if body.dob is not None:
|
|
|
|
|
sets.append("dob=%(dob)s")
|
|
|
|
|
params["dob"] = body.dob
|
|
|
|
|
|
|
|
|
|
if body.gender is not None:
|
|
|
|
|
sets.append("gender=%(gender)s")
|
|
|
|
|
params["gender"] = body.gender
|
|
|
|
|
|
|
|
|
|
sets.append("modified=CURRENT_TIMESTAMP")
|
|
|
|
|
cur.execute(
|
|
|
|
|
f"UPDATE ai_user SET {', '.join(sets)} WHERE id=%(id)s RETURNING *",
|
|
|
|
|
params,
|
|
|
|
|
)
|
|
|
|
|
updated = db_row_to_dict(cur, cur.fetchone())
|
|
|
|
|
return jsonify(updated), 200, CORS_HEADERS
|
|
|
|
|
except IntegrityError as e:
|
|
|
|
|
return (
|
|
|
|
|
jsonify({"errorCode": "DUPLICATE_USER", "details": str(e)}),
|
|
|
|
|
409,
|
|
|
|
|
CORS_HEADERS,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
if conn:
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __delete_user(cursor, id: str):
|
2026-01-27 01:23:56 +07:00
|
|
|
r"""Delete a user from the database by ID.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
cursor: Database cursor object for executing queries.
|
|
|
|
|
id (str): UUID of the user to delete.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict | str: User data dict if deleted successfully,
|
|
|
|
|
or ``"USER_NOT_FOUND"`` string if user doesn't exist.
|
|
|
|
|
|
|
|
|
|
Note:
|
|
|
|
|
This is a private function. Use :func:`make_delete_request` instead.
|
|
|
|
|
"""
|
2026-01-26 11:55:42 +00:00
|
|
|
cursor.execute("SELECT 1 FROM ai_user WHERE id = %(id)s", {"id": id})
|
|
|
|
|
if not cursor.fetchone():
|
|
|
|
|
return "USER_NOT_FOUND"
|
|
|
|
|
|
|
|
|
|
cursor.execute("DELETE FROM ai_user WHERE id = %(id)s RETURNING *", {"id": id})
|
|
|
|
|
row = cursor.fetchone()
|
|
|
|
|
return db_row_to_dict(cursor, row)
|
|
|
|
|
|
|
|
|
|
def make_delete_request():
|
2026-01-27 01:23:56 +07:00
|
|
|
r"""make_delete_request() -> tuple[Response, int, dict]
|
|
|
|
|
|
|
|
|
|
Delete a user by ID.
|
|
|
|
|
|
|
|
|
|
Retrieves the user ID from ``X-Fission-Params-UserID`` header and
|
|
|
|
|
deletes the user from the database if found.
|
2026-01-26 11:55:42 +00:00
|
|
|
|
2026-01-27 01:23:56 +07:00
|
|
|
Returns:
|
|
|
|
|
tuple: A tuple containing:
|
|
|
|
|
- JSON response with deleted user data or error details
|
|
|
|
|
- HTTP status code (200 on success, 400/404/500 on error)
|
|
|
|
|
- CORS headers dict (may be omitted on some error responses)
|
|
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
|
|
>>> # DELETE /ai/admin/users/550e8400-e29b-41d4-a716-446655440000
|
|
|
|
|
>>> # Header: X-Fission-Params-UserID: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
|
>>> # Response: 200 OK
|
|
|
|
|
>>> {
|
|
|
|
|
... "id": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
... "name": "John Doe",
|
|
|
|
|
... "email": "john@example.com"
|
|
|
|
|
... }
|
|
|
|
|
"""
|
2026-01-26 11:55:42 +00:00
|
|
|
user_id = request.headers.get("X-Fission-Params-UserID")
|
|
|
|
|
if not user_id:
|
|
|
|
|
return jsonify({"errorCode": "MISSING_USER_ID"}), 400, CORS_HEADERS
|
|
|
|
|
|
|
|
|
|
conn = None
|
|
|
|
|
try:
|
|
|
|
|
conn = init_db_connection()
|
|
|
|
|
with conn.cursor() as cursor:
|
|
|
|
|
result = __delete_user(cursor, id=user_id)
|
|
|
|
|
if result == "USER_NOT_FOUND":
|
|
|
|
|
return jsonify({"errorCode": "USER_NOT_FOUND"}), 404
|
|
|
|
|
conn.commit()
|
|
|
|
|
return jsonify(result), 200
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
return jsonify({"error": str(ex)}), 500
|
|
|
|
|
finally:
|
|
|
|
|
if conn is not None:
|
|
|
|
|
conn.close()
|
|
|
|
|
current_app.logger.info("Close DB connection")
|