cedarstalking with keyboard shortcuts
at main 205 lines 6.1 kB view raw
1const BASE_URL = "https://selfservice.cedarville.edu"; 2 3export interface DirectoryPerson { 4 Id: string; 5 isFaculty?: boolean; // resolved from Info/Json; heuristic until confirmed 6 Username: string; 7 FirstName: string; 8 LastName: string; 9 MiddleName: string | null; 10 Nickname: string | null; 11 AddressCity: string | null; 12 AddressState: string | null; 13 AddressCountry: string | null; 14 DepartmentDescription: string | null; 15 Title: string | null; 16 OfficeBuildingCode: string | null; 17 OfficeBuildingName: string | null; 18 OfficeRoom: string | null; 19 OfficePhone: string | null; 20 DormCode: string | null; 21 DormName: string | null; 22 DormRoom: string | null; 23 StudentType: string | null; 24 StudentClass: string | null; 25 studentWorker: boolean | null; 26 empInactive: boolean | null; 27 PhotoUrl: string | null; 28} 29 30export class AuthRequiredError extends Error { 31 constructor(public readonly signInUrl: string) { 32 super("Authentication required"); 33 this.name = "AuthRequiredError"; 34 } 35} 36 37function makeHeaders(cookie?: string): Record<string, string> { 38 const headers: Record<string, string> = { 39 accept: "*/*", 40 "accept-language": "en-US,en;q=0.9", 41 referer: `${BASE_URL}/cedarinfo/directory`, 42 "user-agent": 43 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", 44 }; 45 if (cookie) headers["cookie"] = cookie; 46 return headers; 47} 48 49export interface ScheduleItem { 50 title: string; 51 description: string; 52 startTime: string; 53 endTime: string; 54 day: string; 55 type: string; 56} 57 58export interface Term { 59 code: string; 60 desc: string; 61 start: string; 62 end: string; 63} 64 65export interface PersonInfo { 66 faculty: { 67 isFaculty: boolean; 68 facultyDepts: { code: string; description: string; division: string }[]; 69 scheduleItems: ScheduleItem[]; 70 term: { key: string | null; description: string | null }; 71 }; 72 student: { 73 isStudent: boolean; 74 scheduleItems: ScheduleItem[]; 75 programs: { key: string; title: string }[]; 76 majors: { code: string; desc: string }[]; 77 minors: { code: string; desc: string }[]; 78 concentrations: { code: string; desc: string }[]; 79 advisors: { 80 advisor: { id: string; name: string; email: string }; 81 advisement: { type: string }; 82 }[]; 83 term: { key: string | null; description: string | null }; 84 nonScheduledCourses: { code: string; title: string; methods: string }[]; 85 }; 86 person: { 87 box: string | null; 88 }; 89 address: { 90 addresslines: string[]; 91 } | null; 92} 93 94export interface Department { 95 code: string; 96 description: string; 97} 98 99export interface Population { 100 code: number; 101 desc: string; 102} 103 104export async function getDepartments(cookie?: string): Promise<Department[]> { 105 const url = `${BASE_URL}/CedarInfo/Directory/DepartmentJson`; 106 const res = await fetch(url, { headers: makeHeaders(cookie) }); 107 if (!res.ok || !res.headers.get("content-type")?.includes("json")) return []; 108 try { 109 const data = await res.json(); 110 return Array.isArray(data) ? data : []; 111 } catch { 112 return []; 113 } 114} 115 116export async function getPopulations(cookie?: string): Promise<Population[]> { 117 const url = `${BASE_URL}/CedarInfo/Directory/PopulationsJson`; 118 const res = await fetch(url, { headers: makeHeaders(cookie) }); 119 if (!res.ok || !res.headers.get("content-type")?.includes("json")) return []; 120 try { 121 const data = await res.json(); 122 return Array.isArray(data) ? data : []; 123 } catch { 124 return []; 125 } 126} 127 128export async function getPersonTerms( 129 id: string, 130 cookie: string, 131): Promise<Term[]> { 132 const url = `${BASE_URL}/CedarInfo/Json/GetTerms?id=${id}&past=5&future=2&summer=true`; 133 const res = await fetch(url, { headers: makeHeaders(cookie) }); 134 if (!res.ok || !res.headers.get("content-type")?.includes("json")) return []; 135 try { 136 const data = await res.json(); 137 return Array.isArray(data) ? data : []; 138 } catch { 139 return []; 140 } 141} 142 143export async function getPersonInfo( 144 id: string, 145 term: string, 146 cookie: string, 147): Promise<PersonInfo | null> { 148 const url = `${BASE_URL}/CedarInfo/Info/Json?id=${id}&term=${term}`; 149 const res = await fetch(url, { headers: makeHeaders(cookie) }); 150 if (!res.ok || !res.headers.get("content-type")?.includes("json")) 151 return null; 152 try { 153 const data = await res.json(); 154 return data as PersonInfo; 155 } catch { 156 return null; 157 } 158} 159 160export async function searchDirectory( 161 firstName: string, 162 lastName: string, 163 cookie?: string, 164 options?: { department?: string; population?: number }, 165): Promise<DirectoryPerson[]> { 166 const params = new URLSearchParams(); 167 if (firstName) params.set("FirstNameSearch", firstName); 168 if (lastName) params.set("LastNameSearch", lastName); 169 if (options?.department) params.set("Department", options.department); 170 if (options?.population != null) 171 params.set("PopulationSearch", String(options.population)); 172 173 const apiUrl = `${BASE_URL}/CedarInfo/Directory/SearchResultsJson?${params.toString()}`; 174 const response = await fetch(apiUrl, { headers: makeHeaders(cookie) }); 175 176 // Unauthenticated: server redirects us to SSO. fetch follows by default, 177 // so we end up at a non-selfservice URL. 178 const landedOutside = !response.url.includes("selfservice.cedarville.edu"); 179 if (landedOutside || response.status === 401 || response.status === 403) { 180 const signInUrl = landedOutside 181 ? response.url 182 : `${BASE_URL}/cedarinfo/directory`; 183 throw new AuthRequiredError(signInUrl); 184 } 185 186 if (!response.ok) { 187 throw new Error( 188 `Request failed: ${response.status} ${response.statusText}`, 189 ); 190 } 191 192 const data = await response.json(); 193 194 // Server can also return JSON with a redirect URL instead of an array 195 if (!Array.isArray(data)) { 196 const signInUrl = 197 (data as Record<string, unknown>)?.signInUrl ?? 198 (data as Record<string, unknown>)?.SignInUrl ?? 199 (data as Record<string, unknown>)?.redirectUrl; 200 if (typeof signInUrl === "string") throw new AuthRequiredError(signInUrl); 201 throw new AuthRequiredError(`${BASE_URL}/cedarinfo/directory`); 202 } 203 204 return data as DirectoryPerson[]; 205}