add user info, add table drop for dev env, fix invite route
All checks were successful
Code quality checks / biome (push) Successful in 8s

This commit is contained in:
creations 2025-05-04 08:59:36 -04:00
parent 320e2cc121
commit 0cb7ebb245
Signed by: creations
GPG key ID: 8F553AA4320FC711
11 changed files with 209 additions and 21 deletions

View file

@ -64,14 +64,14 @@ async function setup(): Promise<void> {
}
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]);
}
});

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

64
config/setup/teardown.ts Normal file
View file

@ -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<void> {
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<void> {
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]);
}
});

View file

@ -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",

View file

@ -18,7 +18,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
});
}
const { id: invite } = request.params;
const { invite } = request.params;
if (!invite) {
return jsonResponse(400, {
@ -88,7 +88,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
`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<Response> {
`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<Response> {
message: "Joined guild successfully",
data: {
guild_id: guildId,
role: "member",
},
});
} catch (error) {

67
src/routes/user/[id].ts Normal file
View file

@ -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<Response> {
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 };

View file

@ -17,8 +17,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
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<Response> {
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",

View file

@ -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<User, "password">;