mirror of
https://github.com/fosrl/pangolin.git
synced 2025-09-01 07:20:06 +02:00
refactor and reorganize
This commit is contained in:
parent
9732098799
commit
3b4a993704
216 changed files with 519 additions and 2128 deletions
45
server/lib/canUserAccessResource.ts
Normal file
45
server/lib/canUserAccessResource.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import db from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db/schema";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId
|
||||
}: {
|
||||
userId: string;
|
||||
resourceId: number;
|
||||
roleId: number;
|
||||
}): Promise<boolean> {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (roleResourceAccess.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(userResources.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (userResourceAccess.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
206
server/lib/config.ts
Normal file
206
server/lib/config.ts
Normal file
|
@ -0,0 +1,206 @@
|
|||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||
|
||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||
|
||||
const environmentSchema = z.object({
|
||||
app: z.object({
|
||||
base_url: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((url) => url.toLowerCase()),
|
||||
log_level: z.enum(["debug", "info", "warn", "error"]),
|
||||
save_logs: z.boolean()
|
||||
}),
|
||||
server: z.object({
|
||||
external_port: portSchema,
|
||||
internal_port: portSchema,
|
||||
next_port: portSchema,
|
||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||
secure_cookies: z.boolean(),
|
||||
session_cookie_name: z.string(),
|
||||
resource_session_cookie_name: z.string()
|
||||
}),
|
||||
traefik: z.object({
|
||||
http_entrypoint: z.string(),
|
||||
https_entrypoint: z.string().optional(),
|
||||
cert_resolver: z.string().optional(),
|
||||
prefer_wildcard_cert: z.boolean().optional()
|
||||
}),
|
||||
gerbil: z.object({
|
||||
start_port: portSchema,
|
||||
base_endpoint: z.string().transform((url) => url.toLowerCase()),
|
||||
use_subdomain: z.boolean(),
|
||||
subnet_group: z.string(),
|
||||
block_size: z.number().positive().gt(0)
|
||||
}),
|
||||
rate_limits: z.object({
|
||||
global: z.object({
|
||||
window_minutes: z.number().positive().gt(0),
|
||||
max_requests: z.number().positive().gt(0)
|
||||
}),
|
||||
auth: z
|
||||
.object({
|
||||
window_minutes: z.number().positive().gt(0),
|
||||
max_requests: z.number().positive().gt(0)
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
email: z
|
||||
.object({
|
||||
smtp_host: z.string().optional(),
|
||||
smtp_port: portSchema.optional(),
|
||||
smtp_user: z.string().optional(),
|
||||
smtp_pass: z.string().optional(),
|
||||
no_reply: z.string().email().optional()
|
||||
})
|
||||
.optional(),
|
||||
users: z.object({
|
||||
server_admin: z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string()
|
||||
})
|
||||
}),
|
||||
flags: z
|
||||
.object({
|
||||
require_email_verification: z.boolean().optional(),
|
||||
disable_signup_without_invite: z.boolean().optional(),
|
||||
disable_user_create_org: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export class Config {
|
||||
private rawConfig!: z.infer<typeof environmentSchema>;
|
||||
|
||||
constructor() {
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
public loadConfig() {
|
||||
const loadConfig = (configPath: string) => {
|
||||
try {
|
||||
const yamlContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(yamlContent);
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(
|
||||
`Error loading configuration file: ${error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const configFilePath1 = path.join(APP_PATH, "config.yml");
|
||||
const configFilePath2 = path.join(APP_PATH, "config.yaml");
|
||||
|
||||
let environment: any;
|
||||
if (fs.existsSync(configFilePath1)) {
|
||||
environment = loadConfig(configFilePath1);
|
||||
} else if (fs.existsSync(configFilePath2)) {
|
||||
environment = loadConfig(configFilePath2);
|
||||
}
|
||||
if (!environment) {
|
||||
const exampleConfigPath = path.join(
|
||||
__DIRNAME,
|
||||
"config.example.yml"
|
||||
);
|
||||
if (fs.existsSync(exampleConfigPath)) {
|
||||
try {
|
||||
const exampleConfigContent = fs.readFileSync(
|
||||
exampleConfigPath,
|
||||
"utf8"
|
||||
);
|
||||
fs.writeFileSync(
|
||||
configFilePath1,
|
||||
exampleConfigContent,
|
||||
"utf8"
|
||||
);
|
||||
environment = loadConfig(configFilePath1);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(
|
||||
`Error creating configuration file from example: ${
|
||||
error.message
|
||||
}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"No configuration file found and no example configuration available"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("No configuration file found");
|
||||
}
|
||||
|
||||
const parsedConfig = environmentSchema.safeParse(environment);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
const errors = fromError(parsedConfig.error);
|
||||
throw new Error(`Invalid configuration file: ${errors}`);
|
||||
}
|
||||
|
||||
const appVersion = loadAppVersion();
|
||||
if (!appVersion) {
|
||||
throw new Error("Could not load the application version");
|
||||
}
|
||||
process.env.APP_VERSION = appVersion;
|
||||
|
||||
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
|
||||
process.env.SERVER_EXTERNAL_PORT =
|
||||
parsedConfig.data.server.external_port.toString();
|
||||
process.env.SERVER_INTERNAL_PORT =
|
||||
parsedConfig.data.server.internal_port.toString();
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
|
||||
?.require_email_verification
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.session_cookie_name;
|
||||
process.env.RESOURCE_SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.resource_session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
||||
?.disable_signup_without_invite
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
|
||||
?.disable_user_create_org
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
this.rawConfig = parsedConfig.data;
|
||||
}
|
||||
|
||||
public getRawConfig() {
|
||||
return this.rawConfig;
|
||||
}
|
||||
|
||||
public getBaseDomain(): string {
|
||||
const newUrl = new URL(this.rawConfig.app.base_url);
|
||||
const hostname = newUrl.hostname;
|
||||
const parts = hostname.split(".");
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
return parts.slice(1).join(".");
|
||||
}
|
||||
}
|
||||
|
||||
export const config = new Config();
|
||||
|
||||
export default config;
|
8
server/lib/consts.ts
Normal file
8
server/lib/consts.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
export const APP_PATH = path.join("config");
|
1
server/lib/index.ts
Normal file
1
server/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./response";
|
97
server/lib/ip.ts
Normal file
97
server/lib/ip.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
interface IPRange {
|
||||
start: bigint;
|
||||
end: bigint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts IP address string to BigInt for numerical operations
|
||||
*/
|
||||
function ipToBigInt(ip: string): bigint {
|
||||
return ip.split('.')
|
||||
.reduce((acc, octet) => BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(parseInt(octet))), BigInt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts BigInt to IP address string
|
||||
*/
|
||||
function bigIntToIp(num: bigint): string {
|
||||
const octets: number[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
octets.unshift(Number(num & BigInt(255)));
|
||||
num = num >> BigInt(8);
|
||||
}
|
||||
return octets.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts CIDR to IP range
|
||||
*/
|
||||
function cidrToRange(cidr: string): IPRange {
|
||||
const [ip, prefix] = cidr.split('/');
|
||||
const prefixBits = parseInt(prefix);
|
||||
const ipBigInt = ipToBigInt(ip);
|
||||
const mask = BigInt.asUintN(64, (BigInt(1) << BigInt(32 - prefixBits)) - BigInt(1));
|
||||
const start = ipBigInt & ~mask;
|
||||
const end = start | mask;
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next available CIDR block given existing allocations
|
||||
* @param existingCidrs Array of existing CIDR blocks
|
||||
* @param blockSize Desired prefix length for the new block (e.g., 24 for /24)
|
||||
* @param startCidr Optional CIDR to start searching from (default: "0.0.0.0/0")
|
||||
* @returns Next available CIDR block or null if none found
|
||||
*/
|
||||
export function findNextAvailableCidr(
|
||||
existingCidrs: string[],
|
||||
blockSize: number,
|
||||
startCidr: string = "0.0.0.0/0"
|
||||
): string | null {
|
||||
// Convert existing CIDRs to ranges and sort them
|
||||
const existingRanges = existingCidrs
|
||||
.map(cidr => cidrToRange(cidr))
|
||||
.sort((a, b) => (a.start < b.start ? -1 : 1));
|
||||
|
||||
// Calculate block size
|
||||
const blockSizeBigInt = BigInt(1) << BigInt(32 - blockSize);
|
||||
|
||||
// Start from the beginning of the given CIDR
|
||||
let current = cidrToRange(startCidr).start;
|
||||
const maxIp = cidrToRange(startCidr).end;
|
||||
|
||||
// Iterate through existing ranges
|
||||
for (let i = 0; i <= existingRanges.length; i++) {
|
||||
const nextRange = existingRanges[i];
|
||||
|
||||
// Align current to block size
|
||||
const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);
|
||||
|
||||
// Check if we've gone beyond the maximum allowed IP
|
||||
if (alignedCurrent + blockSizeBigInt - BigInt(1) > maxIp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we're at the end of existing ranges or found a gap
|
||||
if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) {
|
||||
return `${bigIntToIp(alignedCurrent)}/${blockSize}`;
|
||||
}
|
||||
|
||||
// Move current pointer to after the current range
|
||||
current = nextRange.end + BigInt(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given IP address is within a CIDR range
|
||||
* @param ip IP address to check
|
||||
* @param cidr CIDR range to check against
|
||||
* @returns boolean indicating if IP is within the CIDR range
|
||||
*/
|
||||
export function isIpInCidr(ip: string, cidr: string): boolean {
|
||||
const ipBigInt = ipToBigInt(ip);
|
||||
const range = cidrToRange(cidr);
|
||||
return ipBigInt >= range.start && ipBigInt <= range.end;
|
||||
}
|
16
server/lib/loadAppVersion.ts
Normal file
16
server/lib/loadAppVersion.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import path from "path";
|
||||
import { __DIRNAME } from "@server/lib/consts";
|
||||
import fs from "fs";
|
||||
|
||||
export function loadAppVersion() {
|
||||
const packageJsonPath = path.join("package.json");
|
||||
let packageJson: any;
|
||||
if (fs.existsSync && fs.existsSync(packageJsonPath)) {
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
|
||||
packageJson = JSON.parse(packageJsonContent);
|
||||
|
||||
if (packageJson.version) {
|
||||
return packageJson.version;
|
||||
}
|
||||
}
|
||||
}
|
17
server/lib/response.ts
Normal file
17
server/lib/response.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { ResponseT } from "@server/types/Response";
|
||||
import { Response } from "express";
|
||||
|
||||
export const response = <T>(
|
||||
res: Response,
|
||||
{ data, success, error, message, status }: ResponseT<T>,
|
||||
) => {
|
||||
return res.status(status).send({
|
||||
data,
|
||||
success,
|
||||
error,
|
||||
message,
|
||||
status,
|
||||
});
|
||||
};
|
||||
|
||||
export default response;
|
8
server/lib/stoi.ts
Normal file
8
server/lib/stoi.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default function stoi(val: any) {
|
||||
if (typeof val === "string") {
|
||||
return parseInt(val)
|
||||
}
|
||||
else {
|
||||
return val;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue