Add authentication #17

Manually merged
Webmaster merged 225 commits from auth into main 2025-03-08 10:37:06 +00:00
2 changed files with 575 additions and 505 deletions
Showing only changes of commit 6df69275b9 - Show all commits

View file

@ -6,6 +6,12 @@ interface BaseRecord {
[key: string]: any; [key: string]: any;
} }
// Interface for request options
interface RequestOptions {
fields?: string[];
disableAutoCancellation?: boolean;
}
export class Get { export class Get {
private auth: Authentication; private auth: Authentication;
private static instance: Get; private static instance: Get;
@ -28,13 +34,13 @@ export class Get {
* Get a single record by ID * Get a single record by ID
* @param collectionName The name of the collection * @param collectionName The name of the collection
* @param recordId The ID of the record to retrieve * @param recordId The ID of the record to retrieve
* @param fields Optional array of fields to select * @param options Optional request options including fields to select and auto-cancellation control
* @returns The requested record * @returns The requested record
*/ */
public async getOne<T extends BaseRecord>( public async getOne<T extends BaseRecord>(
collectionName: string, collectionName: string,
recordId: string, recordId: string,
fields?: string[] options?: RequestOptions,
): Promise<T> { ): Promise<T> {
if (!this.auth.isAuthenticated()) { if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records"); throw new Error("User must be authenticated to retrieve records");
@ -42,8 +48,13 @@ export class Get {
try { try {
const pb = this.auth.getPocketBase(); const pb = this.auth.getPocketBase();
const options = fields ? { fields: fields.join(",") } : undefined; const requestOptions = {
return await pb.collection(collectionName).getOne<T>(recordId, options); ...(options?.fields && { fields: options.fields.join(",") }),
...(options?.disableAutoCancellation && { requestKey: null }),
};
return await pb
.collection(collectionName)
.getOne<T>(recordId, requestOptions);
} catch (err) { } catch (err) {
console.error(`Failed to get record from ${collectionName}:`, err); console.error(`Failed to get record from ${collectionName}:`, err);
throw err; throw err;
@ -54,13 +65,13 @@ export class Get {
* Get multiple records by their IDs * Get multiple records by their IDs
* @param collectionName The name of the collection * @param collectionName The name of the collection
* @param recordIds Array of record IDs to retrieve * @param recordIds Array of record IDs to retrieve
* @param fields Optional array of fields to select * @param options Optional request options including fields to select and auto-cancellation control
* @returns Array of requested records * @returns Array of requested records
*/ */
public async getMany<T extends BaseRecord>( public async getMany<T extends BaseRecord>(
collectionName: string, collectionName: string,
recordIds: string[], recordIds: string[],
fields?: string[] options?: RequestOptions,
): Promise<T[]> { ): Promise<T[]> {
if (!this.auth.isAuthenticated()) { if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records"); throw new Error("User must be authenticated to retrieve records");
@ -69,16 +80,19 @@ export class Get {
try { try {
const pb = this.auth.getPocketBase(); const pb = this.auth.getPocketBase();
const filter = `id ?~ "${recordIds.join("|")}"`; const filter = `id ?~ "${recordIds.join("|")}"`;
const options = { const requestOptions = {
filter, filter,
...(fields && { fields: fields.join(",") }) ...(options?.fields && { fields: options.fields.join(",") }),
...(options?.disableAutoCancellation && { requestKey: null }),
}; };
const result = await pb.collection(collectionName).getFullList<T>(options); const result = await pb
.collection(collectionName)
.getFullList<T>(requestOptions);
// Sort results to match the order of requested IDs // Sort results to match the order of requested IDs
const recordMap = new Map(result.map(record => [record.id, record])); const recordMap = new Map(result.map((record) => [record.id, record]));
return recordIds.map(id => recordMap.get(id)).filter(Boolean) as T[]; return recordIds.map((id) => recordMap.get(id)).filter(Boolean) as T[];
} catch (err) { } catch (err) {
console.error(`Failed to get records from ${collectionName}:`, err); console.error(`Failed to get records from ${collectionName}:`, err);
throw err; throw err;
@ -92,7 +106,7 @@ export class Get {
* @param perPage Number of items per page * @param perPage Number of items per page
* @param filter Optional filter string * @param filter Optional filter string
* @param sort Optional sort string * @param sort Optional sort string
* @param fields Optional array of fields to select * @param options Optional request options including fields to select and auto-cancellation control
* @returns Paginated list of records * @returns Paginated list of records
*/ */
public async getList<T extends BaseRecord>( public async getList<T extends BaseRecord>(
@ -101,7 +115,7 @@ export class Get {
perPage: number = 20, perPage: number = 20,
filter?: string, filter?: string,
sort?: string, sort?: string,
fields?: string[] options?: RequestOptions,
): Promise<{ ): Promise<{
page: number; page: number;
perPage: number; perPage: number;
@ -115,20 +129,23 @@ export class Get {
try { try {
const pb = this.auth.getPocketBase(); const pb = this.auth.getPocketBase();
const options = { const requestOptions = {
...(filter && { filter }), ...(filter && { filter }),
...(sort && { sort }), ...(sort && { sort }),
...(fields && { fields: fields.join(",") }) ...(options?.fields && { fields: options.fields.join(",") }),
...(options?.disableAutoCancellation && { requestKey: null }),
}; };
const result = await pb.collection(collectionName).getList<T>(page, perPage, options); const result = await pb
.collection(collectionName)
.getList<T>(page, perPage, requestOptions);
return { return {
page: result.page, page: result.page,
perPage: result.perPage, perPage: result.perPage,
totalItems: result.totalItems, totalItems: result.totalItems,
totalPages: result.totalPages, totalPages: result.totalPages,
items: result.items items: result.items,
}; };
} catch (err) { } catch (err) {
console.error(`Failed to get list from ${collectionName}:`, err); console.error(`Failed to get list from ${collectionName}:`, err);
@ -141,14 +158,14 @@ export class Get {
* @param collectionName The name of the collection * @param collectionName The name of the collection
* @param filter Optional filter string * @param filter Optional filter string
* @param sort Optional sort string * @param sort Optional sort string
* @param fields Optional array of fields to select * @param options Optional request options including fields to select and auto-cancellation control
* @returns Array of all matching records * @returns Array of all matching records
*/ */
public async getAll<T extends BaseRecord>( public async getAll<T extends BaseRecord>(
collectionName: string, collectionName: string,
filter?: string, filter?: string,
sort?: string, sort?: string,
fields?: string[] options?: RequestOptions,
): Promise<T[]> { ): Promise<T[]> {
if (!this.auth.isAuthenticated()) { if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records"); throw new Error("User must be authenticated to retrieve records");
@ -156,13 +173,14 @@ export class Get {
try { try {
const pb = this.auth.getPocketBase(); const pb = this.auth.getPocketBase();
const options = { const requestOptions = {
...(filter && { filter }), ...(filter && { filter }),
...(sort && { sort }), ...(sort && { sort }),
...(fields && { fields: fields.join(",") }) ...(options?.fields && { fields: options.fields.join(",") }),
...(options?.disableAutoCancellation && { requestKey: null }),
}; };
return await pb.collection(collectionName).getFullList<T>(options); return await pb.collection(collectionName).getFullList<T>(requestOptions);
} catch (err) { } catch (err) {
console.error(`Failed to get all records from ${collectionName}:`, err); console.error(`Failed to get all records from ${collectionName}:`, err);
throw err; throw err;
@ -173,13 +191,13 @@ export class Get {
* Get the first record that matches a filter * Get the first record that matches a filter
* @param collectionName The name of the collection * @param collectionName The name of the collection
* @param filter Filter string * @param filter Filter string
* @param fields Optional array of fields to select * @param options Optional request options including fields to select and auto-cancellation control
* @returns The first matching record or null if none found * @returns The first matching record or null if none found
*/ */
public async getFirst<T extends BaseRecord>( public async getFirst<T extends BaseRecord>(
collectionName: string, collectionName: string,
filter: string, filter: string,
fields?: string[] options?: RequestOptions,
): Promise<T | null> { ): Promise<T | null> {
if (!this.auth.isAuthenticated()) { if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records"); throw new Error("User must be authenticated to retrieve records");
@ -187,14 +205,17 @@ export class Get {
try { try {
const pb = this.auth.getPocketBase(); const pb = this.auth.getPocketBase();
const options = { const requestOptions = {
filter, filter,
...(fields && { fields: fields.join(",") }), ...(options?.fields && { fields: options.fields.join(",") }),
...(options?.disableAutoCancellation && { requestKey: null }),
sort: "created", sort: "created",
perPage: 1 perPage: 1,
}; };
const result = await pb.collection(collectionName).getList<T>(1, 1, options); const result = await pb
.collection(collectionName)
.getList<T>(1, 1, requestOptions);
return result.items.length > 0 ? result.items[0] : null; return result.items.length > 0 ? result.items[0] : null;
} catch (err) { } catch (err) {
console.error(`Failed to get first record from ${collectionName}:`, err); console.error(`Failed to get first record from ${collectionName}:`, err);

View file

@ -63,10 +63,7 @@ const majorsList: string[] = allMajors
>Select your current major</span >Select your current major</span
> >
</label> </label>
<select <select id="majorSelect" class="select select-bordered w-full">
id="majorSelect"
class="select select-bordered w-full"
>
<option value="">Select your major</option> <option value="">Select your major</option>
{ {
majorsList.map((major: string) => ( majorsList.map((major: string) => (
@ -79,17 +76,12 @@ const majorsList: string[] = allMajors
<!-- Graduation Year --> <!-- Graduation Year -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium" <span class="label-text font-medium">Expected Graduation</span>
>Expected Graduation</span
>
<span class="label-text-alt text-base-content/70" <span class="label-text-alt text-base-content/70"
>When do you plan to graduate?</span >When do you plan to graduate?</span
> >
</label> </label>
<select <select id="gradYearSelect" class="select select-bordered w-full">
id="gradYearSelect"
class="select select-bordered w-full"
>
<option value="">Select graduation year</option> <option value="">Select graduation year</option>
{ {
Array.from({ length: 6 }, (_, i) => { Array.from({ length: 6 }, (_, i) => {
@ -123,9 +115,7 @@ const majorsList: string[] = allMajors
<!-- IEEE Member ID --> <!-- IEEE Member ID -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium" <span class="label-text font-medium">IEEE Member ID</span>
>IEEE Member ID</span
>
<span class="label-text-alt text-base-content/70" <span class="label-text-alt text-base-content/70"
>Your IEEE membership number</span >Your IEEE membership number</span
> >
@ -171,27 +161,20 @@ const majorsList: string[] = allMajors
> >
No resume uploaded No resume uploaded
</p> </p>
<p <p class="text-xs text-base-content/70">
class="text-xs text-base-content/70"
>
PDF, DOC, or DOCX PDF, DOC, or DOCX
</p> </p>
</div> </div>
</div> </div>
<div class="flex-none flex gap-2"> <div class="flex-none flex gap-2">
<button <button id="previewResume" class="btn btn-sm btn-ghost">
id="previewResume"
class="btn btn-sm btn-ghost"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4" class="h-4 w-4"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
> >
<path <path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
d="M10 12a2 2 0 100-4 2 2 0 000 4z"
></path>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
@ -215,8 +198,7 @@ const majorsList: string[] = allMajors
<!-- Upload Status --> <!-- Upload Status -->
<div class="text-sm"> <div class="text-sm">
<span id="uploadStatus" class="label-text-alt" <span id="uploadStatus" class="label-text-alt"></span>
></span>
</div> </div>
</div> </div>
</div> </div>
@ -238,24 +220,26 @@ const majorsList: string[] = allMajors
import { Update } from "../pocketbase/Update"; import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog"; import { SendLog } from "../pocketbase/SendLog";
import { FileManager } from "../pocketbase/FileManager"; import { FileManager } from "../pocketbase/FileManager";
import { Get } from "../pocketbase/Get";
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const update = Update.getInstance(); const update = Update.getInstance();
const logger = SendLog.getInstance(); const logger = SendLog.getInstance();
const fileManager = FileManager.getInstance(); const fileManager = FileManager.getInstance();
const get = Get.getInstance();
// Get form elements // Get form elements
const memberIdInput = document.getElementById( const memberIdInput = document.getElementById(
"memberIdInput" "memberIdInput",
) as HTMLInputElement; ) as HTMLInputElement;
const majorSelect = document.getElementById( const majorSelect = document.getElementById(
"majorSelect" "majorSelect",
) as HTMLSelectElement; ) as HTMLSelectElement;
const gradYearSelect = document.getElementById( const gradYearSelect = document.getElementById(
"gradYearSelect" "gradYearSelect",
) as HTMLSelectElement; ) as HTMLSelectElement;
const resumeUpload = document.getElementById( const resumeUpload = document.getElementById(
"resumeUpload" "resumeUpload",
) as HTMLInputElement; ) as HTMLInputElement;
const currentResume = document.getElementById("currentResume"); const currentResume = document.getElementById("currentResume");
const uploadStatus = document.getElementById("uploadStatus"); const uploadStatus = document.getElementById("uploadStatus");
@ -277,28 +261,88 @@ const majorsList: string[] = allMajors
}; };
// Load current user data // Load current user data
const loadUserData = () => { const loadUserData = async () => {
if (auth.isAuthenticated()) { if (auth.isAuthenticated()) {
const user = auth.getCurrentUser(); const user = auth.getCurrentUser();
if (user) { if (user) {
if (memberIdInput) memberIdInput.value = user.member_id || ""; try {
if (majorSelect) majorSelect.value = user.major || ""; // Get full user data with auto-cancellation disabled
const userData = await get.getOne("users", user.id, {
disableAutoCancellation: true,
});
console.log("User major from server:", userData.major);
if (memberIdInput) memberIdInput.value = userData.member_id || "";
if (majorSelect) {
const serverMajor = userData.major?.trim();
console.log("Setting major select to:", serverMajor);
// Get all available options with their normalized values
const availableOptions = Array.from(majorSelect.options).map(
(opt) => ({
element: opt,
normalizedValue: opt.value.trim(),
displayValue: opt.textContent?.trim() || opt.value.trim(),
}),
);
console.log(
"Available options:",
availableOptions.map((o) => o.normalizedValue),
);
// First try exact match
let matchingOption = availableOptions.find(
(opt) => opt.normalizedValue === serverMajor,
);
// If no exact match, try case-insensitive match
if (!matchingOption && serverMajor) {
const serverMajorLower = serverMajor.toLowerCase();
matchingOption = availableOptions.find(
(opt) => opt.normalizedValue.toLowerCase() === serverMajorLower,
);
}
// If still no match, try partial match
if (!matchingOption && serverMajor) {
const serverMajorLower = serverMajor.toLowerCase();
matchingOption = availableOptions.find(
(opt) =>
opt.normalizedValue
.toLowerCase()
.includes(serverMajorLower) ||
serverMajorLower.includes(opt.normalizedValue.toLowerCase()),
);
}
if (matchingOption) {
console.log(
"Found matching major:",
matchingOption.normalizedValue,
);
majorSelect.value = matchingOption.normalizedValue;
} else {
console.log("No matching major found for:", serverMajor);
majorSelect.value = "";
}
}
if (gradYearSelect) if (gradYearSelect)
gradYearSelect.value = user.graduation_year || ""; gradYearSelect.value = userData.graduation_year || "";
// Update resume display // Update resume display
if (currentResume && resumeDisplay) { if (currentResume && resumeDisplay) {
if (user.resume) { if (userData.resume) {
const fileName = user.resume.toString(); const fileName = userData.resume.toString();
currentResume.textContent = currentResume.textContent = fileName || "Resume uploaded";
fileName || "Resume uploaded";
resumeDisplay.classList.remove("hidden"); resumeDisplay.classList.remove("hidden");
// Get the file URL from PocketBase // Get the file URL from PocketBase
const resumeUrl = fileManager.getFileUrl( const resumeUrl = fileManager.getFileUrl(
"users", "users",
user.id, userData.id,
fileName fileName,
); );
// Update preview button to use new modal // Update preview button to use new modal
@ -315,7 +359,7 @@ const majorsList: string[] = allMajors
type: getFileType(fileName), type: getFileType(fileName),
}, },
}, },
} },
); );
window.dispatchEvent(showFileViewerEvent); window.dispatchEvent(showFileViewerEvent);
}; };
@ -325,6 +369,14 @@ const majorsList: string[] = allMajors
resumeDisplay.classList.add("hidden"); resumeDisplay.classList.add("hidden");
} }
} }
} catch (err) {
console.error("Failed to load user data:", err);
await logger.send(
"error",
"profile settings",
`Failed to load user data: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
} }
} }
}; };
@ -339,22 +391,17 @@ const majorsList: string[] = allMajors
const user = auth.getCurrentUser(); const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated"); if (!user) throw new Error("User not authenticated");
await fileManager.uploadFile( await fileManager.uploadFile("users", user.id, "resume", file);
"users",
user.id,
"resume",
file
);
uploadStatus.textContent = "Resume uploaded successfully"; uploadStatus.textContent = "Resume uploaded successfully";
// Refresh the user data to show the new resume // Refresh the user data to show the new resume
loadUserData(); await loadUserData();
// Log successful resume upload // Log successful resume upload
await logger.send( await logger.send(
"update", "update",
"resume upload", "resume upload",
`Successfully uploaded resume: ${file.name}` `Successfully uploaded resume: ${file.name}`,
); );
} catch (err) { } catch (err) {
console.error("Resume upload error:", err); console.error("Resume upload error:", err);
@ -364,7 +411,7 @@ const majorsList: string[] = allMajors
await logger.send( await logger.send(
"error", "error",
"resume upload", "resume upload",
`Failed to upload resume: ${file.name}. Error: ${err instanceof Error ? err.message : "Unknown error"}` `Failed to upload resume: ${file.name}. Error: ${err instanceof Error ? err.message : "Unknown error"}`,
); );
} }
} }
@ -381,10 +428,13 @@ const majorsList: string[] = allMajors
const user = auth.getCurrentUser(); const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated"); if (!user) throw new Error("User not authenticated");
// Get current user data for comparison
const currentUserData = await get.getOne("users", user.id);
const oldData = { const oldData = {
major: user.major || null, major: currentUserData.major || null,
graduation_year: user.graduation_year || null, graduation_year: currentUserData.graduation_year || null,
member_id: user.member_id || null, member_id: currentUserData.member_id || null,
}; };
const newData = { const newData = {
@ -401,17 +451,17 @@ const majorsList: string[] = allMajors
const changes = []; const changes = [];
if (oldData.major !== newData.major) { if (oldData.major !== newData.major) {
changes.push( changes.push(
`Major: "${oldData.major || "none"}" → "${newData.major || "none"}"` `Major: "${oldData.major || "none"}" → "${newData.major || "none"}"`,
); );
} }
if (oldData.graduation_year !== newData.graduation_year) { if (oldData.graduation_year !== newData.graduation_year) {
changes.push( changes.push(
`Graduation Year: "${oldData.graduation_year || "none"}" → "${newData.graduation_year || "none"}"` `Graduation Year: "${oldData.graduation_year || "none"}" → "${newData.graduation_year || "none"}"`,
); );
} }
if (oldData.member_id !== newData.member_id) { if (oldData.member_id !== newData.member_id) {
changes.push( changes.push(
`IEEE Member ID: "${oldData.member_id || "none"}" → "${newData.member_id || "none"}"` `IEEE Member ID: "${oldData.member_id || "none"}" → "${newData.member_id || "none"}"`,
); );
} }
@ -419,7 +469,7 @@ const majorsList: string[] = allMajors
await logger.send( await logger.send(
"update", "update",
"profile settings", "profile settings",
`Updated profile settings:\n${changes.join("\n")}` `Updated profile settings:\n${changes.join("\n")}`,
); );
} }
@ -438,8 +488,8 @@ const majorsList: string[] = allMajors
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000); setTimeout(() => toast.remove(), 3000);
// Only update form data without affecting visibility // Refresh the form data
loadUserData(); await loadUserData();
// Ensure settings view stays visible // Ensure settings view stays visible
const defaultView = document.getElementById("defaultView"); const defaultView = document.getElementById("defaultView");
@ -456,8 +506,7 @@ const majorsList: string[] = allMajors
attempted_major: majorSelect?.value || "none", attempted_major: majorSelect?.value || "none",
attempted_graduation_year: gradYearSelect?.value || "none", attempted_graduation_year: gradYearSelect?.value || "none",
attempted_member_id: memberIdInput?.value || "none", attempted_member_id: memberIdInput?.value || "none",
error_message: error_message: err instanceof Error ? err.message : "Unknown error",
err instanceof Error ? err.message : "Unknown error",
}; };
await logger.send( await logger.send(
@ -467,7 +516,7 @@ const majorsList: string[] = allMajors
`Major: ${errorDetails.attempted_major}\n` + `Major: ${errorDetails.attempted_major}\n` +
`Graduation Year: ${errorDetails.attempted_graduation_year}\n` + `Graduation Year: ${errorDetails.attempted_graduation_year}\n` +
`IEEE Member ID: ${errorDetails.attempted_member_id}\n` + `IEEE Member ID: ${errorDetails.attempted_member_id}\n` +
`Error: ${errorDetails.error_message}` `Error: ${errorDetails.error_message}`,
); );
// Show error toast // Show error toast
@ -491,11 +540,11 @@ const majorsList: string[] = allMajors
} }
// Load initial data // Load initial data
loadUserData(); await loadUserData();
// Update when auth state changes // Update when auth state changes
auth.onAuthStateChange(() => { auth.onAuthStateChange(async () => {
loadUserData(); await loadUserData();
}); });
</script> </script>