first commit

This commit is contained in:
Anurag Jangid 2026-10-08 02:41:41 +05:30
commit d416fe4cce
69 changed files with 8269 additions and 0 deletions

View file

View file

@ -0,0 +1,68 @@
from datetime import datetime, timezone
from fastapi import HTTPException, status
from motor.motor_asyncio import AsyncIOMotorDatabase
from pymongo.errors import DuplicateKeyError
from app.core.security import hash_auth_key, normalize_identifier, verify_auth_key
from app.schemas.auth import AuthLoginRequest, AuthSignupRequest
ACCOUNTS_COLLECTION = "accounts"
async def ensure_auth_indexes(database: AsyncIOMotorDatabase) -> None:
await database[ACCOUNTS_COLLECTION].create_index(
[("identifierType", 1), ("identifier", 1)],
unique=True,
name="uq_accounts_identifier",
)
def _account_filter(identifier_type: str, identifier: str) -> dict[str, str]:
return {
"identifierType": identifier_type,
"identifier": normalize_identifier(identifier_type, identifier),
}
async def create_account(database: AsyncIOMotorDatabase, payload: AuthSignupRequest) -> str:
now = datetime.now(timezone.utc)
account_document = {
"identifierType": payload.identifierType.value,
"identifier": normalize_identifier(payload.identifierType.value, payload.identifier),
"hashedAuthKey": hash_auth_key(payload.authKeyMaterial),
"saltAuth": payload.saltAuth,
"saltEnc": payload.saltEnc,
"createdAt": now,
"updatedAt": now,
}
try:
result = await database[ACCOUNTS_COLLECTION].insert_one(account_document)
except DuplicateKeyError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="An account with that identifier already exists.",
) from exc
return str(result.inserted_id)
async def get_account_by_identifier(database: AsyncIOMotorDatabase, identifier_type: str, identifier: str) -> dict[str, str]:
account = await database[ACCOUNTS_COLLECTION].find_one(_account_filter(identifier_type, identifier))
if account is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Account not found.",
)
return account
async def verify_account_login(database: AsyncIOMotorDatabase, payload: AuthLoginRequest) -> str:
account = await get_account_by_identifier(database, payload.identifierType.value, payload.identifier)
if not verify_auth_key(payload.authKeyMaterial, account["hashedAuthKey"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials.",
)
return str(account["_id"])

View file

@ -0,0 +1,235 @@
from datetime import datetime, timezone
from bson import ObjectId
from fastapi import HTTPException, status
from motor.motor_asyncio import AsyncIOMotorDatabase
from pymongo import ReturnDocument
from pymongo.errors import DuplicateKeyError
from app.schemas.records import (
ChildCreateRequest,
ChildRecordResponse,
ChildUpdateRequest,
ParentCreateRequest,
ParentRecordResponse,
ParentUpdateRequest,
)
PARENTS_COLLECTION = "parents"
CHILDREN_COLLECTION = "children"
class RecordNotFoundError(HTTPException):
def __init__(self, detail: str = "Record not found.") -> None:
super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
class BadRequestError(HTTPException):
def __init__(self, detail: str = "Invalid request.") -> None:
super().__init__(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
def _now() -> datetime:
return datetime.now(timezone.utc)
def _to_object_id(value: str, field_name: str) -> ObjectId:
if not ObjectId.is_valid(value):
raise BadRequestError(f"Invalid {field_name}.")
return ObjectId(value)
def _serialize_parent(document: dict) -> ParentRecordResponse:
return ParentRecordResponse(
id=str(document["_id"]),
ownerId=document["ownerId"],
iv=document["iv"],
ciphertext=document["ciphertext"],
createdAt=document["createdAt"],
updatedAt=document["updatedAt"],
)
def _serialize_child(document: dict) -> ChildRecordResponse:
return ChildRecordResponse(
id=str(document["_id"]),
parentId=str(document["parentId"]),
ownerId=document["ownerId"],
iv=document["iv"],
ciphertext=document["ciphertext"],
createdAt=document["createdAt"],
updatedAt=document["updatedAt"],
)
async def ensure_record_indexes(database: AsyncIOMotorDatabase) -> None:
await database[PARENTS_COLLECTION].create_index([("ownerId", 1)], name="ix_parents_ownerId")
await database[CHILDREN_COLLECTION].create_index([("ownerId", 1)], name="ix_children_ownerId")
await database[CHILDREN_COLLECTION].create_index([("parentId", 1)], name="ix_children_parentId")
await database[CHILDREN_COLLECTION].create_index(
[("ownerId", 1), ("parentId", 1)],
name="ix_children_ownerId_parentId",
)
async def create_parent(database: AsyncIOMotorDatabase, owner_id: str, payload: ParentCreateRequest) -> ParentRecordResponse:
now = _now()
document = {
"ownerId": owner_id,
"iv": payload.iv,
"ciphertext": payload.ciphertext,
"createdAt": now,
"updatedAt": now,
}
result = await database[PARENTS_COLLECTION].insert_one(document)
document["_id"] = result.inserted_id
return _serialize_parent(document)
async def list_parents(database: AsyncIOMotorDatabase, owner_id: str) -> list[ParentRecordResponse]:
parents: list[ParentRecordResponse] = []
cursor = database[PARENTS_COLLECTION].find({"ownerId": owner_id}).sort("createdAt", -1)
async for document in cursor:
parents.append(_serialize_parent(document))
return parents
async def get_parent(database: AsyncIOMotorDatabase, owner_id: str, parent_id: str) -> ParentRecordResponse:
document = await database[PARENTS_COLLECTION].find_one(
{"_id": _to_object_id(parent_id, "parentId"), "ownerId": owner_id}
)
if document is None:
raise RecordNotFoundError("Parent record not found.")
return _serialize_parent(document)
async def update_parent(
database: AsyncIOMotorDatabase,
owner_id: str,
parent_id: str,
payload: ParentUpdateRequest,
) -> ParentRecordResponse:
document = await database[PARENTS_COLLECTION].find_one_and_update(
{"_id": _to_object_id(parent_id, "parentId"), "ownerId": owner_id},
{
"$set": {
"iv": payload.iv,
"ciphertext": payload.ciphertext,
"updatedAt": _now(),
}
},
return_document=ReturnDocument.AFTER,
)
if document is None:
raise RecordNotFoundError("Parent record not found.")
return _serialize_parent(document)
async def delete_parent(database: AsyncIOMotorDatabase, owner_id: str, parent_id: str) -> None:
client = database.client
if client is None:
raise RuntimeError("MongoDB client has not been initialized")
parent_object_id = _to_object_id(parent_id, "parentId")
async with await client.start_session() as session:
async with session.start_transaction():
parent = await database[PARENTS_COLLECTION].find_one(
{"_id": parent_object_id, "ownerId": owner_id},
session=session,
)
if parent is None:
raise RecordNotFoundError("Parent record not found.")
await database[CHILDREN_COLLECTION].delete_many(
{"parentId": parent_object_id, "ownerId": owner_id},
session=session,
)
await database[PARENTS_COLLECTION].delete_one(
{"_id": parent_object_id, "ownerId": owner_id},
session=session,
)
async def create_child(
database: AsyncIOMotorDatabase,
owner_id: str,
payload: ChildCreateRequest,
) -> ChildRecordResponse:
parent_object_id = _to_object_id(payload.parentId, "parentId")
parent = await database[PARENTS_COLLECTION].find_one({"_id": parent_object_id, "ownerId": owner_id})
if parent is None:
raise RecordNotFoundError("Parent record not found.")
now = _now()
document = {
"ownerId": owner_id,
"parentId": parent_object_id,
"iv": payload.iv,
"ciphertext": payload.ciphertext,
"createdAt": now,
"updatedAt": now,
}
result = await database[CHILDREN_COLLECTION].insert_one(document)
document["_id"] = result.inserted_id
return _serialize_child(document)
async def list_children(database: AsyncIOMotorDatabase, owner_id: str, parent_id: str) -> list[ChildRecordResponse]:
parent_object_id = _to_object_id(parent_id, "parentId")
children: list[ChildRecordResponse] = []
cursor = database[CHILDREN_COLLECTION].find({"ownerId": owner_id, "parentId": parent_object_id}).sort("createdAt", -1)
async for document in cursor:
children.append(_serialize_child(document))
return children
async def get_child(database: AsyncIOMotorDatabase, owner_id: str, parent_id: str, child_id: str) -> ChildRecordResponse:
document = await database[CHILDREN_COLLECTION].find_one(
{
"_id": _to_object_id(child_id, "childId"),
"parentId": _to_object_id(parent_id, "parentId"),
"ownerId": owner_id,
}
)
if document is None:
raise RecordNotFoundError("Child record not found.")
return _serialize_child(document)
async def update_child(
database: AsyncIOMotorDatabase,
owner_id: str,
parent_id: str,
child_id: str,
payload: ChildUpdateRequest,
) -> ChildRecordResponse:
document = await database[CHILDREN_COLLECTION].find_one_and_update(
{
"_id": _to_object_id(child_id, "childId"),
"parentId": _to_object_id(parent_id, "parentId"),
"ownerId": owner_id,
},
{
"$set": {
"iv": payload.iv,
"ciphertext": payload.ciphertext,
"updatedAt": _now(),
}
},
return_document=ReturnDocument.AFTER,
)
if document is None:
raise RecordNotFoundError("Child record not found.")
return _serialize_child(document)
async def delete_child(database: AsyncIOMotorDatabase, owner_id: str, parent_id: str, child_id: str) -> None:
result = await database[CHILDREN_COLLECTION].delete_one(
{
"_id": _to_object_id(child_id, "childId"),
"parentId": _to_object_id(parent_id, "parentId"),
"ownerId": owner_id,
}
)
if result.deleted_count == 0:
raise RecordNotFoundError("Child record not found.")