From 451a53f1f9c155ab00ca69c611ec4c9adcfc39f6 Mon Sep 17 00:00:00 2001 From: omer-bental Date: Wed, 20 Jan 2021 19:47:03 +0200 Subject: [PATCH] WIP feat(api): basic implementation, no docs on front, no tests, no documentation yet --- app/database/database.py | 4 +- app/database/models.py | 9 +++ app/main.py | 4 +- app/routers/api.py | 113 ++++++++++++++++++++++++++++++++++ app/routers/profile.py | 4 +- app/static/apiKeyGenerator.js | 76 +++++++++++++++++++++++ app/static/api_style.css | 8 +++ app/static/popover.js | 16 ++--- app/templates/api_docs.html | 67 ++++++++++++++++++++ 9 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 app/routers/api.py create mode 100644 app/static/apiKeyGenerator.js create mode 100644 app/static/api_style.css create mode 100644 app/templates/api_docs.html diff --git a/app/database/database.py b/app/database/database.py index b89bf6d1..099fe286 100644 --- a/app/database/database.py +++ b/app/database/database.py @@ -8,11 +8,13 @@ SQLALCHEMY_DATABASE_URL = os.getenv( - "DATABASE_CONNECTION_STRING", config.DEVELOPMENT_DATABASE_STRING) + "DATABASE_CONNECTION_STRING2", config.DEVELOPMENT_DATABASE_STRING) +#pool_pre_ping=True for POSTGRES engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} ) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/app/database/models.py b/app/database/models.py index 0c92ae94..b0a3e77b 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -20,6 +20,7 @@ class User(Base): events = relationship( "Event", cascade="all, delete", back_populates="owner") + token = relationship("Token", uselist=False, cascade="all, delete", back_populates="owner") class Event(Base): __tablename__ = "events" @@ -32,3 +33,11 @@ class Event(Base): owner_id = Column(Integer, ForeignKey("users.id")) owner = relationship("User", back_populates="events") + + +class Token(Base): + __tablename__ = "tokens" + + id = Column(String, primary_key=True, index=True) + owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True) + owner = relationship("User", back_populates="token") \ No newline at end of file diff --git a/app/main.py b/app/main.py index b48bdae5..aa605936 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from app.database.database import engine from app.dependencies import ( MEDIA_PATH, STATIC_PATH, templates) -from app.routers import agenda, event, profile +from app.routers import agenda, api, event, profile models.Base.metadata.create_all(bind=engine) @@ -14,6 +14,8 @@ app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static") app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media") +app.include_router(api.router) +app.include_router(api.key_gen_router) app.include_router(profile.router) app.include_router(event.router) app.include_router(agenda.router) diff --git a/app/routers/api.py b/app/routers/api.py new file mode 100644 index 00000000..391dd830 --- /dev/null +++ b/app/routers/api.py @@ -0,0 +1,113 @@ +import datetime +import secrets + +from app.database.database import get_db +from app.database.models import Event, Token, User +from app.dependencies import templates +from fastapi import APIRouter, Body, Depends, Request, HTTPException +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from sqlalchemy import and_, or_ +from typing import Optional + + +def check_api_key(key: str, session=Depends(get_db)): + if session.query(Token).filter_by(id=key).first() is None: + raise HTTPException(status_code=400, detail="Token invalid") + + +router = APIRouter( + prefix='/api', + tags=['api'], + dependencies=[Depends(check_api_key)], + responses={404: {"description": "Not found"}}, +) + +key_gen_router = APIRouter( + prefix='/api', + tags=['api_key_generator'], + responses={404: {"description": "Not found"}}, +) + + +@key_gen_router.get("/docs") +async def serve_api_docs( + request: Request, session=Depends(get_db)): + + user = session.query(User).filter_by(id=1).first() + + api_routes = [{'name': '/new_event',}, {'name': '/{date}'}] + api_key = session.query(Token).filter_by(owner_id=user.id).first() + session.close() + no_api_key = True + if api_key is not None: + no_api_key = False + api_key = api_key.id + return templates.TemplateResponse("api_docs.html", { + "request": request, + "user": user, + "routes": api_routes, + "no_api_key": no_api_key, + "api_key": api_key or '' + }) + + +@key_gen_router.post('/generate_key') +async def generate_key(request: Request, session=Depends(get_db)): + data = await request.json() + if data.get('refresh', False): + session.query(Token).filter_by(owner_id=data['user']).delete() + token = secrets.token_urlsafe(32) + while session.query(Token).filter_by(id=token).first() is not None: + token = secrets.token_urlsafe(32) + session.add(Token(id=token, owner_id=data['user'])) + session.commit() + session.close() + return JSONResponse(jsonable_encoder({'key': token})) + + +@key_gen_router.post('/delete_key') +async def delete_key(request: Request, session=Depends(get_db)): + data = await request.json() + session.query(Token).filter_by(owner_id=data['user']).delete() + session.commit() + session.close() + return JSONResponse(jsonable_encoder({'success': True})) + + + +@router.get('/get_events') +async def get_events( + request: Request, + key: str, + date: Optional[datetime.date] = datetime.date.today(), + session=Depends(get_db), +): + user = session.query(User).filter(User.token.has(id=key)).first() + events = session.query(Event).filter_by(owner_id=user.id)\ + .filter(Event.start < datetime.datetime(date.year, date.month, date.day + 1, 0, 0, 0), + Event.end > datetime.datetime(date.year, date.month, date.day - 1, 23, 59, 59))\ + .all() + return JSONResponse(jsonable_encoder([{ + key: value for key, value in event.__dict__.items() + } for event in events])) + + +@router.post('/create_event', status_code=201) +async def new_event( + request: Request, + key: str, + title: str = Body(None), + content: str = Body(None), + start_date: datetime.date = Body(None), + end_date: datetime.date = Body(None), + session=Depends(get_db), +): + user = session.query(User).filter(User.token.has(id=key)).first() + event = Event(title=title, content=content, start=start_date, end=end_date, owner_id=user.id) + d = {key: value for key, value in event.__dict__.items()} + session.add(event) + session.commit() + d['id'] = event.id + session.close() + return JSONResponse(jsonable_encoder(d)) \ No newline at end of file diff --git a/app/routers/profile.py b/app/routers/profile.py index 39724939..77223840 100644 --- a/app/routers/profile.py +++ b/app/routers/profile.py @@ -7,7 +7,7 @@ from app import config from app.database.database import get_db -from app.database.models import User +from app.database.models import User, Token from app.dependencies import MEDIA_PATH, templates @@ -143,4 +143,4 @@ def get_image_crop_area(width, height): delta = (width - height) // 2 return (delta, 0, width - delta, height) delta = (height - width) // 2 - return (0, delta, width, width + delta) + return (0, delta, width, width + delta) \ No newline at end of file diff --git a/app/static/apiKeyGenerator.js b/app/static/apiKeyGenerator.js new file mode 100644 index 00000000..f5206131 --- /dev/null +++ b/app/static/apiKeyGenerator.js @@ -0,0 +1,76 @@ +async function callAPIKeyRoute(url = "", data = {}) { + const response = await fetch(url, { + method: "POST", + body: JSON.stringify(data), + }); + return response.json(); +} + +async function buildAPIContent(state) { + callAPIKeyRoute("/api/generate_key", { user: user_id, refresh: state }).then( + (data) => { + let keyText = document.getElementById("apiKeyHolder"); + keyText.textContent = "API Key: " + data.key; + } + ); +} + +async function removeAPIContent() { + callAPIKeyRoute("/api/delete_key", { user: user_id }).then((data) => { + if (data.success) { + let keyText = document.getElementById("apiKeyHolder"); + let refreshButton = document.getElementById("apiKeyRefresh"); + keyText.insertAdjacentHTML( + "beforebegin", + '' + ); + keyText.remove(); + refreshButton.remove(); + buildGenButton(); + } + }); +} + +function activateGenButton(genButton) { + genButton.insertAdjacentHTML( + "afterend", + '

