mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-19 03:16:40 +00:00
Merge branch 'dev' into redis
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Request } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userActions, roleActions, userOrgs } from "@server/db/schemas";
|
||||
import { userActions, roleActions, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db/schemas";
|
||||
import { roleResources, userResources } from "@server/db";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import db from "@server/db";
|
||||
import { UserInvite, userInvites } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { UserInvite, userInvites } from "@server/db";
|
||||
import { isWithinExpirationDate } from "oslo";
|
||||
import { verifyPassword } from "./password";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@server/db';
|
||||
import { limitsTable } from '@server/db/schemas';
|
||||
import { limitsTable } from '@server/db';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import createHttpError from 'http-errors';
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import db from "@server/db";
|
||||
import { resourceOtp } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { resourceOtp } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { createDate, isWithinExpirationDate, TimeSpan } from "oslo";
|
||||
import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TimeSpan, createDate } from "oslo";
|
||||
import { generateRandomString, alphabet } from "oslo/crypto";
|
||||
import db from "@server/db";
|
||||
import { users, emailVerificationCodes } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { users, emailVerificationCodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendEmail } from "@server/emails";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
sessions,
|
||||
User,
|
||||
users
|
||||
} from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
} from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import type { RandomReader } from "@oslojs/crypto/random";
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Newt, newts, newtSessions, NewtSession } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { Newt, newts, newtSessions, NewtSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { resourceSessions, ResourceSession } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { resourceSessions, ResourceSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { verify } from "@node-rs/argon2";
|
||||
import db from "@server/db";
|
||||
import { twoFactorBackupCodes } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { twoFactorBackupCodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { decodeHex } from "oslo/encoding";
|
||||
import { TOTPController } from "oslo/otp";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
resourceAccessToken,
|
||||
resources
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { isWithinExpirationDate } from "oslo";
|
||||
import { verifyPassword } from "./password";
|
||||
|
||||
72
server/db/README.md
Normal file
72
server/db/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Database
|
||||
|
||||
Pangolin can use a Postgres or SQLite database to store its data.
|
||||
|
||||
## Development
|
||||
|
||||
### Postgres
|
||||
|
||||
To use Postgres, edit `server/db/index.ts` to export all from `server/db/pg/index.ts`:
|
||||
|
||||
```typescript
|
||||
export * from "./pg";
|
||||
```
|
||||
|
||||
Make sure you have a valid config file with a connection string:
|
||||
|
||||
```yaml
|
||||
postgres:
|
||||
connection_string: postgresql://postgres:postgres@localhost:5432
|
||||
```
|
||||
|
||||
You can run an ephemeral Postgres database for local development using Docker:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name postgres \
|
||||
--rm \
|
||||
-p 5432:5432 \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-v $(mktemp -d):/var/lib/postgresql/data \
|
||||
postgres:17
|
||||
```
|
||||
|
||||
### Schema
|
||||
|
||||
`server/db/pg/schema.ts` and `server/db/sqlite/schema.ts` contain the database schema definitions. These need to be kept in sync with with each other.
|
||||
|
||||
Stick to common data types and avoid Postgres-specific features to ensure compatibility with SQLite.
|
||||
|
||||
### SQLite
|
||||
|
||||
To use SQLite, edit `server/db/index.ts` to export all from `server/db/sqlite/index.ts`:
|
||||
|
||||
```typescript
|
||||
export * from "./sqlite";
|
||||
```
|
||||
|
||||
No edits to the config are needed. If you keep the Postgres config, it will be ignored.
|
||||
|
||||
## Generate and Push Migrations
|
||||
|
||||
Ensure drizzle-kit is installed.
|
||||
|
||||
### Postgres
|
||||
|
||||
You must have a connection string in your config file, as shown above.
|
||||
|
||||
```bash
|
||||
npm run db:pg:generate
|
||||
npm run db:pg:push
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
```bash
|
||||
npm run db:sqlite:generate
|
||||
npm run db:sqlite:push
|
||||
```
|
||||
|
||||
## Build Time
|
||||
|
||||
There is a dockerfile for each database type. The dockerfile swaps out the `server/db/index.ts` file to use the correct database type.
|
||||
@@ -1,52 +1,2 @@
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import Database from "better-sqlite3";
|
||||
import * as schema from "@server/db/schemas";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
|
||||
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
export const exists = await checkFileExists(location);
|
||||
|
||||
bootstrapVolume();
|
||||
|
||||
const sqlite = new Database(location);
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
|
||||
export default db;
|
||||
|
||||
async function checkFileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapVolume() {
|
||||
const appPath = APP_PATH;
|
||||
|
||||
const dbDir = path.join(appPath, "db");
|
||||
const logsDir = path.join(appPath, "logs");
|
||||
|
||||
// check if the db directory exists and create it if it doesn't
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
// check if the logs directory exists and create it if it doesn't
|
||||
if (!existsSync(logsDir)) {
|
||||
mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// THIS IS FOR TRAEFIK; NOT REALLY NEEDED, BUT JUST IN CASE
|
||||
|
||||
const traefikDir = path.join(appPath, "traefik");
|
||||
|
||||
// check if the traefik directory exists and create it if it doesn't
|
||||
if (!existsSync(traefikDir)) {
|
||||
mkdirSync(traefikDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
export * from "./sqlite";
|
||||
// export * from "./pg";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { join } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { db } from "@server/db";
|
||||
import { exitNodes, sites } from "./schemas/schema";
|
||||
import { exitNodes, sites } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { __DIRNAME } from "@server/lib/consts";
|
||||
|
||||
|
||||
33
server/db/pg/driver.ts
Normal file
33
server/db/pg/driver.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||
import { withReplicas } from "drizzle-orm/pg-core";
|
||||
|
||||
function createDb() {
|
||||
const config = readConfigFile();
|
||||
|
||||
const connectionString = config.postgres?.connection_string;
|
||||
const replicaConnections = config.postgres?.replicas || [];
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error(
|
||||
"A primary db connection string is required in the configuration file."
|
||||
);
|
||||
}
|
||||
|
||||
const primary = DrizzlePostgres(connectionString);
|
||||
const replicas = [];
|
||||
|
||||
if (!replicaConnections.length) {
|
||||
replicas.push(primary);
|
||||
} else {
|
||||
for (const conn of replicaConnections) {
|
||||
const replica = DrizzlePostgres(conn.connection_string);
|
||||
replicas.push(replica);
|
||||
}
|
||||
}
|
||||
|
||||
return withReplicas(primary, replicas as any);
|
||||
}
|
||||
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema";
|
||||
20
server/db/pg/migrate.ts
Normal file
20
server/db/pg/migrate.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||
import db from "./driver";
|
||||
import path from "path";
|
||||
|
||||
const migrationsFolder = path.join("server/migrations");
|
||||
|
||||
const runMigrations = async () => {
|
||||
console.log("Running migrations...");
|
||||
try {
|
||||
await migrate(db as any, {
|
||||
migrationsFolder: migrationsFolder
|
||||
});
|
||||
console.log("Migrations completed successfully.");
|
||||
} catch (error) {
|
||||
console.error("Error running migrations:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
runMigrations();
|
||||
532
server/db/pg/schema.ts
Normal file
532
server/db/pg/schema.ts
Normal file
@@ -0,0 +1,532 @@
|
||||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
varchar,
|
||||
boolean,
|
||||
integer,
|
||||
bigint,
|
||||
real
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
|
||||
export const domains = pgTable("domains", {
|
||||
domainId: varchar("domainId").primaryKey(),
|
||||
baseDomain: varchar("baseDomain").notNull(),
|
||||
configManaged: boolean("configManaged").notNull().default(false)
|
||||
});
|
||||
|
||||
export const orgs = pgTable("orgs", {
|
||||
orgId: varchar("orgId").primaryKey(),
|
||||
name: varchar("name").notNull()
|
||||
});
|
||||
|
||||
export const orgDomains = pgTable("orgDomains", {
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
domainId: varchar("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const sites = pgTable("sites", {
|
||||
siteId: serial("siteId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
niceId: varchar("niceId").notNull(),
|
||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
name: varchar("name").notNull(),
|
||||
pubKey: varchar("pubKey"),
|
||||
subnet: varchar("subnet").notNull(),
|
||||
megabytesIn: real("bytesIn"),
|
||||
megabytesOut: real("bytesOut"),
|
||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||
online: boolean("online").notNull().default(false),
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||
});
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
resourceId: serial("resourceId").primaryKey(),
|
||||
siteId: integer("siteId")
|
||||
.references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
subdomain: varchar("subdomain"),
|
||||
fullDomain: varchar("fullDomain"),
|
||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
ssl: boolean("ssl").notNull().default(false),
|
||||
blockAccess: boolean("blockAccess").notNull().default(false),
|
||||
sso: boolean("sso").notNull().default(true),
|
||||
http: boolean("http").notNull().default(true),
|
||||
protocol: varchar("protocol").notNull(),
|
||||
proxyPort: integer("proxyPort"),
|
||||
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
isBaseDomain: boolean("isBaseDomain"),
|
||||
applyRules: boolean("applyRules").notNull().default(false),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
stickySession: boolean("stickySession").notNull().default(false),
|
||||
tlsServerName: varchar("tlsServerName"),
|
||||
setHostHeader: varchar("setHostHeader")
|
||||
});
|
||||
|
||||
export const targets = pgTable("targets", {
|
||||
targetId: serial("targetId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.references(() => resources.resourceId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
ip: varchar("ip").notNull(),
|
||||
method: varchar("method"),
|
||||
port: integer("port").notNull(),
|
||||
internalPort: integer("internalPort"),
|
||||
enabled: boolean("enabled").notNull().default(true)
|
||||
});
|
||||
|
||||
export const exitNodes = pgTable("exitNodes", {
|
||||
exitNodeId: serial("exitNodeId").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
address: varchar("address").notNull(),
|
||||
endpoint: varchar("endpoint").notNull(),
|
||||
publicKey: varchar("publicKey").notNull(),
|
||||
listenPort: integer("listenPort").notNull(),
|
||||
reachableAt: varchar("reachableAt")
|
||||
});
|
||||
|
||||
export const users = pgTable("user", {
|
||||
userId: varchar("id").primaryKey(),
|
||||
email: varchar("email"),
|
||||
username: varchar("username").notNull(),
|
||||
name: varchar("name"),
|
||||
type: varchar("type").notNull(), // "internal", "oidc"
|
||||
idpId: integer("idpId").references(() => idp.idpId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
passwordHash: varchar("passwordHash"),
|
||||
twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false),
|
||||
twoFactorSecret: varchar("twoFactorSecret"),
|
||||
emailVerified: boolean("emailVerified").notNull().default(false),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
serverAdmin: boolean("serverAdmin").notNull().default(false)
|
||||
});
|
||||
|
||||
export const newts = pgTable("newt", {
|
||||
newtId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
|
||||
codeId: serial("id").primaryKey(),
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
codeHash: varchar("codeHash").notNull()
|
||||
});
|
||||
|
||||
export const sessions = pgTable("session", {
|
||||
sessionId: varchar("id").primaryKey(),
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const newtSessions = pgTable("newtSession", {
|
||||
sessionId: varchar("id").primaryKey(),
|
||||
newtId: varchar("newtId")
|
||||
.notNull()
|
||||
.references(() => newts.newtId, { onDelete: "cascade" }),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const userOrgs = pgTable("userOrgs", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId),
|
||||
isOwner: boolean("isOwner").notNull().default(false)
|
||||
});
|
||||
|
||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||
codeId: serial("id").primaryKey(),
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
email: varchar("email").notNull(),
|
||||
code: varchar("code").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const passwordResetTokens = pgTable("passwordResetTokens", {
|
||||
tokenId: serial("id").primaryKey(),
|
||||
email: varchar("email").notNull(),
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
tokenHash: varchar("tokenHash").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const actions = pgTable("actions", {
|
||||
actionId: varchar("actionId").primaryKey(),
|
||||
name: varchar("name"),
|
||||
description: varchar("description")
|
||||
});
|
||||
|
||||
export const roles = pgTable("roles", {
|
||||
roleId: serial("roleId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
isAdmin: boolean("isAdmin"),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description")
|
||||
});
|
||||
|
||||
export const roleActions = pgTable("roleActions", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
actionId: varchar("actionId")
|
||||
.notNull()
|
||||
.references(() => actions.actionId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userActions = pgTable("userActions", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
actionId: varchar("actionId")
|
||||
.notNull()
|
||||
.references(() => actions.actionId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleSites = pgTable("roleSites", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userSites = pgTable("userSites", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleResources = pgTable("roleResources", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userResources = pgTable("userResources", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const limitsTable = pgTable("limits", {
|
||||
limitId: serial("limitId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
value: bigint("value", { mode: "number" }).notNull(),
|
||||
description: varchar("description")
|
||||
});
|
||||
|
||||
export const userInvites = pgTable("userInvites", {
|
||||
inviteId: varchar("inviteId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
email: varchar("email").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
tokenHash: varchar("token").notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const resourcePincode = pgTable("resourcePincode", {
|
||||
pincodeId: serial("pincodeId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
pincodeHash: varchar("pincodeHash").notNull(),
|
||||
digitLength: integer("digitLength").notNull()
|
||||
});
|
||||
|
||||
export const resourcePassword = pgTable("resourcePassword", {
|
||||
passwordId: serial("passwordId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
passwordHash: varchar("passwordHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
tokenHash: varchar("tokenHash").notNull(),
|
||||
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||
title: varchar("title"),
|
||||
description: varchar("description"),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const resourceSessions = pgTable("resourceSessions", {
|
||||
sessionId: varchar("id").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
|
||||
doNotExtend: boolean("doNotExtend").notNull().default(false),
|
||||
isRequestToken: boolean("isRequestToken"),
|
||||
userSessionId: varchar("userSessionId").references(
|
||||
() => sessions.sessionId,
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
),
|
||||
passwordId: integer("passwordId").references(
|
||||
() => resourcePassword.passwordId,
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
),
|
||||
pincodeId: integer("pincodeId").references(
|
||||
() => resourcePincode.pincodeId,
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
),
|
||||
whitelistId: integer("whitelistId").references(
|
||||
() => resourceWhitelist.whitelistId,
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
),
|
||||
accessTokenId: varchar("accessTokenId").references(
|
||||
() => resourceAccessToken.accessTokenId,
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export const resourceWhitelist = pgTable("resourceWhitelist", {
|
||||
whitelistId: serial("id").primaryKey(),
|
||||
email: varchar("email").notNull(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const resourceOtp = pgTable("resourceOtp", {
|
||||
otpId: serial("otpId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
email: varchar("email").notNull(),
|
||||
otpHash: varchar("otpHash").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const versionMigrations = pgTable("versionMigrations", {
|
||||
version: varchar("version").primaryKey(),
|
||||
executedAt: bigint("executedAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const resourceRules = pgTable("resourceRules", {
|
||||
ruleId: serial("ruleId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: varchar("action").notNull(), // ACCEPT, DROP
|
||||
match: varchar("match").notNull(), // CIDR, PATH, IP
|
||||
value: varchar("value").notNull()
|
||||
});
|
||||
|
||||
export const supporterKey = pgTable("supporterKey", {
|
||||
keyId: serial("keyId").primaryKey(),
|
||||
key: varchar("key").notNull(),
|
||||
githubUsername: varchar("githubUsername").notNull(),
|
||||
phrase: varchar("phrase"),
|
||||
tier: varchar("tier"),
|
||||
valid: boolean("valid").notNull().default(false)
|
||||
});
|
||||
|
||||
export const idp = pgTable("idp", {
|
||||
idpId: serial("idpId").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
type: varchar("type").notNull(),
|
||||
defaultRoleMapping: varchar("defaultRoleMapping"),
|
||||
defaultOrgMapping: varchar("defaultOrgMapping"),
|
||||
autoProvision: boolean("autoProvision").notNull().default(false)
|
||||
});
|
||||
|
||||
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||
idpOauthConfigId: serial("idpOauthConfigId").primaryKey(),
|
||||
idpId: integer("idpId")
|
||||
.notNull()
|
||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||
clientId: varchar("clientId").notNull(),
|
||||
clientSecret: varchar("clientSecret").notNull(),
|
||||
authUrl: varchar("authUrl").notNull(),
|
||||
tokenUrl: varchar("tokenUrl").notNull(),
|
||||
identifierPath: varchar("identifierPath").notNull(),
|
||||
emailPath: varchar("emailPath"),
|
||||
namePath: varchar("namePath"),
|
||||
scopes: varchar("scopes").notNull()
|
||||
});
|
||||
|
||||
export const licenseKey = pgTable("licenseKey", {
|
||||
licenseKeyId: varchar("licenseKeyId").primaryKey().notNull(),
|
||||
instanceId: varchar("instanceId").notNull(),
|
||||
token: varchar("token").notNull()
|
||||
});
|
||||
|
||||
export const hostMeta = pgTable("hostMeta", {
|
||||
hostMetaId: varchar("hostMetaId").primaryKey().notNull(),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const apiKeys = pgTable("apiKeys", {
|
||||
apiKeyId: varchar("apiKeyId").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
apiKeyHash: varchar("apiKeyHash").notNull(),
|
||||
lastChars: varchar("lastChars").notNull(),
|
||||
createdAt: varchar("dateCreated").notNull(),
|
||||
isRoot: boolean("isRoot").notNull().default(false)
|
||||
});
|
||||
|
||||
export const apiKeyActions = pgTable("apiKeyActions", {
|
||||
apiKeyId: varchar("apiKeyId")
|
||||
.notNull()
|
||||
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
|
||||
actionId: varchar("actionId")
|
||||
.notNull()
|
||||
.references(() => actions.actionId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const apiKeyOrg = pgTable("apiKeyOrg", {
|
||||
apiKeyId: varchar("apiKeyId")
|
||||
.notNull()
|
||||
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const idpOrg = pgTable("idpOrg", {
|
||||
idpId: integer("idpId")
|
||||
.notNull()
|
||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
roleMapping: varchar("roleMapping"),
|
||||
orgMapping: varchar("orgMapping")
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
export type Resource = InferSelectModel<typeof resources>;
|
||||
export type ExitNode = InferSelectModel<typeof exitNodes>;
|
||||
export type Target = InferSelectModel<typeof targets>;
|
||||
export type Session = InferSelectModel<typeof sessions>;
|
||||
export type Newt = InferSelectModel<typeof newts>;
|
||||
export type NewtSession = InferSelectModel<typeof newtSessions>;
|
||||
export type EmailVerificationCode = InferSelectModel<
|
||||
typeof emailVerificationCodes
|
||||
>;
|
||||
export type TwoFactorBackupCode = InferSelectModel<typeof twoFactorBackupCodes>;
|
||||
export type PasswordResetToken = InferSelectModel<typeof passwordResetTokens>;
|
||||
export type Role = InferSelectModel<typeof roles>;
|
||||
export type Action = InferSelectModel<typeof actions>;
|
||||
export type RoleAction = InferSelectModel<typeof roleActions>;
|
||||
export type UserAction = InferSelectModel<typeof userActions>;
|
||||
export type RoleSite = InferSelectModel<typeof roleSites>;
|
||||
export type UserSite = InferSelectModel<typeof userSites>;
|
||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||
export type UserResource = InferSelectModel<typeof userResources>;
|
||||
export type Limit = InferSelectModel<typeof limitsTable>;
|
||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
||||
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
||||
export type Domain = InferSelectModel<typeof domains>;
|
||||
export type SupporterKey = InferSelectModel<typeof supporterKey>;
|
||||
export type Idp = InferSelectModel<typeof idp>;
|
||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
58
server/db/sqlite/driver.ts
Normal file
58
server/db/sqlite/driver.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import Database from "better-sqlite3";
|
||||
import * as schema from "./schema";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||
|
||||
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
export const exists = await checkFileExists(location);
|
||||
|
||||
bootstrapVolume();
|
||||
|
||||
function createDb() {
|
||||
const config = readConfigFile();
|
||||
|
||||
const sqlite = new Database(location);
|
||||
return DrizzleSqlite(sqlite, { schema });
|
||||
}
|
||||
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
|
||||
async function checkFileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapVolume() {
|
||||
const appPath = APP_PATH;
|
||||
|
||||
const dbDir = path.join(appPath, "db");
|
||||
const logsDir = path.join(appPath, "logs");
|
||||
|
||||
// check if the db directory exists and create it if it doesn't
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
// check if the logs directory exists and create it if it doesn't
|
||||
if (!existsSync(logsDir)) {
|
||||
mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// THIS IS FOR TRAEFIK; NOT REALLY NEEDED, BUT JUST IN CASE
|
||||
|
||||
const traefikDir = path.join(appPath, "traefik");
|
||||
|
||||
// check if the traefik directory exists and create it if it doesn't
|
||||
if (!existsSync(traefikDir)) {
|
||||
mkdirSync(traefikDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
2
server/db/sqlite/index.ts
Normal file
2
server/db/sqlite/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import db from "@server/db";
|
||||
import db from "./driver";
|
||||
import path from "path";
|
||||
|
||||
const migrationsFolder = path.join("server/migrations");
|
||||
@@ -7,7 +7,7 @@ const migrationsFolder = path.join("server/migrations");
|
||||
const runMigrations = async () => {
|
||||
console.log("Running migrations...");
|
||||
try {
|
||||
migrate(db, {
|
||||
migrate(db as any, {
|
||||
migrationsFolder: migrationsFolder,
|
||||
});
|
||||
console.log("Migrations completed successfully.");
|
||||
@@ -41,7 +41,10 @@ export const sites = sqliteTable("sites", {
|
||||
megabytesOut: integer("bytesOut"),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
type: text("type").notNull(), // "newt" or "wireguard"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false)
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true)
|
||||
});
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
@@ -4,7 +4,7 @@ import { runSetupFunctions } from "./setup";
|
||||
import { createApiServer } from "./apiServer";
|
||||
import { createNextServer } from "./nextServer";
|
||||
import { createInternalServer } from "./internalServer";
|
||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas";
|
||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
|
||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db/schemas";
|
||||
import { roleResources, userResources } from "@server/db";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
|
||||
@@ -1,226 +1,10 @@
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
__DIRNAME,
|
||||
APP_VERSION,
|
||||
configFilePath1,
|
||||
configFilePath2
|
||||
} from "@server/lib/consts";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import stoi from "./stoi";
|
||||
import db from "@server/db";
|
||||
import { SupporterKey, supporterKey } from "@server/db/schemas";
|
||||
import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
||||
import { db } from "@server/db";
|
||||
import { SupporterKey, supporterKey } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { license } from "@server/license/license";
|
||||
|
||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||
|
||||
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
|
||||
return process.env[envVar] ?? valFromYaml;
|
||||
};
|
||||
|
||||
const configSchema = z.object({
|
||||
app: z.object({
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.pipe(z.string().url())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
log_level: z
|
||||
.enum(["debug", "info", "warn", "error"])
|
||||
.optional()
|
||||
.default("info"),
|
||||
save_logs: z.boolean().optional().default(false),
|
||||
log_failed_attempts: z.boolean().optional().default(false)
|
||||
}),
|
||||
domains: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
base_domain: z
|
||||
.string()
|
||||
.nonempty("base_domain must not be empty")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
||||
prefer_wildcard_cert: z.boolean().optional().default(false)
|
||||
})
|
||||
)
|
||||
.refine(
|
||||
(domains) => {
|
||||
const keys = Object.keys(domains);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "At least one domain must be defined"
|
||||
}
|
||||
),
|
||||
server: z.object({
|
||||
integration_port: portSchema
|
||||
.optional()
|
||||
.default(3003)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema.optional()),
|
||||
external_port: portSchema
|
||||
.optional()
|
||||
.default(3000)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_port: portSchema
|
||||
.optional()
|
||||
.default(3001)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
next_port: portSchema
|
||||
.optional()
|
||||
.default(3002)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_hostname: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pangolin")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z.string().optional().default("p_session_token"),
|
||||
resource_access_token_param: z.string().optional().default("p_token"),
|
||||
resource_access_token_headers: z
|
||||
.object({
|
||||
id: z.string().optional().default("P-Access-Token-Id"),
|
||||
token: z.string().optional().default("P-Access-Token")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
resource_session_request_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("resource_session_request_param"),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
resource_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
cors: z
|
||||
.object({
|
||||
origins: z.array(z.string()).optional(),
|
||||
methods: z.array(z.string()).optional(),
|
||||
allowed_headers: z.array(z.string()).optional(),
|
||||
credentials: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
trust_proxy: z.boolean().optional().default(true),
|
||||
secret: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("SERVER_SECRET"))
|
||||
.pipe(z.string().min(8))
|
||||
}),
|
||||
traefik: z
|
||||
.object({
|
||||
http_entrypoint: z.string().optional().default("web"),
|
||||
https_entrypoint: z.string().optional().default("websecure"),
|
||||
additional_middlewares: z.array(z.string()).optional()
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
gerbil: z
|
||||
.object({
|
||||
start_port: portSchema
|
||||
.optional()
|
||||
.default(51820)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
base_endpoint: z
|
||||
.string()
|
||||
.optional()
|
||||
.pipe(z.string())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
use_subdomain: z.boolean().optional().default(false),
|
||||
subnet_group: z.string().optional().default("100.89.137.0/20"),
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
site_block_size: z.number().positive().gt(0).optional().default(30)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
rate_limits: z
|
||||
.object({
|
||||
global: z
|
||||
.object({
|
||||
window_minutes: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(1),
|
||||
max_requests: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(500)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
auth: z
|
||||
.object({
|
||||
window_minutes: z.number().positive().gt(0),
|
||||
max_requests: z.number().positive().gt(0)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
email: z
|
||||
.object({
|
||||
smtp_host: z.string().optional(),
|
||||
smtp_port: portSchema.optional(),
|
||||
smtp_user: z.string().optional(),
|
||||
smtp_pass: z.string().optional(),
|
||||
smtp_secure: z.boolean().optional(),
|
||||
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
||||
no_reply: z.string().email().optional()
|
||||
})
|
||||
.optional(),
|
||||
users: z.object({
|
||||
server_admin: z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
|
||||
.pipe(z.string().email())
|
||||
.transform((v) => v.toLowerCase()),
|
||||
password: passwordSchema
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
|
||||
.pipe(passwordSchema)
|
||||
})
|
||||
}),
|
||||
flags: z
|
||||
.object({
|
||||
require_email_verification: z.boolean().optional(),
|
||||
disable_signup_without_invite: z.boolean().optional(),
|
||||
disable_user_create_org: z.boolean().optional(),
|
||||
allow_raw_resources: z.boolean().optional(),
|
||||
allow_base_domain_resources: z.boolean().optional(),
|
||||
allow_local_sites: z.boolean().optional(),
|
||||
enable_integration_api: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
import { configSchema, readConfigFile } from "./readConfigFile";
|
||||
|
||||
export class Config {
|
||||
private rawConfig!: z.infer<typeof configSchema>;
|
||||
@@ -232,96 +16,57 @@ export class Config {
|
||||
isDev: boolean = process.env.ENVIRONMENT !== "prod";
|
||||
|
||||
constructor() {
|
||||
this.loadConfig();
|
||||
this.load();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
let environment: any;
|
||||
if (fs.existsSync(configFilePath1)) {
|
||||
environment = loadConfig(configFilePath1);
|
||||
} else if (fs.existsSync(configFilePath2)) {
|
||||
environment = loadConfig(configFilePath2);
|
||||
}
|
||||
|
||||
if (process.env.APP_BASE_DOMAIN) {
|
||||
console.log(
|
||||
"You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
|
||||
);
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(
|
||||
"No configuration file found. Please create one. https://docs.fossorial.io/"
|
||||
);
|
||||
}
|
||||
|
||||
const parsedConfig = configSchema.safeParse(environment);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
const errors = fromError(parsedConfig.error);
|
||||
throw new Error(`Invalid configuration file: ${errors}`);
|
||||
}
|
||||
public load() {
|
||||
const parsedConfig = readConfigFile();
|
||||
|
||||
process.env.APP_VERSION = APP_VERSION;
|
||||
|
||||
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
|
||||
process.env.NEXT_PORT = parsedConfig.server.next_port.toString();
|
||||
process.env.SERVER_EXTERNAL_PORT =
|
||||
parsedConfig.data.server.external_port.toString();
|
||||
parsedConfig.server.external_port.toString();
|
||||
process.env.SERVER_INTERNAL_PORT =
|
||||
parsedConfig.data.server.internal_port.toString();
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
|
||||
parsedConfig.server.internal_port.toString();
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.flags
|
||||
?.require_email_verification
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
|
||||
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.flags
|
||||
?.allow_raw_resources
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
||||
parsedConfig.server.session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.email ? "true" : "false";
|
||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.flags
|
||||
?.disable_signup_without_invite
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
|
||||
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.flags
|
||||
?.disable_user_create_org
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
||||
parsedConfig.data.server.resource_access_token_param;
|
||||
parsedConfig.server.resource_access_token_param;
|
||||
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID =
|
||||
parsedConfig.data.server.resource_access_token_headers.id;
|
||||
parsedConfig.server.resource_access_token_headers.id;
|
||||
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN =
|
||||
parsedConfig.data.server.resource_access_token_headers.token;
|
||||
parsedConfig.server.resource_access_token_headers.token;
|
||||
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
||||
parsedConfig.data.server.resource_session_request_param;
|
||||
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
|
||||
parsedConfig.server.resource_session_request_param;
|
||||
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.flags
|
||||
?.allow_base_domain_resources
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
|
||||
process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
|
||||
|
||||
license.setServerSecret(parsedConfig.data.server.secret);
|
||||
license.setServerSecret(parsedConfig.server.secret);
|
||||
|
||||
this.checkKeyStatus();
|
||||
|
||||
this.rawConfig = parsedConfig.data;
|
||||
this.rawConfig = parsedConfig;
|
||||
}
|
||||
|
||||
private async checkKeyStatus() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.4.0";
|
||||
export const APP_VERSION = "1.5.1";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
271
server/lib/readConfigFile.ts
Normal file
271
server/lib/readConfigFile.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
import { configFilePath1, configFilePath2 } from "./consts";
|
||||
import { z } from "zod";
|
||||
import stoi from "./stoi";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||
|
||||
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
|
||||
return process.env[envVar] ?? valFromYaml;
|
||||
};
|
||||
|
||||
export const configSchema = z.object({
|
||||
app: z.object({
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.pipe(z.string().url())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
log_level: z
|
||||
.enum(["debug", "info", "warn", "error"])
|
||||
.optional()
|
||||
.default("info"),
|
||||
save_logs: z.boolean().optional().default(false),
|
||||
log_failed_attempts: z.boolean().optional().default(false)
|
||||
}),
|
||||
domains: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
base_domain: z
|
||||
.string()
|
||||
.nonempty("base_domain must not be empty")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
||||
prefer_wildcard_cert: z.boolean().optional().default(false)
|
||||
})
|
||||
)
|
||||
.refine(
|
||||
(domains) => {
|
||||
const keys = Object.keys(domains);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "At least one domain must be defined"
|
||||
}
|
||||
),
|
||||
server: z.object({
|
||||
integration_port: portSchema
|
||||
.optional()
|
||||
.default(3003)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema.optional()),
|
||||
external_port: portSchema
|
||||
.optional()
|
||||
.default(3000)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_port: portSchema
|
||||
.optional()
|
||||
.default(3001)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
next_port: portSchema
|
||||
.optional()
|
||||
.default(3002)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_hostname: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pangolin")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z.string().optional().default("p_session_token"),
|
||||
resource_access_token_param: z.string().optional().default("p_token"),
|
||||
resource_access_token_headers: z
|
||||
.object({
|
||||
id: z.string().optional().default("P-Access-Token-Id"),
|
||||
token: z.string().optional().default("P-Access-Token")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
resource_session_request_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("resource_session_request_param"),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
resource_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
cors: z
|
||||
.object({
|
||||
origins: z.array(z.string()).optional(),
|
||||
methods: z.array(z.string()).optional(),
|
||||
allowed_headers: z.array(z.string()).optional(),
|
||||
credentials: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
trust_proxy: z.boolean().optional().default(true),
|
||||
secret: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("SERVER_SECRET"))
|
||||
.pipe(z.string().min(8))
|
||||
}),
|
||||
postgres: z
|
||||
.object({
|
||||
connection_string: z.string(),
|
||||
replicas: z
|
||||
.array(
|
||||
z.object({
|
||||
connection_string: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
.optional(),
|
||||
traefik: z
|
||||
.object({
|
||||
http_entrypoint: z.string().optional().default("web"),
|
||||
https_entrypoint: z.string().optional().default("websecure"),
|
||||
additional_middlewares: z.array(z.string()).optional()
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
gerbil: z
|
||||
.object({
|
||||
start_port: portSchema
|
||||
.optional()
|
||||
.default(51820)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
base_endpoint: z
|
||||
.string()
|
||||
.optional()
|
||||
.pipe(z.string())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
use_subdomain: z.boolean().optional().default(false),
|
||||
subnet_group: z.string().optional().default("100.89.137.0/20"),
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
site_block_size: z.number().positive().gt(0).optional().default(30)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
rate_limits: z
|
||||
.object({
|
||||
global: z
|
||||
.object({
|
||||
window_minutes: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(1),
|
||||
max_requests: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(500)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
auth: z
|
||||
.object({
|
||||
window_minutes: z.number().positive().gt(0),
|
||||
max_requests: z.number().positive().gt(0)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
email: z
|
||||
.object({
|
||||
smtp_host: z.string().optional(),
|
||||
smtp_port: portSchema.optional(),
|
||||
smtp_user: z.string().optional(),
|
||||
smtp_pass: z.string().optional(),
|
||||
smtp_secure: z.boolean().optional(),
|
||||
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
||||
no_reply: z.string().email().optional()
|
||||
})
|
||||
.optional(),
|
||||
users: z.object({
|
||||
server_admin: z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
|
||||
.pipe(z.string().email())
|
||||
.transform((v) => v.toLowerCase()),
|
||||
password: passwordSchema
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
|
||||
.pipe(passwordSchema)
|
||||
})
|
||||
}),
|
||||
flags: z
|
||||
.object({
|
||||
require_email_verification: z.boolean().optional(),
|
||||
disable_signup_without_invite: z.boolean().optional(),
|
||||
disable_user_create_org: z.boolean().optional(),
|
||||
allow_raw_resources: z.boolean().optional(),
|
||||
allow_base_domain_resources: z.boolean().optional(),
|
||||
allow_local_sites: z.boolean().optional(),
|
||||
enable_integration_api: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export function readConfigFile() {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
let environment: any;
|
||||
if (fs.existsSync(configFilePath1)) {
|
||||
environment = loadConfig(configFilePath1);
|
||||
} else if (fs.existsSync(configFilePath2)) {
|
||||
environment = loadConfig(configFilePath2);
|
||||
}
|
||||
|
||||
if (process.env.APP_BASE_DOMAIN) {
|
||||
console.log(
|
||||
"You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
|
||||
);
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(
|
||||
"No configuration file found. Please create one. https://docs.fossorial.io/"
|
||||
);
|
||||
}
|
||||
|
||||
const parsedConfig = configSchema.safeParse(environment);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
const errors = fromError(parsedConfig.error);
|
||||
throw new Error(`Invalid configuration file: ${errors}`);
|
||||
}
|
||||
|
||||
return parsedConfig.data;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function stoi(val: any) {
|
||||
if (typeof val === "string") {
|
||||
return parseInt(val)
|
||||
return parseInt(val);
|
||||
}
|
||||
else {
|
||||
return val;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import db from "@server/db";
|
||||
import { hostMeta, licenseKey, sites } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { hostMeta, licenseKey, sites } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import NodeCache from "node-cache";
|
||||
import { validateJWT } from "./licenseJwt";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs, orgs } from "@server/db/schemas";
|
||||
import { userOrgs, orgs } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas";
|
||||
import { resourceAccessToken, resources, apiKeyOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import db from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys, apiKeyOrg } from "@server/db/schemas";
|
||||
import { apiKeys, apiKeyOrg } from "@server/db";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -3,8 +3,8 @@ import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import db from "@server/db";
|
||||
import { apiKeyActions } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyActions } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export function verifyApiKeyHasAction(action: ActionsEnum) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg } from "@server/db/schemas";
|
||||
import { apiKeyOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resources, apiKeyOrg } from "@server/db/schemas";
|
||||
import { resources, apiKeyOrg } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { roles, apiKeyOrg } from "@server/db/schemas";
|
||||
import { roles, apiKeyOrg } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { db } from "@server/db";
|
||||
import {
|
||||
sites,
|
||||
apiKeyOrg
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resources, targets, apiKeyOrg } from "@server/db/schemas";
|
||||
import { resources, targets, apiKeyOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resourceAccessToken, resources, userOrgs } from "@server/db/schemas";
|
||||
import { resourceAccessToken, resources, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userOrgs } from "@server/db/schemas";
|
||||
import { roles, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas";
|
||||
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
userOrgs,
|
||||
userResources,
|
||||
roleResources,
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userOrgs } from "@server/db/schemas";
|
||||
import { roles, userOrgs } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextFunction, Response } from "express";
|
||||
import ErrorResponse from "@server/types/ErrorResponse";
|
||||
import { db } from "@server/db";
|
||||
import { users } from "@server/db/schemas";
|
||||
import { users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq, inArray, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
userSites,
|
||||
roleSites,
|
||||
roles,
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resources, targets, userOrgs } from "@server/db/schemas";
|
||||
import { resources, targets, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextFunction, Response } from "express";
|
||||
import ErrorResponse from "@server/types/ErrorResponse";
|
||||
import { db } from "@server/db";
|
||||
import { users } from "@server/db/schemas";
|
||||
import { users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -5,9 +5,9 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { resourceAccessToken } from "@server/db/schemas";
|
||||
import { resourceAccessToken } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteAccessTokenParamsSchema = z
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
generateIdFromEntropySize,
|
||||
SESSION_COOKIE_EXPIRES
|
||||
} from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
ResourceAccessToken,
|
||||
resourceAccessToken,
|
||||
resources
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
roleResources,
|
||||
resourceAccessToken,
|
||||
sites
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { apiKeyOrg, apiKeys, orgs } from "@server/db/schemas";
|
||||
import { apiKeyOrg, apiKeys, orgs } from "@server/db";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import { apiKeys } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import { apiKeys } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from "@server/db";
|
||||
import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import { apiKeys } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { actions, apiKeyActions } from "@server/db/schemas";
|
||||
import { actions, apiKeyActions } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg, orgs } from "@server/db/schemas";
|
||||
import { apiKeyOrg, orgs } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
@@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { User, users } from "@server/db/schemas";
|
||||
import { User, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { response } from "@server/lib";
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { User, users } from "@server/db/schemas";
|
||||
import { User, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { response } from "@server/lib";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
generateSessionToken,
|
||||
serializeSessionCookie
|
||||
} from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { users } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { users } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
@@ -37,6 +37,8 @@ export type LoginResponse = {
|
||||
emailVerificationRequired?: boolean;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function login(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function logout(
|
||||
try {
|
||||
await invalidateSession(session.sessionId);
|
||||
} catch (error) {
|
||||
logger.error("Failed to invalidate session", error)
|
||||
logger.error("Failed to invalidate session", error);
|
||||
}
|
||||
|
||||
const isSecure = req.protocol === "https";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { User } from "@server/db/schemas";
|
||||
import { User } from "@server/db";
|
||||
import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { db } from "@server/db";
|
||||
import { passwordResetTokens, users } from "@server/db/schemas";
|
||||
import { passwordResetTokens, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
|
||||
import { createDate } from "oslo";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { encodeHex } from "oslo/encoding";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { db } from "@server/db";
|
||||
import { User, users } from "@server/db/schemas";
|
||||
import { User, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { createTOTPKeyURI } from "oslo/otp";
|
||||
import logger from "@server/logger";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { db } from "@server/db";
|
||||
import { passwordResetTokens, users } from "@server/db/schemas";
|
||||
import { passwordResetTokens, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { hashPassword, verifyPassword } from "@server/auth/password";
|
||||
import { verifyTotpCode } from "@server/auth/totp";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { users } from "@server/db/schemas";
|
||||
import { users } from "@server/db";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { db } from "@server/db";
|
||||
import { User, emailVerificationCodes, users } from "@server/db/schemas";
|
||||
import { User, emailVerificationCodes, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { isWithinExpirationDate } from "oslo";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { db } from "@server/db";
|
||||
import { twoFactorBackupCodes, User, users } from "@server/db/schemas";
|
||||
import { twoFactorBackupCodes, User, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { alphabet, generateRandomString } from "oslo/crypto";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
|
||||
@@ -4,8 +4,8 @@ import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { resourceAccessToken, resources, sessions } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { resourceAccessToken, resources, sessions } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
createResourceSession,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
validateResourceSessionToken
|
||||
} from "@server/auth/sessions/resource";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
userOrgs,
|
||||
userResources,
|
||||
users
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
import { response } from "@server/lib/response";
|
||||
@@ -56,9 +56,16 @@ export type VerifyResourceSessionSchema = z.infer<
|
||||
typeof verifyResourceSessionSchema
|
||||
>;
|
||||
|
||||
type BasicUserData = {
|
||||
username: string;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
|
||||
export type VerifyUserResponse = {
|
||||
valid: boolean;
|
||||
redirectUrl?: string;
|
||||
userData?: BasicUserData;
|
||||
};
|
||||
|
||||
export async function verifyResourceSession(
|
||||
@@ -90,7 +97,28 @@ export async function verifyResourceSession(
|
||||
query
|
||||
} = parsedBody.data;
|
||||
|
||||
const clientIp = requestIp?.split(":")[0];
|
||||
const clientIp = requestIp
|
||||
? (() => {
|
||||
logger.debug("Request IP:", { requestIp });
|
||||
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||
// if brackets are found, extract the IPv6 address from between the brackets
|
||||
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// ivp4
|
||||
// split at last colon
|
||||
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return requestIp.substring(0, lastColonIndex);
|
||||
}
|
||||
return requestIp;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
logger.debug("Client IP:", { clientIp });
|
||||
|
||||
let cleanHost = host;
|
||||
// if the host ends with :443 or :80 remove it
|
||||
@@ -350,23 +378,26 @@ export async function verifyResourceSession(
|
||||
resourceSession.userSessionId
|
||||
}:${resource.resourceId}`;
|
||||
|
||||
let isAllowed: boolean | undefined =
|
||||
let allowedUserData: BasicUserData | null | undefined =
|
||||
cache.get(userAccessCacheKey);
|
||||
|
||||
if (isAllowed === undefined) {
|
||||
isAllowed = await isUserAllowedToAccessResource(
|
||||
if (allowedUserData === undefined) {
|
||||
allowedUserData = await isUserAllowedToAccessResource(
|
||||
resourceSession.userSessionId,
|
||||
resource
|
||||
);
|
||||
|
||||
cache.set(userAccessCacheKey, isAllowed);
|
||||
cache.set(userAccessCacheKey, allowedUserData);
|
||||
}
|
||||
|
||||
if (isAllowed) {
|
||||
if (
|
||||
allowedUserData !== null &&
|
||||
allowedUserData !== undefined
|
||||
) {
|
||||
logger.debug(
|
||||
"Resource allowed because user session is valid"
|
||||
);
|
||||
return allowed(res);
|
||||
return allowed(res, allowedUserData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,15 +479,17 @@ function notAllowed(res: Response, redirectUrl?: string) {
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
function allowed(res: Response) {
|
||||
function allowed(res: Response, userData?: BasicUserData) {
|
||||
const data = {
|
||||
data: { valid: true },
|
||||
data:
|
||||
userData !== undefined && userData !== null
|
||||
? { valid: true, ...userData }
|
||||
: { valid: true },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access allowed",
|
||||
status: HttpCode.OK
|
||||
};
|
||||
logger.debug(JSON.stringify(data));
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
@@ -496,7 +529,7 @@ async function createAccessTokenSession(
|
||||
async function isUserAllowedToAccessResource(
|
||||
userSessionId: string,
|
||||
resource: Resource
|
||||
): Promise<boolean> {
|
||||
): Promise<BasicUserData | null> {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
@@ -507,14 +540,14 @@ async function isUserAllowedToAccessResource(
|
||||
const session = res.session;
|
||||
|
||||
if (!user || !session) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
config.getRawConfig().flags?.require_email_verification &&
|
||||
!user.emailVerified
|
||||
) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const userOrgRole = await db
|
||||
@@ -529,7 +562,7 @@ async function isUserAllowedToAccessResource(
|
||||
.limit(1);
|
||||
|
||||
if (userOrgRole.length === 0) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const roleResourceAccess = await db
|
||||
@@ -544,7 +577,11 @@ async function isUserAllowedToAccessResource(
|
||||
.limit(1);
|
||||
|
||||
if (roleResourceAccess.length > 0) {
|
||||
return true;
|
||||
return {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name
|
||||
};
|
||||
}
|
||||
|
||||
const userResourceAccess = await db
|
||||
@@ -559,10 +596,14 @@ async function isUserAllowedToAccessResource(
|
||||
.limit(1);
|
||||
|
||||
if (userResourceAccess.length > 0) {
|
||||
return true;
|
||||
return {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
async function checkRules(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { domains, orgDomains, users } from "@server/db/schemas";
|
||||
import { domains, orgDomains, users } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
getUserOrgs,
|
||||
verifyUserIsServerAdmin,
|
||||
verifyIsLoggedInUser,
|
||||
verifyApiKeyAccess,
|
||||
verifyApiKeyAccess
|
||||
} from "@server/middlewares";
|
||||
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
@@ -124,6 +124,37 @@ authenticated.delete(
|
||||
site.deleteSite
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/status",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.dockerStatus
|
||||
);
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/online",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.dockerOnline
|
||||
);
|
||||
authenticated.post(
|
||||
"/site/:siteId/docker/check",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.checkDockerSocket
|
||||
);
|
||||
authenticated.post(
|
||||
"/site/:siteId/docker/trigger",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.triggerFetchContainers
|
||||
);
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/containers",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.listContainers
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
verifyOrgAccess,
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { sites, resources, targets, exitNodes } from '@server/db/schemas';
|
||||
import { db } from '@server/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { sites, resources, targets, exitNodes } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
import createHttpError from 'http-errors';
|
||||
import logger from '@server/logger';
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { getUniqueExitNodeEndpointName } from '@server/db/names';
|
||||
import { getUniqueExitNodeEndpointName } from "../../db/names";
|
||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||
import { fromError } from 'zod-validation-error';
|
||||
import { getAllowedIps } from '../target/helpers';
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { getAllowedIps } from "../target/helpers";
|
||||
// Define Zod schema for request validation
|
||||
const getConfigSchema = z.object({
|
||||
publicKey: z.string(),
|
||||
reachableAt: z.string().optional(),
|
||||
reachableAt: z.string().optional()
|
||||
});
|
||||
|
||||
export type GetConfigResponse = {
|
||||
@@ -25,9 +25,13 @@ export type GetConfigResponse = {
|
||||
publicKey: string | null;
|
||||
allowedIps: string[];
|
||||
}[];
|
||||
}
|
||||
};
|
||||
|
||||
export async function getConfig(req: Request, res: Response, next: NextFunction): Promise<any> {
|
||||
export async function getConfig(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Validate request parameters
|
||||
const parsedParams = getConfigSchema.safeParse(req.body);
|
||||
@@ -43,11 +47,16 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
|
||||
const { publicKey, reachableAt } = parsedParams.data;
|
||||
|
||||
if (!publicKey) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, 'publicKey is required'));
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "publicKey is required")
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch exit node
|
||||
let exitNodeQuery = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
|
||||
let exitNodeQuery = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.publicKey, publicKey));
|
||||
let exitNode;
|
||||
if (exitNodeQuery.length === 0) {
|
||||
const address = await getNextAvailableSubnet();
|
||||
@@ -60,40 +69,53 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
|
||||
}
|
||||
|
||||
// create a new exit node
|
||||
exitNode = await db.insert(exitNodes).values({
|
||||
publicKey,
|
||||
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
||||
address,
|
||||
listenPort,
|
||||
reachableAt,
|
||||
name: `Exit Node ${publicKey.slice(0, 8)}`,
|
||||
}).returning().execute();
|
||||
exitNode = await db
|
||||
.insert(exitNodes)
|
||||
.values({
|
||||
publicKey,
|
||||
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
||||
address,
|
||||
listenPort,
|
||||
reachableAt,
|
||||
name: `Exit Node ${publicKey.slice(0, 8)}`
|
||||
})
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
logger.info(`Created new exit node ${exitNode[0].name} with address ${exitNode[0].address} and port ${exitNode[0].listenPort}`);
|
||||
logger.info(
|
||||
`Created new exit node ${exitNode[0].name} with address ${exitNode[0].address} and port ${exitNode[0].listenPort}`
|
||||
);
|
||||
} else {
|
||||
exitNode = exitNodeQuery;
|
||||
}
|
||||
|
||||
if (!exitNode) {
|
||||
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to create exit node"));
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create exit node"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch sites for this exit node
|
||||
const sitesRes = await db.query.sites.findMany({
|
||||
where: eq(sites.exitNodeId, exitNode[0].exitNodeId),
|
||||
});
|
||||
const sitesRes = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.exitNodeId, exitNode[0].exitNodeId));
|
||||
|
||||
const peers = await Promise.all(sitesRes.map(async (site) => {
|
||||
return {
|
||||
publicKey: site.pubKey,
|
||||
allowedIps: await getAllowedIps(site.siteId)
|
||||
};
|
||||
}));
|
||||
const peers = await Promise.all(
|
||||
sitesRes.map(async (site) => {
|
||||
return {
|
||||
publicKey: site.pubKey,
|
||||
allowedIps: await getAllowedIps(site.siteId)
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const configResponse: GetConfigResponse = {
|
||||
listenPort: exitNode[0].listenPort || 51820,
|
||||
ipAddress: exitNode[0].address,
|
||||
peers,
|
||||
peers
|
||||
};
|
||||
|
||||
logger.debug("Sending config: ", configResponse);
|
||||
@@ -101,32 +123,49 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
|
||||
return res.status(HttpCode.OK).send(configResponse);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred..."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextAvailableSubnet(): Promise<string> {
|
||||
// Get all existing subnets from routes table
|
||||
const existingAddresses = await db.select({
|
||||
address: exitNodes.address,
|
||||
}).from(exitNodes);
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
address: exitNodes.address
|
||||
})
|
||||
.from(exitNodes);
|
||||
|
||||
const addresses = existingAddresses.map(a => a.address);
|
||||
let subnet = findNextAvailableCidr(addresses, config.getRawConfig().gerbil.block_size, config.getRawConfig().gerbil.subnet_group);
|
||||
const addresses = existingAddresses.map((a) => a.address);
|
||||
let subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().gerbil.block_size,
|
||||
config.getRawConfig().gerbil.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error('No available subnets remaining in space');
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
// replace the last octet with 1
|
||||
subnet = subnet.split('.').slice(0, 3).join('.') + '.1' + '/' + subnet.split('/')[1];
|
||||
subnet =
|
||||
subnet.split(".").slice(0, 3).join(".") +
|
||||
".1" +
|
||||
"/" +
|
||||
subnet.split("/")[1];
|
||||
return subnet;
|
||||
}
|
||||
|
||||
async function getNextAvailablePort(): Promise<number> {
|
||||
// Get all existing ports from exitNodes table
|
||||
const existingPorts = await db.select({
|
||||
listenPort: exitNodes.listenPort,
|
||||
}).from(exitNodes);
|
||||
const existingPorts = await db
|
||||
.select({
|
||||
listenPort: exitNodes.listenPort
|
||||
})
|
||||
.from(exitNodes);
|
||||
|
||||
// Find the first available port between 1024 and 65535
|
||||
let nextPort = config.getRawConfig().gerbil.start_port;
|
||||
@@ -136,7 +175,7 @@ async function getNextAvailablePort(): Promise<number> {
|
||||
}
|
||||
nextPort++;
|
||||
if (nextPort > 65535) {
|
||||
throw new Error('No available ports remaining in space');
|
||||
throw new Error("No available ports remaining in space");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import logger from '@server/logger';
|
||||
import db from '@server/db';
|
||||
import { exitNodes } from '@server/db/schemas';
|
||||
import { db } from "@server/db";
|
||||
import { exitNodes } from '@server/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function addPeer(exitNodeId: number, peer: {
|
||||
@@ -24,7 +24,7 @@ export async function addPeer(exitNodeId: number, peer: {
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Peer added successfully:', response.data.status);
|
||||
logger.info('Peer added successfully:', { peer: response.data.status });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { DrizzleError, eq } from "drizzle-orm";
|
||||
import { sites, resources, targets, exitNodes } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sites, } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -29,10 +29,11 @@ export const receiveBandwidth = async (
|
||||
for (const peer of bandwidthData) {
|
||||
const { publicKey, bytesIn, bytesOut } = peer;
|
||||
|
||||
// Find the site by public key
|
||||
const site = await trx.query.sites.findFirst({
|
||||
where: eq(sites.pubKey, publicKey)
|
||||
});
|
||||
const [site] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.pubKey, publicKey))
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
logger.warn(`Site not found for public key: ${publicKey}`);
|
||||
@@ -84,10 +85,4 @@ export const receiveBandwidth = async (
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function calculateSubnet(index: number): string {
|
||||
const baseIp = 10 << 24;
|
||||
const subnetSize = 16;
|
||||
return `${(baseIp | (index * subnetSize)).toString()}/28`;
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import config from "@server/lib/config";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { idp, idpOrg } from "@server/db/schemas";
|
||||
import { idp, idpOrg } from "@server/db";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
|
||||
@@ -7,7 +7,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas";
|
||||
import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db";
|
||||
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas";
|
||||
import { idp, idpOidcConfig, idpOrg } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { idp, idpOrg } from "@server/db/schemas";
|
||||
import { idp, idpOrg } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas";
|
||||
import { idp, idpOidcConfig, idpOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import * as arctic from "arctic";
|
||||
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { idp, idpOidcConfig } from "@server/db/schemas";
|
||||
import { idp, idpOidcConfig } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { idpOrg } from "@server/db/schemas";
|
||||
import { idpOrg } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { domains, idp, orgDomains, users, idpOrg } from "@server/db/schemas";
|
||||
import { domains, idp, orgDomains, users, idpOrg } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
@@ -8,7 +8,7 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { idp, idpOrg } from "@server/db/schemas";
|
||||
import { idp, idpOrg } from "@server/db";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
|
||||
@@ -7,7 +7,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { idp, idpOidcConfig } from "@server/db/schemas";
|
||||
import { idp, idpOidcConfig } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
roles,
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db/schemas";
|
||||
} from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import * as arctic from "arctic";
|
||||
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||
|
||||
@@ -280,6 +280,13 @@ authenticated.delete(
|
||||
role.deleteRole
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/role/:roleId",
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getRole),
|
||||
role.getRole
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/role/:roleId/add/:userId",
|
||||
verifyApiKeyRoleAccess,
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as badger from "./badger";
|
||||
import * as auth from "@server/routers/auth";
|
||||
import * as supporterKey from "@server/routers/supporterKey";
|
||||
import * as license from "@server/routers/license";
|
||||
import * as idp from "@server/routers/idp";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import {
|
||||
verifyResourceAccess,
|
||||
@@ -38,10 +39,11 @@ internalRouter.get(
|
||||
supporterKey.isSupporterKeyVisible
|
||||
);
|
||||
|
||||
internalRouter.get(
|
||||
`/license/status`,
|
||||
license.getLicenseStatus
|
||||
);
|
||||
internalRouter.get(`/license/status`, license.getLicenseStatus);
|
||||
|
||||
internalRouter.get("/idp", idp.listIdps);
|
||||
|
||||
internalRouter.get("/idp/:idpId", idp.getIdp);
|
||||
|
||||
// Gerbil routes
|
||||
const gerbilRouter = Router();
|
||||
|
||||
@@ -5,9 +5,9 @@ import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import db from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { licenseKey } from "@server/db/schemas";
|
||||
import { licenseKey } from "@server/db";
|
||||
import license, { LicenseStatus } from "@server/license/license";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user