Merge pull request from bugout-dev/backend-users-tokens

Backend users tokens
pull/15/head
Neeraj Kashyap 2021-07-21 10:02:17 -07:00 zatwierdzone przez GitHub
commit 50952d1c29
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 402 dodań i 37 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
]
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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"

Wyświetl plik

@ -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

Wyświetl plik

@ -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>"