chore: Initial commit of Opencord base state
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user