[core] Add core REST API

This commit is contained in:
Cathy Hu 2020-07-01 21:34:16 +02:00
parent 647fe0ef36
commit 54c40e5ee8
5 changed files with 314 additions and 0 deletions

View 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
View 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()

View 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()

View 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()

View 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()