Skip to content

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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@


SQLALCHEMY_DATABASE_URL = os.getenv(
"DATABASE_CONNECTION_STRING", config.DEVELOPMENT_DATABASE_STRING)
"DATABASE_CONNECTION_STRING2", config.DEVELOPMENT_DATABASE_STRING)
Copy link
Member

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mistake?


#pool_pre_ping=True for POSTGRES
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Expand Down
9 changes: 9 additions & 0 deletions app/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -32,3 +33,11 @@ class Event(Base):
owner_id = Column(Integer, ForeignKey("users.id"))

owner = relationship("User", back_populates="events")


class Token(Base):
Copy link
Contributor

Choose a reason for hiding this comment

The 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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a good practice, we can add expiry_date. If you really into it, read about security in APIs: https://owasp.org/www-project-api-security/

4 changes: 3 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
113 changes: 113 additions & 0 deletions app/routers/api.py
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)):
Copy link
Member

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can change these lines to key = getattr(api_key, 'id', None) and remove the no_api_key variable?

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job for using secrets!

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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should use @key_gen_route.delete?

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

Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as @IdanPelled comment

return JSONResponse(jsonable_encoder(d))
4 changes: 2 additions & 2 deletions app/routers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
76 changes: 76 additions & 0 deletions app/static/apiKeyGenerator.js
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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
}
8 changes: 8 additions & 0 deletions app/static/api_style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#apiKeyDelete {
font-size: 0.6rem;
margin-left: 20px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer working with em/%

}

#apiKeyRefresh {
margin-left: 30px;
}
16 changes: 8 additions & 8 deletions app/static/popover.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Enable bootstrap popovers
// // Enable bootstrap popovers
Copy link
Contributor

Choose a reason for hiding this comment

The 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
// })
// });
67 changes: 67 additions & 0 deletions app/templates/api_docs.html
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 %}
Copy link
Contributor

@IdanPelled IdanPelled Jan 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great work!
remember, tests and documentation are essential