From dcb65a04cdd628f3c0c8eab4804c415babe4617e Mon Sep 17 00:00:00 2001 From: AllenZheng-05 Date: Wed, 25 Mar 2026 17:28:14 -0500 Subject: [PATCH 01/27] created swagger page at root api.acmutd.co domain and dashboard frontend skeleton --- .gitignore | 6 +- frontend/README.md | 31 +++++ frontend/postcss.config.js | 6 + .../src/dashboard/AdminDashboardSection.jsx | 96 ++++++++++++++ frontend/src/dashboard/DashboardApp.jsx | 85 ++++++++++++ .../src/dashboard/UserDashboardSection.jsx | 80 ++++++++++++ frontend/src/dashboard/dashboard.html | 15 +++ .../src/dashboard/debug/adminDashboardData.js | 39 ++++++ .../src/dashboard/debug/userDashboardData.js | 16 +++ frontend/src/dashboard/main.jsx | 10 ++ frontend/src/dashboard/tailwind.css | 14 ++ frontend/src/swagger/swagger.html | 65 ++++++++++ frontend/tailwind.config.js | 8 ++ frontend/vite.config.js | 15 +++ internal/firebase/apikeys.go | 3 + internal/server/docs/openapi.yaml | 121 ++++++++++++++++++ internal/server/handlers/dashboard_pages.go | 38 ++++++ internal/server/handlers/defaulthandler.go | 51 ++++++++ internal/server/router/router.go | 5 + 19 files changed, 703 insertions(+), 1 deletion(-) create mode 100644 frontend/README.md create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/dashboard/AdminDashboardSection.jsx create mode 100644 frontend/src/dashboard/DashboardApp.jsx create mode 100644 frontend/src/dashboard/UserDashboardSection.jsx create mode 100644 frontend/src/dashboard/dashboard.html create mode 100644 frontend/src/dashboard/debug/adminDashboardData.js create mode 100644 frontend/src/dashboard/debug/userDashboardData.js create mode 100644 frontend/src/dashboard/main.jsx create mode 100644 frontend/src/dashboard/tailwind.css create mode 100644 frontend/src/swagger/swagger.html create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.js create mode 100644 internal/server/docs/openapi.yaml create mode 100644 internal/server/handlers/dashboard_pages.go create mode 100644 internal/server/handlers/defaulthandler.go diff --git a/.gitignore b/.gitignore index 86512d8..66eb6f4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,8 @@ __pycache__/ *.xlsx *.xlsb *.csv -venv \ No newline at end of file +venv +acmutd-api +acmapi-key.pem +frontend/node_modules/ +frontend/dist/ \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d34ca93 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,31 @@ +# Frontend Dashboard (Vite + Tailwind) + +This folder contains the frontend pages served by the Go API. + +## Pages + +- Dashboard entry: `src/dashboard/dashboard.html` -> served at `/dashboard` and `/admin` +- Docs landing page: `src/swagger/swagger.html` -> served at `/` by the backend + +The dashboard page uses Tailwind stylesheet at `src/dashboard/tailwind.css`. + +## Development + +```bash +npm install +npm run dev +``` + +## Production Build + +```bash +npm install +npm run build +``` + +Build output is written to `frontend/dist`. + +The backend expects built files in: + +- `frontend/dist/src/dashboard/dashboard.html` +- `frontend/dist/assets/*` diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..ba80730 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/src/dashboard/AdminDashboardSection.jsx b/frontend/src/dashboard/AdminDashboardSection.jsx new file mode 100644 index 0000000..c9bc7b7 --- /dev/null +++ b/frontend/src/dashboard/AdminDashboardSection.jsx @@ -0,0 +1,96 @@ +function formatDate(value) { + if (!value) { + return "-"; + } + return new Date(value).toLocaleString(); +} + +export function AdminDashboardSection({ requests, keys, approve, reject, deactivate }) { + return ( + <> +
+

Requests

+
+ + + + + + + + + + + + + {requests.map((item) => ( + + + + + + + + + ))} + +
UIDEmailRequestedTypeStatusActions
{item.uid || "-"}{item.email || "-"}{formatDate(item.requested_at)}{item.request_type || "-"}{item.status || "-"} +
+ + +
+
+
+
+ +
+

Issued Keys

