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