add fast-jwt ( not tested ) and make the user actual register
All checks were successful
Code quality checks / biome (push) Successful in 8s

This commit is contained in:
creations 2025-05-02 18:41:10 -04:00
parent f93cef442a
commit 57fb8d8bb1
Signed by: creations
GPG key ID: 8F553AA4320FC711
10 changed files with 227 additions and 27 deletions

View file

@ -24,17 +24,30 @@ const cassandra: CassandraConfig = {
authEnabled: process.env.CASSANDRA_AUTH_ENABLED === "false",
};
const jwt: JWTConfig = {
secret: process.env.JWT_SECRET || "",
expiration: process.env.JWT_EXPIRATION || "1h",
issuer: process.env.JWT_ISSUER || "",
algorithm: process.env.JWT_ALGORITHM || "HS256",
};
function verifyRequiredVariables(): void {
const requiredVariables = [
"HOST",
"PORT",
"REDIS_URL",
"REDIS_TTL",
"CASSANDRA_HOST",
"CASSANDRA_PORT",
"CASSANDRA_CONTACT_POINTS",
"CASSANDRA_AUTH_ENABLED",
"CASSANDRA_DATACENTER",
"JWT_SECRET",
"JWT_EXPIRATION",
"JWT_ISSUER",
];
let hasError = false;
@ -52,4 +65,4 @@ function verifyRequiredVariables(): void {
}
}
export { environment, cassandra, redisTtl, verifyRequiredVariables };
export { environment, cassandra, redisTtl, verifyRequiredVariables, jwt };

View file

@ -22,6 +22,7 @@
"dependencies": {
"@creations.works/logger": "^1.0.3",
"cassandra-driver": "^4.8.0",
"fast-jwt": "^6.0.1",
"pika-id": "^1.1.3"
}
}

121
src/lib/jwt.ts Normal file
View file

@ -0,0 +1,121 @@
import { environment, jwt } from "@config/environment";
import { redis } from "bun";
import { createDecoder, createSigner, createVerifier } from "fast-jwt";
const signer = createSigner({ key: jwt.secret, expiresIn: jwt.expiration });
const verifier = createVerifier({ key: jwt.secret });
const decoder = createDecoder();
export async function createSession(
payload: UserSession,
userAgent: string,
): Promise<string> {
const token = signer(payload);
const sessionKey = `session:${payload.id}:${token}`;
await redis.set(sessionKey, JSON.stringify({ ...payload, userAgent }));
await redis.expire(sessionKey, getExpirationInSeconds());
return generateCookie(token);
}
export async function getSession(
request: Request,
): Promise<UserSession | null> {
const token = extractToken(request);
if (!token) return null;
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) return null;
const raw = await redis.get(keys[0]);
return raw ? JSON.parse(raw) : null;
}
export async function updateSession(
request: Request,
payload: UserSession,
userAgent: string,
): Promise<string> {
const token = extractToken(request);
if (!token) throw new Error("Session token not found");
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) throw new Error("Session not found or expired");
await redis.set(keys[0], JSON.stringify({ ...payload, userAgent }));
await redis.expire(keys[0], getExpirationInSeconds());
return generateCookie(token);
}
export async function verifySession(token: string): Promise<UserSession> {
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) throw new Error("Session not found or expired");
return verifier(token);
}
export async function decodeSession(token: string): Promise<UserSession> {
return decoder(token);
}
export async function invalidateSession(request: Request): Promise<void> {
const token = extractToken(request);
if (!token) return;
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) return;
await redis.del(keys[0]);
}
// helpers
function extractToken(request: Request): string | null {
return request.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1] || null;
}
function generateCookie(
token: string,
maxAge = getExpirationInSeconds(),
options?: {
secure?: boolean;
httpOnly?: boolean;
sameSite?: "Strict" | "Lax" | "None";
path?: string;
domain?: string;
},
): string {
const {
secure = !environment.development,
httpOnly = true,
sameSite = environment.development ? "Lax" : "None",
path = "/",
domain,
} = options || {};
let cookie = `session=${encodeURIComponent(token)}; Path=${path}; Max-Age=${maxAge}`;
if (httpOnly) cookie += "; HttpOnly";
if (secure) cookie += "; Secure";
if (sameSite) cookie += `; SameSite=${sameSite}`;
if (domain) cookie += `; Domain=${domain}`;
return cookie;
}
function getExpirationInSeconds(): number {
const match = jwt.expiration.match(/^(\d+)([smhd])$/);
if (!match) throw new Error("Invalid expiresIn format in jwt config");
const [, value, unit] = match;
const num = Number(value);
switch (unit) {
case "s":
return num;
case "m":
return num * 60;
case "h":
return num * 3600;
case "d":
return num * 86400;
default:
throw new Error("Invalid time unit in expiresIn");
}
}
export const sessionManager = {
createSession,
getSession,
updateSession,
verifySession,
decodeSession,
invalidateSession,
};