+
+ + + + + + + + + + + + + + {keys.map((item) => ( + + + + + + + + + + ))} + +
UIDEmailMasked KeyStatusUsageUpdatedAction
{item.uid || "-"}{item.email || "-"}{item.masked_key || "-"}{item.status || "-"}{String(item.usage_count || 0)}{formatDate(item.updated_at)} + +
+
+
+ + ); +} diff --git a/frontend/src/dashboard/DashboardApp.jsx b/frontend/src/dashboard/DashboardApp.jsx new file mode 100644 index 0000000..f9172db --- /dev/null +++ b/frontend/src/dashboard/DashboardApp.jsx @@ -0,0 +1,85 @@ +import { useState } from "react"; +import { UserDashboardSection } from "./UserDashboardSection"; +import { AdminDashboardSection } from "./AdminDashboardSection"; +import { debugUserDashboardData } from "./debug/userDashboardData"; +import { debugAdminDashboardData } from "./debug/adminDashboardData"; + +export function DashboardApp() { + const [debugViewRole, setDebugViewRole] = useState("user"); + const isAdminView = debugViewRole === "admin"; + const activeUser = isAdminView ? debugAdminDashboardData.user : debugUserDashboardData.user; + const userDashboard = debugUserDashboardData.dashboard; + + const status = userDashboard?.status || "none"; + const keyInfo = userDashboard?.key_info || null; + const hasActiveKey = status === "active"; + const registrationMode = userDashboard?.mode || "request-only"; + + function switchDebugView() { + setDebugViewRole((previous) => (previous === "admin" ? "user" : "admin")); + } + + function noop() {} + + return ( +
+
+
+

Dashboard

+ + {isAdminView ? "admin" : "user"} + +
+

+ Debug-only dashboard preview with static data. +

+ +
+ Debug mode + +
+ +
+ {activeUser && ( + + Signed in as {activeUser.email || ""} + + )} +
+
+ + {!isAdminView && ( + + )} + + {isAdminView && ( + + )} +
+ ); +} diff --git a/frontend/src/dashboard/UserDashboardSection.jsx b/frontend/src/dashboard/UserDashboardSection.jsx new file mode 100644 index 0000000..c725c11 --- /dev/null +++ b/frontend/src/dashboard/UserDashboardSection.jsx @@ -0,0 +1,80 @@ +export function UserDashboardSection({ + hasActiveKey, + registrationMode, + keyInfo, + generatedKey, + loading, + requestOrRegenerateKey, + revokeKey +}) { + return ( +
+
+

Key Overview

+ + {hasActiveKey ? "active" : "inactive"} + +
+ +

Registration mode: {registrationMode}

+ + {keyInfo && ( +
+
+
Masked API key
+
{keyInfo.masked_key || "-"}
+
+ +
+
Rate limit
+
{String(keyInfo.rate_limit || "-")}
+
+ +
+
Window seconds
+
{String(keyInfo.window_seconds || "-")}
+
+ +
+
Usage count
+
{String(keyInfo.usage_count || 0)}
+
+
+ )} + + {generatedKey && ( +
+

New key generated. This is shown once:

+
+            {generatedKey}
+          
+
+ )} + +
+ + {keyInfo && ( + + )} +
+
+ ); +} diff --git a/frontend/src/dashboard/dashboard.html b/frontend/src/dashboard/dashboard.html new file mode 100644 index 0000000..2bd101e --- /dev/null +++ b/frontend/src/dashboard/dashboard.html @@ -0,0 +1,15 @@ + + + + + + ACM UTD Dashboard + + + +
+ + + diff --git a/frontend/src/dashboard/debug/adminDashboardData.js b/frontend/src/dashboard/debug/adminDashboardData.js new file mode 100644 index 0000000..73bbc40 --- /dev/null +++ b/frontend/src/dashboard/debug/adminDashboardData.js @@ -0,0 +1,39 @@ +export const debugAdminDashboardData = { + user: { + email: "debug-admin@local.test" + }, + requests: [ + { + uid: "debug-user-001", + email: "student1@utdallas.edu", + requested_at: "2026-03-25T18:30:00.000Z", + request_type: "new", + status: "pending" + }, + { + uid: "debug-user-002", + email: "student2@utdallas.edu", + requested_at: "2026-03-25T17:15:00.000Z", + request_type: "regenerate", + status: "pending" + } + ], + keys: [ + { + uid: "debug-user-010", + email: "activeuser@utdallas.edu", + masked_key: "acm_****_debug", + status: "active", + usage_count: 42, + updated_at: "2026-03-25T19:05:00.000Z" + }, + { + uid: "debug-user-011", + email: "inactiveuser@utdallas.edu", + masked_key: "acm_****_old", + status: "inactive", + usage_count: 3, + updated_at: "2026-03-24T08:10:00.000Z" + } + ] +}; diff --git a/frontend/src/dashboard/debug/userDashboardData.js b/frontend/src/dashboard/debug/userDashboardData.js new file mode 100644 index 0000000..d88ab40 --- /dev/null +++ b/frontend/src/dashboard/debug/userDashboardData.js @@ -0,0 +1,16 @@ +export const debugUserDashboardData = { + user: { + email: "debug-user@local.test" + }, + dashboard: { + status: "active", + mode: "open", + key_info: { + masked_key: "acm_****_debug", + rate_limit: 120, + window_seconds: 60, + usage_count: 7 + } + }, + generatedKey: "acm_debug_generated_key_for_ui_preview" +}; diff --git a/frontend/src/dashboard/main.jsx b/frontend/src/dashboard/main.jsx new file mode 100644 index 0000000..2f4d464 --- /dev/null +++ b/frontend/src/dashboard/main.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { DashboardApp } from "./DashboardApp"; +import "./tailwind.css"; + +createRoot(document.getElementById("root")).render( + + + +); diff --git a/frontend/src/dashboard/tailwind.css b/frontend/src/dashboard/tailwind.css new file mode 100644 index 0000000..bad0587 --- /dev/null +++ b/frontend/src/dashboard/tailwind.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: light; +} + +body { + margin: 0; + font-family: "Inter", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; + background: radial-gradient(circle at 20% 0%, #eef6ff 0%, #f9fbff 40%, #ffffff 100%); + color: #0f172a; +} diff --git a/frontend/src/swagger/swagger.html b/frontend/src/swagger/swagger.html new file mode 100644 index 0000000..3b99cec --- /dev/null +++ b/frontend/src/swagger/swagger.html @@ -0,0 +1,65 @@ + + + + + + ACM UTD API Docs + + + + + +
+ + + + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..0cb31a2 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./*.html", "./src/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: {} + }, + plugins: [] +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..1e4d23b --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { resolve } from "node:path"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: "dist", + rollupOptions: { + input: { + dashboard: resolve(__dirname, "src/dashboard/dashboard.html") + } + } + } +}); diff --git a/internal/firebase/apikeys.go b/internal/firebase/apikeys.go index 5ef4524..0c36b15 100644 --- a/internal/firebase/apikeys.go +++ b/internal/firebase/apikeys.go @@ -71,6 +71,9 @@ func (c *Firestore) UpdateKeyUsage(ctx context.Context, key string) error { func (c *Firestore) GetAPIKey(ctx context.Context, key string) (*types.APIKey, error) { doc, err := c.Collection("api_keys").Doc(key).Get(ctx) if err != nil { + if status.Code(err) == codes.NotFound { + return nil, nil + } return nil, err } diff --git a/internal/server/docs/openapi.yaml b/internal/server/docs/openapi.yaml new file mode 100644 index 0000000..db0f55d --- /dev/null +++ b/internal/server/docs/openapi.yaml @@ -0,0 +1,121 @@ +openapi: 3.0.3 +info: + title: ACM UTD API + version: 1.0.0 + description: API documentation for public endpoints, dashboard routes, and admin operations. +servers: + - url: / +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key +paths: + /health: + get: + summary: Service health check + tags: [Public] + responses: + "200": + description: Healthy + + /api/v1/terms: + get: + summary: List available terms + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Terms list + + /api/v1/courses: + get: + summary: Query courses by term and filters + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Courses list + + /api/v1/professors/id/{id}: + get: + summary: Get professor by ID + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Professor details + + /api/v1/professors/name/{name}: + get: + summary: Search professors by name + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Professor search results + + /api/v1/grades/prof/id/{id}: + get: + summary: Get grades by professor ID + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Grade distributions + + /api/v1/grades/prof/name/{name}: + get: + summary: Get grades by professor name + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Grade distributions + + /api/v1/grades/term/{term}: + get: + summary: Get grades by term + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Grade distributions + + /api/v1/grades/prefix/{prefix}: + get: + summary: Get grades by course prefix + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Grade distributions + + /api/v1/grades/prefix/{prefix}/number/{number}: + get: + summary: Get grades by course prefix and number + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Grade distributions + + /api/v1/grades/prefix/{prefix}/number/{number}/term/{term}/section/{section}: + get: + summary: Get grades by exact section + tags: [API v1] + security: + - ApiKeyAuth: [] + responses: + "200": + description: Grade distributions diff --git a/internal/server/handlers/dashboard_pages.go b/internal/server/handlers/dashboard_pages.go new file mode 100644 index 0000000..9db2dc9 --- /dev/null +++ b/internal/server/handlers/dashboard_pages.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" +) + +func (h *Handler) UserDashboardPage(c *gin.Context) { + h.serveDashboardAppPage(c) +} + +func (h *Handler) AdminDashboardPage(c *gin.Context) { + h.serveDashboardAppPage(c) +} + +func (h *Handler) serveDashboardAppPage(c *gin.Context) { + htmlPath := filepath.Join(frontendDistDir(), "src", "dashboard", "dashboard.html") + content, err := os.ReadFile(htmlPath) + if err != nil { + c.Data(http.StatusServiceUnavailable, "text/plain; charset=utf-8", []byte(fmt.Sprintf("Dashboard assets are not built yet. Run: cd frontend && npm install && npm run build\nMissing file: %s", htmlPath))) + return + } + + html := strings.ReplaceAll(string(content), "__FIREBASE_CONFIG__", "{}") + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html)) +} + +func frontendDistDir() string { + if custom := strings.TrimSpace(os.Getenv("FRONTEND_DIST_DIR")); custom != "" { + return custom + } + return filepath.Join("frontend", "dist") +} diff --git a/internal/server/handlers/defaulthandler.go b/internal/server/handlers/defaulthandler.go new file mode 100644 index 0000000..517873d --- /dev/null +++ b/internal/server/handlers/defaulthandler.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" +) + +// Default serves an embedded Swagger UI page. +func (h *Handler) Default(c *gin.Context) { + pagePath := swaggerUIPagePath() + content, err := os.ReadFile(pagePath) + if err != nil { + c.Data(http.StatusServiceUnavailable, "text/plain; charset=utf-8", []byte(fmt.Sprintf("Swagger UI page is missing. Expected file: %s", pagePath))) + return + } + + c.Data(http.StatusOK, "text/html; charset=utf-8", content) +} + +// OpenAPISpec serves the OpenAPI document consumed by Swagger UI. +func (h *Handler) OpenAPISpec(c *gin.Context) { + specPath := openAPISpecPath() + content, err := os.ReadFile(specPath) + if err != nil { + c.Data(http.StatusServiceUnavailable, "text/plain; charset=utf-8", []byte(fmt.Sprintf("OpenAPI spec is missing. Expected file: %s", specPath))) + return + } + + c.Data(http.StatusOK, "application/yaml; charset=utf-8", content) +} + +func openAPISpecPath() string { + if custom := strings.TrimSpace(os.Getenv("OPENAPI_SPEC_PATH")); custom != "" { + return custom + } + + return filepath.Join("internal", "server", "docs", "openapi.yaml") +} + +func swaggerUIPagePath() string { + if custom := strings.TrimSpace(os.Getenv("SWAGGER_UI_PAGE_PATH")); custom != "" { + return custom + } + + return filepath.Join("frontend", "src", "swagger", "swagger.html") +} diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 80b5c57..28972e3 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -11,8 +11,13 @@ import ( // New wires handlers and middleware into an HTTP router. func New(handler *handlers.Handler, mw *middleware.Manager) http.Handler { router := gin.Default() + router.Static("/assets", "frontend/dist/assets") + router.GET("/", handler.Default) + router.GET("/openapi.yaml", handler.OpenAPISpec) router.GET("/health", handler.Health) + router.GET("/dashboard", handler.UserDashboardPage) + router.GET("/admin", handler.AdminDashboardPage) admin := router.Group("/admin") admin.Use(mw.Auth(), mw.RateLimit(), mw.Admin()) From 58cfbc74fc33fa0fb2047f4123837fbaf0046030 Mon Sep 17 00:00:00 2001 From: AllenZheng-05 Date: Wed, 25 Mar 2026 17:40:03 -0500 Subject: [PATCH 02/27] made full dashboard frontend skeleton --- .../src/dashboard/AdminDashboardSection.jsx | 308 +++++++++++++++++- frontend/src/dashboard/DashboardApp.jsx | 24 +- .../src/dashboard/UserDashboardSection.jsx | 284 +++++++++++++--- .../src/dashboard/debug/adminDashboardData.js | 143 +++++++- .../src/dashboard/debug/userDashboardData.js | 66 +++- 5 files changed, 757 insertions(+), 68 deletions(-) diff --git a/frontend/src/dashboard/AdminDashboardSection.jsx b/frontend/src/dashboard/AdminDashboardSection.jsx index c9bc7b7..a6a4e00 100644 --- a/frontend/src/dashboard/AdminDashboardSection.jsx +++ b/frontend/src/dashboard/AdminDashboardSection.jsx @@ -5,17 +5,90 @@ function formatDate(value) { return new Date(value).toLocaleString(); } -export function AdminDashboardSection({ requests, keys, approve, reject, deactivate }) { +function miniBarWidth(current, maxValue) { + if (!maxValue) { + return "8%"; + } + return `${Math.min(100, Math.max(8, (current / maxValue) * 100))}%`; +} + +export function AdminDashboardSection({ + user, + auth, + approval, + requests, + keys, + manualTokenForm, + server, + scraperPipeline, + analytics, + scraperLogs, + cronLogs, + approve, + reject, + deactivate +}) { + const dailySeries = analytics?.requests_per_day || []; + const byToken = analytics?.by_token || []; + const byEndpoint = analytics?.by_endpoint || []; + const dayMax = dailySeries.reduce((maxValue, day) => Math.max(maxValue, day.count), 0); + const tokenMax = byToken.reduce((maxValue, item) => Math.max(maxValue, item.requests), 0); + const endpointMax = byEndpoint.reduce((maxValue, item) => Math.max(maxValue, item.requests), 0); + return ( <>
-

Requests

+
+
+

Admin Access & Policy

+

Officer access is gated by Firestore isAdmin and Google OAuth domain rules.

+
+ + {auth?.admin_gate_status || "granted"} + +
+ +
+
+
Signed in admin
+
{user?.name || user?.email || "-"}
+
+
+
Provider
+
{auth?.oauth_provider || "google.com"}
+
+
+
Domain lock
+
@{auth?.domain_restriction || "utdallas.edu"}
+
+
+
Approval mode
+
{approval?.mode || "manual"}
+
+
+
+ +
+
+

Approval Queue

+
+ Manual approval + +
+
+ +

{approval?.global_toggle_hint}

+
+ + @@ -27,8 +100,10 @@ export function AdminDashboardSection({ requests, keys, approve, reject, deactiv + + - +
UID EmailLabelDescription Requested Type Status
{item.uid || "-"} {item.email || "-"}{item.label || "-"}{item.description || "-"} {formatDate(item.requested_at)}{item.request_type || "-"}{item.requested_key_type || item.request_type || "-"} {item.status || "-"}
@@ -54,17 +129,52 @@ export function AdminDashboardSection({ requests, keys, approve, reject, deactiv
-

Issued Keys

+
+

Token Management

+ + Admins may hold multiple keys + +
+ +
+
+

Add Token Manually

+

Create random token or provide custom token value.

+
+
Label: {manualTokenForm?.label_placeholder}
+
Type: {(manualTokenForm?.key_type_options || []).join(" / ")}
+
Expiry presets: {(manualTokenForm?.expiry_presets || []).join(" | ")}
+
+
+ + +
+
+ +
+

Token Edit Actions

+
    +
  • Update label and description
  • +
  • Adjust expiry date and semester policy
  • +
  • Toggle active/inactive state
  • +
  • Deactivate or remove token permanently
  • +
+
+
+
- + + + + - + @@ -73,10 +183,13 @@ export function AdminDashboardSection({ requests, keys, approve, reject, deactiv - + + + + - +
UID EmailMasked KeyLabelTypeRequestedExpiry Status UsageUpdatedLast used Action
{item.uid || "-"} {item.email || "-"}{item.masked_key || "-"}{item.label || "-"}{item.type || "standard"}{formatDate(item.created_at)}{formatDate(item.expires_at)} {item.status || "-"} {String(item.usage_count || 0)}{formatDate(item.updated_at)}{formatDate(item.last_used_at)} + + + + + + + + +
+
+

HackUTD Enable Sequence

+
    + {(server?.hackutd_actions || []).map((step) => ( +
  1. {step}
  2. + ))} +
+
+
+

HackUTD Disable Sequence

+
    + {(server?.normal_mode_actions || []).map((step) => ( +
  1. {step}
  2. + ))} +
+
+
+ + +
+
+

Statistics & Monitoring

+ + Static charts from debug fixtures + +
+ +
+
+

Requests Per Day + Error Count

+
    + {dailySeries.map((day) => ( +
  • +
    + {day.day} + {day.count} req / {day.errors} errors +
    +
    +
    +
    +
  • + ))} +
+

Error rate over time (current): {String(analytics?.error_rate_percent || 0)}%

+
+ +
+

Requests By Token

+
    + {byToken.map((entry) => ( +
  • +
    + {entry.label} + {entry.requests} +
    +
    +
    +
    +
  • + ))} +
+
+
+ +
+

Requests By Endpoint

+
    + {byEndpoint.map((entry) => ( +
  • +
    + {entry.endpoint} + {entry.requests} +
    +
    +
    +
    +
  • + ))} +
+
+
+ +
+
+
+

Scraper/Pipeline Log Feed

+

+ Last run {formatDate(scraperPipeline?.last_run_at)} | Uploaded {String(scraperPipeline?.courses_uploaded || 0)} courses | Match rate {scraperPipeline?.merge_match_rate} +

+
+ {(scraperLogs || []).map((entry, index) => ( +
+ [{formatDate(entry.time)}] {entry.level.toUpperCase()} {entry.message} +
+ ))} +
+
+ +
+

Lambda Cron Log Feed

+

Static preview of cronLogs from Firestore.

+
+ + + + + + + + + + + {(cronLogs || []).map((entry, index) => ( + + + + + + + ))} + +
DateCheckedValidAction
{formatDate(entry.time)}{String(entry.tokens_checked)}{String(entry.valid_count)}{entry.action}
+
+
+
+
); } diff --git a/frontend/src/dashboard/DashboardApp.jsx b/frontend/src/dashboard/DashboardApp.jsx index f9172db..4b02ad7 100644 --- a/frontend/src/dashboard/DashboardApp.jsx +++ b/frontend/src/dashboard/DashboardApp.jsx @@ -9,6 +9,8 @@ export function DashboardApp() { const isAdminView = debugViewRole === "admin"; const activeUser = isAdminView ? debugAdminDashboardData.user : debugUserDashboardData.user; const userDashboard = debugUserDashboardData.dashboard; + const userAuth = debugUserDashboardData.auth; + const allowedActions = debugUserDashboardData.allowed_actions; const status = userDashboard?.status || "none"; const keyInfo = userDashboard?.key_info || null; @@ -25,7 +27,7 @@ export function DashboardApp() {
-

Dashboard

+

ACM UTD Dashboard Skeleton

- Debug-only dashboard preview with static data. + Debug-only UI preview for Cloudflare Pages frontend. All sections use static fixtures under src/dashboard/debug.

@@ -56,15 +58,24 @@ export function DashboardApp() { Signed in as {activeUser.email || ""} )} + + Frontend auth target: Firebase ID token + Google OAuth (@utdallas.edu) +
{!isAdminView && ( + {active ? activeLabel : inactiveLabel} + + ); +} + export function UserDashboardSection({ + user, + auth, hasActiveKey, registrationMode, keyInfo, + usage, generatedKey, + expiryNotifications, + hackutdMode, + allowedActions, loading, requestOrRegenerateKey, revokeKey }) { - return ( -
-
-

Key Overview

- - {hasActiveKey ? "active" : "inactive"} - -
- -

Registration mode: {registrationMode}

+ const endpointBreakdown = usage?.endpoint_breakdown || []; + const recentRequests = usage?.recent_requests || []; - {keyInfo && ( -
+ return ( + <> +
+
-
Masked API key
-
{keyInfo.masked_key || "-"}
+

Authentication & Eligibility

+

Google OAuth via Firebase Auth with UTD domain enforcement.

+ +
+
-
Rate limit
-
{String(keyInfo.rate_limit || "-")}
+
Signed in user
+
{user?.name || user?.email || "-"}
-
-
Window seconds
-
{String(keyInfo.window_seconds || "-")}
+
Email domain policy
+
@{auth?.allowed_domain || "-"}
-
-
Usage count
-
{String(keyInfo.usage_count || 0)}
+
OAuth provider
+
{user?.auth_provider || "google.com"}
+
+
+
Registration mode
+
{registrationMode}
- )} +
+ +
+
+
+

Key Management

+ +
+ + Limit: {String(allowedActions?.max_keys_per_user || 1)} key per user + +
- {generatedKey && ( -
-

New key generated. This is shown once:

-
-            {generatedKey}
-          
+
+
+

Masked key

+

{keyInfo?.masked_key || "-"}

+
+
+

Created

+

{formatDate(keyInfo?.created_at)}

+
+
+

Expires

+

{formatDate(keyInfo?.expires_at)}

+
+
+

Key type

+

{keyInfo?.key_type || "standard"}

+
+
+ +
+
+

Request New Key

+

Provide identity from OAuth and a short label/description for admin review.

+
+
+ + +
+
+ + +
+

UI skeleton only: wired for POST /dashboard/key in production.

+
+
+ +
+

Lifecycle Rules

+
    +
  • Semester policy: keys expire after the current semester.
  • +
  • HackUTD mode auto-approves and sets expiry to the day after the event.
  • +
  • Regeneration is atomic: old key deactivated before new key issuance.
  • +
  • Revoke removes access immediately through DELETE /dashboard/key.
  • +
+
+ HackUTD mode: {hackutdMode?.enabled ? "enabled" : "disabled"} | forced expiry {formatDate(hackutdMode?.forced_expiry_at)} +
+
- )} -
- - {keyInfo && ( + {generatedKey && ( +
+

New key generated. This full value is shown once:

+
+              {generatedKey}
+            
+
+ )} + +
+ - )} -
-
+
+ + +
+

Expiry Warning Email

+
+
+

Email warnings

+

{expiryNotifications?.email_warning_enabled ? "enabled" : "disabled"}

+
+
+

Warning windows

+

{(expiryNotifications?.warning_windows_days || []).join(", ")} days

+
+
+

Next warning

+

{formatDate(expiryNotifications?.next_warning_at)}

+
+
+
+ +
+
+

Usage Statistics (Stretch Goal UI)

+ + Static debug data + +
+ +
+
+

Total requests

+

{String(usage?.total_requests || 0)}

+
+
+

Last used

+

{formatDate(usage?.last_used_at)}

+
+
+

Top endpoint

+

+ {endpointBreakdown.length ? endpointBreakdown[0].endpoint : "-"} +

+
+
+ +
+
+

Most Frequent Endpoints

+
    + {endpointBreakdown.map((entry) => ( +
  • +
    + {entry.endpoint} + {entry.requests} req +
    +
    +
    +
    +
  • + ))} +
+
+ +
+

Recent Requests

+
+ + + + + + + + + + {recentRequests.map((request, index) => ( + + + + + + ))} + +
TimestampEndpointStatus
{formatDate(request.at)}{request.endpoint} + = 400 ? "bg-rose-100 text-rose-700" : "bg-emerald-100 text-emerald-700" + }`} + > + {String(request.status)} + +
+
+
+
+
+ ); } diff --git a/frontend/src/dashboard/debug/adminDashboardData.js b/frontend/src/dashboard/debug/adminDashboardData.js index 73bbc40..f818285 100644 --- a/frontend/src/dashboard/debug/adminDashboardData.js +++ b/frontend/src/dashboard/debug/adminDashboardData.js @@ -1,6 +1,19 @@ export const debugAdminDashboardData = { user: { - email: "debug-admin@local.test" + uid: "debug-admin-001", + name: "Priya Officer", + email: "officer@utdallas.edu", + is_admin: true + }, + auth: { + oauth_provider: "google.com", + admin_gate_source: "firestore:isAdmin", + admin_gate_status: "granted", + domain_restriction: "utdallas.edu" + }, + approval: { + mode: "manual", + global_toggle_hint: "New key requests require officer approval when manual mode is active" }, requests: [ { @@ -8,32 +21,152 @@ export const debugAdminDashboardData = { email: "student1@utdallas.edu", requested_at: "2026-03-25T18:30:00.000Z", request_type: "new", - status: "pending" + status: "pending", + label: "Senior design API", + description: "Need course/professor data for capstone dashboard", + requested_key_type: "standard" }, { uid: "debug-user-002", email: "student2@utdallas.edu", requested_at: "2026-03-25T17:15:00.000Z", request_type: "regenerate", - status: "pending" + status: "pending", + label: "HackUTD team service", + description: "Token leaked in logs; requesting urgent rotation", + requested_key_type: "standard" } ], keys: [ { - uid: "debug-user-010", + uid: "debug-user-010-key-a", email: "activeuser@utdallas.edu", + label: "Course recommendation microservice", + type: "standard", masked_key: "acm_****_debug", status: "active", usage_count: 42, + created_at: "2026-01-10T13:00:00.000Z", + expires_at: "2026-05-14T23:59:59.000Z", + last_used_at: "2026-03-25T19:05:00.000Z", updated_at: "2026-03-25T19:05:00.000Z" }, { - uid: "debug-user-011", + uid: "debug-user-011-key-a", email: "inactiveuser@utdallas.edu", + label: "Old analytics script", + type: "standard", masked_key: "acm_****_old", status: "inactive", usage_count: 3, + created_at: "2025-09-05T08:00:00.000Z", + expires_at: "2026-01-15T23:59:59.000Z", + last_used_at: "2026-01-03T08:10:00.000Z", updated_at: "2026-03-24T08:10:00.000Z" + }, + { + uid: "debug-admin-001-key-admin", + email: "officer@utdallas.edu", + label: "Officer emergency admin key", + type: "admin", + masked_key: "acm_****_admin", + status: "active", + usage_count: 8, + created_at: "2026-02-01T12:00:00.000Z", + expires_at: "2026-08-31T23:59:59.000Z", + last_used_at: "2026-03-25T20:20:00.000Z", + updated_at: "2026-03-25T20:20:00.000Z" + } + ], + manual_token_form: { + label_placeholder: "e.g. Internal ETL token", + expiry_presets: ["End of semester", "30 days", "Custom date"], + key_type_options: ["standard", "admin"] + }, + server: { + ec2_instance_id: "i-0abc123def4567890", + state: "running", + instance_type: "t3.large", + public_ip: "34.130.220.91", + uptime_human: "2d 14h 21m", + hackutd_mode_enabled: true, + hackutd_actions: [ + "stop instance", + "set instance type to t3.large", + "start instance" + ], + normal_mode_actions: [ + "stop instance", + "set instance type to t3.micro", + "start instance" + ] + }, + scraperPipeline: { + last_run_at: "2026-03-25T18:40:00.000Z", + status: "success", + courses_uploaded: 1342, + merge_match_rate: "96.2%", + run_steps: ["scrape", "merge", "upload-dev"], + latest_errors: [] + }, + analytics: { + requests_per_day: [ + { day: "Mon", count: 1200, errors: 12 }, + { day: "Tue", count: 1420, errors: 14 }, + { day: "Wed", count: 1380, errors: 9 }, + { day: "Thu", count: 1690, errors: 22 }, + { day: "Fri", count: 1580, errors: 11 }, + { day: "Sat", count: 830, errors: 6 }, + { day: "Sun", count: 760, errors: 5 } + ], + by_token: [ + { label: "Course recommendation microservice", requests: 610 }, + { label: "HackUTD team service", requests: 482 }, + { label: "Officer emergency admin key", requests: 122 } + ], + by_endpoint: [ + { endpoint: "/v1/courses/search", requests: 790 }, + { endpoint: "/v1/grades", requests: 410 }, + { endpoint: "/v1/professors", requests: 380 }, + { endpoint: "/v1/terms", requests: 134 } + ], + error_rate_percent: 1.4 + }, + scraperLogs: [ + { + time: "2026-03-25T18:41:20.000Z", + level: "info", + message: "Pipeline completed. Uploaded 1342 courses to dev dataset." + }, + { + time: "2026-03-25T18:40:31.000Z", + level: "info", + message: "Merge stage complete. Match rate 96.2%." + }, + { + time: "2026-03-25T18:37:02.000Z", + level: "info", + message: "Scraper stage complete. 11 college files updated." + } + ], + cronLogs: [ + { + time: "2026-03-25T12:00:00.000Z", + tokens_checked: 401, + valid_count: 387, + action: "no-op" + }, + { + time: "2026-03-24T12:00:00.000Z", + tokens_checked: 389, + valid_count: 380, + action: "started" + }, + { + time: "2026-03-23T12:00:00.000Z", + tokens_checked: 384, + valid_count: 369, + action: "stopped" } ] }; diff --git a/frontend/src/dashboard/debug/userDashboardData.js b/frontend/src/dashboard/debug/userDashboardData.js index d88ab40..0f27278 100644 --- a/frontend/src/dashboard/debug/userDashboardData.js +++ b/frontend/src/dashboard/debug/userDashboardData.js @@ -1,16 +1,74 @@ export const debugUserDashboardData = { user: { - email: "debug-user@local.test" + uid: "debug-user-001", + name: "Alex Morgan", + email: "alex.morgan@utdallas.edu", + photo_url: "https://i.pravatar.cc/96?img=12", + auth_provider: "google.com", + domain_restricted: true + }, + auth: { + oauth_enabled: true, + allowed_domain: "utdallas.edu", + firebase_token_status: "valid" }, dashboard: { status: "active", - mode: "open", + mode: "manual-approval", + semester_expiry_policy: "current-semester", + hackutd_mode: { + enabled: true, + auto_approve: true, + ends_at: "2026-11-16T05:00:00.000Z", + forced_expiry_at: "2026-11-17T17:00:00.000Z" + }, + request_form: { + default_key_type: "standard", + name_placeholder: "e.g. ACM Discord bot", + description_placeholder: "Short description for admins" + }, key_info: { masked_key: "acm_****_debug", + full_key_shown_once: "acm_live_visible_once_x7f9v1", + created_at: "2026-01-19T20:12:00.000Z", + expires_at: "2026-05-14T23:59:59.000Z", + last_rotated_at: "2026-02-28T16:04:00.000Z", + key_type: "standard", rate_limit: 120, window_seconds: 60, - usage_count: 7 + usage_count: 743, + is_admin_key: false, + request_description: "Used for class project API integration" + }, + usage: { + total_requests: 743, + last_used_at: "2026-03-25T17:02:10.000Z", + endpoint_breakdown: [ + { endpoint: "/v1/courses/search", requests: 322 }, + { endpoint: "/v1/professors", requests: 216 }, + { endpoint: "/v1/grades", requests: 141 }, + { endpoint: "/v1/terms", requests: 64 } + ], + recent_requests: [ + { at: "2026-03-25T16:58:11.000Z", endpoint: "/v1/courses/search", status: 200 }, + { at: "2026-03-25T16:58:02.000Z", endpoint: "/v1/courses/search", status: 200 }, + { at: "2026-03-25T16:57:31.000Z", endpoint: "/v1/grades", status: 429 }, + { at: "2026-03-25T16:55:10.000Z", endpoint: "/v1/professors", status: 200 }, + { at: "2026-03-25T16:52:40.000Z", endpoint: "/v1/terms", status: 200 } + ] + }, + expiry_notifications: { + email_warning_enabled: true, + warning_windows_days: [14, 7, 1], + next_warning_at: "2026-05-01T15:00:00.000Z" } }, - generatedKey: "acm_debug_generated_key_for_ui_preview" + generatedKey: "acm_debug_generated_key_for_ui_preview", + allowed_actions: { + can_request_new_key: true, + can_regenerate_key: true, + can_revoke_key: true, + max_keys_per_user: 1, + admin_override_allows_multiple: true + } }; From e3b0c27cefb53cff4c83590d1ccfd2bd358375a9 Mon Sep 17 00:00:00 2001 From: AllenZheng-05 Date: Wed, 25 Mar 2026 17:47:12 -0500 Subject: [PATCH 03/27] added everything in the user page to also the admin page --- frontend/src/dashboard/DashboardApp.jsx | 66 ++++++++++++++++++------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/frontend/src/dashboard/DashboardApp.jsx b/frontend/src/dashboard/DashboardApp.jsx index 4b02ad7..cfc8084 100644 --- a/frontend/src/dashboard/DashboardApp.jsx +++ b/frontend/src/dashboard/DashboardApp.jsx @@ -7,11 +7,14 @@ import { debugAdminDashboardData } from "./debug/adminDashboardData"; export function DashboardApp() { const [debugViewRole, setDebugViewRole] = useState("user"); const isAdminView = debugViewRole === "admin"; - const activeUser = isAdminView ? debugAdminDashboardData.user : debugUserDashboardData.user; const userDashboard = debugUserDashboardData.dashboard; const userAuth = debugUserDashboardData.auth; const allowedActions = debugUserDashboardData.allowed_actions; + // Admin can view user dashboard for their own account + admin-specific features + const activeUser = isAdminView ? debugAdminDashboardData.user : debugUserDashboardData.user; + const adminAsUser = isAdminView ? debugAdminDashboardData.user : null; + const status = userDashboard?.status || "none"; const keyInfo = userDashboard?.key_info || null; const hasActiveKey = status === "active"; @@ -55,7 +58,7 @@ export function DashboardApp() { - Signed in as {activeUser.email || ""} + Signed in as {activeUser.email || ""} {isAdminView && "(admin account)"} )} @@ -83,22 +86,49 @@ export function DashboardApp() { )} {isAdminView && ( - + <> +
+

Your User Dashboard

+

View and manage your own keys as a user would, even while having admin privileges.

+
+ + +
+

Admin Controls & Monitoring

+

Officer-only operations for key approvals, server management, and analytics.

+
+ + + )} ); From a5bfc346f9c0620ee019ddef8976ac10851623b1 Mon Sep 17 00:00:00 2001 From: AllenZheng-05 Date: Wed, 25 Mar 2026 20:05:55 -0500 Subject: [PATCH 04/27] changed frontend domain restrictions --- .../src/dashboard/AdminDashboardSection.jsx | 31 +++++++++++++++++-- frontend/src/dashboard/DashboardApp.jsx | 28 +++++++++++------ .../src/dashboard/UserDashboardSection.jsx | 8 +++-- .../src/dashboard/debug/adminDashboardData.js | 17 ++++++---- .../src/dashboard/debug/userDashboardData.js | 5 +-- 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/frontend/src/dashboard/AdminDashboardSection.jsx b/frontend/src/dashboard/AdminDashboardSection.jsx index a6a4e00..f1ebf8b 100644 --- a/frontend/src/dashboard/AdminDashboardSection.jsx +++ b/frontend/src/dashboard/AdminDashboardSection.jsx @@ -41,7 +41,7 @@ export function AdminDashboardSection({

Admin Access & Policy

-

Officer access is gated by Firestore isAdmin and Google OAuth domain rules.

+

Officer access is gated by Firestore isAdmin with Google OAuth. Account creation is open to all domains.

{auth?.admin_gate_status || "granted"} @@ -58,8 +58,8 @@ export function AdminDashboardSection({
{auth?.oauth_provider || "google.com"}
-
Domain lock
-
@{auth?.domain_restriction || "utdallas.edu"}
+
Account domain policy
+
{auth?.domain_restriction === "none" ? "Open sign-up (all domains)" : `@${auth?.domain_restriction}`}
Approval mode
@@ -81,6 +81,31 @@ export function AdminDashboardSection({

{approval?.global_toggle_hint}

+
+

Domain Auto-Approve Rules

+

Configure which request domains bypass manual review.

+
+ + + +
+
+
diff --git a/frontend/src/dashboard/DashboardApp.jsx b/frontend/src/dashboard/DashboardApp.jsx index cfc8084..9c28303 100644 --- a/frontend/src/dashboard/DashboardApp.jsx +++ b/frontend/src/dashboard/DashboardApp.jsx @@ -30,14 +30,22 @@ export function DashboardApp() {
-

ACM UTD Dashboard Skeleton

- - {isAdminView ? "admin" : "user"} - +

ACM API Dashboard Skeleton

+
+ + Go to Documentation + + + {isAdminView ? "admin" : "user"} + +

Debug-only UI preview for Cloudflare Pages frontend. All sections use static fixtures under src/dashboard/debug. @@ -62,7 +70,7 @@ export function DashboardApp() { )} - Frontend auth target: Firebase ID token + Google OAuth (@utdallas.edu) + Frontend auth target: Firebase ID token + Google OAuth (open sign-up; key domain groups for approvals)

@@ -93,7 +101,7 @@ export function DashboardApp() {

Authentication & Eligibility

-

Google OAuth via Firebase Auth with UTD domain enforcement.

+

Google OAuth via Firebase Auth with open sign-up for all account domains.

@@ -53,7 +53,7 @@ export function UserDashboardSection({
Email domain policy
-
@{auth?.allowed_domain || "-"}
+
{auth?.open_signup ? "Open sign-up (all domains)" : "Restricted"}
OAuth provider
@@ -64,6 +64,10 @@ export function UserDashboardSection({
{registrationMode}
+ +

+ Important domains for approval policy: {(auth?.recognized_domains || []).map((domain) => `@${domain}`).join(", ") || "-"} +

diff --git a/frontend/src/dashboard/debug/adminDashboardData.js b/frontend/src/dashboard/debug/adminDashboardData.js index f818285..12e53b3 100644 --- a/frontend/src/dashboard/debug/adminDashboardData.js +++ b/frontend/src/dashboard/debug/adminDashboardData.js @@ -1,19 +1,24 @@ export const debugAdminDashboardData = { user: { uid: "debug-admin-001", - name: "Priya Officer", - email: "officer@utdallas.edu", + name: "Allen Zheng", + email: "allen.zheng@acmutd.co", is_admin: true }, auth: { oauth_provider: "google.com", admin_gate_source: "firestore:isAdmin", admin_gate_status: "granted", - domain_restriction: "utdallas.edu" + domain_restriction: "none" }, approval: { mode: "manual", - global_toggle_hint: "New key requests require officer approval when manual mode is active" + global_toggle_hint: "New key requests require officer approval when manual mode is active unless the request domain is auto-approved.", + auto_approve_by_domain: { + acmutd_co: true, + utdallas_edu: false, + other_domains: false + } }, requests: [ { @@ -66,7 +71,7 @@ export const debugAdminDashboardData = { }, { uid: "debug-admin-001-key-admin", - email: "officer@utdallas.edu", + email: "officer@acmutd.co", label: "Officer emergency admin key", type: "admin", masked_key: "acm_****_admin", @@ -89,7 +94,7 @@ export const debugAdminDashboardData = { instance_type: "t3.large", public_ip: "34.130.220.91", uptime_human: "2d 14h 21m", - hackutd_mode_enabled: true, + hackutd_mode_enabled: false, hackutd_actions: [ "stop instance", "set instance type to t3.large", diff --git a/frontend/src/dashboard/debug/userDashboardData.js b/frontend/src/dashboard/debug/userDashboardData.js index 0f27278..3b19c33 100644 --- a/frontend/src/dashboard/debug/userDashboardData.js +++ b/frontend/src/dashboard/debug/userDashboardData.js @@ -5,11 +5,12 @@ export const debugUserDashboardData = { email: "alex.morgan@utdallas.edu", photo_url: "https://i.pravatar.cc/96?img=12", auth_provider: "google.com", - domain_restricted: true + domain_restricted: false }, auth: { oauth_enabled: true, - allowed_domain: "utdallas.edu", + open_signup: true, + recognized_domains: ["acmutd.co", "utdallas.edu"], firebase_token_status: "valid" }, dashboard: { From 1a368ad3ecc739b4ca2cc017b26a3b2b017acdfd Mon Sep 17 00:00:00 2001 From: AllenZheng-05 Date: Tue, 31 Mar 2026 14:12:49 -0500 Subject: [PATCH 05/27] commit before completely redoing the dashboard --- frontend/src/dashboard/debug/adminDashboardData.js | 2 +- internal/server/router/router.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/dashboard/debug/adminDashboardData.js b/frontend/src/dashboard/debug/adminDashboardData.js index 12e53b3..4884ad9 100644 --- a/frontend/src/dashboard/debug/adminDashboardData.js +++ b/frontend/src/dashboard/debug/adminDashboardData.js @@ -91,7 +91,7 @@ export const debugAdminDashboardData = { server: { ec2_instance_id: "i-0abc123def4567890", state: "running", - instance_type: "t3.large", + instance_type: "t3.micro", public_ip: "34.130.220.91", uptime_human: "2d 14h 21m", hackutd_mode_enabled: false, diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 28972e3..339ee07 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -13,7 +13,7 @@ func New(handler *handlers.Handler, mw *middleware.Manager) http.Handler { router := gin.Default() router.Static("/assets", "frontend/dist/assets") - router.GET("/", handler.Default) + router.GET("/swagger", handler.Default) router.GET("/openapi.yaml", handler.OpenAPISpec) router.GET("/health", handler.Health) router.GET("/dashboard", handler.UserDashboardPage) From ce14f087e0c81d40f96301e7d0c1a2a7a44b2e12 Mon Sep 17 00:00:00 2001 From: AllenZheng-05 Date: Tue, 31 Mar 2026 16:46:29 -0500 Subject: [PATCH 06/27] completely redid the user and admin dashboard frontend skeleton --- frontend/README.md | 37 +- .../dashboard/dashboard.html => index.html} | 9 +- frontend/src/App.tsx | 71 + .../src/components/layout/PortalLayout.tsx | 147 +++ frontend/src/components/ui/Badge.tsx | 26 + frontend/src/components/ui/Button.tsx | 28 + frontend/src/components/ui/Card.tsx | 23 + frontend/src/components/ui/Field.tsx | 40 + frontend/src/components/ui/Modal.tsx | 34 + .../src/dashboard/AdminDashboardSection.jsx | 413 ------ frontend/src/dashboard/DashboardApp.jsx | 143 -- .../src/dashboard/UserDashboardSection.jsx | 270 ---- .../src/dashboard/debug/adminDashboardData.js | 177 --- .../src/dashboard/debug/userDashboardData.js | 75 -- frontend/src/dashboard/main.jsx | 10 - frontend/src/dashboard/tailwind.css | 14 - frontend/src/lib/api.ts | 375 ++++++ frontend/src/lib/constants.ts | 9 + frontend/src/lib/format.ts | 27 + frontend/src/lib/mockData.ts | 346 +++++ frontend/src/main.tsx | 19 + frontend/src/pages/AdminDashboardPage.tsx | 1165 +++++++++++++++++ frontend/src/pages/LandingPage.tsx | 62 + frontend/src/pages/NotFoundPage.tsx | 20 + frontend/src/pages/UserDashboardPage.tsx | 434 ++++++ frontend/src/state/AuthContext.tsx | 82 ++ frontend/src/state/ToastContext.tsx | 50 + frontend/src/styles.css | 26 + frontend/src/types/models.ts | 160 +++ frontend/src/vite-env.d.ts | 1 + frontend/vite.config.js | 11 +- internal/server/router/router.go | 15 +- 32 files changed, 3185 insertions(+), 1134 deletions(-) rename frontend/{src/dashboard/dashboard.html => index.html} (50%) create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/layout/PortalLayout.tsx create mode 100644 frontend/src/components/ui/Badge.tsx create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Card.tsx create mode 100644 frontend/src/components/ui/Field.tsx create mode 100644 frontend/src/components/ui/Modal.tsx delete mode 100644 frontend/src/dashboard/AdminDashboardSection.jsx delete mode 100644 frontend/src/dashboard/DashboardApp.jsx delete mode 100644 frontend/src/dashboard/UserDashboardSection.jsx delete mode 100644 frontend/src/dashboard/debug/adminDashboardData.js delete mode 100644 frontend/src/dashboard/debug/userDashboardData.js delete mode 100644 frontend/src/dashboard/main.jsx delete mode 100644 frontend/src/dashboard/tailwind.css create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/constants.ts create mode 100644 frontend/src/lib/format.ts create mode 100644 frontend/src/lib/mockData.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/AdminDashboardPage.tsx create mode 100644 frontend/src/pages/LandingPage.tsx create mode 100644 frontend/src/pages/NotFoundPage.tsx create mode 100644 frontend/src/pages/UserDashboardPage.tsx create mode 100644 frontend/src/state/AuthContext.tsx create mode 100644 frontend/src/state/ToastContext.tsx create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/types/models.ts create mode 100644 frontend/src/vite-env.d.ts diff --git a/frontend/README.md b/frontend/README.md index d34ca93..f5b10b2 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,13 +1,28 @@ -# Frontend Dashboard (Vite + Tailwind) +# ACM UTD API Portal Frontend (Vite + React + TypeScript) -This folder contains the frontend pages served by the Go API. +This frontend is a pure mocked skeleton of the ACM UTD API Portal. -## Pages +## Routes -- Dashboard entry: `src/dashboard/dashboard.html` -> served at `/dashboard` and `/admin` -- Docs landing page: `src/swagger/swagger.html` -> served at `/` by the backend +- `/` Landing/auth stub +- `/dashboard` User dashboard +- `/admin` Admin dashboard (mock admin-only) -The dashboard page uses Tailwind stylesheet at `src/dashboard/tailwind.css`. +## Stack + +- Vite +- React 18 +- TypeScript +- React Router +- Tailwind utility classes + +## Mock architecture + +- Mock Firestore-shaped models: `src/types/models.ts` +- In-memory mock documents: `src/lib/mockData.ts` +- Async no-op/mock API layer: `src/lib/api.ts` + +All API integration points return promises and include artificial delay for realistic UI behavior. ## Development @@ -16,16 +31,10 @@ npm install npm run dev ``` -## Production Build +## Production build ```bash -npm install npm run build ``` -Build output is written to `frontend/dist`. - -The backend expects built files in: - -- `frontend/dist/src/dashboard/dashboard.html` -- `frontend/dist/assets/*` +Build output is generated in `frontend/dist`. diff --git a/frontend/src/dashboard/dashboard.html b/frontend/index.html similarity index 50% rename from frontend/src/dashboard/dashboard.html rename to frontend/index.html index 2bd101e..d21a00c 100644 --- a/frontend/src/dashboard/dashboard.html +++ b/frontend/index.html @@ -3,13 +3,10 @@ - ACM UTD Dashboard + ACM UTD API Portal - - +
- + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..c59bae4 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,71 @@ +import { Navigate, Route, Routes } from "react-router-dom"; +import { LandingPage } from "./pages/LandingPage"; +import { UserDashboardPage } from "./pages/UserDashboardPage"; +import { AdminDashboardPage } from "./pages/AdminDashboardPage"; +import { NotFoundPage } from "./pages/NotFoundPage"; +import { useAuth } from "./state/AuthContext"; + +function RequireAuth({ children }: { children: JSX.Element }) { + const { currentUser, loading } = useAuth(); + + if (loading) { + return ( +
+ Loading session... +
+ ); + } + + if (!currentUser) { + return ; + } + + return children; +} + +function RequireAdmin({ children }: { children: JSX.Element }) { + const { currentUser, loading } = useAuth(); + + if (loading) { + return ( +
+ Loading session... +
+ ); + } + + if (!currentUser) { + return ; + } + + if (!currentUser.isAdmin) { + return ; + } + + return children; +} + +export function App() { + return ( + + } /> + + + + } + /> + + + + } + /> + } /> + + ); +} diff --git a/frontend/src/components/layout/PortalLayout.tsx b/frontend/src/components/layout/PortalLayout.tsx new file mode 100644 index 0000000..e7a1591 --- /dev/null +++ b/frontend/src/components/layout/PortalLayout.tsx @@ -0,0 +1,147 @@ +import { Link, useNavigate } from "react-router-dom"; +import { AppConfig, User } from "../../types/models"; +import { Button } from "../ui/Button"; +import { useState } from "react"; + +interface NavItem { + key: string; + label: string; +} + +export function PortalLayout({ + user, + appConfig, + title, + navItems, + activeTab, + onTabChange, + onSignOut, + children, +}: { + user: User; + appConfig: AppConfig; + title: string; + navItems: NavItem[]; + activeTab: string; + onTabChange: (tab: string) => void; + onSignOut: () => Promise; + children: React.ReactNode; +}) { + const [menuOpen, setMenuOpen] = useState(false); + const navigate = useNavigate(); + + return ( +
+ + +
+
+
+ + ACM UTD API + +

{title}

+
+ +
+ + Semester: {appConfig.currentSemester} + + + {appConfig.instanceType} + + {appConfig.hackutdModeEnabled && ( + + HackUTD + + )} +
+ +
+ + + {menuOpen && ( +
+
+

+ {user.displayName} + + {user.isAdmin ? "Admin" : "User"} + +

+

{user.email}

+ +

+ Status: {user.approvalStatus} +

+
+ +
+ )} +
+
+ +
+
+ {navItems.map((item) => ( + + ))} +
+
+ +
+ {children} +
+
+
+ ); +} diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx new file mode 100644 index 0000000..d46a487 --- /dev/null +++ b/frontend/src/components/ui/Badge.tsx @@ -0,0 +1,26 @@ +import { APIKeyStatus } from "../../types/models"; + +export function StatusBadge({ + status, +}: { + status: APIKeyStatus | "running" | "stopped" | "success" | "error"; +}) { + const classes: Record = { + active: "bg-emerald-100 text-emerald-700", + inactive: "bg-slate-200 text-slate-700", + pending: "bg-amber-100 text-amber-700", + rejected: "bg-red-100 text-red-700", + running: "bg-emerald-100 text-emerald-700", + stopped: "bg-slate-200 text-slate-700", + success: "bg-emerald-100 text-emerald-700", + error: "bg-red-100 text-red-700", + }; + + return ( + + {status} + + ); +} diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..e6b0274 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,28 @@ +import { ButtonHTMLAttributes } from "react"; + +type Variant = "primary" | "secondary" | "danger" | "ghost"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: Variant; +} + +const variantClasses: Record = { + primary: "bg-slate-900 text-white hover:bg-slate-700", + secondary: + "bg-white text-slate-900 border border-slate-300 hover:bg-slate-100", + danger: "bg-red-600 text-white hover:bg-red-500", + ghost: "bg-transparent text-slate-700 hover:bg-slate-100", +}; + +export function Button({ + variant = "primary", + className = "", + ...props +}: ButtonProps) { + return ( +