kopia lustrzana https://github.com/bugout-dev/moonstream
commit
50952d1c29
|
@ -2,43 +2,21 @@
|
|||
The Moonstream HTTP API
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
import uuid
|
||||
|
||||
from fastapi import (
|
||||
BackgroundTasks,
|
||||
Depends,
|
||||
FastAPI,
|
||||
Form,
|
||||
HTTPException,
|
||||
Path,
|
||||
Query,
|
||||
Request,
|
||||
Response,
|
||||
)
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from . import data
|
||||
from .settings import DOCS_TARGET_PATH, ORIGINS
|
||||
from .routes.subscriptions import app as subscriptions_api
|
||||
from .routes.users import app as users_api
|
||||
from .settings import ORIGINS
|
||||
from .version import MOONSTREAM_VERSION
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tags_metadata = [{"name": "users", "description": "Operations with users."}]
|
||||
app = FastAPI(openapi_url=None)
|
||||
|
||||
app = FastAPI(
|
||||
title=f"Moonstream API.",
|
||||
description="The Bugout blockchain inspector API.",
|
||||
version=MOONSTREAM_VERSION,
|
||||
openapi_tags=tags_metadata,
|
||||
openapi_url="/openapi.json",
|
||||
docs_url=None,
|
||||
redoc_url=f"/{DOCS_TARGET_PATH}",
|
||||
)
|
||||
|
||||
# CORS settings
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=ORIGINS,
|
||||
|
@ -49,10 +27,14 @@ app.add_middleware(
|
|||
|
||||
|
||||
@app.get("/ping", response_model=data.PingResponse)
|
||||
async def ping() -> data.PingResponse:
|
||||
async def ping_handler() -> data.PingResponse:
|
||||
return data.PingResponse(status="ok")
|
||||
|
||||
|
||||
@app.get("/version", response_model=data.VersionResponse)
|
||||
async def version() -> data.VersionResponse:
|
||||
async def version_handler() -> data.VersionResponse:
|
||||
return data.VersionResponse(version=MOONSTREAM_VERSION)
|
||||
|
||||
|
||||
app.mount("/subscriptions", subscriptions_api)
|
||||
app.mount("/users", users_api)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""
|
||||
Pydantic schemas for the Moonstream HTTP API
|
||||
"""
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PingResponse(BaseModel):
|
||||
|
@ -18,3 +20,24 @@ class VersionResponse(BaseModel):
|
|||
"""
|
||||
|
||||
version: str
|
||||
|
||||
|
||||
class SubscriptionRequest(BaseModel):
|
||||
"""
|
||||
Schema for data retrieving from frontend about subscription.
|
||||
"""
|
||||
|
||||
blockchain: str
|
||||
|
||||
|
||||
class SubscriptionResponse(BaseModel):
|
||||
"""
|
||||
User subscription storing in Bugout resources.
|
||||
"""
|
||||
|
||||
user_id: str
|
||||
blockchain: str
|
||||
|
||||
|
||||
class SubscriptionsListResponse(BaseModel):
|
||||
subscriptions: List[SubscriptionResponse] = Field(default_factory=list)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import logging
|
||||
from typing import Awaitable, Callable, Dict, Optional
|
||||
|
||||
from bugout.data import BugoutUser
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from fastapi import Request, Response
|
||||
|
||||
from .settings import MOONSTREAM_APPLICATION_ID, bugout_client as bc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BroodAuthMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Checks the authorization header on the request. If it represents a verified Brood user,
|
||||
create another request and get groups user belongs to, after this
|
||||
adds a brood_user attribute to the request.state. Otherwise raises a 403 error.
|
||||
"""
|
||||
|
||||
def __init__(self, app, whitelist: Optional[Dict[str, str]] = None):
|
||||
self.whitelist: Dict[str, str] = {}
|
||||
if whitelist is not None:
|
||||
self.whitelist = whitelist
|
||||
super().__init__(app)
|
||||
|
||||
async def dispatch(
|
||||
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||||
):
|
||||
# Filter out endpoints with proper method to work without Bearer token (as create_user, login, etc)
|
||||
path = request.url.path.rstrip("/")
|
||||
method = request.method
|
||||
if path in self.whitelist.keys() and self.whitelist[path] == method:
|
||||
return await call_next(request)
|
||||
|
||||
authorization_header = request.headers.get("authorization")
|
||||
if authorization_header is None:
|
||||
return Response(
|
||||
status_code=403, content="No authorization header passed with request"
|
||||
)
|
||||
user_token_list = authorization_header.split()
|
||||
if len(user_token_list) != 2:
|
||||
return Response(status_code=403, content="Wrong authorization header")
|
||||
user_token: str = user_token_list[-1]
|
||||
|
||||
try:
|
||||
user: BugoutUser = bc.get_user(user_token)
|
||||
if not user.verified:
|
||||
logger.info(
|
||||
f"Attempted journal access by unverified Brood account: {user.id}"
|
||||
)
|
||||
return Response(
|
||||
status_code=403,
|
||||
content="Only verified accounts can access journals",
|
||||
)
|
||||
if str(user.application_id) != str(MOONSTREAM_APPLICATION_ID):
|
||||
return Response(
|
||||
status_code=403, content="User does not belong to this application"
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
return Response(status_code=e.status_code, content=e.detail)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
return Response(status_code=500, content="Internal server error")
|
||||
|
||||
request.state.user = user
|
||||
request.state.token = user_token
|
||||
return await call_next(request)
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
The Moonstream subscriptions HTTP API
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from bugout.data import BugoutResource, BugoutResources
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from fastapi import Body, FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .. import data
|
||||
from ..middleware import BroodAuthMiddleware
|
||||
from ..settings import (
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
DOCS_TARGET_PATH,
|
||||
ORIGINS,
|
||||
DOCS_PATHS,
|
||||
bugout_client as bc,
|
||||
)
|
||||
from ..version import MOONSTREAM_VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tags_metadata = [
|
||||
{"name": "subscriptions", "description": "Operations with subscriptions."},
|
||||
]
|
||||
|
||||
app = FastAPI(
|
||||
title=f"Moonstream subscriptions API.",
|
||||
description="User subscriptions endpoints.",
|
||||
version=MOONSTREAM_VERSION,
|
||||
openapi_tags=tags_metadata,
|
||||
openapi_url="/openapi.json",
|
||||
docs_url=None,
|
||||
redoc_url=f"/{DOCS_TARGET_PATH}",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
whitelist_paths: Dict[str, str] = {}
|
||||
whitelist_paths.update(DOCS_PATHS)
|
||||
app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
|
||||
|
||||
|
||||
@app.post("/", tags=["subscriptions"], response_model=data.SubscriptionResponse)
|
||||
async def add_subscription_handler(
|
||||
request: Request, subscription_data: data.SubscriptionRequest = Body(...)
|
||||
) -> data.SubscriptionResponse:
|
||||
"""
|
||||
Add subscription to blockchain stream data for user.
|
||||
"""
|
||||
token = request.state.token
|
||||
user = request.state.user
|
||||
resource_data = {"user_id": str(user.id)}
|
||||
resource_data.update(subscription_data)
|
||||
try:
|
||||
resource: BugoutResource = bc.create_resource(
|
||||
token=token,
|
||||
application_id=MOONSTREAM_APPLICATION_ID,
|
||||
resource_data=resource_data,
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return data.SubscriptionResponse(
|
||||
user_id=resource.resource_data["user_id"],
|
||||
blockchain=resource.resource_data["blockchain"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/", tags=["subscriptions"], response_model=data.SubscriptionsListResponse)
|
||||
async def get_subscriptions_handler(request: Request) -> data.SubscriptionsListResponse:
|
||||
"""
|
||||
Get user's subscriptions.
|
||||
"""
|
||||
token = request.state.token
|
||||
params = {"user_id": str(request.state.user.id)}
|
||||
try:
|
||||
resources: BugoutResources = bc.list_resources(token=token, params=params)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return data.SubscriptionsListResponse(
|
||||
subscriptions=[
|
||||
data.SubscriptionResponse(
|
||||
user_id=resource.resource_data["user_id"],
|
||||
blockchain=resource.resource_data["blockchain"],
|
||||
)
|
||||
for resource in resources.resources
|
||||
]
|
||||
)
|
|
@ -0,0 +1,172 @@
|
|||
"""
|
||||
The Moonstream users HTTP API
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
import uuid
|
||||
|
||||
from bugout.data import BugoutToken, BugoutUser
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Form,
|
||||
HTTPException,
|
||||
Request,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from ..middleware import BroodAuthMiddleware
|
||||
from ..settings import (
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
DOCS_TARGET_PATH,
|
||||
ORIGINS,
|
||||
DOCS_PATHS,
|
||||
bugout_client as bc,
|
||||
)
|
||||
from ..version import MOONSTREAM_VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tags_metadata = [
|
||||
{"name": "users", "description": "Operations with users."},
|
||||
{"name": "tokens", "description": "Operations with user tokens."},
|
||||
]
|
||||
|
||||
app = FastAPI(
|
||||
title=f"Moonstream users API.",
|
||||
description="User, token and password handlers.",
|
||||
version=MOONSTREAM_VERSION,
|
||||
openapi_tags=tags_metadata,
|
||||
openapi_url="/openapi.json",
|
||||
docs_url=None,
|
||||
redoc_url=f"/{DOCS_TARGET_PATH}",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
whitelist_paths: Dict[str, str] = {}
|
||||
whitelist_paths.update(DOCS_PATHS)
|
||||
whitelist_paths.update(
|
||||
{
|
||||
"/users": "POST",
|
||||
"/users/token": "POST",
|
||||
"/users/password/restore": "POST",
|
||||
"/users/password/reset": "POST",
|
||||
}
|
||||
)
|
||||
app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
|
||||
|
||||
|
||||
@app.post("/", tags=["users"], response_model=BugoutUser)
|
||||
async def create_user_handler(
|
||||
username: str = Form(...), email: str = Form(...), password: str = Form(...)
|
||||
) -> BugoutUser:
|
||||
try:
|
||||
user: BugoutUser = bc.create_user(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
application_id=MOONSTREAM_APPLICATION_ID,
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/", tags=["users"], response_model=BugoutUser)
|
||||
async def get_user_handler(request: Request) -> BugoutUser:
|
||||
user: BugoutUser = request.state.user
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/password/restore", tags=["users"], response_model=Dict[str, Any])
|
||||
async def restore_password_handler(request: Request) -> Dict[str, Any]:
|
||||
user = request.state.user
|
||||
try:
|
||||
response = bc.restore_password(email=user.email)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/password/reset", tags=["users"], response_model=BugoutUser)
|
||||
async def reset_password_handler(
|
||||
reset_id: str = Form(...), new_password: str = Form(...)
|
||||
) -> BugoutUser:
|
||||
try:
|
||||
response = bc.reset_password(reset_id=reset_id, new_password=new_password)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/password/change", tags=["users"], response_model=BugoutUser)
|
||||
async def change_password_handler(
|
||||
request: Request, current_password: str = Form(...), new_password: str = Form(...)
|
||||
) -> BugoutUser:
|
||||
token = request.state.token
|
||||
try:
|
||||
user = bc.change_password(
|
||||
token=token, current_password=current_password, new_password=new_password
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return user
|
||||
|
||||
|
||||
@app.delete("/", tags=["users"], response_model=BugoutUser)
|
||||
async def delete_user_handler(
|
||||
request: Request, password: str = Form(...)
|
||||
) -> BugoutUser:
|
||||
user = request.state.user
|
||||
token = request.state.token
|
||||
try:
|
||||
user = bc.delete_user(token=token, user_id=user.id, password=password)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/token", tags=["tokens"], response_model=BugoutToken)
|
||||
async def login_handler(
|
||||
username: str = Form(...), password: str = Form(...)
|
||||
) -> BugoutToken:
|
||||
try:
|
||||
token: BugoutToken = bc.create_token(
|
||||
username=username,
|
||||
password=password,
|
||||
application_id=MOONSTREAM_APPLICATION_ID,
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return token
|
||||
|
||||
|
||||
@app.delete("/token", tags=["tokens"], response_model=uuid.UUID)
|
||||
async def logout_handler(request: Request) -> uuid.UUID:
|
||||
token = request.state.token
|
||||
try:
|
||||
token_id: uuid.UUID = bc.revoke_token(token=token)
|
||||
except BugoutResponseException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500)
|
||||
return token_id
|
|
@ -1,12 +1,25 @@
|
|||
import os
|
||||
|
||||
from bugout.app import Bugout
|
||||
|
||||
# Bugout
|
||||
bugout_client = Bugout()
|
||||
|
||||
MOONSTREAM_APPLICATION_ID = os.environ.get("MOONSTREAM_APPLICATION_ID")
|
||||
if MOONSTREAM_APPLICATION_ID is None:
|
||||
raise ValueError("MOONSTREAM_APPLICATION_ID environment variable must be set")
|
||||
|
||||
MOONSTREAM_DATA_JOURNAL_ID = os.environ.get("MOONSTREAM_DATA_JOURNAL_ID")
|
||||
if MOONSTREAM_DATA_JOURNAL_ID is None:
|
||||
raise ValueError("MOONSTREAM_DATA_JOURNAL_ID environment variable must be set")
|
||||
|
||||
# Origin
|
||||
RAW_ORIGIN = os.environ.get("MOONSTREAM_CORS_ALLOWED_ORIGINS")
|
||||
if RAW_ORIGIN is None:
|
||||
RAW_ORIGINS = os.environ.get("MOONSTREAM_CORS_ALLOWED_ORIGINS")
|
||||
if RAW_ORIGINS is None:
|
||||
raise ValueError(
|
||||
"MOONSTREAM_CORS_ALLOWED_ORIGINS environment variable must be set (comma-separated list of CORS allowed origins"
|
||||
"MOONSTREAM_CORS_ALLOWED_ORIGINS environment variable must be set (comma-separated list of CORS allowed origins)"
|
||||
)
|
||||
ORIGINS = RAW_ORIGIN.split(",")
|
||||
ORIGINS = RAW_ORIGINS.split(",")
|
||||
|
||||
# OpenAPI
|
||||
DOCS_TARGET_PATH = "docs"
|
||||
|
@ -14,3 +27,8 @@ MOONSTREAM_OPENAPI_LIST = []
|
|||
MOONSTREAM_OPENAPI_LIST_RAW = os.environ.get("MOONSTREAM_OPENAPI_LIST")
|
||||
if MOONSTREAM_OPENAPI_LIST_RAW is not None:
|
||||
MOONSTREAM_OPENAPI_LIST = MOONSTREAM_OPENAPI_LIST_RAW.split(",")
|
||||
|
||||
DOCS_PATHS = {}
|
||||
for path in MOONSTREAM_OPENAPI_LIST:
|
||||
DOCS_PATHS[f"/{path}/{DOCS_TARGET_PATH}"] = "GET"
|
||||
DOCS_PATHS[f"/{path}/{DOCS_TARGET_PATH}/openapi.json"] = "GET"
|
||||
|
|
|
@ -3,7 +3,7 @@ asgiref==3.4.1
|
|||
black==21.7b0
|
||||
boto3==1.18.1
|
||||
botocore==1.21.1
|
||||
bugout==0.1.12
|
||||
bugout==0.1.14
|
||||
certifi==2021.5.30
|
||||
charset-normalizer==2.0.3
|
||||
click==8.0.1
|
||||
|
@ -14,9 +14,9 @@ jmespath==0.10.0
|
|||
mypy==0.910
|
||||
mypy-extensions==0.4.3
|
||||
pathspec==0.9.0
|
||||
pkg-resources==0.0.0
|
||||
pydantic==1.8.2
|
||||
python-dateutil==2.8.2
|
||||
python-multipart==0.0.5
|
||||
regex==2021.7.6
|
||||
requests==2.26.0
|
||||
s3transfer==0.5.0
|
|
@ -1,2 +1,4 @@
|
|||
export MOONSTREAM_CORS_ALLOWED_ORIGINS="http://localhost:3000,https://moonstream.to"
|
||||
export MOONSTREAM_OPENAPI_LIST=""
|
||||
export MOONSTREAM_OPENAPI_LIST="users,subscriptions"
|
||||
export MOONSTREAM_APPLICATION_ID="<issued_bugout_application_id>"
|
||||
export MOONSTREAM_DATA_JOURNAL_ID="<bugout_journal_id_to_store_blockchain_data>"
|
||||
|
|
Ładowanie…
Reference in New Issue