0
src/routes/user/login.ts Normal file
View file

View file

View file

@ -1,3 +1,4 @@
import { logger } from "@creations.works/logger";
import { cassandra } from "@lib/cassandra";
import { jsonResponse } from "@lib/http";
import { pika } from "@lib/pika";
@ -6,6 +7,7 @@ import {
isValidPassword,
isValidUsername,
} from "@lib/validators";
import type { Client } from "cassandra-driver";
const routeDef: RouteDef = {
method: "POST",
@ -19,9 +21,9 @@ async function handler(
requestBody: unknown,
): Promise<Response> {
const { username, password, email } = requestBody as {
username?: string;
password?: string;
email?: string;
username: string;
password: string;
email: string;
};
const fields = { username, password, email };
@ -52,17 +54,23 @@ async function handler(
});
}
const usernameResult = await cassandra
.getClient()
.execute("SELECT id FROM users WHERE username = ?", [username], {
prepare: true,
});
const cassandraClient: Client = cassandra.getClient();
const emailResult = await cassandra
.getClient()
.execute("SELECT id FROM users WHERE email = ?", [email], {
const usernameResult = await cassandraClient.execute(
"SELECT id FROM users WHERE username = ?",
[username],
{
prepare: true,
});
},
);
const emailResult = await cassandraClient.execute(
"SELECT id FROM users WHERE email = ?",
[email],
{
prepare: true,
},
);
if (usernameResult.rowLength > 0 || emailResult.rowLength > 0) {
const errorMessages: string[] = [];
@ -80,14 +88,48 @@ async function handler(
});
}
const userId = pika.gen("user");
const user: UserInsert = {
id: pika.gen("user"),
username,
email,
password: await Bun.password.hash(password),
created_at: new Date(),
updated_at: new Date(),
};
return jsonResponse(200, {
try {
await cassandraClient.execute(
"INSERT INTO users (id, username, email, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
[
user.id,
user.username,
user.email,
user.password,
user.created_at,
user.updated_at,
],
{ prepare: true },
);
logger.custom(
"[REGISTER]",
`(${user.id})`,
`${user.username} - ${user.email}`,
"34",
);
} catch (error) {
return jsonResponse(500, {
message: "Internal server error",
error: "Failed to register user",
});
}
return jsonResponse(201, {
message: "User registered successfully",
data: {
username,
email,
id: userId,
id: user.id,
username: user.username,
email: user.email,
},
});
}

7
types/config.d.ts vendored
View file

@ -14,3 +14,10 @@ type CassandraConfig = {
contactPoints: string[];
authEnabled: boolean;
};
type JWTConfig = {
secret: string;
expiration: string;
issuer: string;
algorithm: string;
};

9
types/http.d.ts vendored
View file

@ -4,3 +4,12 @@ interface GenericJsonResponseOptions {
error?: string;
[key: string]: unknown;
}
interface UserSession {
id: string;
username: string;
email: string;
is_verified: boolean;
iat?: number; // issued at (added by JWT libs)
exp?: number; // expiration (added by JWT libs)
}

9
types/logger.d.ts vendored
View file

@ -1,9 +0,0 @@
type ILogMessagePart = { value: string; color: string };
type ILogMessageParts = {
level: ILogMessagePart;
filename: ILogMessagePart;
readableTimestamp: ILogMessagePart;
message: ILogMessagePart;
[key: string]: ILogMessagePart;
};

16
types/tables/user.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
type User = {
id: string;
username: string;
display_name: string;
email: string;
password: string;
avatar_url: string;
is_verified: boolean;
created_at: Date;
updated_at: Date;
};
type UserInsert = Pick<
User,
"id" | "username" | "email" | "password" | "created_at" | "updated_at"
>;