1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-04 15:39:45 +00:00

feat: add healthz endpoint (#494)

This commit is contained in:
Alessandro (Ale) Segala
2025-05-06 13:14:18 -07:00
committed by GitHub
parent c55fef057c
commit 3c87e4ec14
6 changed files with 66 additions and 34 deletions

View File

@@ -49,19 +49,18 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
r.Use(otelgin.Middleware("pocket-id-backend")) r.Use(otelgin.Middleware("pocket-id-backend"))
} }
rateLimitMiddleware := middleware.NewRateLimitMiddleware() rateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60)
// Setup global middleware // Setup global middleware
r.Use(middleware.NewCorsMiddleware().Add()) r.Use(middleware.NewCorsMiddleware().Add())
r.Use(middleware.NewErrorHandlerMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add())
r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60))
// Initialize middleware for specific routes // Initialize middleware for specific routes
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService) authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware() fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
// Set up API routes // Set up API routes
apiGroup := r.Group("/api") apiGroup := r.Group("/api", rateLimitMiddleware)
controller.NewApiKeyController(apiGroup, authMiddleware, svc.apiKeyService) controller.NewApiKeyController(apiGroup, authMiddleware, svc.apiKeyService)
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.webauthnService, svc.appConfigService) controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.webauthnService, svc.appConfigService)
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService) controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService)
@@ -79,9 +78,13 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
} }
// Set up base routes // Set up base routes
baseGroup := r.Group("/") baseGroup := r.Group("/", rateLimitMiddleware)
controller.NewWellKnownController(baseGroup, svc.jwtService) controller.NewWellKnownController(baseGroup, svc.jwtService)
// Set up healthcheck routes
// These are not rate-limited
controller.NewHealthzController(r)
// Set up the server // Set up the server
srv := &http.Server{ srv := &http.Server{
Addr: net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port), Addr: net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port),

View File

@@ -0,0 +1,29 @@
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
)
// NewHealthzController creates a new controller for the healthcheck endpoints
// @Summary Healthcheck controller
// @Description Initializes healthcheck endpoints
// @Tags Health
func NewHealthzController(r *gin.Engine) {
hc := &HealthzController{}
r.GET("/healthz", hc.healthzHandler)
}
type HealthzController struct{}
// healthzHandler godoc
// @Summary Responds to healthchecks
// @Description Responds with a successful status code to healthcheck requests
// @Tags Health
// @Success 204 ""
// @Router /healthz [get]
func (hc *HealthzController) healthzHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}

View File

@@ -7,9 +7,9 @@ services:
- 3000:80 - 3000:80
volumes: volumes:
- "./data:/app/backend/data" - "./data:/app/backend/data"
# Optional healthcheck # Optional healthcheck
healthcheck: healthcheck:
test: "curl -f http://localhost/health" test: "curl -f http://localhost/healthz"
interval: 1m30s interval: 1m30s
timeout: 5s timeout: 5s
retries: 2 retries: 2

View File

@@ -23,33 +23,33 @@ const paraglideHandle: Handle = ({ event, resolve }) => {
const authenticationHandle: Handle = async ({ event, resolve }) => { const authenticationHandle: Handle = async ({ event, resolve }) => {
const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const isUnauthenticatedOnlyPath = event.url.pathname.startsWith('/login') || event.url.pathname.startsWith('/lc') const path = event.url.pathname;
const isPublicPath = ['/authorize', '/device', '/health'].includes(event.url.pathname); const isUnauthenticatedOnlyPath = path == '/login' || path.startsWith('/login/') || path == '/lc' || path.startsWith('/lc/')
const isAdminPath = event.url.pathname.startsWith('/settings/admin'); const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
if (!isUnauthenticatedOnlyPath && !isPublicPath && !isSignedIn) { if (!isUnauthenticatedOnlyPath && !isPublicPath && !isSignedIn) {
return new Response(null, { return new Response(null, {
status: 302, status: 303,
headers: { location: '/login' } headers: { location: '/login' }
}); });
} }
if (isUnauthenticatedOnlyPath && isSignedIn) { if (isUnauthenticatedOnlyPath && isSignedIn) {
return new Response(null, { return new Response(null, {
status: 302, status: 303,
headers: { location: '/settings' } headers: { location: '/settings' }
}); });
} }
if (isAdminPath && !isAdmin) { if (isAdminPath && !isAdmin) {
return new Response(null, { return new Response(null, {
status: 302, status: 303,
headers: { location: '/settings' } headers: { location: '/settings' }
}); });
} }
const response = await resolve(event); return resolve(event);
return response;
}; };
export const handle: Handle = sequence(paraglideHandle, authenticationHandle); export const handle: Handle = sequence(paraglideHandle, authenticationHandle);

View File

@@ -1,20 +1,2 @@
import AppConfigService from '$lib/services/app-config-service'; // /health is an alias of /healthz, for backwards-compatibility reasons
import type { RequestHandler } from '@sveltejs/kit'; export {GET} from '../healthz/+server';
export const GET: RequestHandler = async () => {
const appConfigService = new AppConfigService();
let backendOk = true;
await appConfigService.list().catch(() => (backendOk = false));
return new Response(
JSON.stringify({
status: backendOk ? 'HEALTHY' : 'UNHEALTHY'
}),
{
status: backendOk ? 200 : 500,
headers: {
'content-type': 'application/json'
}
}
);
};

View File

@@ -0,0 +1,18 @@
import type { RequestHandler } from '@sveltejs/kit';
import axios from 'axios';
export const GET: RequestHandler = async () => {
const backendOK = await axios
.get(process!.env!.INTERNAL_BACKEND_URL + '/healthz')
.then(() => true, () => false);
return new Response(
backendOK ? `{"status":"HEALTHY"}` : `{"status":"UNHEALTHY"}`,
{
status: backendOK ? 200 : 500,
headers: {
'content-type': 'application/json'
}
}
);
};