The UAR (User Access Request) Portal is a full-stack access request, account provisioning, and operations console for the Mitchell C. Hill Student Data Center and the Cal Poly Pomona student-led Security Operations Center.
This README is the canonical public reference for this repository. It is written against the current codebase in my-app/ and is intended to be the single human-readable source of truth for what the application does, how it is structured, and how it is started.
- At a Glance
- What the Portal Does
- End-to-End Workflows
- Runtime Surface
- Architecture
- Data Model
- Security and Control Model
- Technology Stack
- Getting Started
- Environment Variables
- Deployment and Operations
- Repository Layout
- Maintaining This README
The portal combines public intake, identity verification, LDAP-backed authentication, administrative review, account provisioning, support workflows, and operational automation in one Next.js App Router application.
The current codebase contains:
| Area | Current Count |
|---|---|
| App pages | 26 |
| API route handlers | 87 |
| Admin components | 24 |
| Prisma models | 30 |
The application is not a generic demo. It expects real infrastructure:
- PostgreSQL for operational data
- LDAP / Active Directory over LDAPS for authentication and account operations
- SMTP for transactional email
- Redis or Upstash Redis for distributed rate limiting
- Cloudflare Turnstile for public-form abuse protection
The UAR Portal manages the full access lifecycle for two main audiences:
- Internal users, primarily
@cpp.eduusers who may require Active Directory-backed access - External users, such as visitors, event participants, or temporary users who may require time-bound access
Core responsibilities in the current codebase:
- Public request intake for internal and external access requests
- Email verification before requests enter reviewer queues
- Student director review and routing
- Faculty review and approval paths
- Active Directory account creation, linking, password operations, and expiration handling
- VPN account management, status changes, imports, and matching
- First-time account activation and password reset flows
- User profile lookup from LDAP
- Authenticated support ticket creation and response handling
- Batch account creation for multi-user operational work
- Account lifecycle scheduling and queued processing
- System settings, banners, notifications, blocklists, audit logging, and session management
- Infrastructure synchronization between existing directory/infrastructure records and portal records
flowchart TD
A[Public user submits request] --> B{Internal or external}
B -->|Internal| C[Validate @cpp.edu email and derive BroncoName]
B -->|External| D[Validate institution and event or reason]
C --> E[Turnstile verification]
D --> E
E --> F[Rate limiting and input validation]
F --> G[Blocklist check]
G --> H{Active request already exists?}
H -->|Yes| I[Return silent success to reduce enumeration]
H -->|No| J[Create AccessRequest in pending_verification]
J --> K[Send verification email]
K --> L[User opens verify flow]
L --> M{Verification token valid?}
M -->|No| N[Show invalid, expired, or already-verified state]
M -->|Yes| O[Notify admins and move to pending_student_directors]
O --> P[Student director review]
P --> Q{Needs faculty action?}
Q -->|Yes| R[pending_faculty]
Q -->|No| S[Approve and provision]
R --> S
S --> T[Create or link AD/VPN account records]
T --> U[Send activation, credentials, or notification email]
What the code does during submission:
POST /api/requestvalidates JSON payload size, required fields, and string lengths.- Public submissions require a valid Cloudflare Turnstile token.
- Internal requests must use
@cpp.edu. - External requests must include an institution and either an event reason or one or more active event IDs.
- Selected event IDs are checked against active
Eventrecords. - Blocked email addresses are rejected before a request record is created.
- Duplicate active requests are intentionally answered with a success response instead of a conflict response to reduce user enumeration.
- Verification tokens are generated for 24 hours.
Internal-request handling has additional logic:
- The email prefix is extracted and used as the expected BroncoName / username basis.
- LDAP is queried to detect a "grandfathered" account: an existing AD account without an email address already attached.
- If such an account exists, the request is marked to link to the existing account instead of creating a new one.
Verification behavior:
POST /api/verify/confirm?token=...rate-limits both by IP and by token.- Verification attempts are counted in the database.
- If admin notification succeeds, the request becomes
pending_student_directors. - If notification email fails, the request is marked for notification retry and the user still sees a successful verification state.
The status values explicitly observed in code are:
pending_verificationpending_student_directorspending_facultyapprovedrejected
flowchart TD
A[Login form] --> B[Turnstile validation]
B --> C[LDAP bind authentication]
C --> D{Valid credentials?}
D -->|No| E[Generic invalid-credentials response]
D -->|Yes| F[Fetch LDAP profile and admin status]
F --> G[Create Prisma session and set session_token cookie]
G --> H[Authenticated app and admin usage]
I[Forgot password] --> J[Rate limit and optional Turnstile]
J --> K[Resolve email from LDAP or user input]
K --> L[Create hashed PasswordResetToken]
L --> M[Send reset email]
M --> N[Reset form posts token and new password]
N --> O[Atomically mark token used]
O --> P[Change LDAP password]
Q[Activation email] --> R[User submits token, username, and password]
R --> S[Atomically validate and consume AccountActivationToken]
S --> T[Set LDAP password]
T --> U[Send activation success email]
Current authentication behavior from the code:
POST /api/auth/loginchecks whether logins are globally disabled inSystemSettings.- Logins require Cloudflare Turnstile plus LDAP authentication.
- Admin state is not trusted solely from the session. Admin API routes re-check domain-admin membership through LDAP.
- Sessions are stored in Prisma with hashed tokens, expiry, last activity, IP address, and user agent.
- Creating a new session deletes any existing sessions for that username, so the portal enforces one active session per user.
Current session behavior:
- Session cookie name:
session_token - Cookie flags:
HttpOnly,SameSite=strict,secureby default - Default max age: 30 minutes for admins, 60 minutes for non-admin users
- Idle timeout: 15 minutes
AUTH_SESSION_MAX_AGEcan override the max age
Password reset behavior:
- Public reset requests require Turnstile when the user is not already identified by username.
- The portal returns a uniform success message even when the target account does not exist or is in an ineligible state.
- Password reset tokens are stored as SHA-256 hashes, not plaintext.
- Reset token consumption is wrapped in a serializable Prisma transaction to reduce race conditions.
- If the LDAP password-change operation fails, token usage is rolled back so the user can retry.
Account activation behavior:
- Activation tokens are also stored hashed and consumed transactionally.
- The request must already be
approved. - The request must belong to an internal user.
- The submitted username must match the request's stored LDAP username.
- If LDAP password-setting fails, token state is rolled back and the user receives a safe error response.
flowchart LR
A[Admin dashboard] --> B[Requests, events, users, support, VPN, batch, settings]
B --> C[Admin API routes]
C --> D[(PostgreSQL via Prisma)]
C --> E[LDAP / Active Directory]
C --> F[SMTP notifications]
C --> G[Audit log entries]
H[Lifecycle queue] --> I[/api/admin/account-lifecycle/process]
J[Cron caller] --> K[/api/cron/process-lifecycle-queue]
I --> L[Lifecycle processor]
K --> L
L --> D
L --> E
Operational workflows implemented in the code include:
- Request review, comments, routing, approval, rejection, resend flows, and manual assignment
- Batch account creation that validates accounts, generates usernames/passwords, provisions LDAP users, and emails credentials
- VPN account management, imports, record matching, comments, bulk status updates, and cleanup routes
- Account lifecycle queue processing for disable, enable, revoke, restore, and related actions
- Infrastructure sync from existing systems back into the portal database, including
dryRun, latest status, history, and detail views
The current page routes in my-app/app are:
| Area | Routes |
|---|---|
| Public landing and intake | /, /request/internal, /request/external, /request/success, /instructions |
| Authentication and recovery | /login, /forgot-password, /reset-password |
| Verification and onboarding | /verify/confirm, /verify/success, /verify/error, /verify/already-verified, /account/welcome, /account/activate, /account/activate/expired, /account/reset-password |
| User self-service | /profile, /support/create, /support/tickets, /support/tickets/[id] |
| Admin | /admin, /admin/search, /admin/batch-accounts, /admin/batch-accounts/[id], /admin/requests/[id], /admin/support/tickets/[id] |
The current API surface in my-app/app/api is grouped below by function.
| Domain | Routes |
|---|---|
| Public request intake and verification | /api/request, /api/verify, /api/verify/confirm, /api/events/active |
| Authentication, session, and CSRF | /api/auth/login, /api/auth/logout, /api/auth/session, /api/auth/check-admin, /api/csrf-token |
| Password recovery and account activation | /api/auth/request-password-reset, /api/auth/reset-password, /api/account/activate |
| User profile and email verification | /api/profile, /api/profile/check-records, /api/profile/verify-email, /api/profile/verify-email/confirm |
| User support and banner data | /api/support/tickets, /api/support/tickets/[id], /api/support/tickets/[id]/responses, /api/settings/banner |
| Scheduled processing | /api/cron/process-lifecycle-queue |
| Admin access request management | /api/admin/requests, /api/admin/requests/[id], /api/admin/requests/[id]/acknowledge, /api/admin/requests/[id]/approve, /api/admin/requests/[id]/reject, /api/admin/requests/[id]/comments, /api/admin/requests/[id]/create-account, /api/admin/requests/[id]/save-credentials, /api/admin/requests/[id]/manual-assign, /api/admin/requests/[id]/send-to-faculty, /api/admin/requests/[id]/return-to-faculty, /api/admin/requests/[id]/move-back, /api/admin/requests/[id]/resend-verification, /api/admin/requests/[id]/resend-activation, /api/admin/requests/[id]/resend-notification, /api/admin/requests/[id]/reset-password, /api/admin/requests/[id]/update-account, /api/admin/requests/[id]/notify-faculty, /api/admin/requests/[id]/undo-notify-faculty |
| Admin directory, account search, and group management | /api/admin/users, /api/admin/groups, /api/admin/groups/[groupName]/members, /api/admin/ad-search, /api/admin/ad-comments/[accountId], /api/admin/ad-comments/comment/[id], /api/admin/search, /api/admin/check-username, /api/admin/generate-password |
| Admin VPN management and import pipeline | /api/admin/vpn-accounts, /api/admin/vpn-accounts/[id], /api/admin/vpn-accounts/[id]/status, /api/admin/vpn-accounts/[id]/comments, /api/admin/vpn-accounts/bulk-status, /api/admin/vpn-import, /api/admin/vpn-import/[id], /api/admin/vpn-import/process, /api/admin/vpn-import/match, /api/admin/vpn-import/cleanup, /api/admin/vpn-import/clear |
| Admin batch operations | /api/admin/batch-accounts, /api/admin/batch-accounts/create, /api/admin/batch-accounts/[id], /api/admin/batch-accounts/[id]/cancel, /api/admin/batch-accounts/cleanup |
| Admin lifecycle and sync | /api/admin/account-lifecycle, /api/admin/account-lifecycle/process, /api/admin/account-lifecycle/batch, /api/admin/account-lifecycle/[id], /api/admin/account-lifecycle/[id]/retry, /api/admin/account-lifecycle/[id]/cancel, /api/admin/sync-status, /api/admin/settings/infrastructure-sync |
| Admin governance and platform operations | /api/admin/settings, /api/admin/notifications, /api/admin/notifications/[id], /api/admin/blocklist, /api/admin/blocklist/[id], /api/admin/events, /api/admin/events/[id], /api/admin/logs, /api/admin/sessions, /api/admin/track-view, /api/admin/cleanup-passwords, /api/admin/support/tickets, /api/admin/logout |
The main admin dashboard imports and renders these tab modules:
| Admin tab | Purpose |
|---|---|
requests |
Review, comment on, route, approve, reject, assign, and provision access requests |
events |
Manage external-event records used by the public external request flow |
users |
Inspect LDAP-backed users and group/account state |
support |
Review support tickets, responses, and ticket state |
batch |
Create and inspect batch account creation work |
vpn |
Manage VPN accounts, imports, matches, and state transitions |
blocklist |
Prevent specific email addresses from submitting new requests |
settings |
Manage system controls, banners, and infrastructure sync tooling |
logs |
Review audit and operational log output |
sessions |
Inspect and revoke active user sessions |
lifecycle |
Queue, inspect, retry, cancel, and process lifecycle actions |
sync-status |
Inspect match state between portal, AD, and VPN records |
communications |
Drive manual communication and resend flows tied to requests |
The application is a single Next.js App Router project under my-app/ with both frontend and backend code in the same repository.
flowchart LR
A[Browser] --> B[Next.js pages in app/]
A --> C[Next.js route handlers in app/api/]
B --> C
C --> D[Business logic in lib/]
D --> E[(PostgreSQL via Prisma)]
D --> F[LDAP / Active Directory]
D --> G[SMTP email]
D --> H[(Redis or Upstash Redis)]
A --> I[Cloudflare Turnstile]
I --> C
J[Admin dashboard] --> C
K[Cron or operator] --> C
Current architectural characteristics:
- Frontend pages and backend APIs live in the same Next.js application.
- Prisma is used for the system-of-record database.
- LDAP / Active Directory is used for identity validation, account creation, group membership work, and password operations.
- Redis-backed rate limiting is used when
REDIS_URLis configured; otherwise the code falls back to in-memory limits. - Most non-UI business logic lives in
my-app/lib/. middleware.tshandles admin page gating, CSRF enforcement, request logging, and security headers.next.config.tsvalidates required environment variables at startup and build time before the app boots.
The Prisma schema currently defines 30 models. The main domains are:
| Domain | Models |
|---|---|
| Access governance | AccessRequest, RequestComment, Event |
| Tokenized onboarding and recovery | PasswordResetToken, AccountActivationToken |
| Support operations | SupportTicket, TicketResponse, TicketStatusLog |
| Batch account operations | BatchAccountCreation, BatchAccountItem, BatchAuditLog |
| VPN management | VPNAccount, VPNAccountStatusLog, VPNAccountComment, VPNImport, VPNImportRecord, VPNRoleChange, VPNAccountActivityLog |
| Sessions, settings, and governance | Session, BlockedEmail, SystemSettings, NotificationBanner, AuditLog |
| Lifecycle and sync | AccountLifecycleAction, AccountLifecycleBatch, AccountLifecycleHistory, ADAccountSync, ADAccountMatch, ADAccountActivityLog, ADAccountComment |
Key AccessRequest data tracked in the schema:
- Request identity and requestor metadata
- Internal vs external classification
- Event linkage and expiration data
- Verification token state and verification attempts
- Review and approval timestamps
- LDAP username, VPN username, and account password storage fields
- Provisioning status and error fields
- Manual assignment and grandfathered-account linkage
- AD and VPN lifecycle state fields such as disable, revoke, restore, and related reasons
Support ticket behavior reflected in the schema:
- Tickets belong to authenticated usernames
- Tickets can optionally reference an
AccessRequest - Ticket responses and ticket status changes are stored separately
- Batch account jobs can be linked back to a support ticket
Current schema caveat:
- The repository contains
schema.prisma, but there is no checked-in Prisma migration directory atmy-app/prisma/migrations.
- LDAP authentication is the source of truth for login.
- Admin access requires both a portal session marked as admin and a fresh LDAP admin-group check on protected admin API routes.
- Sessions are stored server-side in Prisma using hashed tokens.
- Session cookies are
HttpOnly,SameSite=strict, and secure by default. - Idle sessions are invalidated after 15 minutes of inactivity.
- New logins revoke previous sessions for the same username.
- Public request submission, login, and public password reset flows use Cloudflare Turnstile.
- CSRF tokens are issued through
GET /api/csrf-token. middleware.tsenforces CSRF checks on mutating requests except for explicitly exempt paths and adminGETroutes.- Reset and activation tokens are stored hashed and consumed transactionally.
- Duplicate request handling and password reset responses intentionally avoid revealing whether a user or request exists.
- LDAP timeout and retry behavior are configurable through environment variables. The current LDAP client helper defaults
LDAP_ALLOW_INVALID_CERTStotrueunless overridden, so production deployments should explicitly setLDAP_ALLOW_INVALID_CERTS=falseunless self-signed certificates are intentionally trusted.
next.config.ts and middleware.ts both contribute defense-in-depth headers, including:
Content-Security-PolicyStrict-Transport-SecurityX-Frame-OptionsX-Content-Type-OptionsReferrer-PolicyPermissions-PolicyCross-Origin-Embedder-PolicyCross-Origin-Opener-PolicyCross-Origin-Resource-Policy
Rate limiting is implemented in my-app/lib/ratelimit.ts and can run on Redis or in memory.
| Scope | Current behavior |
|---|---|
| Login preset | 200 requests per 15 minutes per IP |
| Request-submission preset | 200 requests per hour per IP, often with an additional identifier such as email |
| Password-reset preset | 100 requests per hour per IP, sometimes keyed by email or username |
| Verification preset | 600 requests per hour per IP |
| Admin-operation preset | 4000 requests per minute per IP, usually keyed by session ID |
| CSRF token endpoint | 100 requests per minute per IP |
| Verification token attempts | Additional 3 requests per hour per token |
| Activation attempts | Additional 5 requests per hour per IP plus username |
Versions below reflect the currently pinned dependencies in my-app/package.json.
| Layer | Current implementation |
|---|---|
| Framework | Next.js 16.0.8 |
| UI runtime | React 19.2.1 |
| Language | TypeScript 5.9.3 |
| Styling | Tailwind CSS 4, Radix UI, shadcn-style component patterns |
| Database access | Prisma 7.0.0 with PostgreSQL |
| Directory integration | ldapts |
nodemailer |
|
| Rate limiting / cache | redis and @upstash/redis support |
| Security helpers | csrf-csrf, Cloudflare Turnstile integration, bcrypt, custom session/token handling |
| Logging | Winston plus database-backed audit logging |
| Motion / UX utilities | Framer Motion, Sonner, Lucide icons |
For a meaningful local, staging, or production deployment you need:
- Node.js 20+
- npm 10+
- PostgreSQL
- Redis or Upstash Redis
- LDAP / Active Directory reachable over LDAPS
- SMTP credentials
- Cloudflare Turnstile site and secret keys
Optional but useful:
- Docker and Docker Compose
- A scheduler that can call the lifecycle cron endpoint
The actual Next.js application root is my-app/.
Run application commands from there:
cd my-appcd my-app
npm ciFor local development, put runtime variables in:
my-app/.env.local, or- your shell environment before starting Next.js
Important current behavior:
next.config.tsvalidates environment variables during startup and build.- The app exits early if required variables are missing or malformed.
DATABASE_URLmust includesslmode=requirebecause that requirement is enforced in code.LDAP_URLmust begin withldaps://.
The repository includes a Prisma schema but no checked-in migration history.
For a fresh local database, the current simplest workflow is:
cd my-app
npx prisma generate
npx prisma db pushcd my-app
npm run devDefault local address:
http://localhost:3000
If you only want the infrastructure services locally:
docker compose up -d postgres redisThis does not remove the need for valid LDAP, SMTP, and Turnstile configuration.
The validator-backed required variables come from my-app/lib/env-validator.ts. Additional operational variables below are also referenced directly in the codebase.
| Variable | Required | Notes |
|---|---|---|
DATABASE_URL |
Yes | Must be PostgreSQL and must include sslmode=require. |
SMTP_HOST |
Yes | SMTP hostname. |
SMTP_PORT |
Yes | SMTP port number. |
SMTP_USER |
Yes | SMTP username. |
SMTP_PASSWORD |
Yes | SMTP password. |
EMAIL_FROM |
Yes | Default sender address. |
ADMIN_EMAIL |
Yes | Default admin notification address. |
LDAP_URL |
Yes | Must start with ldaps://. |
LDAP_BIND_DN |
Yes | LDAP bind DN / service account DN. |
LDAP_BIND_PASSWORD |
Yes | LDAP bind password. |
LDAP_SEARCH_BASE |
Yes | Primary LDAP search base. |
LDAP_DOMAIN |
Yes | Domain used for auth and account naming. |
LDAP_ADMIN_GROUPS |
Yes | Group list used for portal admin checks. |
LDAP_GROUP2ADD |
Yes | Default group used during provisioning. |
LDAP_KAMINO_INTERNAL_GROUP |
Yes | Required by environment validation. |
LDAP_KAMINO_EXTERNAL_GROUP |
Yes | Required by environment validation. |
LDAP_GROUPSEARCH |
Yes | Group search base used by the app. |
NEXT_PUBLIC_APP_URL |
Yes | Public base URL for generated links and metadata. |
NEXTAUTH_SECRET |
Yes | Must be at least 32 characters. |
ENCRYPTION_SECRET |
Yes | Must be at least 32 characters. |
ENCRYPTION_SALT |
Yes | Must be at least 32 characters with sufficient entropy. |
NEXT_PUBLIC_TURNSTILE_SITE_KEY |
Yes | Public Turnstile site key. |
TURNSTILE_SECRET_KEY |
Yes | Server-side Turnstile secret. |
| Variable | Purpose |
|---|---|
REDIS_URL |
Enables Redis-backed distributed rate limiting. |
REDIS_TOKEN |
Required for Upstash Redis connections. |
FACULTY_EMAIL |
Faculty notification target. |
STUDENT_DIRECTOR_EMAILS |
Student director notification targets. |
CRON_SECRET |
Bearer token for /api/cron/process-lifecycle-queue. |
AUTH_SESSION_MAX_AGE |
Overrides default session max age in seconds. |
SESSION_COOKIE_ALLOW_INSECURE |
Development-only cookie relaxation. |
LDAP_TIMEOUT |
LDAP timeout override in milliseconds. |
LDAP_MAX_RETRIES |
LDAP retry count override. |
LDAP_RETRY_DELAY |
Initial LDAP retry delay in milliseconds before exponential backoff. |
LDAP_ALLOW_INVALID_CERTS |
Controls LDAP TLS certificate validation. Set this explicitly to false in production unless you intentionally trust self-signed certificates. |
LOG_LEVEL |
Winston log level. |
LOG_FORMAT |
Winston log format. |
LOG_FILE_PATH |
Optional file-backed log output path. |
NODE_ENV |
Standard runtime mode. |
Generate strong random values for the major secrets:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Use that for:
NEXTAUTH_SECRETENCRYPTION_SECRETENCRYPTION_SALT
Run these from my-app/.
| Command | Purpose |
|---|---|
npm run dev |
Start the Next.js development server |
npm run build |
Create a production build |
npm run start |
Start the production server |
npm run lint |
Run ESLint |
npm run reset-login-lock |
Run the maintenance script in scripts/reset-login-lock.js |
Useful Prisma commands:
npx prisma generate
npx prisma db pushThe repository includes:
Dockerfilefor a standalone Next.js builddocker-compose.ymlwith PostgreSQL, Redis, and the app service
Current runtime details from those files:
- The production container exposes port
3002. - The Dockerfile performs
prisma generatebeforenpm run build. - The Dockerfile injects dummy build-time values for several required variables so
next buildcan run.
Current caveats that matter before public deployment:
docker-compose.ymlsetsDATABASE_URLwithoutsslmode=require, but the app currently rejects database URLs that do not include it.docker-compose.ymlpassesLDAP_BASE, but the validator-backed required variable isLDAP_GROUPSEARCH.docker-compose.ymldoes not currently provideLDAP_KAMINO_INTERNAL_GROUP,LDAP_KAMINO_EXTERNAL_GROUP, orLDAP_GROUPSEARCH.- The checked-in Dockerfile build args also do not define dummy values for those three required LDAP variables.
In other words, treat the Docker assets as a starting point, not as a guaranteed ready-to-run production definition. Align them with the validator-backed environment contract before relying on them.
Queued lifecycle work can be processed in two ways:
- Manually through
POST /api/admin/account-lifecycle/process - Automatically through
GETorPOST /api/cron/process-lifecycle-queue
The cron route requires:
CRON_SECRETto be set- An
Authorization: Bearer <CRON_SECRET>header
Typical invocation pattern:
curl -H "Authorization: Bearer $CRON_SECRET" \
https://your-host.example/api/cron/process-lifecycle-queueThe cron endpoint:
- Rejects unauthorized callers
- Processes queued lifecycle actions
- Returns a summary with total, successful, and failed counts
- Logs start and completion details
Infrastructure sync is exposed through /api/admin/settings/infrastructure-sync.
Current supported behaviors in code:
POSTwith optionaldryRunGETlatest sync statusGETsync historyGETsync details by sync ID
uar-web-2/
├── README.md
├── Dockerfile
├── docker-compose.yml
└── my-app/
├── app/ # App Router pages and API handlers
├── components/ # Shared UI and admin modules
├── hooks/ # Client hooks
├── lib/ # Business logic, auth, LDAP, email, security
├── prisma/ # Prisma schema
├── public/ # Static assets
├── scripts/ # Small maintenance scripts
├── middleware.ts # Request gating, CSRF, logging, headers
├── next.config.ts # Build config and env validation
└── package.json
Important repository notes:
my-app/is the application root.- The repository root contains deployment and orchestration files.
- Public documentation for the project should be maintained here in
README.mdso it stays aligned with the codebase.