diff --git a/backend/Platform/middleware.py b/backend/Platform/middleware.py new file mode 100644 index 00000000..001a0216 --- /dev/null +++ b/backend/Platform/middleware.py @@ -0,0 +1,11 @@ +from django.http import HttpResponse + + +class HealthCheckMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.path == "/health": + return HttpResponse("ok") + return self.get_response(request) diff --git a/backend/Platform/urls.py b/backend/Platform/urls.py index 6dbbff85..b93269b4 100644 --- a/backend/Platform/urls.py +++ b/backend/Platform/urls.py @@ -13,6 +13,7 @@ path("accounts/", include("accounts.urls", namespace="oauth2_provider")), path("options/", include("options.urls", namespace="options")), path("identity/", include("identity.urls", namespace="identity")), + path("healthcheck/", include("health.urls", namespace="healthcheck")), path("s/", include("shortener.urls", namespace="shortener")), path( "openapi/", diff --git a/backend/health/__init__.py b/backend/health/__init__.py new file mode 100644 index 00000000..e860540e --- /dev/null +++ b/backend/health/__init__.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class HealthConfig(AppConfig): + name = "health" diff --git a/backend/health/apps.py b/backend/health/apps.py new file mode 100644 index 00000000..e860540e --- /dev/null +++ b/backend/health/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class HealthConfig(AppConfig): + name = "health" diff --git a/backend/health/urls.py b/backend/health/urls.py new file mode 100644 index 00000000..f8e0b527 --- /dev/null +++ b/backend/health/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from health.views import HealthView + + +app_name = "health" + +urlpatterns = [ + path("backend/", HealthView.as_view(), name="backend"), +] diff --git a/backend/health/views.py b/backend/health/views.py new file mode 100644 index 00000000..ba5bb8f2 --- /dev/null +++ b/backend/health/views.py @@ -0,0 +1,25 @@ +from http import HTTPStatus + +from django.http import JsonResponse +from django.views.generic import View + + +class HealthView(View): + def get(self, request): + """ + Health check endpoint to confirm the backend is running. + --- + summary: Health Check + responses: + "200": + content: + application/json: + schema: + type: object + properties: + message: + type: string + enum: ["OK"] + --- + """ + return JsonResponse({"message": "OK"}, status=HTTPStatus.OK) diff --git a/backend/tests/identity/test_views.py b/backend/tests/identity/test_views.py index d7a93dc1..e44e3d4d 100644 --- a/backend/tests/identity/test_views.py +++ b/backend/tests/identity/test_views.py @@ -179,3 +179,14 @@ def test_garbage_bearer(self): self.assertIsInstance(content, dict) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code) self.assertNotIn("access", content) + + +class HealthTestCase(TestCase): + def setUp(self): + self.client = Client() + + def test_health(self): + url = reverse("healthcheck:backend") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), {"message": "OK"}) diff --git a/frontend/components/accounts/modals/verification.tsx b/frontend/components/accounts/modals/verification.tsx index 30ef902a..d260edbd 100644 --- a/frontend/components/accounts/modals/verification.tsx +++ b/frontend/components/accounts/modals/verification.tsx @@ -3,7 +3,6 @@ import { Modal } from "react-bulma-components"; import { mutateResourceListFunction } from "@pennlabs/rest-hooks/dist/types"; import toast from "react-hot-toast"; -import styles from "../../../styles/Verification.module.css"; import { verifyContact } from "../../../data-fetching/accounts"; import { ContactType, ContactInfo } from "../../../types"; import { logException } from "../../../utils/sentry"; @@ -38,9 +37,9 @@ const VerificationForm = (props: VerificationFormProps) => { onChange={handleInputChange} validChars="0-9" classNames={{ - container: styles.container, - character: styles.character, - characterSelected: styles["character--selected"], + container: "verification-modal-container", + character: "verification-modal-character", + characterSelected: "verification-modal-character-selected", }} removeDefaultStyles /> @@ -59,7 +58,7 @@ const VerificationModal = (props: VerificationModalProps) => { const { show, closeFunc, type, contact, id, mutate } = props; const prettyType = type === ContactType.Email ? "Email" : "Phone Number"; return ( - + diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 7b7aa2c7..52e831b4 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,2 +1,5 @@ /// -/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/pages/_app.js b/frontend/pages/_app.js index fb1b7e0b..15c6eb6a 100644 --- a/frontend/pages/_app.js +++ b/frontend/pages/_app.js @@ -1,5 +1,6 @@ import "bulma/css/bulma.min.css"; import "../styles/globals.css"; +import "../styles/Verification.module.css"; const MyApp = ({ Component, pageProps }) => { return ; diff --git a/frontend/pages/health.tsx b/frontend/pages/health.tsx new file mode 100644 index 00000000..611d3fc9 --- /dev/null +++ b/frontend/pages/health.tsx @@ -0,0 +1,24 @@ +import { GetServerSideProps } from "next"; + +const HealthPage = () => { + return
OK
; +}; + +export const getServerSideProps: GetServerSideProps = async ({ req }) => { + const userAgent = req.headers["user-agent"] || ""; + + if (userAgent !== "service-status") { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + return { + props: {}, + }; +}; + +export default HealthPage; diff --git a/frontend/styles/Verification.module.css b/frontend/styles/Verification.module.css index 44c91c18..fd11bf2b 100644 --- a/frontend/styles/Verification.module.css +++ b/frontend/styles/Verification.module.css @@ -1,10 +1,10 @@ -.container { +.verification-modal-container { height: 50px; width: 300px; margin-top: 10px; } -.character { +.verification-modal-character { line-height: 50px; font-size: 36px; color: black; @@ -14,10 +14,10 @@ margin-left: 8px; } -.character:first-child { +.verification-modal-character:first-child { margin-left: 0; } -.character--selected { +.verification-modal-character-selected { border: 1px solid #b0b0b0; -} \ No newline at end of file +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index b4fb8bbe..7b97ef3c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -25,6 +25,7 @@ "include": [ "next-env.d.ts", "**/*.ts", - "**/*.tsx" + "**/*.tsx", + "**/*.css" ] } diff --git a/frontend/utils/auth.tsx b/frontend/utils/auth.tsx index 24b398e3..c7fd5bb8 100644 --- a/frontend/utils/auth.tsx +++ b/frontend/utils/auth.tsx @@ -7,7 +7,7 @@ import { doApiRequest } from "./fetch"; import { User } from "../types"; export interface AuthProps { - user: User; + user: User | null; } type GetServerSidePropsResultDiscUnion = @@ -39,6 +39,21 @@ export function withAuth(getServerSidePropsFunc: GetServerSidePropsFunc) { return async ( ctx: GetServerSidePropsContext ): Promise> => { + if (ctx.resolvedUrl === "/health") { + const wrapped = await getServerSidePropsFunc(ctx); + const casted = convertGetServerSidePropsResult(wrapped); + + if (casted.tag === "props") { + return { + props: { ...casted.props, user: null } as T & AuthProps, + }; + } else if (casted.tag === "notFound") { + return { notFound: casted.notFound }; + } else { + return { redirect: casted.redirect }; + } + } + const headers = { credentials: "include", headers: { cookie: ctx.req.headers.cookie }, diff --git a/k8s/main.ts b/k8s/main.ts index e42ea780..69c887a2 100644 --- a/k8s/main.ts +++ b/k8s/main.ts @@ -48,9 +48,11 @@ export class MyChart extends PennLabsChart { "/openapi", "/documentation", "/Shibboleth.sso", + "/healthcheck", ], isSubdomain: true, }], + ingressProps: { annotations: { ["ingress.kubernetes.io/protocol"]: "https",