Skip to content

Commit b70cdda

Browse files
committed
feat(#78): add SIS deep-link API route with audit logging
1 parent 2e7b9e0 commit b70cdda

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { type NextRequest, NextResponse } from "next/server"
2+
import { mkdir, appendFile } from "fs/promises"
3+
import path from "path"
4+
import { getPool } from "@/lib/db"
5+
import type { Role } from "@/lib/roles"
6+
7+
const ALLOWED_ROLES: Role[] = ["admin", "advisor", "ir"]
8+
9+
const LOGS_DIR = path.join(process.cwd(), "logs")
10+
const LOG_FILE = path.join(LOGS_DIR, "query-history.jsonl")
11+
12+
export async function GET(
13+
request: NextRequest,
14+
{ params }: { params: Promise<{ guid: string }> }
15+
) {
16+
// Feature disabled if SIS_BASE_URL is not configured
17+
const sisBaseUrl = process.env.SIS_BASE_URL
18+
if (!sisBaseUrl) {
19+
return NextResponse.json({ url: null }, { status: 404 })
20+
}
21+
22+
// Role check
23+
const role = request.headers.get("x-user-role") as Role | null
24+
if (!role || !ALLOWED_ROLES.includes(role)) {
25+
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
26+
}
27+
28+
const { guid } = await params
29+
if (!guid) {
30+
return NextResponse.json({ error: "Missing student GUID" }, { status: 400 })
31+
}
32+
33+
try {
34+
// Look up SIS ID from mapping table
35+
const pool = getPool()
36+
const result = await pool.query(
37+
"SELECT sis_id FROM guid_sis_map WHERE student_guid = $1 LIMIT 1",
38+
[guid]
39+
)
40+
41+
if (result.rows.length === 0) {
42+
return NextResponse.json({ url: null }, { status: 404 })
43+
}
44+
45+
// Build URL server-side — SIS ID never reaches the client
46+
const sisIdParam = process.env.SIS_ID_PARAM || "id"
47+
const sisId = result.rows[0].sis_id
48+
const url = `${sisBaseUrl}?${encodeURIComponent(sisIdParam)}=${encodeURIComponent(sisId)}`
49+
50+
// Audit log — GUID and role only, never the SIS ID
51+
const logEntry = {
52+
event: "sis_link_accessed",
53+
guid,
54+
role,
55+
timestamp: new Date().toISOString(),
56+
}
57+
await mkdir(LOGS_DIR, { recursive: true })
58+
await appendFile(LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8")
59+
60+
return NextResponse.json({ url })
61+
} catch (error) {
62+
console.error("SIS link lookup error:", error)
63+
return NextResponse.json(
64+
{ error: "Failed to look up SIS link" },
65+
{ status: 500 }
66+
)
67+
}
68+
}

0 commit comments

Comments
 (0)