' + ); + buildAPIContent(false).then(genButton.remove()); + buildRefreshButton(); + buildDeleteButton(); +} + +function buildRefreshButton() { + const refreshButton = document.getElementById("apiKeyRefresh"); + refreshButton.addEventListener("click", function () { + buildAPIContent(true); + }); +} + +function buildDeleteButton() { + const delButton = document.getElementById("apiKeyDelete"); + delButton.addEventListener("click", function () { + removeAPIContent(); + delButton.remove(); + }); +} + +function buildGenButton() { + const genButton = document.getElementById("apiKeyGen"); + genButton.addEventListener("click", function () { + activateGenButton(genButton); + }); +} + +if (document.getElementById("apiKeyGen")) { + buildGenButton(); +} + +if (document.getElementById("apiKeyRefresh")) { + buildRefreshButton(); +} + +if (document.getElementById("apiKeyDelete")) { + buildDeleteButton(); +} diff --git a/app/static/api_style.css b/app/static/api_style.css new file mode 100644 index 00000000..9098e56a --- /dev/null +++ b/app/static/api_style.css @@ -0,0 +1,8 @@ +#apiKeyDelete { + font-size: 0.6rem; + margin-left: 20px; +} + +#apiKeyRefresh { + margin-left: 30px; +} diff --git a/app/static/popover.js b/app/static/popover.js index 4243853c..501818e7 100644 --- a/app/static/popover.js +++ b/app/static/popover.js @@ -1,9 +1,9 @@ -// Enable bootstrap popovers +// // Enable bootstrap popovers -var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { - return new bootstrap.Popover(popoverTriggerEl, { - container: 'body', - html: true, - sanitize: false - }) -}); +// var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { +// return new bootstrap.Popover(popoverTriggerEl, { +// container: 'body', +// html: true, +// sanitize: false +// }) +// }); diff --git a/app/templates/api_docs.html b/app/templates/api_docs.html new file mode 100644 index 00000000..49e1dca8 --- /dev/null +++ b/app/templates/api_docs.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} {% block head %} {{ super() }} + +{% endblock %} {% block content %} + +
+
+ +
+ +
+ +
+ {% for route in routes %} + + +
+
+ + Little Header + + + + + + + +
+
+

The Route {{ route.name }}

+
+
+ + + {% endfor %} +
+
+ {% if no_api_key %} + + {% else %} +

API Key: {{ api_key }}

+ + + {% endif %} +
+
+
+
+ + +
+
+
+ + + +{% endblock %}