From 0cb7ebb245927182210be7010038e89c14cc0631 Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 4 May 2025 08:59:36 -0400 Subject: [PATCH] add user info, add table drop for dev env, fix invite route --- config/setup/index.ts | 14 ++-- config/setup/tables/guilds/index.ts | 16 ++++- config/setup/tables/guilds/invites.ts | 10 ++- config/setup/tables/guilds/members.ts | 14 +++- config/setup/tables/users.ts | 16 ++++- config/setup/teardown.ts | 64 ++++++++++++++++++ package.json | 3 +- .../guild/{join[id].ts => join[invite].ts} | 7 +- src/routes/user/[id].ts | 67 +++++++++++++++++++ src/routes/user/logout.ts | 6 +- types/tables/user.d.ts | 13 ++++ 11 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 config/setup/teardown.ts rename src/routes/guild/{join[id].ts => join[invite].ts} (94%) create mode 100644 src/routes/user/[id].ts diff --git a/config/setup/index.ts b/config/setup/index.ts index 785c933..b5d3019 100644 --- a/config/setup/index.ts +++ b/config/setup/index.ts @@ -64,14 +64,14 @@ async function setup(): Promise { } setup() - .catch((error: Error) => { + .catch((error) => { logger.error(error); process.exit(1); }) - .finally(() => { - cassandra.shutdown().catch((error: Error) => { - logger.error(["Error shutting down Cassandra client:", error]); - }); - - process.exit(0); + .finally(async () => { + try { + await cassandra.shutdown(); + } catch (error) { + logger.error(["Error shutting down Cassandra client:", error as Error]); + } }); diff --git a/config/setup/tables/guilds/index.ts b/config/setup/tables/guilds/index.ts index f7ea061..2d1cba8 100644 --- a/config/setup/tables/guilds/index.ts +++ b/config/setup/tables/guilds/index.ts @@ -13,7 +13,7 @@ async function createTable() { system_channel_id TEXT, rules_channel_id TEXT, - anouncements_channel_id TEXT, + announcements_channel_id TEXT, created_at TIMESTAMP, updated_at TIMESTAMP @@ -31,4 +31,16 @@ async function createTable() { `); } -export { createTable }; +async function dropTable() { + const client = cassandra.getClient(); + + await client.execute(` + DROP TABLE IF EXISTS guilds; + `); + + await client.execute(` + DROP TABLE IF EXISTS guilds_by_owner; + `); +} + +export { createTable, dropTable }; diff --git a/config/setup/tables/guilds/invites.ts b/config/setup/tables/guilds/invites.ts index 67a5c32..7477f4d 100644 --- a/config/setup/tables/guilds/invites.ts +++ b/config/setup/tables/guilds/invites.ts @@ -17,4 +17,12 @@ async function createTable() { `); } -export { createTable }; +async function dropTable() { + const client = cassandra.getClient(); + + await client.execute(` + DROP TABLE IF EXISTS guild_invites; + `); +} + +export { createTable, dropTable }; diff --git a/config/setup/tables/guilds/members.ts b/config/setup/tables/guilds/members.ts index 694aba2..90318dd 100644 --- a/config/setup/tables/guilds/members.ts +++ b/config/setup/tables/guilds/members.ts @@ -29,4 +29,16 @@ async function createTable() { `); } -export { createTable }; +async function dropTable() { + const client = cassandra.getClient(); + + await client.execute(` + DROP TABLE IF EXISTS guild_members; + `); + + await client.execute(` + DROP TABLE IF EXISTS members_by_user; + `); +} + +export { createTable, dropTable }; diff --git a/config/setup/tables/users.ts b/config/setup/tables/users.ts index e7dbb95..838228a 100644 --- a/config/setup/tables/users.ts +++ b/config/setup/tables/users.ts @@ -24,4 +24,18 @@ async function createTable() { `); } -export { createTable }; +async function dropTable() { + await cassandra.getClient().execute(` + DROP TABLE IF EXISTS users; + `); + + await cassandra.getClient().execute(` + DROP INDEX IF EXISTS users_username_idx; + `); + + await cassandra.getClient().execute(` + DROP INDEX IF EXISTS users_email_idx; + `); +} + +export { createTable, dropTable }; diff --git a/config/setup/teardown.ts b/config/setup/teardown.ts new file mode 100644 index 0000000..96406ee --- /dev/null +++ b/config/setup/teardown.ts @@ -0,0 +1,64 @@ +import { readdir, stat } from "node:fs/promises"; +import { extname, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { cassandra as cassandraConfig, verifyRequiredVariables } from "@config"; +import { logger } from "@creations.works/logger"; +import { cassandra } from "@lib/cassandra"; + +async function dropTables(dir: string): Promise { + const entries = await readdir(dir); + + for (const entry of entries) { + const fullPath = join(dir, entry); + const stats = await stat(fullPath); + + if (stats.isDirectory()) { + await dropTables(fullPath); + continue; + } + + if (extname(entry) !== ".ts") continue; + + const modulePath = pathToFileURL(fullPath).href; + const mod = await import(modulePath); + + if (typeof mod.dropTable === "function") { + await mod.dropTable(); + logger.info(`Ran dropTable from ${fullPath}`); + } else { + logger.warn(`No dropTable export found in ${fullPath}`); + } + } +} + +async function teardown(): Promise { + verifyRequiredVariables(); + await cassandra.connect({ withKeyspace: true }); + + const keyspace = cassandraConfig.keyspace; + + if (!keyspace) { + logger.error("No Cassandra keyspace configured in environment."); + process.exit(1); + } + + logger.info(`Dropping all tables in keyspace "${keyspace}"...`); + + const tablesDir = resolve("config", "setup", "tables"); + await dropTables(tablesDir); + + logger.info("Teardown complete."); +} + +teardown() + .catch((error) => { + logger.error(error); + process.exit(1); + }) + .finally(async () => { + try { + await cassandra.shutdown(); + } catch (error) { + logger.error(["Error shutting down Cassandra client:", error as Error]); + } + }); diff --git a/package.json b/package.json index 215861a..8bf88ec 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", "cleanup": "rm -rf logs node_modules bun.lockdb", - "setup": "bun run config/setup/index.ts" + "setup": "bun run config/setup/index.ts", + "teardown": "bun run config/setup/teardown.ts" }, "devDependencies": { "@types/bun": "^1.2.11", diff --git a/src/routes/guild/join[id].ts b/src/routes/guild/join[invite].ts similarity index 94% rename from src/routes/guild/join[id].ts rename to src/routes/guild/join[invite].ts index b441b76..e2ce865 100644 --- a/src/routes/guild/join[id].ts +++ b/src/routes/guild/join[invite].ts @@ -18,7 +18,7 @@ async function handler(request: ExtendedRequest): Promise { }); } - const { id: invite } = request.params; + const { invite } = request.params; if (!invite) { return jsonResponse(400, { @@ -88,7 +88,7 @@ async function handler(request: ExtendedRequest): Promise { `INSERT INTO guild_members ( guild_id, user_id, roles, joined_at, is_banned, invite_id ) VALUES (?, ?, ?, ?, ?, ?)`, - [guildId, user.id, ["member"], now, false, invite], + [guildId, user.id, [], now, false, invite], { prepare: true }, ); @@ -96,7 +96,7 @@ async function handler(request: ExtendedRequest): Promise { `INSERT INTO members_by_user ( user_id, guild_id, roles, joined_at, is_banned, invite_id ) VALUES (?, ?, ?, ?, ?, ?)`, - [user.id, guildId, ["member"], now, false, invite], + [user.id, guildId, [], now, false, invite], { prepare: true }, ); @@ -117,7 +117,6 @@ async function handler(request: ExtendedRequest): Promise { message: "Joined guild successfully", data: { guild_id: guildId, - role: "member", }, }); } catch (error) { diff --git a/src/routes/user/[id].ts b/src/routes/user/[id].ts new file mode 100644 index 0000000..1407ef7 --- /dev/null +++ b/src/routes/user/[id].ts @@ -0,0 +1,67 @@ +import { cassandra } from "@lib/cassandra"; +import { jsonResponse } from "@lib/http"; + +function toSafeUser(user: User, sessionId?: string): UserSafe | UserPrivate { + const base: UserSafe = { + id: user.id, + username: user.username, + display_name: user.display_name, + avatar_url: user.avatar_url, + is_verified: user.is_verified, + created_at: user.created_at, + updated_at: user.updated_at, + }; + + return sessionId === user.id ? { ...base, email: user.email } : base; +} + +const routeDef: RouteDef = { + method: "GET", + accepts: "*/*", + returns: "application/json;charset=utf-8", +}; + +async function handler(request: ExtendedRequest): Promise { + const { id: userId } = request.params; + + if (!userId) { + return jsonResponse(400, { error: "Missing user ID in request." }); + } + + try { + const client = cassandra.getClient(); + const query = ` + SELECT id, username, display_name, email, avatar_url, is_verified, created_at, updated_at + FROM users + WHERE id = ? + `; + const result = await client.execute(query, [userId], { prepare: true }); + + if (result.rowLength === 0) { + return jsonResponse(404, { + error: "User not found.", + message: "No user exists with the given ID.", + }); + } + + const row = result.first(); + const user: User = { + id: row.id, + username: row.username, + display_name: row.display_name, + email: row.email, + password: "", // this is never actually used or returned + avatar_url: row.avatar_url, + is_verified: row.is_verified, + created_at: row.created_at, + updated_at: row.updated_at, + }; + + return jsonResponse(200, { user: toSafeUser(user, request.session?.id) }); + } catch (err) { + console.error(err); + return jsonResponse(500, { error: "Internal server error." }); + } +} + +export { handler, routeDef }; diff --git a/src/routes/user/logout.ts b/src/routes/user/logout.ts index cf968b7..09a4bd7 100644 --- a/src/routes/user/logout.ts +++ b/src/routes/user/logout.ts @@ -17,8 +17,7 @@ async function handler(request: ExtendedRequest): Promise { await redis.del(key); } - return new Response(null, { - status: 204, + return jsonResponse(204, { headers: { "Set-Cookie": "session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict", @@ -43,8 +42,7 @@ async function handler(request: ExtendedRequest): Promise { await sessionManager.invalidateSession(request); - return new Response(null, { - status: 204, + return jsonResponse(204, { headers: { "Set-Cookie": "session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict", diff --git a/types/tables/user.d.ts b/types/tables/user.d.ts index ac4a01b..8b7917f 100644 --- a/types/tables/user.d.ts +++ b/types/tables/user.d.ts @@ -14,3 +14,16 @@ type UserInsert = Pick< User, "id" | "username" | "email" | "password" | "created_at" | "updated_at" >; + +type UserSafe = Pick< + User, + | "id" + | "username" + | "display_name" + | "avatar_url" + | "is_verified" + | "created_at" + | "updated_at" +>; + +type UserPrivate = Omit;