chore: Initial commit of Opencord base state

This commit is contained in:
Jannis Braun
2026-02-18 02:49:21 +01:00
commit 4fd17084a5
124 changed files with 17955 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { config } from '../config.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
export interface JwtPayload {
userId: string;
username: string;
}
export function signJwt(payload: JwtPayload): string {
const options: jwt.SignOptions = {
expiresIn: config.jwtExpiresIn as unknown as jwt.SignOptions['expiresIn'],
};
return jwt.sign(payload, config.jwtSecret, options);
}
export function verifyJwt(token: string): JwtPayload {
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
return decoded;
}
export async function authenticate(
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
reply.code(401).send({ error: 'Missing or invalid authorization header', statusCode: 401 });
return;
}
const token = authHeader.slice(7);
try {
const payload = verifyJwt(token);
(request as FastifyRequest & { userId: string; username: string }).userId = payload.userId;
(request as FastifyRequest & { userId: string; username: string }).username = payload.username;
} catch {
reply.code(401).send({ error: 'Invalid or expired token', statusCode: 401 });
}
}
declare module 'fastify' {
interface FastifyRequest {
userId: string;
username: string;
}
}
+55
View File
@@ -0,0 +1,55 @@
import { eq, and } from 'drizzle-orm';
import { getDb, schema } from '../db/index.js';
import type { MemberRole } from '@opencord/shared';
export function getMember(serverId: string, userId: string) {
const db = getDb();
return db.select().from(schema.serverMembers)
.where(and(
eq(schema.serverMembers.serverId, serverId),
eq(schema.serverMembers.userId, userId),
))
.get();
}
export function isMember(serverId: string, userId: string): boolean {
return getMember(serverId, userId) !== undefined;
}
export function getMemberRole(serverId: string, userId: string): MemberRole | null {
const member = getMember(serverId, userId);
return member ? (member.role as MemberRole) : null;
}
export function isOwner(serverId: string, userId: string): boolean {
const role = getMemberRole(serverId, userId);
return role === 'owner';
}
export function isAdmin(serverId: string, userId: string): boolean {
const role = getMemberRole(serverId, userId);
return role === 'owner' || role === 'admin';
}
export function isServerOwner(serverId: string, userId: string): boolean {
const db = getDb();
const server = db.select().from(schema.servers).where(eq(schema.servers.id, serverId)).get();
return server?.ownerId === userId;
}
export function getChannelServerId(channelId: string): string | null {
const db = getDb();
const channel = db.select().from(schema.channels).where(eq(schema.channels.id, channelId)).get();
return channel?.serverId ?? null;
}
export function isDmMember(dmChannelId: string, userId: string): boolean {
const db = getDb();
const member = db.select().from(schema.dmMembers)
.where(and(
eq(schema.dmMembers.dmChannelId, dmChannelId),
eq(schema.dmMembers.userId, userId),
))
.get();
return member !== undefined;
}
+50
View File
@@ -0,0 +1,50 @@
/**
* Discord-style Snowflake ID Generator
*
* Structure (64-bit):
* - 42 bits: milliseconds since custom epoch (Jan 1, 2024)
* - 10 bits: worker/process ID
* - 12 bits: sequence number (per-millisecond)
*
* This gives us:
* - ~139 years of IDs from epoch
* - 1024 workers
* - 4096 IDs per millisecond per worker
*/
const EPOCH = 1704067200000n; // Jan 1, 2024 00:00:00 UTC
const WORKER_ID = BigInt(process.pid % 1024);
let sequence = 0n;
let lastTimestamp = -1n;
export function generateSnowflake(): string {
let timestamp = BigInt(Date.now());
if (timestamp === lastTimestamp) {
sequence = (sequence + 1n) & 0xFFFn; // 12-bit mask
if (sequence === 0n) {
// Sequence exhausted, wait for next millisecond
while (timestamp <= lastTimestamp) {
timestamp = BigInt(Date.now());
}
}
} else {
sequence = 0n;
}
lastTimestamp = timestamp;
const id =
((timestamp - EPOCH) << 22n) |
(WORKER_ID << 12n) |
sequence;
return id.toString();
}
export function snowflakeToTimestamp(snowflake: string): number {
const id = BigInt(snowflake);
const timestamp = (id >> 22n) + EPOCH;
return Number(timestamp);
}