diff --git a/src/data/models.rs b/src/data/models.rs index 0f79a32..32c70e5 100644 --- a/src/data/models.rs +++ b/src/data/models.rs @@ -1,10 +1,46 @@ //! `sqlx` models for the database schema. use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use ts_rs::TS; +/// Serialize an `i64` as a string to avoid JavaScript precision loss for values exceeding 2^53. +fn serialize_i64_as_string(value: &i64, serializer: S) -> Result { + serializer.serialize_str(&value.to_string()) +} + +/// Deserialize an `i64` from either a number or a string. +fn deserialize_i64_from_string<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result { + use serde::de; + + struct I64OrStringVisitor; + + impl<'de> de::Visitor<'de> for I64OrStringVisitor { + type Value = i64; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an integer or a string containing an integer") + } + + fn visit_i64(self, value: i64) -> Result { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result { + i64::try_from(value).map_err(|_| E::custom(format!("u64 {value} out of i64 range"))) + } + + fn visit_str(self, value: &str) -> Result { + value.parse().map_err(de::Error::custom) + } + } + + deserializer.deserialize_any(I64OrStringVisitor) +} + /// Represents a meeting time stored as JSONB in the courses table. #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(export)] @@ -162,6 +198,11 @@ pub struct ScrapeJob { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct User { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string" + )] + #[ts(type = "string")] pub discord_id: i64, pub discord_username: String, pub discord_avatar_hash: Option, diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 6f12fb9..a746b3c 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -167,7 +167,7 @@ export class BannerApiClient { return this.request("/admin/users"); } - async setUserAdmin(discordId: bigint, isAdmin: boolean): Promise { + async setUserAdmin(discordId: string, isAdmin: boolean): Promise { const response = await this.fetchFn(`${this.baseUrl}/admin/users/${discordId}/admin`, { method: "PUT", headers: { "Content-Type": "application/json" }, diff --git a/web/src/routes/admin/users/+page.svelte b/web/src/routes/admin/users/+page.svelte index 74fca0d..6ce37ea 100644 --- a/web/src/routes/admin/users/+page.svelte +++ b/web/src/routes/admin/users/+page.svelte @@ -6,7 +6,7 @@ import { Shield, ShieldOff } from "@lucide/svelte"; let users = $state([]); let error = $state(null); -let updating = $state(null); +let updating = $state(null); onMount(async () => { try {