Skip to content

Commit abc8628

Browse files
committed
refactor(#78): simplify route, consolidate button markup, use executemany
1 parent a9d17ab commit abc8628

File tree

3 files changed

+27
-51
lines changed

3 files changed

+27
-51
lines changed

codebenders-dashboard/app/api/students/[guid]/sis-link/route.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,40 @@ import { canAccess, type Role } from "@/lib/roles"
66

77
const LOGS_DIR = path.join(process.cwd(), "logs")
88
const LOG_FILE = path.join(LOGS_DIR, "query-history.jsonl")
9+
const SIS_ID_PARAM = process.env.SIS_ID_PARAM || "id"
10+
11+
let logDirReady = false
12+
13+
function writeAuditLog(entry: Record<string, unknown>) {
14+
const doWrite = async () => {
15+
if (!logDirReady) {
16+
await mkdir(LOGS_DIR, { recursive: true })
17+
logDirReady = true
18+
}
19+
await appendFile(LOG_FILE, JSON.stringify(entry) + "\n", "utf8")
20+
}
21+
doWrite().catch(err => console.error("SIS audit log write failed:", err))
22+
}
923

1024
export async function GET(
1125
request: NextRequest,
1226
{ params }: { params: Promise<{ guid: string }> }
1327
) {
14-
// Feature disabled if SIS_BASE_URL is not configured
1528
const sisBaseUrl = process.env.SIS_BASE_URL
1629
if (!sisBaseUrl) {
1730
return NextResponse.json({ url: null }, { status: 404 })
1831
}
1932

20-
// Role check
2133
const role = request.headers.get("x-user-role") as Role | null
2234
if (!role || !canAccess("/api/students", role)) {
2335
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
2436
}
2537

2638
const { guid } = await params
27-
if (!guid) {
28-
return NextResponse.json({ error: "Missing student GUID" }, { status: 400 })
29-
}
3039

3140
let url: string
3241

3342
try {
34-
// Look up SIS ID from mapping table
3543
const pool = getPool()
3644
const result = await pool.query(
3745
"SELECT sis_id FROM guid_sis_map WHERE student_guid = $1 LIMIT 1",
@@ -42,10 +50,9 @@ export async function GET(
4250
return NextResponse.json({ url: null }, { status: 404 })
4351
}
4452

45-
// Build URL server-side — SIS ID never reaches the client
46-
const sisIdParam = process.env.SIS_ID_PARAM || "id"
53+
// SIS ID is embedded in the URL but never returned as a standalone field
4754
const sisId = result.rows[0].sis_id
48-
url = `${sisBaseUrl}?${encodeURIComponent(sisIdParam)}=${encodeURIComponent(sisId)}`
55+
url = `${sisBaseUrl}?${encodeURIComponent(SIS_ID_PARAM)}=${encodeURIComponent(sisId)}`
4956
} catch (error) {
5057
console.error("SIS link lookup error:", error)
5158
return NextResponse.json(
@@ -54,19 +61,7 @@ export async function GET(
5461
)
5562
}
5663

57-
// Audit log — GUID and role only, never the SIS ID
58-
const logEntry = {
59-
event: "sis_link_accessed",
60-
guid,
61-
role,
62-
timestamp: new Date().toISOString(),
63-
}
64-
try {
65-
await mkdir(LOGS_DIR, { recursive: true })
66-
await appendFile(LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8")
67-
} catch (auditErr) {
68-
console.error("SIS audit log write failed:", auditErr)
69-
}
64+
writeAuditLog({ event: "sis_link_accessed", guid, role, timestamp: new Date().toISOString() })
7065

7166
return NextResponse.json({ url })
7267
}

codebenders-dashboard/app/students/[guid]/page.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -217,24 +217,14 @@ export default function StudentDetailPage() {
217217
{sisStatus === "loading" && (
218218
<div className="h-7 w-24 rounded bg-muted animate-pulse" />
219219
)}
220-
{sisStatus === "available" && sisLink && (
220+
{(sisStatus === "available" || sisStatus === "unavailable") && (
221221
<Button
222222
variant="outline"
223223
size="sm"
224224
className="gap-1.5"
225-
onClick={() => window.open(sisLink, "_blank", "noopener,noreferrer")}
226-
>
227-
<ExternalLink className="h-3.5 w-3.5" />
228-
Open in SIS
229-
</Button>
230-
)}
231-
{sisStatus === "unavailable" && (
232-
<Button
233-
variant="outline"
234-
size="sm"
235-
className="gap-1.5"
236-
disabled
237-
title="No SIS record linked for this student"
225+
disabled={sisStatus === "unavailable"}
226+
title={sisStatus === "unavailable" ? "No SIS record linked for this student" : undefined}
227+
onClick={sisLink ? () => window.open(sisLink, "_blank", "noopener,noreferrer") : undefined}
238228
>
239229
<ExternalLink className="h-3.5 w-3.5" />
240230
Open in SIS

operations/seed_guid_sis_map.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ def seed_guid_sis_map():
1414
cursor = connection.cursor()
1515

1616
try:
17-
# Create table
1817
cursor.execute("""
1918
CREATE TABLE IF NOT EXISTS guid_sis_map (
2019
student_guid TEXT PRIMARY KEY,
@@ -23,7 +22,6 @@ def seed_guid_sis_map():
2322
""")
2423
print("✓ guid_sis_map table created/verified")
2524

26-
# Pick ~20 random GUIDs from student_level_with_predictions
2725
cursor.execute("""
2826
SELECT "Student_GUID"
2927
FROM student_level_with_predictions
@@ -36,23 +34,16 @@ def seed_guid_sis_map():
3634
print("✗ No students found in student_level_with_predictions")
3735
return False
3836

39-
# Clear existing demo data and insert fresh mappings
4037
cursor.execute("DELETE FROM guid_sis_map")
4138

42-
for i, guid in enumerate(guids, start=100001):
43-
sis_id = f"BSC-{i}"
44-
cursor.execute(
45-
"INSERT INTO guid_sis_map (student_guid, sis_id) VALUES (%s, %s)",
46-
(guid, sis_id)
47-
)
39+
rows = [(guid, f"BSC-{i}") for i, guid in enumerate(guids, start=100001)]
40+
cursor.executemany(
41+
"INSERT INTO guid_sis_map (student_guid, sis_id) VALUES (%s, %s)",
42+
rows
43+
)
4844

4945
connection.commit()
50-
print(f"✓ Seeded {len(guids)} GUID → SIS ID mappings (BSC-100001 .. BSC-{100000 + len(guids)})")
51-
52-
# Verify
53-
cursor.execute("SELECT COUNT(*) AS count FROM guid_sis_map")
54-
count = cursor.fetchone()['count']
55-
print(f"✓ Verified: {count} records in guid_sis_map")
46+
print(f"✓ Seeded {len(rows)} GUID → SIS ID mappings ({rows[0][1]} .. {rows[-1][1]})")
5647

5748
return True
5849

0 commit comments

Comments
 (0)