Skip to content

Commit

Permalink
Merge pull request #261 from GSA/mmeyer/require_jwt_api
Browse files Browse the repository at this point in the history
Secure training certificates
  • Loading branch information
mark-meyer authored Jul 25, 2023
2 parents 1a5a3c3 + f2f2eb3 commit 822832b
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 53 deletions.
29 changes: 21 additions & 8 deletions training-front-end/src/components/CertificateTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
}
const certificates = ref([])
onMounted(async() => {
certificates.value = await fetch(`${api_url}/api/v1/certificates/${user.value.id}`).then((r) => r.json())
certificates.value = await fetch(`${api_url}/api/v1/certificates/`, {
method: 'GET',
headers: {'Authorization': `Bearer ${user.value.jwt}`}
}
).then((r) => r.json())
})
const data_format = { year:"numeric", month:"long", day:"numeric"}
Expand Down Expand Up @@ -69,13 +73,22 @@
{{ formatted_date(cert.completion_date) }}
</td>
<td>
<a
:href="`${api_url}/api/v1/certificate/${cert.id}`"
class="usa-button usa-button--unstyled"
download="sample.pdf"
<form
:action="`${api_url}/api/v1/certificate/${cert.id}`"
method="post"
>
<FileDownLoad /> Download
</a>
<input
type="hidden"
name="jwtToken"
:value="user.jwt"
>
<button
class="usa-button usa-button--unstyled"
type="submit"
>
<FileDownLoad /> Download
</button>
</form>
</td>
</tr>
</tbody>
Expand Down
30 changes: 23 additions & 7 deletions training-front-end/src/components/QuizResults.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
import ErrorIcon from './icons/ErrorIcon.vue';
import QuizResult from './QuizResult.vue';
import USWDS from "@uswds/uswds/js";
import FileDownLoad from "./icons/FileDownload.vue"
import { useStore } from '@nanostores/vue'
import { profile} from '../stores/user'
const user = useStore(profile)
const { accordion } = USWDS;
const api_base = import.meta.env.PUBLIC_API_BASE_URL
Expand Down Expand Up @@ -47,13 +54,22 @@
<p>
You got <b>{{ result_string }}</b> questions correct, for a total score of <b>{{ percentage }}%</b>, which meets the 75% or higher requirement to pass.
</p>
<a
:href="quiz_certificate_url"
download="sample.pdf"
class="usa-button usa-button--outline margin-bottom-3"
>
Download your certificate of completion
</a>
<form
:action=quiz_certificate_url

Check warning on line 58 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 8 spaces but found 12 spaces

Check warning on line 58 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected to be enclosed by double quotes
method="post"

Check warning on line 59 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 8 spaces but found 12 spaces
>

Check warning on line 60 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 6 spaces but found 10 spaces
<input

Check warning on line 61 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 8 spaces but found 12 spaces
type="hidden"

Check warning on line 62 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 10 spaces but found 14 spaces
name="jwtToken"

Check warning on line 63 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 10 spaces but found 14 spaces
:value="user.jwt"

Check warning on line 64 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 10 spaces but found 14 spaces
>

Check warning on line 65 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 8 spaces but found 12 spaces
<button

Check warning on line 66 in training-front-end/src/components/QuizResults.vue

View workflow job for this annotation

GitHub Actions / unit-tests-and-lint

Expected indentation of 8 spaces but found 12 spaces
class="usa-button usa-button--outline margin-bottom-3"
type="submit"
>
<FileDownLoad /> Download your certificate of completion
</button>
</form>
</div>
<div v-else>
<div class="usa-prose">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ describe('CertificateTable', async () => {
const rows = wrapper.findAll('tr')
expect(rows.length).toBe(3)

const anchorOne = rows[1].find('a')
expect(anchorOne.attributes('href')).toBe("http://localhost:8000/api/v1/certificate/2")
const anchorOne = rows[1].find('form')
expect(anchorOne.attributes('action')).toBe("http://localhost:8000/api/v1/certificate/2")

const anchorTwo = rows[2].find('a')
expect(anchorTwo.attributes('href')).toBe("http://localhost:8000/api/v1/certificate/68")
const anchorTwo = rows[2].find('form')
expect(anchorTwo.attributes('action')).toBe("http://localhost:8000/api/v1/certificate/68")
})

