-
Notifications
You must be signed in to change notification settings - Fork 52
WIP - feature/basic api for app #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,11 +8,13 @@ | |
|
||
|
||
SQLALCHEMY_DATABASE_URL = os.getenv( | ||
"DATABASE_CONNECTION_STRING", config.DEVELOPMENT_DATABASE_STRING) | ||
"DATABASE_CONNECTION_STRING2", config.DEVELOPMENT_DATABASE_STRING) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mistake? |
||
|
||
#pool_pre_ping=True for POSTGRES | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove |
||
engine = create_engine( | ||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} | ||
) | ||
|
||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
|
||
Base = declarative_base() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it could be nice to add a short documentation for this table. |
||
__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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a good practice, we can add |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should implement rate limit to this check |
||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it necessary? The session is being closed at the end of the get_db |
||
no_api_key = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can change these lines to |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great job for using |
||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should use |
||
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})) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too many blank lines :) |
||
|
||
|
||
@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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as @IdanPelled comment |
||
return JSONResponse(jsonable_encoder(d)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can't you use f"string {param}" ? |
||
} | ||
); | ||
} | ||
|
||
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", | ||
'<button id="apiKeyGen" type="button" class="btn btn-primary">Generate API Key</button>' | ||
); | ||
keyText.remove(); | ||
refreshButton.remove(); | ||
buildGenButton(); | ||
} | ||
}); | ||
} | ||
|
||
function activateGenButton(genButton) { | ||
genButton.insertAdjacentHTML( | ||
"afterend", | ||
'<p id="apiKeyHolder" class="m-0"></p><button id="apiKeyRefresh" type="button" class="btn btn-primary">Refresh API Key</button><button id="apiKeyDelete" type="button" class="btn btn-primary">Delete API Key</button>' | ||
); | ||
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(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
#apiKeyDelete { | ||
font-size: 0.6rem; | ||
margin-left: 20px; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer working with em/% |
||
} | ||
|
||
#apiKeyRefresh { | ||
margin-left: 30px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
// Enable bootstrap popovers | ||
// // Enable bootstrap popovers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revisit this file and remove unneeded code. |
||
|
||
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 | ||
// }) | ||
// }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
{% extends "base.html" %} {% block head %} {{ super() }} | ||
<link href="{{ url_for('static', path='/api_style.css') }}" rel="stylesheet" /> | ||
{% endblock %} {% block content %} | ||
|
||
<div class="container mt-4"> | ||
<div class="row"> | ||
<!-- Left side --> | ||
<div class="col-2"></div> | ||
<!-- Center --> | ||
<div class="col-7"> | ||
<!-- Upcoming events --> | ||
<div class="mb-3"> | ||
{% for route in routes %} | ||
|
||
<!-- Events card --> | ||
<div class="card card-event mb-2 pb-0"> | ||
<div class="card-header d-flex justify-content-between"> | ||
<small> | ||
<i>Little Header</i> | ||
</small> | ||
<span> | ||
<!-- Event settings --> | ||
<a class="text-dark" href="#"> | ||
<i class="fas fa-ellipsis-h"></i> | ||
</a> | ||
</span> | ||
</div> | ||
<div class="card-body pb-1"> | ||
<p class="card-text">The Route {{ route.name }}</p> | ||
</div> | ||
</div> | ||
<!-- End Events card --> | ||
|
||
{% endfor %} | ||
<div class="card card-event mb-2 pb-0"> | ||
<div class="card-body mx-auto d-flex align-items-center"> | ||
{% if no_api_key %} | ||
<button id="apiKeyGen" type="button" class="btn btn-primary"> | ||
Generate API Key | ||
</button> | ||
{% else %} | ||
<p id="apiKeyHolder" class="m-0">API Key: {{ api_key }}</p> | ||
<button id="apiKeyRefresh" type="button" class="btn btn-primary"> | ||
Refresh API Key | ||
</button> | ||
<button id="apiKeyDelete" type="button" class="btn btn-primary"> | ||
Delete API Key | ||
</button> | ||
{% endif %} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<!-- Right side --> | ||
<div class="col-3"></div> | ||
</div> | ||
</div> | ||
|
||
<script type="text/javascript"> | ||
var user_id = {{ user.id }}; | ||
</script> | ||
<script | ||
type="text/javascript" | ||
src="{{ url_for('static', path='/apiKeyGenerator.js') }}" | ||
></script> | ||
{% endblock %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great work! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make sure you don't commit this change :)