mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-02 16:56:39 +00:00
use resource guid in url closes #1517
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
|||||||
text
|
text
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
@@ -66,6 +67,10 @@ export const sites = pgTable("sites", {
|
|||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
resourceId: serial("resourceId").primaryKey(),
|
resourceId: serial("resourceId").primaryKey(),
|
||||||
|
resourceGuid: varchar("resourceGuid", { length: 36 })
|
||||||
|
.unique()
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => randomUUID()),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -96,7 +101,7 @@ export const resources = pgTable("resources", {
|
|||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
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", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -117,7 +122,7 @@ export const targets = pgTable("targets", {
|
|||||||
internalPort: integer("internalPort"),
|
internalPort: integer("internalPort"),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
path: text("path"),
|
path: text("path"),
|
||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType") // exact, prefix, regex
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = pgTable("exitNodes", {
|
export const exitNodes = pgTable("exitNodes", {
|
||||||
@@ -135,7 +140,8 @@ export const exitNodes = pgTable("exitNodes", {
|
|||||||
region: varchar("region")
|
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(),
|
siteResourceId: serial("siteResourceId").primaryKey(),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -149,7 +155,7 @@ export const siteResources = pgTable("siteResources", { // this is for the clien
|
|||||||
proxyPort: integer("proxyPort").notNull(),
|
proxyPort: integer("proxyPort").notNull(),
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
destinationPort: integer("destinationPort").notNull(),
|
||||||
destinationIp: varchar("destinationIp").notNull(),
|
destinationIp: varchar("destinationIp").notNull(),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
@@ -72,6 +73,10 @@ export const sites = sqliteTable("sites", {
|
|||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||||
|
resourceGuid: text("resourceGuid", { length: 36 })
|
||||||
|
.unique()
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => randomUUID()),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -108,7 +113,7 @@ export const resources = sqliteTable("resources", {
|
|||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
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", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -129,7 +134,7 @@ export const targets = sqliteTable("targets", {
|
|||||||
internalPort: integer("internalPort"),
|
internalPort: integer("internalPort"),
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
path: text("path"),
|
path: text("path"),
|
||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType") // exact, prefix, regex
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = sqliteTable("exitNodes", {
|
export const exitNodes = sqliteTable("exitNodes", {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export async function verifyResourceSession(
|
|||||||
endpoint = config.getRawConfig().app.dashboard_url!;
|
endpoint = config.getRawConfig().app.dashboard_url!;
|
||||||
}
|
}
|
||||||
const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent(
|
const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent(
|
||||||
resource.resourceId
|
resource.resourceGuid
|
||||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||||
|
|
||||||
// check for access token in headers
|
// check for access token in headers
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ authenticated.post(
|
|||||||
);
|
);
|
||||||
authenticated.post(`/supporter-key/hide`, supporterKey.hideSupporterKey);
|
authenticated.post(`/supporter-key/hide`, supporterKey.hideSupporterKey);
|
||||||
|
|
||||||
unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
|
unauthenticated.get("/resource/:resourceGuid/auth", resource.getResourceAuthInfo);
|
||||||
|
|
||||||
// authenticated.get(
|
// authenticated.get(
|
||||||
// "/role/:roleId/resources",
|
// "/role/:roleId/resources",
|
||||||
|
|||||||
@@ -15,16 +15,15 @@ import logger from "@server/logger";
|
|||||||
|
|
||||||
const getResourceAuthInfoSchema = z
|
const getResourceAuthInfoSchema = z
|
||||||
.object({
|
.object({
|
||||||
resourceId: z
|
resourceGuid: z.string()
|
||||||
.string()
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive())
|
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetResourceAuthInfoResponse = {
|
export type GetResourceAuthInfoResponse = {
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
|
resourceGuid: string;
|
||||||
resourceName: string;
|
resourceName: string;
|
||||||
|
niceId: string;
|
||||||
password: boolean;
|
password: boolean;
|
||||||
pincode: boolean;
|
pincode: boolean;
|
||||||
sso: boolean;
|
sso: boolean;
|
||||||
@@ -51,7 +50,7 @@ export async function getResourceAuthInfo(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resourceId } = parsedParams.data;
|
const { resourceGuid } = parsedParams.data;
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -64,7 +63,7 @@ export async function getResourceAuthInfo(
|
|||||||
resourcePassword,
|
resourcePassword,
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
eq(resourcePassword.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.where(eq(resources.resourceId, resourceId))
|
.where(eq(resources.resourceGuid, resourceGuid))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const resource = result?.resources;
|
const resource = result?.resources;
|
||||||
@@ -81,6 +80,8 @@ export async function getResourceAuthInfo(
|
|||||||
|
|
||||||
return response<GetResourceAuthInfoResponse>(res, {
|
return response<GetResourceAuthInfoResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
niceId: resource.niceId,
|
||||||
|
resourceGuid: resource.resourceGuid,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
password: password !== null,
|
password: password !== null,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { db } from "@server/db/pg/driver";
|
import { db } from "@server/db/pg/driver";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
const version = "1.10.4";
|
const version = "1.10.4";
|
||||||
|
|
||||||
@@ -10,7 +11,9 @@ export default async function migration() {
|
|||||||
try {
|
try {
|
||||||
await db.execute(sql`BEGIN`);
|
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 {
|
const webauthnCredentials = webauthnCredentialsQuery.rows as {
|
||||||
credentialId: string;
|
credentialId: string;
|
||||||
@@ -24,8 +27,16 @@ export default async function migration() {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
for (const webauthnCredential of webauthnCredentials) {
|
for (const webauthnCredential of webauthnCredentials) {
|
||||||
const newCredentialId = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.credentialId, 'base64')));
|
const newCredentialId = isoBase64URL.fromBuffer(
|
||||||
const newPublicKey = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.publicKey, 'base64')));
|
new Uint8Array(
|
||||||
|
Buffer.from(webauthnCredential.credentialId, "base64")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const newPublicKey = isoBase64URL.fromBuffer(
|
||||||
|
new Uint8Array(
|
||||||
|
Buffer.from(webauthnCredential.publicKey, "base64")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Delete the old record
|
// Delete the old record
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
@@ -38,6 +49,32 @@ export default async function migration() {
|
|||||||
INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
|
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})
|
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`);
|
await db.execute(sql`COMMIT`);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
|
|||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
const version = "1.10.4";
|
const version = "1.10.4";
|
||||||
|
|
||||||
@@ -11,18 +12,38 @@ export default async function migration() {
|
|||||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
const db = new Database(location);
|
const db = new Database(location);
|
||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
|
const webauthnCredentials = db
|
||||||
const webauthnCredentials = db.prepare(`SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'`).all() as {
|
.prepare(
|
||||||
credentialId: string; publicKey: string; userId: string; signCount: number; transports: string | null; name: string | null; lastUsed: string; dateCreated: string;
|
`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) {
|
for (const webauthnCredential of webauthnCredentials) {
|
||||||
const newCredentialId = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.credentialId, 'base64')));
|
const newCredentialId = isoBase64URL.fromBuffer(
|
||||||
const newPublicKey = isoBase64URL.fromBuffer(new Uint8Array(Buffer.from(webauthnCredential.publicKey, 'base64')));
|
new Uint8Array(
|
||||||
|
Buffer.from(webauthnCredential.credentialId, "base64")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const newPublicKey = isoBase64URL.fromBuffer(
|
||||||
|
new Uint8Array(
|
||||||
|
Buffer.from(webauthnCredential.publicKey, "base64")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Delete the old record
|
// 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
|
// Insert the updated record with converted values
|
||||||
db.prepare(
|
db.prepare(
|
||||||
@@ -38,7 +59,30 @@ export default async function migration() {
|
|||||||
webauthnCredential.dateCreated
|
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`);
|
console.log(`${version} migration complete`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import AutoLoginHandler from "@app/components/AutoLoginHandler";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function ResourceAuthPage(props: {
|
export default async function ResourceAuthPage(props: {
|
||||||
params: Promise<{ resourceId: number }>;
|
params: Promise<{ resourceGuid: number }>;
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
redirect: string | undefined;
|
redirect: string | undefined;
|
||||||
token: string | undefined;
|
token: string | undefined;
|
||||||
@@ -37,7 +37,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
try {
|
try {
|
||||||
const res = await internal.get<
|
const res = await internal.get<
|
||||||
AxiosResponse<GetResourceAuthInfoResponse>
|
AxiosResponse<GetResourceAuthInfoResponse>
|
||||||
>(`/resource/${params.resourceId}/auth`, authHeader);
|
>(`/resource/${params.resourceGuid}/auth`, authHeader);
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
authInfo = res.data.data;
|
authInfo = res.data.data;
|
||||||
@@ -48,10 +48,8 @@ export default async function ResourceAuthPage(props: {
|
|||||||
const user = await getUser({ skipCheckVerifyEmail: true });
|
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||||
|
|
||||||
if (!authInfo) {
|
if (!authInfo) {
|
||||||
// TODO: fix this
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
{/* @ts-ignore */}
|
|
||||||
<ResourceNotFound />
|
<ResourceNotFound />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -86,7 +84,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
|
|
||||||
if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
|
if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
|
||||||
redirect(
|
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<
|
const res = await priv.post<
|
||||||
AxiosResponse<GetExchangeTokenResponse>
|
AxiosResponse<GetExchangeTokenResponse>
|
||||||
>(
|
>(
|
||||||
`/resource/${params.resourceId}/get-exchange-token`,
|
`/resource/${authInfo.resourceId}/get-exchange-token`,
|
||||||
{},
|
{},
|
||||||
await authCookieHeader()
|
await authCookieHeader()
|
||||||
);
|
);
|
||||||
@@ -132,7 +130,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<AccessToken
|
<AccessToken
|
||||||
token={searchParams.token}
|
token={searchParams.token}
|
||||||
resourceId={params.resourceId}
|
resourceId={authInfo.resourceId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user