[core] Add core REST API
This commit is contained in:
parent
647fe0ef36
commit
54c40e5ee8
20
kibicara/webapi/__init__.py
Normal file
20
kibicara/webapi/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from kibicara.platforms.test.webapi import router as test_router
|
||||||
|
from kibicara.webapi.admin import router as admin_router
|
||||||
|
from kibicara.webapi.hoods import router as hoods_router
|
||||||
|
from kibicara.webapi.hoods.badwords import router as badwords_router
|
||||||
|
from kibicara.webapi.hoods.triggers import router as triggers_router
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
router.include_router(admin_router, prefix='/admin', tags=['admin'])
|
||||||
|
hoods_router.include_router(triggers_router, prefix='/{hood_id}/triggers')
|
||||||
|
hoods_router.include_router(badwords_router, prefix='/{hood_id}/badwords')
|
||||||
|
hoods_router.include_router(
|
||||||
|
test_router, prefix='/{hood_id}/test', tags=['test'])
|
||||||
|
router.include_router(hoods_router, prefix='/hoods', tags=['hoods'])
|
110
kibicara/webapi/admin.py
Normal file
110
kibicara/webapi/admin.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
|
from kibicara.email import send_email
|
||||||
|
from kibicara.model import Admin, AdminHoodRelation
|
||||||
|
from logging import getLogger
|
||||||
|
from nacl.encoding import URLSafeBase64Encoder
|
||||||
|
from nacl.exceptions import CryptoError
|
||||||
|
from nacl.secret import SecretBox
|
||||||
|
from nacl.utils import random
|
||||||
|
from passlib.hash import argon2
|
||||||
|
from ormantic.exceptions import NoMatch
|
||||||
|
from pickle import dumps, loads
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BodyAdmin(BaseModel):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/api/admin/login')
|
||||||
|
secret_box = SecretBox(random(SecretBox.KEY_SIZE))
|
||||||
|
|
||||||
|
|
||||||
|
def to_token(**kwargs):
|
||||||
|
return secret_box.encrypt(dumps(kwargs), encoder=URLSafeBase64Encoder) \
|
||||||
|
.decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def from_token(token):
|
||||||
|
return loads(secret_box.decrypt(
|
||||||
|
token.encode('ascii'),
|
||||||
|
encoder=URLSafeBase64Encoder))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_auth(email, password):
|
||||||
|
try:
|
||||||
|
admin = await Admin.objects.get(email=email)
|
||||||
|
if argon2.verify(password, admin.passhash):
|
||||||
|
return admin
|
||||||
|
raise ValueError
|
||||||
|
except NoMatch:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
async def get_admin(access_token=Depends(oauth2_scheme)):
|
||||||
|
try:
|
||||||
|
admin = await get_auth(**from_token(access_token))
|
||||||
|
except (CryptoError, ValueError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail='Invalid authentication credentials',
|
||||||
|
headers={'WWW-Authenticate': 'Bearer'})
|
||||||
|
return admin
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/register/', status_code=status.HTTP_202_ACCEPTED)
|
||||||
|
async def admin_register(values: BodyAdmin):
|
||||||
|
register_token = to_token(**values.__dict__)
|
||||||
|
logger.debug(register_token)
|
||||||
|
try:
|
||||||
|
send_email(
|
||||||
|
to=values.email, subject='Confirm Account',
|
||||||
|
# XXX create real confirm link
|
||||||
|
body=register_token)
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
logger.exception('Email sending failed')
|
||||||
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/confirm/{register_token}')
|
||||||
|
async def admin_confirm(register_token: str):
|
||||||
|
try:
|
||||||
|
values = from_token(register_token)
|
||||||
|
passhash = argon2.hash(values['password'])
|
||||||
|
await Admin.objects.create(email=values['email'], passhash=passhash)
|
||||||
|
return {'access_token': register_token, 'token_type': 'bearer'}
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/login/')
|
||||||
|
async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
try:
|
||||||
|
await get_auth(form_data.username, form_data.password)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='Incorrect email or password')
|
||||||
|
token = to_token(email=form_data.username, password=form_data.password)
|
||||||
|
return {'access_token': token, 'token_type': 'bearer'}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/hoods/')
|
||||||
|
async def admin_hood_read_all(admin=Depends(get_admin)):
|
||||||
|
return await AdminHoodRelation.objects.select_related('hood') \
|
||||||
|
.filter(admin=admin).all()
|
70
kibicara/webapi/hoods/__init__.py
Normal file
70
kibicara/webapi/hoods/__init__.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
|
from kibicara.model import AdminHoodRelation, Hood
|
||||||
|
from kibicara.webapi.admin import get_admin
|
||||||
|
from ormantic.exceptions import NoMatch
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
|
class BodyHood(BaseModel):
|
||||||
|
name: str
|
||||||
|
landingpage: str = '''
|
||||||
|
Default Landing Page
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
async def get_hood(hood_id: int, admin=Depends(get_admin)):
|
||||||
|
try:
|
||||||
|
hood = await Hood.objects.get(id=hood_id)
|
||||||
|
except NoMatch:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
try:
|
||||||
|
await AdminHoodRelation.objects.get(admin=admin, hood=hood)
|
||||||
|
except NoMatch:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
headers={'WWW-Authenticate': 'Bearer'})
|
||||||
|
return hood
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
|
async def hood_read_all():
|
||||||
|
return await Hood.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/', status_code=status.HTTP_201_CREATED)
|
||||||
|
async def hood_create(
|
||||||
|
values: BodyHood, response: Response,
|
||||||
|
admin=Depends(get_admin)):
|
||||||
|
try:
|
||||||
|
hood = await Hood.objects.create(**values.__dict__)
|
||||||
|
await AdminHoodRelation.objects.create(admin=admin.id, hood=hood.id)
|
||||||
|
response.headers['Location'] = '%d' % hood.id
|
||||||
|
return hood
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{hood_id}')
|
||||||
|
async def hood_read(hood=Depends(get_hood)):
|
||||||
|
return hood
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def hood_update(values: BodyHood, hood=Depends(get_hood)):
|
||||||
|
await hood.update(**values.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def hood_delete(hood=Depends(get_hood)):
|
||||||
|
for relation in await AdminHoodRelation.objects.filter(hood=hood).all():
|
||||||
|
await relation.delete()
|
||||||
|
await hood.delete()
|
57
kibicara/webapi/hoods/badwords.py
Normal file
57
kibicara/webapi/hoods/badwords.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
|
from kibicara.model import BadWord
|
||||||
|
from kibicara.webapi.hoods import get_hood
|
||||||
|
from ormantic.exceptions import NoMatch
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
|
class BodyBadWord(BaseModel):
|
||||||
|
pattern: str
|
||||||
|
|
||||||
|
|
||||||
|
async def get_badword(badword_id: int, hood=Depends(get_hood)):
|
||||||
|
try:
|
||||||
|
return await BadWord.objects.get(id=badword_id, hood=hood)
|
||||||
|
except NoMatch:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
|
async def badword_read_all(hood=Depends(get_hood)):
|
||||||
|
return await BadWord.objects.filter(hood=hood).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/', status_code=status.HTTP_201_CREATED)
|
||||||
|
async def badword_create(
|
||||||
|
values: BodyBadWord, response: Response,
|
||||||
|
hood=Depends(get_hood)):
|
||||||
|
try:
|
||||||
|
badword = await BadWord.objects.create(hood=hood, **values.__dict__)
|
||||||
|
response.headers['Location'] = '%d' % badword.id
|
||||||
|
return badword
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{badword_id}')
|
||||||
|
async def badword_read(badword=Depends(get_badword)):
|
||||||
|
return badword
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/{badword_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def badword_update(values: BodyBadWord, badword=Depends(get_badword)):
|
||||||
|
await badword.update(**values.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete('/{badword_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def badword_delete(badword=Depends(get_badword)):
|
||||||
|
await badword.delete()
|
57
kibicara/webapi/hoods/triggers.py
Normal file
57
kibicara/webapi/hoods/triggers.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
|
from kibicara.model import Trigger
|
||||||
|
from kibicara.webapi.hoods import get_hood
|
||||||
|
from ormantic.exceptions import NoMatch
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
|
class BodyTrigger(BaseModel):
|
||||||
|
pattern: str
|
||||||
|
|
||||||
|
|
||||||
|
async def get_trigger(trigger_id: int, hood=Depends(get_hood)):
|
||||||
|
try:
|
||||||
|
return await Trigger.objects.get(id=trigger_id, hood=hood)
|
||||||
|
except NoMatch:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
|
async def trigger_read_all(hood=Depends(get_hood)):
|
||||||
|
return await Trigger.objects.filter(hood=hood).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/', status_code=status.HTTP_201_CREATED)
|
||||||
|
async def trigger_create(
|
||||||
|
values: BodyTrigger, response: Response,
|
||||||
|
hood=Depends(get_hood)):
|
||||||
|
try:
|
||||||
|
trigger = await Trigger.objects.create(hood=hood, **values.__dict__)
|
||||||
|
response.headers['Location'] = '%d' % trigger.id
|
||||||
|
return trigger
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{trigger_id}')
|
||||||
|
async def trigger_read(trigger=Depends(get_trigger)):
|
||||||
|
return trigger
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/{trigger_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def trigger_update(values: BodyTrigger, trigger=Depends(get_trigger)):
|
||||||
|
await trigger.update(**values.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete('/{trigger_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def trigger_delete(trigger=Depends(get_trigger)):
|
||||||
|
await trigger.delete()
|
Loading…
Reference in a new issue