cedarstalking with keyboard shortcuts
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}