This commit is contained in:
jd
2026-02-13 00:07:19 +00:00
commit eca7405974
11 changed files with 300 additions and 0 deletions

25
utilities/guard.ts Normal file
View File

@@ -0,0 +1,25 @@
import jwt from 'jsonwebtoken';
import {Claims} from "./orm";
export function guardRedirect(method: Function, redirectMethod: Function, guardedClaims: string[] | undefined = undefined) {
try {
return guard(method, guardedClaims);
} catch (e) {
return redirectMethod();
}
}
export function guard(method: Function, guardedClaims: string[] | undefined = undefined) {
return (request: Request): any => {
const authHeader: string | null = request.headers.get('Authorization')?.replace(/^Bearer /, '') as string ?? null;
try {
const userClaims: Claims = jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as Claims;
if (guardedClaims !== undefined && !userClaims.claims.some(x => guardedClaims.includes(x))) {
throw new Error('Unauthorized');
}
return method(request, userClaims);
} catch (e) {
return Response.json({message: 'Authentication failed.'}, {status: 401})
}
}
}

121
utilities/orm.ts Normal file
View File

@@ -0,0 +1,121 @@
import argon2 from "argon2";
import {first} from "lodash";
import {sql} from "bun";
export class Claims {
userId: string | undefined;
claims: string[] = [];
public static test(userClaims: Claims, guardClaim: string): Boolean {
return userClaims.claims.some(x => x === guardClaim);
}
}
class ClaimsOrm {
async getByUserId(userId: string): Promise<Claims> {
const dbResults: any[] = await sql`SELECT c.name
from user_claims as uc
JOIN claims as c on uc.claimid = c.id
where uc.userid = ${userId};`;
const claims = new Claims();
claims.userId = userId;
claims.claims = dbResults.map(x => x.name);
return claims;
}
async getDefaultClaims(): Promise<number[]> {
const dbResults: any[] = await sql`SELECT id
FROM claims
WHERE is_default = true;`;
return dbResults.map(x => x.id);
}
}
class User {
id: string;
name: string;
isAdmin: boolean;
isActive: boolean;
constructor(id: string, name: string, isAdmin: boolean = false, isActive: boolean = true) {
this.id = id;
this.name = name;
this.isAdmin = isAdmin;
this.isActive = isActive;
}
}
class UsersOrm {
#claims: ClaimsOrm;
constructor(claims: ClaimsOrm) {
this.#claims = claims;
}
async get(id: string, claims: Claims): Promise<User> {
if (!(
Claims.test(claims, 'ADMIN') ||
Claims.test(claims, 'USERS_OTHER_READ') ||
(Claims.test(claims, 'USERS_SELF_READ') && id === claims.userId)
)) {
throw new Error('Unauthorized');
}
const dbResult: any = first(await sql`select *
from users
where id = ${id}
and is_active = true limit 1`);
return new User(dbResult.id, dbResult.username, dbResult.is_admin);
}
async verify(username: string, password: string): Promise<Claims | null> {
try {
const dbResult: any = first(await sql`select *
from users
where username = ${username} limit 1`);
if (!await argon2.verify(dbResult.pass_hash, password)) {
return null;
}
return this.#claims.getByUserId(dbResult.id);
} catch (error) {
console.log(error);
throw error;
}
}
async create(username: string, password: string, claims: Claims): Promise<User> {
const existingUser: any = first(await sql`SELECT id
FROM users
WHERE username = ${username} LIMIT 1`);
if (existingUser) {
throw new Error(`User with id ${existingUser.id} already exists`);
}
const defaultClaims: number[] = await this.#claims.getDefaultClaims();
const passwordHash = await argon2.hash(password);
await sql`INSERT INTO users (username, pass_hash)
VALUES (${username}, ${passwordHash})`;
const newUserId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
await sql.transaction(async (tx) => {
for (let i in defaultClaims) {
await tx`INSERT INTO user_claims (userid, claimid)
VALUES (${newUserId}, ${defaultClaims[i]})`;
}
})
return await this.get(newUserId, claims);
}
}
class Orm {
claims: ClaimsOrm;
users: UsersOrm;
constructor() {
this.claims = new ClaimsOrm();
this.users = new UsersOrm(this.claims);
}
}
export const orm = new Orm();