mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
use resource guid in url closes #1517
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
text
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export const domains = pgTable("domains", {
|
||||
domainId: varchar("domainId").primaryKey(),
|
||||
@@ -66,6 +67,10 @@ export const sites = pgTable("sites", {
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
resourceId: serial("resourceId").primaryKey(),
|
||||
resourceGuid: varchar("resourceGuid", { length: 36 })
|
||||
.unique()
|
||||
.notNull()
|
||||
.$defaultFn(() => randomUUID()),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
@@ -96,7 +101,7 @@ export const resources = pgTable("resources", {
|
||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||
headers: text("headers") // comma-separated list of headers to add to the request
|
||||
});
|
||||
|
||||
export const targets = pgTable("targets", {
|
||||
@@ -117,7 +122,7 @@ export const targets = pgTable("targets", {
|
||||
internalPort: integer("internalPort"),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
path: text("path"),
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
pathMatchType: text("pathMatchType") // exact, prefix, regex
|
||||
});
|
||||
|
||||
export const exitNodes = pgTable("exitNodes", {
|
||||
@@ -135,7 +140,8 @@ export const exitNodes = pgTable("exitNodes", {
|
||||
region: varchar("region")
|
||||
});
|
||||
|
||||
export const siteResources = pgTable("siteResources", { // this is for the clients
|
||||
export const siteResources = pgTable("siteResources", {
|
||||
// this is for the clients
|
||||
siteResourceId: serial("siteResourceId").primaryKey(),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
@@ -149,7 +155,7 @@ export const siteResources = pgTable("siteResources", { // this is for the clien
|
||||
proxyPort: integer("proxyPort").notNull(),
|
||||
destinationPort: integer("destinationPort").notNull(),
|
||||
destinationIp: varchar("destinationIp").notNull(),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
enabled: boolean("enabled").notNull().default(true)
|
||||
});
|
||||
|
||||
export const users = pgTable("user", {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
|
||||
@@ -72,6 +73,10 @@ export const sites = sqliteTable("sites", {
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||
resourceGuid: text("resourceGuid", { length: 36 })
|
||||
.unique()
|
||||
.notNull()
|
||||
.$defaultFn(() => randomUUID()),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
@@ -108,7 +113,7 @@ export const resources = sqliteTable("resources", {
|
||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||
headers: text("headers") // comma-separated list of headers to add to the request
|
||||
});
|
||||
|
||||
export const targets = sqliteTable("targets", {
|
||||
@@ -129,7 +134,7 @@ export const targets = sqliteTable("targets", {
|
||||
internalPort: integer("internalPort"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
path: text("path"),
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
pathMatchType: text("pathMatchType") // exact, prefix, regex
|
||||
});
|
||||
|
||||
export const exitNodes = sqliteTable("exitNodes", {
|
||||
|
||||
@@ -206,7 +206,7 @@ export async function verifyResourceSession(
|
||||
endpoint = config.getRawConfig().app.dashboard_url!;
|
||||
}
|
||||
const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent(
|
||||
resource.resourceId
|
||||
resource.resourceGuid
|
||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
|
||||
// check for access token in headers
|
||||
|
||||
@@ -540,7 +540,7 @@ authenticated.post(
|
||||
);
|
||||
authenticated.post(`/supporter-key/hide`, supporterKey.hideSupporterKey);
|
||||
|
||||
unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
|
||||
unauthenticated.get("/resource/:resourceGuid/auth", resource.getResourceAuthInfo);
|
||||
|
||||
// authenticated.get(
|
||||
// "/role/:roleId/resources",
|
||||
|
||||
@@ -15,16 +15,15 @@ import logger from "@server/logger";
|
||||
|
||||
const getResourceAuthInfoSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
resourceGuid: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type GetResourceAuthInfoResponse = {
|
||||
resourceId: number;
|
||||
resourceGuid: string;
|
||||
resourceName: string;
|
||||
niceId: string;
|
||||
password: boolean;
|
||||
pincode: boolean;
|
||||
sso: boolean;
|
||||
@@ -51,7 +50,7 @@ export async function getResourceAuthInfo(
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { resourceGuid } = parsedParams.data;
|
||||
|
||||
const [result] = await db
|
||||
.select()
|
||||
@@ -64,7 +63,7 @@ export async function getResourceAuthInfo(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.where(eq(resources.resourceGuid, resourceGuid))
|
||||
.limit(1);
|
||||
|
||||
const resource = result?.resources;
|
||||
@@ -81,6 +80,8 @@ export async function getResourceAuthInfo(
|
||||
|
||||
return response<GetResourceAuthInfoResponse>(res, {
|
||||
data: {
|
||||
niceId: resource.niceId,
|
||||
resourceGuid: resource.resourceGuid,
|
||||
resourceId: resource.resourceId,
|
||||
resourceName: resource.name,
|
||||
password: password !== null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db } from "@server/db/pg/driver";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const version = "1.10.4";
|
||||
|
||||
@@ -10,34 +11,70 @@ export default async function migration() {
|
||||
try {
|
||||
await db.execute(sql`BEGIN`);
|
||||
|
||||
const webauthnCredentialsQuery = await db.execute(sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"`);
|
||||
const webauthnCredentialsQuery = await db.execute(
|
||||
sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"`
|
||||
);
|
||||
|
||||
const webauthnCredentials = webauthnCredentialsQuery.rows as {
|
||||
credentialId: string;
|
||||
publicKey: string;
|
||||
userId: string;
|
||||
signCount: number;
|
||||
transports: string | null;
|
||||
name: string | null;
|
||||
lastUsed: string;
|
||||
const webauthnCredentials = webauthnCredentialsQuery.rows as {
|
||||
credentialId: string;
|
||||
publicKey: string;
|
||||
userId: string;
|
||||
signCount: number;
|
||||
transports: string | null;
|
||||
name: string | null;
|
||||
lastUsed: string;
|
||||
dateCreated: string;
|
||||
}[];
|
||||
|
||||
for (const webauthnCredential of webauthnCredentials) {
|
||||
const newCredentialId = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.credentialId, 'base64')));
|
||||
const newPublicKey = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.publicKey, 'base64')));
|
||||
|
||||
const newCredentialId = isoBase64URL.fromBuffer(
|
||||
new Uint8Array(
|
||||
Buffer.from(webauthnCredential.credentialId, "base64")
|
||||
)
|
||||
);
|
||||
const newPublicKey = isoBase64URL.fromBuffer(
|
||||
new Uint8Array(
|
||||
Buffer.from(webauthnCredential.publicKey, "base64")
|
||||
)
|
||||
);
|
||||
|
||||
// Delete the old record
|
||||
await db.execute(sql`
|
||||
DELETE FROM "webauthnCredentials"
|
||||
DELETE FROM "webauthnCredentials"
|
||||
WHERE "credentialId" = ${webauthnCredential.credentialId}
|
||||
`);
|
||||
|
||||
|
||||
// Insert the updated record with converted values
|
||||
await db.execute(sql`
|
||||
INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
|
||||
VALUES (${newCredentialId}, ${newPublicKey}, ${webauthnCredential.userId}, ${webauthnCredential.signCount}, ${webauthnCredential.transports}, ${webauthnCredential.name}, ${webauthnCredential.lastUsed}, ${webauthnCredential.dateCreated})
|
||||
`);
|
||||
|
||||
// 1. Add the column with placeholder so NOT NULL is satisfied
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "resources"
|
||||
ADD COLUMN IF NOT EXISTS "resourceGuid" varchar(36) NOT NULL DEFAULT 'PLACEHOLDER'
|
||||
`);
|
||||
|
||||
// 2. Fetch every row to backfill UUIDs
|
||||
const rows = await db.execute(
|
||||
sql`SELECT "resourceId" FROM "resources" WHERE "resourceGuid" = 'PLACEHOLDER'`
|
||||
);
|
||||
const resources = rows.rows as { resourceId: number }[];
|
||||
|
||||
for (const r of resources) {
|
||||
await db.execute(sql`
|
||||
UPDATE "resources"
|
||||
SET "resourceGuid" = ${randomUUID()}
|
||||
WHERE "resourceId" = ${r.resourceId}
|
||||
`);
|
||||
}
|
||||
|
||||
// 3. Add UNIQUE constraint now that values are filled
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "resources"
|
||||
ADD CONSTRAINT "resources_resourceGuid_unique" UNIQUE("resourceGuid")
|
||||
`);
|
||||
}
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async function migration() {
|
||||
ALTER TABLE "sites" ADD COLUMN "remoteSubnets" text;
|
||||
ALTER TABLE "user" ADD COLUMN "termsAcceptedTimestamp" varchar;
|
||||
ALTER TABLE "user" ADD COLUMN "termsVersion" varchar;
|
||||
|
||||
|
||||
COMMIT;
|
||||
`);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const version = "1.10.4";
|
||||
|
||||
@@ -11,34 +12,77 @@ export default async function migration() {
|
||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
const db = new Database(location);
|
||||
|
||||
db.transaction(() => {
|
||||
|
||||
const webauthnCredentials = db.prepare(`SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'`).all() as {
|
||||
credentialId: string; publicKey: string; userId: string; signCount: number; transports: string | null; name: string | null; lastUsed: string; dateCreated: string;
|
||||
db.transaction(() => {
|
||||
const webauthnCredentials = db
|
||||
.prepare(
|
||||
`SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'`
|
||||
)
|
||||
.all() as {
|
||||
credentialId: string;
|
||||
publicKey: string;
|
||||
userId: string;
|
||||
signCount: number;
|
||||
transports: string | null;
|
||||
name: string | null;
|
||||
lastUsed: string;
|
||||
dateCreated: string;
|
||||
}[];
|
||||
|
||||
for (const webauthnCredential of webauthnCredentials) {
|
||||
const newCredentialId = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.credentialId, 'base64')));
|
||||
const newPublicKey = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.publicKey, 'base64')));
|
||||
|
||||
const newCredentialId = isoBase64URL.fromBuffer(
|
||||
new Uint8Array(
|
||||
Buffer.from(webauthnCredential.credentialId, "base64")
|
||||
)
|
||||
);
|
||||
const newPublicKey = isoBase64URL.fromBuffer(
|
||||
new Uint8Array(
|
||||
Buffer.from(webauthnCredential.publicKey, "base64")
|
||||
)
|
||||
);
|
||||
|
||||
// Delete the old record
|
||||
db.prepare(`DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?`).run(webauthnCredential.credentialId);
|
||||
|
||||
db.prepare(
|
||||
`DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?`
|
||||
).run(webauthnCredential.credentialId);
|
||||
|
||||
// Insert the updated record with converted values
|
||||
db.prepare(
|
||||
`INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
newCredentialId,
|
||||
newPublicKey,
|
||||
webauthnCredential.userId,
|
||||
webauthnCredential.signCount,
|
||||
webauthnCredential.transports,
|
||||
webauthnCredential.name,
|
||||
webauthnCredential.lastUsed,
|
||||
newCredentialId,
|
||||
newPublicKey,
|
||||
webauthnCredential.userId,
|
||||
webauthnCredential.signCount,
|
||||
webauthnCredential.transports,
|
||||
webauthnCredential.name,
|
||||
webauthnCredential.lastUsed,
|
||||
webauthnCredential.dateCreated
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
// 1. Add the column (nullable or with placeholder) if it doesn’t exist yet
|
||||
db.prepare(
|
||||
`ALTER TABLE resources ADD COLUMN resourceGuid TEXT DEFAULT 'PLACEHOLDER';`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`CREATE UNIQUE INDEX resources_resourceGuid_unique ON resources ('resourceGuid');`
|
||||
).run();
|
||||
|
||||
// 2. Select all rows
|
||||
const rows = db.prepare(`SELECT resourceId FROM resources`).all() as {
|
||||
resourceId: number;
|
||||
}[];
|
||||
|
||||
// 3. Prefill with random UUIDs
|
||||
const updateStmt = db.prepare(
|
||||
`UPDATE resources SET resourceGuid = ? WHERE resourceId = ?`
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
updateStmt.run(randomUUID(), row.resourceId);
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import AutoLoginHandler from "@app/components/AutoLoginHandler";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ResourceAuthPage(props: {
|
||||
params: Promise<{ resourceId: number }>;
|
||||
params: Promise<{ resourceGuid: number }>;
|
||||
searchParams: Promise<{
|
||||
redirect: string | undefined;
|
||||
token: string | undefined;
|
||||
@@ -37,7 +37,7 @@ export default async function ResourceAuthPage(props: {
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<GetResourceAuthInfoResponse>
|
||||
>(`/resource/${params.resourceId}/auth`, authHeader);
|
||||
>(`/resource/${params.resourceGuid}/auth`, authHeader);
|
||||
|
||||
if (res && res.status === 200) {
|
||||
authInfo = res.data.data;
|
||||
@@ -48,10 +48,8 @@ export default async function ResourceAuthPage(props: {
|
||||
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||
|
||||
if (!authInfo) {
|
||||
// TODO: fix this
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
{/* @ts-ignore */}
|
||||
<ResourceNotFound />
|
||||
</div>
|
||||
);
|
||||
@@ -86,7 +84,7 @@ export default async function ResourceAuthPage(props: {
|
||||
|
||||
if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
|
||||
redirect(
|
||||
`/auth/verify-email?redirect=/auth/resource/${authInfo.resourceId}`
|
||||
`/auth/verify-email?redirect=/auth/resource/${authInfo.resourceGuid}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +101,7 @@ export default async function ResourceAuthPage(props: {
|
||||
const res = await priv.post<
|
||||
AxiosResponse<GetExchangeTokenResponse>
|
||||
>(
|
||||
`/resource/${params.resourceId}/get-exchange-token`,
|
||||
`/resource/${authInfo.resourceId}/get-exchange-token`,
|
||||
{},
|
||||
await authCookieHeader()
|
||||
);
|
||||
@@ -132,7 +130,7 @@ export default async function ResourceAuthPage(props: {
|
||||
<div className="w-full max-w-md">
|
||||
<AccessToken
|
||||
token={searchParams.token}
|
||||
resourceId={params.resourceId}
|
||||
resourceId={authInfo.resourceId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
Reference in New Issue
Block a user