it('show correct message when the user has not taken a quiz', async () => {
Expand Down
24 changes: 17 additions & 7 deletions training/api/api_v1/certificates.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
from typing import List
from typing import List, Any
from fastapi import APIRouter, status, HTTPException, Depends, Response
from training.schemas import UserCertificate
from training.repositories import CertificateRepository
from training.api.deps import certificate_repository
from training.services.certificate import Certificate
from training.api.auth import JWTUser, user_from_form


router = APIRouter()


@router.get("/certificates/{user_id}", response_model=List[UserCertificate])
def get_certificates_by_userid(user_id: int, repo: CertificateRepository = Depends(certificate_repository)):
db_user_certificates = repo.get_certificates_by_userid(user_id)
@router.get("/certificates/", response_model=List[UserCertificate])
def get_certificates_by_userid(
repo: CertificateRepository = Depends(certificate_repository),
user: dict[str, Any] = Depends(JWTUser())
):
db_user_certificates = repo.get_certificates_by_userid(user["id"])
if db_user_certificates is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return db_user_certificates


@router.get("/certificate/{id}", response_model=UserCertificate)
@router.post("/certificate/{id}", response_model=UserCertificate)
def get_certificate_by_id(
id: int,
repo: CertificateRepository = Depends(certificate_repository),
certificate: Certificate = Depends(Certificate)
certificate: Certificate = Depends(Certificate),
user=Depends(user_from_form)
):

db_user_certificate = repo.get_certificate_by_id(id)

if db_user_certificate is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

if db_user_certificate.user_id != user["id"]:
raise HTTPException(status_code=401, detail="Not Authorized")

pdf_bytes = certificate.generate_pdf(
db_user_certificate.quiz_name,
db_user_certificate.user_name,
Expand Down
5 changes: 3 additions & 2 deletions training/api/api_v1/quizzes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
from fastapi import APIRouter, status, HTTPException, Depends
from training.api.auth import JWTUser
from training.errors import IncompleteQuizResponseError, QuizNotFoundError
from training.schemas import Quiz, QuizPublic, QuizGrade, QuizSubmission, QuizCreate
from training.schemas import QuizPublic, QuizGrade, QuizSubmission # , Quiz, QuizCreate
from training.repositories import QuizRepository
from training.services import QuizService
from training.api.deps import quiz_repository, quiz_service


router = APIRouter()


''' Disabling for now
@router.post("/quizzes", response_model=Quiz, status_code=status.HTTP_201_CREATED)
def create_quiz(quiz: QuizCreate, repo: QuizRepository = Depends(quiz_repository)):
db_quiz = repo.create(quiz)
return db_quiz
'''


@router.get("/quizzes", response_model=list[QuizPublic])
Expand Down
8 changes: 0 additions & 8 deletions training/api/api_v1/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ def get_users(
return repo.find_all()


@router.get("/users/{id}", response_model=User)
def get_user(id: int, repo: UserRepository = Depends(user_repository)):
db_user = repo.find_by_id(id)
if db_user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return db_user


@router.put("/users/edit-user-for-reporting", response_model=User)
def edit_user_by_id(
user_id: int,
Expand Down
3 changes: 3 additions & 0 deletions training/tests/test_api_quizzes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from fastapi.testclient import TestClient
from fastapi import status
from training.errors import IncompleteQuizResponseError, QuizNotFoundError
Expand All @@ -11,6 +12,7 @@
client = TestClient(app)


@pytest.mark.skip(reason="quiz creation disabled for now")
def test_create_quiz_valid(
valid_quiz_create: QuizCreate,
mock_quiz_repo: QuizRepository
Expand All @@ -23,6 +25,7 @@ def test_create_quiz_valid(
assert response.status_code == status.HTTP_201_CREATED


@pytest.mark.skip(reason="quiz creation disabled for now")
def test_create_quiz_invalid():
quiz_create = QuizCreateSchemaFactory.build()
quiz_create.audience = "Invalid" # type: ignore
Expand Down
17 changes: 0 additions & 17 deletions training/tests/test_api_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,6 @@ def test_get_users_by_agency(goodJWT, mock_user_repo: UserRepository):
assert len(response.json()) == 5


def test_get_user(mock_user_repo: UserRepository):
user = UserSchemaFactory.build(id=1)
mock_user_repo.find_by_id.return_value = user
response = client.get(
"/api/v1/users/1"
)
assert response.status_code == status.HTTP_200_OK


def test_get_user_invalid_id(mock_user_repo: UserRepository):
mock_user_repo.find_by_id.return_value = None
response = client.get(
"/api/v1/users/1"
)
assert response.status_code == status.HTTP_404_NOT_FOUND


@patch('training.config.settings', 'JWT_SECRET', 'super_secret')
def test_search_users_by_name(goodJWT, mock_user_repo: UserRepository):
users = [UserSchemaFactory.build(name="test name") for x in range(2)]
Expand Down

0 comments on commit 822832b

Please sign in to comment.