Security Overview
CPR handles sensitive medical data -- patient records, billing information, prescription details, and surgical schedules. Security is a fundamental requirement, not an optional enhancement.
Medical Data Sensitivity
CPR stores and displays Protected Health Information (PHI) including:
- Patient personal details (name, birthdate, contact information, address)
- Medical records (visual acuity, refraction, tonometry, funduscopy, surgical history)
- Prescription data (medicines, dosages, frequencies)
- Billing and insurance information
- Visit history and doctor notes
All of this data must be protected from unauthorized access, both in transit (HTTPS) and at rest (server-side encryption).
Authentication Architecture
CPR uses a Bearer token authentication system that supports both JWT and Laravel Sanctum tokens:
- User submits username and password to the API (
POST /auth/login). - The API returns a user object and a token.
- The token and user data are stored in
sessionStorage. - Every subsequent API request includes the token in the
Authorization: Bearer <token>header. - The auth middleware checks token validity on every route navigation.
See Authentication for detailed flow documentation.
Permission System
CPR implements a role-based and permission-based access control system. The AuthUser type includes both roles and granular permissions:
export interface AuthUser {
id: number;
name: string;
username: string;
email: string;
phone?: string;
role?: string[]; // e.g., ['doctor', 'admin']
permissions?: string[]; // e.g., ['view_patients', 'create_billing']
default_branch?: Branch;
}Permission checking utilities are defined in app/types/permission.type.ts:
export interface PermissionUtils {
can: PermissionChecker; // can('view_patients')
cannot: PermissionChecker; // cannot('delete_billing')
canAny: MultiPermissionChecker; // canAny(['edit_patients', 'admin'])
canAll: MultiPermissionChecker; // canAll(['view_patients', 'create_billing'])
hasRole: RoleChecker; // hasRole('doctor')
hasAnyRole: MultiRoleChecker; // hasAnyRole(['doctor', 'admin'])
}Permission Enforcement
Permissions should be checked at two levels:
- Frontend -- Hide or disable UI elements the user cannot access. This is a UX improvement, not a security boundary.
- Backend -- The API must validate permissions on every request. The frontend check can be bypassed, so the server is the source of truth.
<template>
<!-- Only show delete button if user has permission -->
<UiButton
v-if="can('delete_patient')"
variant="danger"
@click="confirmDelete"
>
Delete Patient
</UiButton>
</template>XSS Prevention
Vue provides built-in protection against Cross-Site Scripting (XSS) by automatically escaping template interpolation. See XSS Prevention for details.
Secure Token Storage
Why sessionStorage
CPR uses sessionStorage instead of localStorage for token storage:
| sessionStorage | localStorage | |
|---|---|---|
| Persistence | Tab only | Across tabs and sessions |
| Cleared on tab close | Yes | No |
| Shared workstation risk | Lower (session ends) | Higher (persists) |
| XSS exposure window | Current session | Until manually cleared |
In clinical settings, workstations are often shared among staff. sessionStorage ensures that closing the browser tab ends the session, reducing the risk of unauthorized access.
Token Security Best Practices
- Never store tokens in cookies accessible to JavaScript (unless
HttpOnly). - Check token expiry before every route navigation (CPR's auth middleware does this for JWTs).
- Clear all auth data on logout -- both Pinia store state and
sessionStorage. - Do not log tokens -- Production builds strip
console.*calls, but avoid logging tokens even in development.
Production Security Configuration
CPR's nuxt.config.ts includes security-relevant production settings:
// Drop console and debugger in production
vite: {
esbuild: {
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
},
},
// Source maps exist for Sentry but are not served to users
sourcemap: {
client: 'hidden',
},Sentry for Security Monitoring
CPR integrates Sentry for error monitoring. Authentication errors are captured with context:
// From auth.sevice.ts
catch (err: unknown) {
const fetchError = err as FetchError;
const message = fetchError.data?.message ?? fetchError.message ?? 'Something went wrong';
Sentry.captureException(err);
throw new Error(message);
}This allows the team to detect:
- Unusual authentication failure patterns (brute force attempts)
- Unexpected API errors that may indicate security issues
- Client-side errors that could expose sensitive data
Security Checklist
| Concern | CPR Implementation |
|---|---|
| Token storage | sessionStorage (tab-scoped) |
| Token transmission | Authorization: Bearer header |
| Token expiry check | Auth middleware checks JWT exp claim |
| Console output in prod | Stripped via esbuild.drop |
| Source maps in prod | Hidden (Sentry only) |
| XSS protection | Vue template escaping |
| Error monitoring | Sentry integration |
| Permission system | Role + permission-based checks |
| HTTPS | Enforced at infrastructure level |
Related Pages
- Authentication -- Login flow, middleware, and password reset
- XSS Prevention -- Cross-site scripting protection
