From 87a315f340641dba5d94f04706b9dbc6ec83765b Mon Sep 17 00:00:00 2001 From: Cathy Hu Date: Sat, 25 Jul 2020 13:38:10 +0200 Subject: [PATCH] [frontend] Add openapi generator and annotate OpenAPI in backend --- kibicara-frontend/package-lock.json | 5 ++ kibicara-frontend/package.json | 1 + kibicara-frontend/src/app/app.module.ts | 13 +-- .../src/environments/environment.ts | 3 +- kibicara/platforms/email/webapi.py | 83 ++++++++++++++++--- kibicara/platforms/telegram/webapi.py | 51 ++++++++++-- kibicara/platforms/twitter/webapi.py | 53 ++++++++++-- kibicara/webapi/admin.py | 30 +++++-- kibicara/webapi/hoods/__init__.py | 27 ++++-- kibicara/webapi/hoods/badwords.py | 31 +++++-- kibicara/webapi/hoods/triggers.py | 31 +++++-- 11 files changed, 268 insertions(+), 60 deletions(-) diff --git a/kibicara-frontend/package-lock.json b/kibicara-frontend/package-lock.json index 579b751..bef8189 100644 --- a/kibicara-frontend/package-lock.json +++ b/kibicara-frontend/package-lock.json @@ -1623,6 +1623,11 @@ } } }, + "@openapitools/openapi-generator-cli": { + "version": "1.0.15-4.3.1", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-1.0.15-4.3.1.tgz", + "integrity": "sha512-U+sanspDmeBElVNjYHQ4U7BbSEJUQzjNKmiTzXpcEw/r93sgxmzS2Sew5t+Zj6kyN1YTvjhRjJikNcW9/bmTKA==" + }, "@schematics/angular": { "version": "9.1.12", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-9.1.12.tgz", diff --git a/kibicara-frontend/package.json b/kibicara-frontend/package.json index ba3fe30..c7fbb23 100644 --- a/kibicara-frontend/package.json +++ b/kibicara-frontend/package.json @@ -19,6 +19,7 @@ "@angular/platform-browser": "~9.1.4", "@angular/platform-browser-dynamic": "~9.1.4", "@angular/router": "~9.1.4", + "@openapitools/openapi-generator-cli": "^1.0.15-4.3.1", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/kibicara-frontend/src/app/app.module.ts b/kibicara-frontend/src/app/app.module.ts index 2c3ba29..4fac076 100644 --- a/kibicara-frontend/src/app/app.module.ts +++ b/kibicara-frontend/src/app/app.module.ts @@ -5,14 +5,9 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - AppRoutingModule - ], + declarations: [AppComponent], + imports: [BrowserModule, AppRoutingModule], providers: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/kibicara-frontend/src/environments/environment.ts b/kibicara-frontend/src/environments/environment.ts index 7b4f817..66485f9 100644 --- a/kibicara-frontend/src/environments/environment.ts +++ b/kibicara-frontend/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + backendUrl: 'http://localhost:8000/api', }; /* diff --git a/kibicara/platforms/email/webapi.py b/kibicara/platforms/email/webapi.py index 25bacaa..e89edae 100644 --- a/kibicara/platforms/email/webapi.py +++ b/kibicara/platforms/email/webapi.py @@ -70,12 +70,21 @@ async def get_subscriber(subscriber_id: int, hood=Depends(get_hood)): router = APIRouter() -@router.get('/') +@router.get( + '/', + # TODO response_model + operation_id='get_all', +) async def email_read_all(hood=Depends(get_hood)): return await Email.objects.filter(hood=hood).all() -@router.post('/', status_code=status.HTTP_201_CREATED) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + # TODO response_model + operation_id='create', +) async def email_create(values: BodyEmail, response: Response, hood=Depends(get_hood)): """ Create an Email bot. Call this when creating a hood. @@ -92,31 +101,52 @@ async def email_create(values: BodyEmail, response: Response, hood=Depends(get_h raise HTTPException(status_code=status.HTTP_409_CONFLICT) -@router.get('/status', status_code=status.HTTP_200_OK) +@router.get( + '/status', + status_code=status.HTTP_200_OK, + # TODO response_model + operation_id='status', +) async def email_status(hood=Depends(get_hood)): return {'status': spawner.get(hood).status.name} -@router.post('/start', status_code=status.HTTP_200_OK) +@router.post( + '/start', + status_code=status.HTTP_200_OK, + # TODO response_model + operation_id='start', +) async def email_start(hood=Depends(get_hood)): await hood.update(email_enabled=True) spawner.get(hood).start() return {} -@router.post('/stop', status_code=status.HTTP_200_OK) +@router.post( + '/stop', + status_code=status.HTTP_200_OK, + # TODO response_model + operation_id='stop', +) async def email_stop(hood=Depends(get_hood)): await hood.update(email_enabled=False) spawner.get(hood).stop() return {} -@router.get('/{email_id}') +@router.get( + '/{email_id}', + # TODO response_model + operation_id='get', +) async def email_read(email=Depends(get_email)): return email -@router.delete('/{email_id}', status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + '/{email_id}', status_code=status.HTTP_204_NO_CONTENT, operation_id='delete' +) async def email_delete(email=Depends(get_email)): """ Delete an Email bot. Stops and deletes the Email bot. @@ -126,7 +156,12 @@ async def email_delete(email=Depends(get_email)): await email.delete() -@router.post('/subscribe/', status_code=status.HTTP_202_ACCEPTED) +@router.post( + '/subscribe/', + status_code=status.HTTP_202_ACCEPTED, + operation_id='subscribe', + response_model=BaseModel, +) async def email_subscribe( subscriber: BodySubscriber, hood=Depends(get_hood_unauthorized) ): @@ -160,7 +195,12 @@ async def email_subscribe( raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY) -@router.post('/subscribe/confirm/{token}', status_code=status.HTTP_201_CREATED) +@router.post( + '/subscribe/confirm/{token}', + status_code=status.HTTP_201_CREATED, + operation_id='confirm_subscriber', + response_model=BaseModel, +) async def email_subscribe_confirm(token, hood=Depends(get_hood_unauthorized)): """ Confirm a new subscriber and add them to the database. @@ -179,7 +219,11 @@ async def email_subscribe_confirm(token, hood=Depends(get_hood_unauthorized)): raise HTTPException(status_code=status.HTTP_409_CONFLICT) -@router.delete('/unsubscribe/{token}', status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + '/unsubscribe/{token}', + status_code=status.HTTP_204_NO_CONTENT, + operation_id='unsubscribe', +) async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)): """ Remove a subscriber from the database when they click on an unsubscribe link. @@ -197,17 +241,30 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)): await subscriber.delete() -@router.get('/subscribers/') +@router.get( + '/subscribers/', + # TODO response_model + operation_id='get_subscribers', +) async def subscribers_read_all(hood=Depends(get_hood)): return await EmailSubscribers.objects.filter(hood=hood).all() -@router.get('/subscribers/{subscriber_id}') +@router.get( + '/subscribers/{subscriber_id}', + # TODO response_model + operation_id='get_subscribers', +) async def subscribers_read(subscriber=Depends(get_subscriber)): return subscriber -@router.post('/messages/', status_code=status.HTTP_201_CREATED) +@router.post( + '/messages/', + status_code=status.HTTP_201_CREATED, + # TODO response_model + operation_id='create_email', +) async def email_message_create( message: BodyMessage, hood=Depends(get_hood_unauthorized) ): diff --git a/kibicara/platforms/telegram/webapi.py b/kibicara/platforms/telegram/webapi.py index e7e7dc3..b5d42a5 100644 --- a/kibicara/platforms/telegram/webapi.py +++ b/kibicara/platforms/telegram/webapi.py @@ -41,23 +41,38 @@ router = APIRouter() telegram_callback_router = APIRouter() -@router.get('/') +@router.get( + '/', + # TODO response_model, + operation_id='get_all', +) async def telegram_read_all(hood=Depends(get_hood)): return await Telegram.objects.filter(hood=hood).all() -@router.get('/{telegram_id}') +@router.get( + '/{telegram_id}', + # TODO response_model, + operation_id='get', +) async def telegram_read(telegram=Depends(get_telegram)): return telegram -@router.delete('/{telegram_id}', status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + '/{telegram_id}', status_code=status.HTTP_204_NO_CONTENT, operation_id='delete' +) async def telegram_delete(telegram=Depends(get_telegram)): spawner.stop(telegram) await telegram.delete() -@router.post('/', status_code=status.HTTP_201_CREATED) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + # TODO response_model, + operation_id='create', +) async def telegram_create( response: Response, values: BodyTelegram, hood=Depends(get_hood) ): @@ -70,7 +85,12 @@ async def telegram_create( raise HTTPException(status_code=status.HTTP_409_CONFLICT) -@router.put('/{telegram_id}', status_code=status.HTTP_202_ACCEPTED) +@router.put( + '/{telegram_id}', + status_code=status.HTTP_202_ACCEPTED, + # TODO response_model, + operation_id='update', +) async def telegram_update(values: BodyTelegram, telegram=Depends(get_telegram)): try: spawner.stop(telegram) @@ -81,19 +101,34 @@ async def telegram_update(values: BodyTelegram, telegram=Depends(get_telegram)): raise HTTPException(status_code=status.HTTP_409_CONFLICT) -@router.get('/{telegram_id}/status', status_code=status.HTTP_200_OK) +@router.get( + '/{telegram_id}/status', + status_code=status.HTTP_200_OK, + # TODO response_model, + operation_id='status', +) async def telegram_status(telegram=Depends(get_telegram)): return {'status': spawner.get(telegram).status.name} -@router.post('/{telegram_id}/start', status_code=status.HTTP_200_OK) +@router.post( + '/{telegram_id}/start', + status_code=status.HTTP_200_OK, + # TODO response_model, + operation_id='start', +) async def telegram_start(telegram=Depends(get_telegram)): await telegram.update(enabled=True) spawner.get(telegram).start() return {} -@router.post('/{telegram_id}/stop', status_code=status.HTTP_200_OK) +@router.post( + '/{telegram_id}/stop', + status_code=status.HTTP_200_OK, + # TODO response_model, + operation_id='stop', +) async def telegram_stop(telegram=Depends(get_telegram)): await telegram.update(enabled=False) spawner.get(telegram).stop() diff --git a/kibicara/platforms/twitter/webapi.py b/kibicara/platforms/twitter/webapi.py index 233a66c..76c8eba 100644 --- a/kibicara/platforms/twitter/webapi.py +++ b/kibicara/platforms/twitter/webapi.py @@ -28,42 +28,75 @@ router = APIRouter() twitter_callback_router = APIRouter() -@router.get('/') +@router.get( + '/', + # TODO response_model, + operation_id='get_all', +) async def twitter_read_all(hood=Depends(get_hood)): return await Twitter.objects.filter(hood=hood).all() -@router.get('/{twitter_id}') +@router.get( + '/{twitter_id}', + # TODO response_model + operation_id='get', +) async def twitter_read(twitter=Depends(get_twitter)): return twitter -@router.delete('/{twitter_id}', status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + '/{twitter_id}', + status_code=status.HTTP_204_NO_CONTENT, + # TODO response_model + operation_id='delete', +) async def twitter_delete(twitter=Depends(get_twitter)): spawner.stop(twitter) await twitter.delete() -@router.get('/{twitter_id}/status', status_code=status.HTTP_200_OK) +@router.get( + '/{twitter_id}/status', + status_code=status.HTTP_200_OK, + # TODO response_model + operation_id='status', +) async def twitter_status(twitter=Depends(get_twitter)): return {'status': spawner.get(twitter).status.name} -@router.post('/{twitter_id}/start', status_code=status.HTTP_200_OK) +@router.post( + '/{twitter_id}/start', + status_code=status.HTTP_200_OK, + # TODO response_model + operation_id='start', +) async def twitter_start(twitter=Depends(get_twitter)): await twitter.update(enabled=True) spawner.get(twitter).start() return {} -@router.post('/{twitter_id}/stop', status_code=status.HTTP_200_OK) +@router.post( + '/{twitter_id}/stop', + status_code=status.HTTP_200_OK, + # TODO response_model + operation_id='stop', +) async def twitter_stop(twitter=Depends(get_twitter)): await twitter.update(enabled=False) spawner.get(twitter).stop() return {} -@router.post('/', status_code=status.HTTP_201_CREATED) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + # TODO response_model + operation_id='create', +) async def twitter_create(response: Response, hood=Depends(get_hood)): """ `https://api.twitter.com/oauth/authorize?oauth_token=` @@ -89,7 +122,11 @@ async def twitter_create(response: Response, hood=Depends(get_hood)): raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) -@twitter_callback_router.get('/callback') +@twitter_callback_router.get( + '/callback', + # TODO response_model + operation_id='callback', +) async def twitter_read_callback(oauth_token: str, oauth_verifier: str): try: twitter = await Twitter.objects.filter(access_token=oauth_token).get() diff --git a/kibicara/webapi/admin.py b/kibicara/webapi/admin.py index af8ddd0..9f34201 100644 --- a/kibicara/webapi/admin.py +++ b/kibicara/webapi/admin.py @@ -31,6 +31,11 @@ class BodyAdmin(BaseModel): password: str +class BodyAccessToken(BaseModel): + access_token: str + token_type: str = 'bearer' + + oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/api/admin/login') secret_box = SecretBox(random(SecretBox.KEY_SIZE)) @@ -72,7 +77,12 @@ async def get_admin(access_token=Depends(oauth2_scheme)): router = APIRouter() -@router.post('/register/', status_code=status.HTTP_202_ACCEPTED) +@router.post( + '/register/', + status_code=status.HTTP_202_ACCEPTED, + response_model=BaseModel, + operation_id='register', +) async def admin_register(values: BodyAdmin): """ Sends an email with a confirmation link. @@ -100,7 +110,9 @@ async def admin_register(values: BodyAdmin): return {} -@router.post('/confirm/{register_token}') +@router.post( + '/confirm/{register_token}', response_model=BodyAccessToken, operation_id='confirm', +) async def admin_confirm(register_token: str): """ Registration confirmation and account creation. @@ -110,12 +122,14 @@ async def admin_confirm(register_token: str): 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'} + return BodyAccessToken(access_token=register_token) except IntegrityError: raise HTTPException(status_code=status.HTTP_409_CONFLICT) -@router.post('/login/') +@router.post( + '/login/', response_model=BodyAccessToken, operation_id='login', +) async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()): """ Get an access token. @@ -130,10 +144,14 @@ async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()): detail='Incorrect email or password', ) token = to_token(email=form_data.username, password=form_data.password) - return {'access_token': token, 'token_type': 'bearer'} + return BodyAccessToken(access_token=token) -@router.get('/hoods/') +@router.get( + '/hoods/', + # TODO response_model, + operation_id='get_hoods', +) async def admin_hood_read_all(admin=Depends(get_admin)): """ Get a list of all hoods of a given admin. """ return ( diff --git a/kibicara/webapi/hoods/__init__.py b/kibicara/webapi/hoods/__init__.py index a5e2059..d09eee4 100644 --- a/kibicara/webapi/hoods/__init__.py +++ b/kibicara/webapi/hoods/__init__.py @@ -43,13 +43,22 @@ async def get_hood(hood=Depends(get_hood_unauthorized), admin=Depends(get_admin) router = APIRouter() -@router.get('/') +@router.get( + '/', + # TODO response_model, + operation_id='get_all', +) async def hood_read_all(): """ Get all existing hoods. """ return await Hood.objects.all() -@router.post('/', status_code=status.HTTP_201_CREATED) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + # TODO response_model, + operation_id='create_hood', +) async def hood_create(values: BodyHood, response: Response, admin=Depends(get_admin)): """ Creates a hood. @@ -66,13 +75,19 @@ async def hood_create(values: BodyHood, response: Response, admin=Depends(get_ad raise HTTPException(status_code=status.HTTP_409_CONFLICT) -@router.get('/{hood_id}') +@router.get( + '/{hood_id}', + # TODO response_model, + operation_id='get_hood', +) async def hood_read(hood=Depends(get_hood)): """ Get hood with id **hood_id**. """ return hood -@router.put('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT) +@router.put( + '/{hood_id}', status_code=status.HTTP_204_NO_CONTENT, operation_id='update_hood', +) async def hood_update(values: BodyHood, hood=Depends(get_hood)): """ Updates hood with id **hood_id**. @@ -82,7 +97,9 @@ 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) +@router.delete( + '/{hood_id}', status_code=status.HTTP_204_NO_CONTENT, operation_id='update_hood', +) async def hood_delete(hood=Depends(get_hood)): """ Deletes hood with id **hood_id**. """ for relation in await AdminHoodRelation.objects.filter(hood=hood).all(): diff --git a/kibicara/webapi/hoods/badwords.py b/kibicara/webapi/hoods/badwords.py index 2ac4750..d7bc539 100644 --- a/kibicara/webapi/hoods/badwords.py +++ b/kibicara/webapi/hoods/badwords.py @@ -33,13 +33,22 @@ async def get_badword(badword_id: int, hood=Depends(get_hood)): router = APIRouter() -@router.get('/') +@router.get( + '/', + # TODO response_model, + operation_id='get_badwords', +) async def badword_read_all(hood=Depends(get_hood)): """ Get all badwords of hood with id **hood_id**. """ return await BadWord.objects.filter(hood=hood).all() -@router.post('/', status_code=status.HTTP_201_CREATED) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + # TODO response_model, + operation_id='create_badword', +) async def badword_create( values: BodyBadWord, response: Response, hood=Depends(get_hood) ): @@ -58,13 +67,21 @@ async def badword_create( raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) -@router.get('/{badword_id}') +@router.get( + '/{badword_id}', + # TODO response_model, + operation_id='get_badword', +) async def badword_read(badword=Depends(get_badword)): """ Reads badword with id **badword_id** for hood with id **hood_id**. """ return badword -@router.put('/{badword_id}', status_code=status.HTTP_204_NO_CONTENT) +@router.put( + '/{badword_id}', + status_code=status.HTTP_204_NO_CONTENT, + operation_id='update_badword', +) async def badword_update(values: BodyBadWord, badword=Depends(get_badword)): """ Updates badword with id **badword_id** for hood with id **hood_id**. @@ -73,7 +90,11 @@ 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) +@router.delete( + '/{badword_id}', + status_code=status.HTTP_204_NO_CONTENT, + operation_id='delete_badword', +) async def badword_delete(badword=Depends(get_badword)): """ Deletes badword with id **badword_id** for hood with id **hood_id**. """ await badword.delete() diff --git a/kibicara/webapi/hoods/triggers.py b/kibicara/webapi/hoods/triggers.py index 69d8199..cdd71b0 100644 --- a/kibicara/webapi/hoods/triggers.py +++ b/kibicara/webapi/hoods/triggers.py @@ -34,13 +34,22 @@ async def get_trigger(trigger_id: int, hood=Depends(get_hood)): router = APIRouter() -@router.get('/') +@router.get( + '/', + # TODO response_model, + operation_id='get_triggers', +) async def trigger_read_all(hood=Depends(get_hood)): """ Get all triggers of hood with id **hood_id**. """ return await Trigger.objects.filter(hood=hood).all() -@router.post('/', status_code=status.HTTP_201_CREATED) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + # TODO response_model, + operation_id='create_trigger', +) async def trigger_create( values: BodyTrigger, response: Response, hood=Depends(get_hood) ): @@ -59,13 +68,21 @@ async def trigger_create( raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) -@router.get('/{trigger_id}') +@router.get( + '/{trigger_id}', + # TODO response_model, + operation_id='get_trigger', +) async def trigger_read(trigger=Depends(get_trigger)): """ Reads trigger with id **trigger_id** for hood with id **hood_id**. """ return trigger -@router.put('/{trigger_id}', status_code=status.HTTP_204_NO_CONTENT) +@router.put( + '/{trigger_id}', + status_code=status.HTTP_204_NO_CONTENT, + operation_id='update_trigger', +) async def trigger_update(values: BodyTrigger, trigger=Depends(get_trigger)): """ Updates trigger with id **trigger_id** for hood with id **hood_id**. @@ -74,7 +91,11 @@ 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) +@router.delete( + '/{trigger_id}', + status_code=status.HTTP_204_NO_CONTENT, + operation_id='delete_trigger', +) async def trigger_delete(trigger=Depends(get_trigger)): """ Deletes trigger with id **trigger_id** for hood with id **hood_id**. """ await trigger.delete()