Compare commits

..

50 Commits

Author SHA1 Message Date
Owen
02c68b6cd3 Reconnect newts when a exit node comes back online 2026-05-20 15:38:40 -07:00
Owen Schwartz
c47c411161 Merge pull request #3114 from Fredkiss3/fix/tag-input-scroll
fix: make tag input wrap around instead of scrolling
2026-05-19 20:03:59 -07:00
Owen Schwartz
e88e262abe Merge pull request #3004 from Fredkiss3/feat/labels-on-sites-and-resources
feat: site & resource labels
2026-05-19 20:03:22 -07:00
Fred KISSIE
6cacc9b83f 💄 limit tag width 2026-05-19 22:52:44 +02:00
Fred KISSIE
1f1791feb7 💄 make tag input wrap around instead of scrolling 2026-05-19 22:48:15 +02:00
Owen
08a08e73b3 derived only from roles that the user holds AND are assigned to the target resource 2026-05-19 10:53:54 -07:00
Fred KISSIE
2d9c082607 💄 UI 2026-05-18 22:17:49 +02:00
Fred KISSIE
7968c4357b edit org label 2026-05-18 22:14:49 +02:00
Fred KISSIE
25c08e7279 Create label dialog 2026-05-18 21:57:44 +02:00
Fred KISSIE
68d7b0a416 🚧 wip: label 2026-05-14 22:43:29 +02:00
Fred KISSIE
43546c84eb 🚧 wip: create label dialog 2026-05-14 22:42:01 +02:00
Fred KISSIE
eac36ee442 delete label 2026-05-14 22:15:43 +02:00
Fred KISSIE
9a88394efe 🛂 gate label endpoints behing subscription 2026-05-14 21:17:58 +02:00
Fred KISSIE
173562654b delete org label endpoint 2026-05-14 21:09:48 +02:00
Fred KISSIE
8f7e5ab1ed 🚧 wip: org labels page 2026-05-14 19:31:53 +02:00
Fred KISSIE
4334480675 ♻️ refactor 2026-05-14 18:33:29 +02:00
Fred KISSIE
6aa406927a 🐛 fix error message 2026-05-14 18:20:26 +02:00
Fred KISSIE
5b50024712 Merge branch 'dev' into feat/labels-on-sites-and-resources 2026-05-14 18:15:14 +02:00
Fred KISSIE
ce746a2a21 Handle labels for machine clients 2026-05-12 22:32:56 +02:00
Fred KISSIE
7120ab4b22 ♻️ filter sites & resources by labels 2026-05-12 20:45:12 +02:00
Fred KISSIE
12e777b32e Add labels column to private resources table 2026-05-12 20:25:32 +02:00
Fred KISSIE
9378103ddd handle private resources filtering by labels 2026-05-12 20:24:34 +02:00
Fred KISSIE
ec794d5de2 attach/detach private resources 2026-05-12 20:01:33 +02:00
Fred KISSIE
12b18a3e8c attach labels to private resources 2026-05-12 19:58:44 +02:00
Fred KISSIE
91e8a13e59 🗃️ Add site resource labels schema 2026-05-12 17:55:56 +02:00
Fred KISSIE
931ba0f540 💄 px-2 button 2026-05-12 17:46:46 +02:00
Fred KISSIE
d321d7275c 🚧 tried to memo proxy resource table, failed 2026-05-11 21:06:20 +02:00
Fred KISSIE
3855486a00 ️ prevent SitetableCell from rerendering unnecessarily 2026-05-11 19:27:00 +02:00
Fred KISSIE
ab494521b1 labels on proxy resources 2026-05-11 18:37:16 +02:00
Fred KISSIE
549e1ead1d handle labels in resources too 2026-05-11 18:30:23 +02:00
Fred KISSIE
a0759a79a1 🗃️ add unique indexes to site & resource labels in sqlite 2026-05-11 18:28:40 +02:00
Fred KISSIE
14e1a119d3 🚧 WIP: showing labels in proxy resources table 2026-05-11 18:24:47 +02:00
Fred KISSIE
6e066d38b0 🚚 Make label badge its own component 2026-05-11 18:17:29 +02:00
Fred KISSIE
21f72639b6 🚧 make labels column paid, and cleanup 2026-05-11 18:13:19 +02:00
Fred KISSIE
8a0c2031d4 search list by labels too 2026-05-11 18:02:59 +02:00
Fred KISSIE
56d3a466e5 💄 make controlled data table input a search input 2026-05-11 18:02:44 +02:00
Fred KISSIE
563e505cc1 💸 add labels to paid features 2026-05-11 18:02:15 +02:00
Fred KISSIE
c44c02b8ba 💄 make site labels column design nicer 2026-05-11 17:04:44 +02:00
Fred KISSIE
b9ab35a05b 🐛 handle idempotency when adding/removing labels from sites/resources 2026-05-11 16:57:53 +02:00
Fred KISSIE
2fd519e102 add and toggle site labels 2026-05-08 22:31:36 +02:00
Fred KISSIE
a63c1ec364 💄 label selector (with create label) 2026-05-08 21:49:20 +02:00
Fred KISSIE
e61ef2ca2a 🚧 wip: label selector 2026-05-08 20:06:42 +02:00
Fred KISSIE
39b09b7f3f Merge branch 'dev' into feat/labels-on-sites-and-resources 2026-05-08 18:21:46 +02:00
Fred KISSIE
840cc214e3 🚧 wip 2026-05-08 18:21:09 +02:00
Fred KISSIE
72524db52d 💄 shrink button 2026-05-08 02:48:47 +02:00
Fred KISSIE
ab8fc11ab3 🚧 add labels button 2026-05-08 02:46:16 +02:00
Fred KISSIE
1831ca4e75 ♻️ detach label from site/resoirce 2026-05-08 00:33:47 +02:00
Fred KISSIE
0d04cc365f attach label to item 2026-05-05 21:35:10 +02:00
Fred KISSIE
09baf2f32e 🗃️ add sqlite table for labels 2026-05-05 21:08:22 +02:00
Fred KISSIE
3253d60900 🚧 Add CRUD endpoints and tables for labels 2026-05-05 20:53:16 +02:00
224 changed files with 4473 additions and 3853 deletions

View File

@@ -255,6 +255,23 @@
"resourceGoTo": "Go to Resource",
"resourceDelete": "Delete Resource",
"resourceDeleteConfirm": "Confirm Delete Resource",
"labelDelete": "Delete Label",
"labelAdd": "Add Label",
"labelCreateSuccessMessage": "Label Created Successfully",
"labelEditSuccessMessage": "Label Modified Successfully",
"labelNameField": "Label Name",
"labelColorField": "Label Color",
"labelPlaceholder": "Ex: homelab",
"labelCreate": "Create Label",
"createLabelDialogTitle": "Create Label",
"createLabelDialogDescription": "Create a new label that can be attached to this organization",
"labelEdit": "Edit Label",
"editLabelDialogTitle": "Update Label",
"editLabelDialogDescription": "Edit a new label that can be attached to this organization",
"labelDeleteConfirm": "Confirm Delete Label",
"labelErrorDelete": "Failed to delete label",
"labelMessageRemove": "This action is permanent. All sites, resources, and clients tagged with this label will be untagged.",
"labelQuestionRemove": "Are you sure you want to remove the label from the organization?",
"visibility": "Visibility",
"enabled": "Enabled",
"disabled": "Disabled",
@@ -1140,6 +1157,15 @@
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
"idpErrorNotFound": "IdP not found",
"inviteInvalid": "Invalid Invite",
"labels": "Labels",
"orgLabelsDescription": "Manage labels in this organization.",
"addLabels": "Add labels",
"siteLabelsTab": "Labels",
"siteLabelsDescription": "Manage labels associated with this site.",
"labelsNotFound": "Labels not found",
"labelSearch": "Search labels",
"selectColor": "Select color",
"createNewLabel": "Create new org label \"{label}\"",
"inviteInvalidDescription": "The invite link is invalid.",
"inviteErrorWrongUser": "Invite is not for this user",
"inviteErrorUserNotExists": "User does not exist. Please create an account first.",

View File

@@ -148,6 +148,12 @@ export enum ActionsEnum {
updateAlertRule = "updateAlertRule",
deleteAlertRule = "deleteAlertRule",
listAlertRules = "listAlertRules",
listOrgLabels = "listOrgLabels",
createOrgLabel = "createOrgLabel",
updateOrgLabel = "updateOrgLabel",
deleteOrgLabel = "deleteOrgLabel",
attachLabelToItem = "attachLabelToItem",
detachLabelFromItem = "detachLabelFromItem",
getAlertRule = "getAlertRule",
createHealthCheck = "createHealthCheck",
updateHealthCheck = "updateHealthCheck",

View File

@@ -162,6 +162,89 @@ export const resources = pgTable("resources", {
wildcard: boolean("wildcard").notNull().default(false)
});
export const labels = pgTable("labels", {
labelId: serial("labelId").primaryKey(),
name: varchar("name").notNull(),
color: varchar("color").notNull(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const siteLabels = pgTable(
"siteLabels",
{
siteLabelId: serial("siteLabelId").primaryKey(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("site_label_uniq").on(t.siteId, t.labelId)]
);
export const resourceLabels = pgTable(
"resourceLabels",
{
resourceLabelId: serial("resourceLabelId").primaryKey(),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("resource_label_uniq").on(t.resourceId, t.labelId)]
);
export const siteResourceLabels = pgTable(
"siteResourceLabels",
{
siteResourceLabelId: serial("siteResourceLabelId").primaryKey(),
siteResourceId: integer("siteResourceId")
.references(() => siteResources.siteResourceId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("site_resource_label_uniq").on(t.siteResourceId, t.labelId)]
);
export const clientLabels = pgTable(
"clientLabels",
{
clientLabelId: serial("clientLabelId").primaryKey(),
clientId: integer("clientId")
.references(() => clients.clientId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
);
export const targets = pgTable("targets", {
targetId: serial("targetId").primaryKey(),
resourceId: integer("resourceId")
@@ -196,9 +279,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
onDelete: "cascade"
})
.notNull(),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
}).notNull(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
name: varchar("name"),
hcEnabled: boolean("hcEnabled").notNull().default(false),
hcPath: varchar("hcPath"),
@@ -1097,19 +1182,30 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
complete: boolean("complete").notNull().default(false)
});
export const statusHistory = pgTable("statusHistory", {
id: serial("id").primaryKey(),
entityType: varchar("entityType").notNull(),
entityId: integer("entityId").notNull(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
status: varchar("status").notNull(),
timestamp: integer("timestamp").notNull(),
}, (table) => [
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
]);
export const statusHistory = pgTable(
"statusHistory",
{
id: serial("id").primaryKey(),
entityType: varchar("entityType").notNull(),
entityId: integer("entityId").notNull(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
status: varchar("status").notNull(),
timestamp: integer("timestamp").notNull()
},
(table) => [
index("idx_statusHistory_entity").on(
table.entityType,
table.entityId,
table.timestamp
),
index("idx_statusHistory_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
@@ -1179,3 +1275,4 @@ export type RoundTripMessageTracker = InferSelectModel<
>;
export type Network = InferSelectModel<typeof networks>;
export type StatusHistory = InferSelectModel<typeof statusHistory>;
export type Label = InferSelectModel<typeof labels>;

View File

@@ -183,6 +183,95 @@ export const resources = sqliteTable("resources", {
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false)
});
export const labels = sqliteTable("labels", {
labelId: integer("labelId").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
color: text("color").notNull(),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const siteLabels = sqliteTable(
"siteLabels",
{
siteLabelId: integer("siteLabelId").primaryKey({ autoIncrement: true }),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("site_label_uniq").on(t.siteId, t.labelId)]
);
export const resourceLabels = sqliteTable(
"resourceLabels",
{
resourceLabelId: integer("resourceLabelId").primaryKey({
autoIncrement: true
}),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("resource_label_uniq").on(t.resourceId, t.labelId)]
);
export const siteResourceLabels = sqliteTable(
"siteResourceLabels",
{
siteResourceLabelId: integer("siteResourceLabelId").primaryKey({
autoIncrement: true
}),
siteResourceId: integer("siteResourceId")
.references(() => siteResources.siteResourceId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("site_resource_label_uniq").on(t.siteResourceId, t.labelId)]
);
export const clientLabels = sqliteTable(
"clientLabels",
{
clientLabelId: integer("clientLabelId").primaryKey({
autoIncrement: true
}),
clientId: integer("clientId")
.references(() => clients.clientId, {
onDelete: "cascade"
})
.notNull(),
labelId: integer("labelId")
.references(() => labels.labelId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
);
export const targets = sqliteTable("targets", {
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
resourceId: integer("resourceId")
@@ -219,9 +308,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
onDelete: "cascade"
})
.notNull(),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
}).notNull(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
name: text("name"),
hcEnabled: integer("hcEnabled", { mode: "boolean" })
.notNull()
@@ -1196,19 +1287,30 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
});
export const statusHistory = sqliteTable("statusHistory", {
id: integer("id").primaryKey({ autoIncrement: true }),
entityType: text("entityType").notNull(), // "site" | "healthCheck"
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
timestamp: integer("timestamp").notNull(), // unix epoch seconds
}, (table) => [
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
]);
export const statusHistory = sqliteTable(
"statusHistory",
{
id: integer("id").primaryKey({ autoIncrement: true }),
entityType: text("entityType").notNull(), // "site" | "healthCheck"
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
timestamp: integer("timestamp").notNull() // unix epoch seconds
},
(table) => [
index("idx_statusHistory_entity").on(
table.entityType,
table.entityId,
table.timestamp
),
index("idx_statusHistory_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
@@ -1278,3 +1380,4 @@ export type RoundTripMessageTracker = InferSelectModel<
typeof roundTripMessageTracker
>;
export type StatusHistory = InferSelectModel<typeof statusHistory>;
export type Label = InferSelectModel<typeof labels>;

View File

@@ -152,17 +152,11 @@ function getOpenApiDocumentation() {
if (!hasExistingResponses) {
def.route.responses = {
"200": {
description: "Successful response",
"*": {
description: "",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
schema: z.object({})
}
}
}

View File

@@ -24,10 +24,12 @@ export enum TierFeature {
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
StandaloneHealthChecks = "standaloneHealthChecks",
AlertingRules = "alertingRules",
WildcardSubdomain = "wildcardSubdomain"
WildcardSubdomain = "wildcardSubdomain",
Labels = "labels"
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.Labels]: ["tier2", "tier3", "enterprise"],
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],

View File

@@ -873,13 +873,7 @@ export const portRangeStringSchema = z
message:
'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.'
}
)
.openapi({
type: "string",
description:
'Port range string. Use "*" for all ports, a comma-separated list of ports, or ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.',
example: "80,443,8000-9000"
});
);
/**
* Parses a port range string into an array of port range objects

View File

@@ -1,11 +0,0 @@
import { z } from "zod";
export function createApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
return z.object({
data: dataSchema.nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
});
}

View File

@@ -24,7 +24,8 @@ import { LogStreamingManager } from "./LogStreamingManager";
*/
export const logStreamingManager = new LogStreamingManager();
if (build != "saas") { // this is handled separately in the saas build, so we don't want to start it here
if (build !== "saas") {
// this is handled separately in the saas build, so we don't want to start it here
logStreamingManager.start();
}

View File

@@ -25,7 +25,7 @@ export function verifyValidSubscription(tiers: Tier[]) {
next: NextFunction
): Promise<any> {
try {
if (build != "saas") {
if (build !== "saas") {
return next();
}

View File

@@ -202,22 +202,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function createAlertRule(

View File

@@ -38,22 +38,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteAlertRule(

View File

@@ -49,22 +49,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getAlertRule(

View File

@@ -95,22 +95,7 @@ registry.registerPath({
query: querySchema,
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listAlertRules(

View File

@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import {
alertRules,
@@ -149,10 +148,6 @@ const bodySchema = z
export type UpdateAlertRuleResponse = {
alertRuleId: number;
};
const UpdateAlertRuleResponseDataSchema = z.object({
alertRuleId: z.number()
});
registry.registerPath({
method: "post",
@@ -169,16 +164,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateAlertRuleResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateAlertRule(

View File

@@ -24,7 +24,7 @@ import type { NextFunction, Request, Response } from "express";
const paramsSchema = z.strictObject({
orgId: z.string(),
approvalId: z.coerce.number().int().positive()
approvalId: z.string().transform(Number).pipe(z.int().positive())
});
const bodySchema = z.strictObject({

View File

@@ -18,7 +18,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger";
import {
queryAccessAuditLogsParams,
@@ -38,22 +37,7 @@ registry.registerPath({
query: queryAccessAuditLogsQuery,
params: queryAccessAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function exportAccessAuditLogs(

View File

@@ -18,7 +18,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger";
import {
queryActionAuditLogsParams,
@@ -38,22 +37,7 @@ registry.registerPath({
query: queryActionAuditLogsQuery,
params: queryActionAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function exportActionAuditLogs(

View File

@@ -18,7 +18,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger";
import {
queryConnectionAuditLogsParams,
@@ -38,22 +37,7 @@ registry.registerPath({
query: queryConnectionAuditLogsQuery,
params: queryConnectionAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function exportConnectionAuditLogs(

View File

@@ -324,22 +324,7 @@ registry.registerPath({
query: queryAccessAuditLogsQuery,
params: queryAccessAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function queryAccessAuditLogs(

View File

@@ -165,22 +165,7 @@ registry.registerPath({
query: queryActionAuditLogsQuery,
params: queryActionAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function queryActionAuditLogs(

View File

@@ -439,22 +439,7 @@ registry.registerPath({
query: queryConnectionAuditLogsQuery,
params: queryConnectionAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function queryConnectionAuditLogs(

View File

@@ -39,22 +39,7 @@ const getOrgSchema = z.strictObject({
// request: {
// params: getOrgSchema
// },
// responses: {
// 200: {
// description: "Successful response",
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
// status: z.number()
// })
// }
// }
// }
// }
// responses: {}
// });
export async function getOrgUsage(

View File

@@ -115,22 +115,7 @@ registry.registerPath({
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getCertificate(

View File

@@ -25,7 +25,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const restartCertificateParamsSchema = z.strictObject({
certId: z.coerce.number().int().positive(),
certId: z.string().transform(stoi).pipe(z.int().positive()),
orgId: z.string()
});
@@ -36,26 +36,11 @@ registry.registerPath({
tags: ["Certificate"],
request: {
params: z.object({
certId: z.coerce.number().int().positive(),
certId: z.string().transform(stoi).pipe(z.int().positive()),
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function restartCertificate(

View File

@@ -42,22 +42,7 @@ registry.registerPath({
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function checkDomainNamespaceAvailability(

View File

@@ -25,7 +25,6 @@ import { OpenAPITags, registry } from "@server/openApi";
import { isSubscribed } from "#private/lib/isSubscribed";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const paramsSchema = z.strictObject({});
@@ -66,20 +65,6 @@ export type ListDomainNamespacesResponse = {
pagination: { total: number; limit: number; offset: number };
};
const ListDomainNamespacesResponseDataSchema = z.object({
domainNamespaces: z.array(
z.object({
domainNamespaceId: z.string(),
domainId: z.string()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/domains/namepaces",
@@ -88,18 +73,7 @@ registry.registerPath({
request: {
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListDomainNamespacesResponseDataSchema
)
}
}
}
}
responses: {}
});
export async function listDomainNamespaces(

View File

@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import { eventStreamingDestinations } from "@server/db";
import { logStreamingManager } from "#private/lib/logStreaming";
@@ -43,10 +42,6 @@ const bodySchema = z.strictObject({
export type CreateEventStreamingDestinationResponse = {
destinationId: number;
};
const CreateEventStreamingDestinationResponseDataSchema = z.object({
destinationId: z.number()
});
registry.registerPath({
method: "put",
@@ -63,16 +58,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateEventStreamingDestinationResponseDataSchema)
}
}
}
}
responses: {}
});
export async function createEventStreamingDestination(

View File

@@ -38,22 +38,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteEventStreamingDestination(

View File

@@ -24,7 +24,6 @@ import { OpenAPITags, registry } from "@server/openApi";
import { eq, sql } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -68,31 +67,6 @@ export type ListEventStreamingDestinationsResponse = {
};
};
const ListEventStreamingDestinationsResponseDataSchema = z.object({
destinations: z.array(
z.object({
destinationId: z.number(),
orgId: z.string(),
type: z.string(),
config: z.string(),
enabled: z.boolean(),
lastError: z.string().nullable(),
lastErrorAt: z.number().nullable(),
createdAt: z.number(),
updatedAt: z.number(),
sendConnectionLogs: z.boolean(),
sendRequestLogs: z.boolean(),
sendActionLogs: z.boolean(),
sendAccessLogs: z.boolean()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
async function query(orgId: string, limit: number, offset: number) {
const res = await db
.select()
@@ -114,18 +88,7 @@ registry.registerPath({
query: querySchema,
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListEventStreamingDestinationsResponseDataSchema
)
}
}
}
}
responses: {}
});
export async function listEventStreamingDestinations(

View File

@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import { eventStreamingDestinations } from "@server/db";
import response from "@server/lib/response";
@@ -46,10 +45,6 @@ const bodySchema = z.strictObject({
export type UpdateEventStreamingDestinationResponse = {
destinationId: number;
};
const UpdateEventStreamingDestinationResponseDataSchema = z.object({
destinationId: z.number()
});
registry.registerPath({
method: "post",
@@ -66,16 +61,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateEventStreamingDestinationResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateEventStreamingDestination(

View File

@@ -31,6 +31,7 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks";
import * as labels from "#private/routers/labels";
import {
verifyOrgAccess,
@@ -732,6 +733,59 @@ authenticated.get(
alertRule.getAlertRule
);
authenticated.get(
"/org/:orgId/labels",
verifyValidLicense,
verifyOrgAccess,
verifyValidSubscription(tierMatrix.labels),
verifyUserHasAction(ActionsEnum.listOrgLabels),
labels.listOrgLabels
);
authenticated.post(
"/org/:orgId/labels",
verifyValidLicense,
verifyOrgAccess,
verifyValidSubscription(tierMatrix.labels),
verifyUserHasAction(ActionsEnum.createOrgLabel),
labels.createOrgLabel
);
authenticated.patch(
"/org/:orgId/label/:labelId",
verifyValidLicense,
verifyOrgAccess,
verifyValidSubscription(tierMatrix.labels),
verifyUserHasAction(ActionsEnum.updateOrgLabel),
labels.updateOrgLabel
);
authenticated.delete(
"/org/:orgId/label/:labelId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.deleteOrgLabel),
labels.deleteOrgLabel
);
authenticated.put(
"/org/:orgId/label/:labelId/attach",
verifyValidLicense,
verifyOrgAccess,
verifyValidSubscription(tierMatrix.labels),
verifyUserHasAction(ActionsEnum.attachLabelToItem),
labels.attachLabelToItem
);
authenticated.put(
"/org/:orgId/label/:labelId/detach",
verifyValidLicense,
verifyOrgAccess,
verifyValidSubscription(tierMatrix.labels),
verifyUserHasAction(ActionsEnum.detachLabelFromItem),
labels.detachLabelFromItem
);
authenticated.get(
"/org/:orgId/health-checks",
verifyValidLicense,

View File

@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db, targetHealthCheck, newts, sites } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
@@ -53,10 +52,6 @@ const bodySchema = z.strictObject({
export type CreateHealthCheckResponse = {
targetHealthCheckId: number;
};
const CreateHealthCheckResponseDataSchema = z.object({
targetHealthCheckId: z.number()
});
registry.registerPath({
method: "put",
@@ -73,16 +68,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateHealthCheckResponseDataSchema)
}
}
}
}
responses: {}
});
export async function createHealthCheck(

View File

@@ -41,22 +41,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteHealthCheck(

View File

@@ -68,22 +68,7 @@ registry.registerPath({
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listHealthChecks(

View File

@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db, targetHealthCheck, newts, sites } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -82,29 +81,6 @@ export type UpdateHealthCheckResponse = {
hcHealthyThreshold: number | null;
hcUnhealthyThreshold: number | null;
};
const UpdateHealthCheckResponseDataSchema = z.object({
targetHealthCheckId: z.number(),
name: z.string().nullable(),
siteId: z.number().nullable(),
hcEnabled: z.boolean(),
hcHealth: z.string().nullable(),
hcMode: z.string().nullable(),
hcHostname: z.string().nullable(),
hcPort: z.number().nullable(),
hcPath: z.string().nullable(),
hcScheme: z.string().nullable(),
hcMethod: z.string().nullable(),
hcInterval: z.number().nullable(),
hcUnhealthyInterval: z.number().nullable(),
hcTimeout: z.number().nullable(),
hcHeaders: z.string().nullable(),
hcFollowRedirects: z.boolean().nullable(),
hcStatus: z.number().nullable(),
hcTlsServerName: z.string().nullable(),
hcHealthyThreshold: z.number().nullable(),
hcUnhealthyThreshold: z.number().nullable()
});
registry.registerPath({
method: "post",
@@ -121,16 +97,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateHealthCheckResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateHealthCheck(

View File

@@ -0,0 +1,224 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import {
clients,
clientLabels,
db,
labels,
resourceLabels,
resources,
siteLabels,
siteResourceLabels,
siteResources,
sites
} from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { and, eq, isNull } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
labelId: z.string().transform(Number).pipe(z.int().positive())
});
const attachLabelBodySchema = z.strictObject({
siteId: z.number().int().optional(),
resourceId: z.number().int().optional(),
siteResourceId: z.number().int().optional(),
clientId: z.number().int().optional()
});
export async function attachLabelToItem(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, labelId } = parsedParams.data;
const parsedBody = attachLabelBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { siteId, resourceId, siteResourceId, clientId } =
parsedBody.data;
if (!siteId && !resourceId && !siteResourceId && !clientId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"At least one of `siteId`, `resourceId`, `siteResourceId` or `clientId` should be provided."
)
);
}
const [existing] = await db
.select()
.from(labels)
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
if (!existing) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Label with Id ${labelId} not found`
)
);
}
if (siteId) {
const siteCount = await db.$count(
sites,
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
);
if (siteCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with Id ${siteId} doesn't exist.`
)
);
}
// idempotent, calling this endpoint multiple times should attach the label only once
await db
.insert(siteLabels)
.values({
labelId,
siteId
})
.onConflictDoNothing();
}
if (resourceId) {
const resourceCount = await db.$count(
resources,
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
);
if (resourceCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with Id ${resourceId} doesn't exist.`
)
);
}
// idempotent, calling this endpoint multiple times should attach the label only once
await db
.insert(resourceLabels)
.values({
labelId,
resourceId
})
.onConflictDoNothing();
}
if (siteResourceId) {
const resourceCount = await db.$count(
siteResources,
and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.orgId, orgId)
)
);
if (resourceCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`SiteResource with Id ${siteResourceId} doesn't exist.`
)
);
}
// idempotent, calling this endpoint multiple times should attach the label only once
await db
.insert(siteResourceLabels)
.values({
labelId,
siteResourceId
})
.onConflictDoNothing();
}
if (clientId) {
const clientCount = await db.$count(
clients,
and(
eq(clients.clientId, clientId),
eq(clients.orgId, orgId),
isNull(clients.userId)
)
);
if (clientCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with Id ${clientId} doesn't exist.`
)
);
}
// idempotent, calling this endpoint multiple times should attach the label only once
await db
.insert(clientLabels)
.values({
labelId,
clientId
})
.onConflictDoNothing();
}
return response(res, {
data: {},
success: true,
error: false,
message: "Label attached successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,149 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import {
db,
labels,
resourceLabels,
resources,
siteLabels,
sites
} from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const bodySchema = z.strictObject({
name: z.string().nonempty(),
color: z
.string()
.regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i)
.nonempty(),
siteId: z.number().int().optional(),
resourceId: z.number().int().optional()
});
export async function createOrgLabel(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { name, color, siteId, resourceId } = parsedBody.data;
if (siteId) {
const siteCount = await db.$count(
sites,
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
);
if (siteCount === 0) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Site with Id ${siteId} doesn't exist.`
)
);
}
}
if (resourceId) {
const resourceCount = await db.$count(
resources,
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
);
if (resourceCount === 0) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Resource with Id ${resourceId} doesn't exist.`
)
);
}
}
const label = await db.transaction(async (tx) => {
const [label] = await tx
.insert(labels)
.values({
name,
color,
orgId
})
.returning();
if (siteId) {
await tx.insert(siteLabels).values({
siteId,
labelId: label.labelId
});
}
if (resourceId) {
await tx.insert(resourceLabels).values({
resourceId,
labelId: label.labelId
});
}
return label;
});
return response<CreateOrEditLabelResponse>(res, {
data: { label },
success: true,
error: false,
message: "Org Label created successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,72 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { db, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
labelId: z.string().transform(Number).pipe(z.int().positive())
});
export async function deleteOrgLabel(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, labelId } = parsedParams.data;
const [existing] = await db
.select()
.from(labels)
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
if (!existing) {
return next(createHttpError(HttpCode.NOT_FOUND, "Label not found"));
}
await db
.delete(labels)
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
return response(res, {
data: null,
success: true,
error: false,
message: "Label deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,224 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import {
clients,
clientLabels,
db,
labels,
resourceLabels,
resources,
siteLabels,
siteResourceLabels,
siteResources,
sites
} from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { and, eq, isNull } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
labelId: z.string().transform(Number).pipe(z.int().positive())
});
const detachLabelBodySchema = z.strictObject({
siteId: z.number().int().optional(),
resourceId: z.number().int().optional(),
siteResourceId: z.number().int().optional(),
clientId: z.number().int().optional()
});
export async function detachLabelFromItem(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, labelId } = parsedParams.data;
const parsedBody = detachLabelBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { siteId, resourceId, siteResourceId, clientId } =
parsedBody.data;
if (!siteId && !resourceId && !siteResourceId && !clientId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"At least one of `siteId`, `resourceId`, `siteResourceId` or `clientId` should be provided."
)
);
}
const [existing] = await db
.select()
.from(labels)
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
if (!existing) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Label with Id ${labelId} not found`
)
);
}
if (siteId) {
const siteCount = await db.$count(
sites,
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
);
if (siteCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with Id ${siteId} doesn't exist.`
)
);
}
await db
.delete(siteLabels)
.where(
and(
eq(siteLabels.labelId, labelId),
eq(siteLabels.siteId, siteId)
)
);
}
if (resourceId) {
const resourceCount = await db.$count(
resources,
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
);
if (resourceCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with Id ${resourceId} doesn't exist.`
)
);
}
await db
.delete(resourceLabels)
.where(
and(
eq(resourceLabels.labelId, labelId),
eq(resourceLabels.resourceId, resourceId)
)
);
}
if (siteResourceId) {
const resourceCount = await db.$count(
siteResources,
and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.orgId, orgId)
)
);
if (resourceCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`SiteResource with Id ${siteResourceId} doesn't exist.`
)
);
}
await db
.delete(siteResourceLabels)
.where(
and(
eq(siteResourceLabels.labelId, labelId),
eq(siteResourceLabels.siteResourceId, siteResourceId)
)
);
}
if (clientId) {
const clientCount = await db.$count(
clients,
and(
eq(clients.clientId, clientId),
eq(clients.orgId, orgId),
isNull(clients.userId)
)
);
if (clientCount === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with Id ${clientId} doesn't exist.`
)
);
}
await db
.delete(clientLabels)
.where(
and(
eq(clientLabels.labelId, labelId),
eq(clientLabels.clientId, clientId)
)
);
}
return response(res, {
data: {},
success: true,
error: false,
message: "Label detached successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,19 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
export * from "./listOrgLabels";
export * from "./createOrgLabel";
export * from "./updateOrgLabel";
export * from "./attachLabelToItem";
export * from "./detachLabelFromItem";
export * from "./deleteOrgLabel";

View File

@@ -0,0 +1,155 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { db, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
import HttpCode from "@server/types/HttpCode";
import { and, asc, eq, like, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const listLabelsSchema = z.object({
pageSize: z.coerce
.number<string>() // for prettier formatting
.int()
.positive()
.optional()
.catch(20)
.default(20)
.openapi({
type: "integer",
default: 20,
description: "Number of items per page"
}),
page: z.coerce
.number<string>() // for prettier formatting
.int()
.min(0)
.optional()
.catch(1)
.default(1)
.openapi({
type: "integer",
default: 1,
description: "Page number to retrieve"
}),
query: z.string().optional()
});
function queryLabelsBase() {
return db
.select({
labelId: labels.labelId,
name: labels.name,
color: labels.color
})
.from(labels);
}
export async function listOrgLabels(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = listLabelsSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const { orgId } = parsedParams.data;
if (req.user && orgId && orgId !== req.userOrgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
const { pageSize, page, query } = parsedQuery.data;
const conditions = [and(eq(labels.orgId, orgId))];
if (query) {
conditions.push(
like(
sql`LOWER(${labels.name})`,
"%" + query.toLowerCase() + "%"
)
);
}
const baseQuery = queryLabelsBase().where(and(...conditions));
// we need to add `as` so that drizzle filters the result as a subquery
const countQuery = db.$count(
queryLabelsBase()
.where(and(...conditions))
.as("filtered_labels")
);
const labelListQuery = baseQuery
.limit(pageSize)
.offset(pageSize * (page - 1))
.orderBy(asc(labels.name));
const [totalCount, rows] = await Promise.all([
countQuery,
labelListQuery
]);
return response<ListOrgLabelsResponse>(res, {
data: {
labels: rows,
pagination: {
total: totalCount,
pageSize,
page
}
},
success: true,
error: false,
message: "Labels retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,101 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { db, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
labelId: z.string().transform(Number).pipe(z.int().positive())
});
const updateLabelBodySchema = z.strictObject({
name: z.string().min(1).max(255).optional(),
color: z
.string()
.regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i)
.nonempty()
});
export async function updateOrgLabel(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, labelId } = parsedParams.data;
const parsedBody = updateLabelBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const [existing] = await db
.select()
.from(labels)
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
if (!existing) {
return next(createHttpError(HttpCode.NOT_FOUND, "Label not found"));
}
const { name, color } = parsedBody.data;
const [label] = await db
.update(labels)
.set({
name,
color
})
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)))
.returning();
return response<CreateOrEditLabelResponse>(res, {
data: {
label
},
success: true,
error: false,
message: "Label updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -63,22 +63,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function createOrgOidcIdp(

View File

@@ -38,22 +38,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteOrgIdp(

View File

@@ -56,22 +56,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getOrgIdp(

View File

@@ -72,22 +72,7 @@ registry.registerPath({
query: querySchema,
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listOrgIdps(

View File

@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db, idpOrg } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -55,10 +54,6 @@ const bodySchema = z.strictObject({
export type UpdateOrgIdpResponse = {
idpId: number;
};
const UpdateOrgIdpResponseDataSchema = z.object({
idpId: z.number()
});
registry.registerPath({
method: "post",
@@ -75,16 +70,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateOrgIdpResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateOrgOidcIdp(

View File

@@ -28,7 +28,7 @@ import { OlmErrorCodes, sendOlmError } from "@server/routers/olm/error";
import { sendTerminateClient } from "@server/routers/client/terminate";
const reGenerateSecretParamsSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
const reGenerateSecretBodySchema = z.strictObject({

View File

@@ -27,7 +27,7 @@ import { getAllowedIps } from "@server/routers/target/helpers";
import { disconnectClient, sendToClient } from "#private/routers/ws";
const updateSiteParamsSchema = z.strictObject({
siteId: z.coerce.number().int().positive()
siteId: z.string().transform(Number).pipe(z.int().positive())
});
const updateSiteBodySchema = z.strictObject({

View File

@@ -0,0 +1,202 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import axios from "axios";
import { db, exitNodes, newts, sites } from "@server/db";
import { eq } from "drizzle-orm";
import logger from "@server/logger";
import redisManager from "#private/lib/redis";
import { sendToClient } from "#private/routers/ws";
const INITIAL_DELAY_MS = 15 * 1000; // 15 seconds before first check
const CHECK_INTERVAL_MS = 10 * 1000; // Check every 10 seconds
const MAX_DURATION_MS = 5 * 60 * 1000; // Give up after 5 minutes
const REDIS_PENDING_SET = "exit-node-reconnect-pending";
const REDIS_HASH_PREFIX = "exit-node-reconnect:";
interface PendingReconnect {
startTime: number;
reachableAt: string;
}
// In-memory tracking for this node
const pendingReconnects = new Map<number, PendingReconnect>();
let schedulerInterval: NodeJS.Timeout | null = null;
/**
* Schedules a reconnect check for newts connected to the given exit node.
* Called when an exit node transitions from offline to online.
*/
export async function scheduleExitNodeReconnect(
exitNodeId: number,
reachableAt: string
): Promise<void> {
logger.info(
`Scheduling newt reconnect for exit node ${exitNodeId} (reachableAt: ${reachableAt})`
);
const entry: PendingReconnect = {
startTime: Date.now(),
reachableAt
};
pendingReconnects.set(exitNodeId, entry);
// Store in Redis if available for cross-node coordination
if (redisManager.isRedisEnabled()) {
await redisManager.sadd(REDIS_PENDING_SET, exitNodeId.toString());
await redisManager.hset(
`${REDIS_HASH_PREFIX}${exitNodeId}`,
"startTime",
entry.startTime.toString()
);
await redisManager.hset(
`${REDIS_HASH_PREFIX}${exitNodeId}`,
"reachableAt",
reachableAt
);
}
}
/**
* Starts the background interval that checks pending exit node reconnects.
*/
export function startExitNodeReconnectScheduler(): void {
if (schedulerInterval) {
return;
}
schedulerInterval = setInterval(async () => {
try {
await processPendingReconnects();
} catch (error) {
logger.error("Error in exit node reconnect scheduler", { error });
}
}, CHECK_INTERVAL_MS);
logger.debug("Started exit node reconnect scheduler");
}
async function processPendingReconnects(): Promise<void> {
// Merge in-memory and Redis-tracked pending reconnects
const toProcess = new Map(pendingReconnects);
if (redisManager.isRedisEnabled()) {
const redisIds = await redisManager.smembers(REDIS_PENDING_SET);
for (const idStr of redisIds) {
const id = parseInt(idStr, 10);
if (!toProcess.has(id)) {
const startTimeStr = await redisManager.hget(
`${REDIS_HASH_PREFIX}${id}`,
"startTime"
);
const reachableAt = await redisManager.hget(
`${REDIS_HASH_PREFIX}${id}`,
"reachableAt"
);
if (startTimeStr && reachableAt) {
toProcess.set(id, {
startTime: parseInt(startTimeStr, 10),
reachableAt
});
}
}
}
}
const now = Date.now();
for (const [exitNodeId, entry] of toProcess) {
const elapsed = now - entry.startTime;
// Give up after max duration
if (elapsed >= MAX_DURATION_MS) {
logger.warn(
`Exit node reconnect check timed out for exit node ${exitNodeId} after 5 minutes`
);
await removePending(exitNodeId);
continue;
}
// Respect initial delay
if (elapsed < INITIAL_DELAY_MS) {
continue;
}
// Check if the exit node HTTP endpoint is reachable
const pingUrl = `${entry.reachableAt}/ping`;
try {
await axios.get(pingUrl, { timeout: 5000 });
} catch {
logger.debug(
`Exit node ${exitNodeId} not yet reachable at ${pingUrl}`
);
continue;
}
// Node is reachable — send reconnect to all connected newts
logger.info(
`Exit node ${exitNodeId} is reachable. Sending newt/wg/reconnect to connected newts.`
);
await sendReconnectToNewts(exitNodeId);
await removePending(exitNodeId);
}
}
async function sendReconnectToNewts(exitNodeId: number): Promise<void> {
try {
const connectedNewts = await db
.select({ newtId: newts.newtId })
.from(newts)
.innerJoin(sites, eq(newts.siteId, sites.siteId))
.where(eq(sites.exitNodeId, exitNodeId));
if (connectedNewts.length === 0) {
logger.debug(
`No newts found for exit node ${exitNodeId}, nothing to reconnect`
);
return;
}
logger.info(
`Sending newt/wg/reconnect to ${connectedNewts.length} newt(s) for exit node ${exitNodeId}`
);
const reconnectMessage = {
type: "newt/wg/reconnect",
data: {}
};
await Promise.allSettled(
connectedNewts.map(({ newtId }) =>
sendToClient(newtId, reconnectMessage)
)
);
} catch (error) {
logger.error(
`Failed to send reconnect messages for exit node ${exitNodeId}`,
{ error }
);
}
}
async function removePending(exitNodeId: number): Promise<void> {
pendingReconnects.delete(exitNodeId);
if (redisManager.isRedisEnabled()) {
await redisManager.srem(REDIS_PENDING_SET, exitNodeId.toString());
await redisManager.del(`${REDIS_HASH_PREFIX}${exitNodeId}`);
}
}

View File

@@ -16,6 +16,7 @@ import { MessageHandler } from "@server/routers/ws";
import { RemoteExitNode } from "@server/db";
import { eq } from "drizzle-orm";
import logger from "@server/logger";
import { scheduleExitNodeReconnect } from "./exitNodeReconnectScheduler";
/**
* Handles ping messages from clients and responds with pong
@@ -37,6 +38,13 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (
}
try {
// Fetch the current state before updating so we can detect the offline→online transition
const [currentExitNode] = await db
.select({ online: exitNodes.online, reachableAt: exitNodes.reachableAt })
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId))
.limit(1);
// Update the exit node's last ping timestamp
await db
.update(exitNodes)
@@ -45,6 +53,16 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (
online: true
})
.where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId));
// If the exit node was offline and is now coming online, schedule newt reconnects
if (currentExitNode && !currentExitNode.online && currentExitNode.reachableAt) {
scheduleExitNodeReconnect(
remoteExitNode.exitNodeId,
currentExitNode.reachableAt
).catch((error) => {
logger.error("Failed to schedule exit node reconnect", { error });
});
}
} catch (error) {
logger.error("Error handling ping message", { error });
}

View File

@@ -22,3 +22,4 @@ export * from "./listRemoteExitNodes";
export * from "./pickRemoteExitNodeDefaults";
export * from "./quickStartRemoteExitNode";
export * from "./offlineChecker";
export * from "./exitNodeReconnectScheduler";

View File

@@ -19,6 +19,7 @@ import {
logsDb,
newts,
roles,
roleSiteResources,
roundTripMessageTracker,
siteResources,
siteNetworks,
@@ -92,22 +93,7 @@ export type SignSshKeyResponse = {
// }
// }
// },
// responses: {
// 200: {
// description: "Successful response",
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
// status: z.number()
// })
// }
// }
// }
// }
// responses: {}
// });
export async function signSshKey(
@@ -376,9 +362,26 @@ export async function signSshKey(
}
const roleRows = await db
.select()
.select({
sshSudoCommands: roles.sshSudoCommands,
sshUnixGroups: roles.sshUnixGroups,
sshCreateHomeDir: roles.sshCreateHomeDir,
sshSudoMode: roles.sshSudoMode
})
.from(roles)
.where(inArray(roles.roleId, roleIds));
.innerJoin(
roleSiteResources,
eq(roleSiteResources.roleId, roles.roleId)
)
.where(
and(
inArray(roles.roleId, roleIds),
eq(
roleSiteResources.siteResourceId,
resource.siteResourceId
)
)
);
const parsedSudoCommands: string[] = [];
const parsedGroupsSet = new Set<string>();
@@ -394,13 +397,17 @@ export async function signSshKey(
}
try {
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
if (Array.isArray(grps)) grps.forEach((g: string) => parsedGroupsSet.add(g));
if (Array.isArray(grps))
grps.forEach((g: string) => parsedGroupsSet.add(g));
} catch {
// skip
}
if (roleRow?.sshCreateHomeDir === true) homedir = true;
const m = roleRow?.sshSudoMode ?? "none";
if (sudoModeOrder[m as keyof typeof sudoModeOrder] > sudoModeOrder[sudoMode]) {
if (
sudoModeOrder[m as keyof typeof sudoModeOrder] >
sudoModeOrder[sudoMode]
) {
sudoMode = m as "none" | "commands" | "full";
}
}

View File

@@ -27,7 +27,7 @@ import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAs
const addUserRoleParamsSchema = z.strictObject({
userId: z.string(),
roleId: z.coerce.number()
roleId: z.string().transform(stoi).pipe(z.number())
});
registry.registerPath({
@@ -38,22 +38,7 @@ registry.registerPath({
request: {
params: addUserRoleParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function addUserRole(

View File

@@ -27,7 +27,7 @@ import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAs
const removeUserRoleParamsSchema = z.strictObject({
userId: z.string(),
roleId: z.coerce.number()
roleId: z.string().transform(stoi).pipe(z.number())
});
registry.registerPath({
@@ -39,22 +39,7 @@ registry.registerPath({
request: {
params: removeUserRoleParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function removeUserRole(

View File

@@ -14,7 +14,8 @@
import {
handleRemoteExitNodeRegisterMessage,
handleRemoteExitNodePingMessage,
startRemoteExitNodeOfflineChecker
startRemoteExitNodeOfflineChecker,
startExitNodeReconnectScheduler
} from "#private/routers/remoteExitNode";
import { MessageHandler } from "@server/routers/ws";
import { build } from "@server/build";
@@ -29,4 +30,5 @@ export const messageHandlers: Record<string, MessageHandler> = {
if (build != "saas") {
startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes
startExitNodeReconnectScheduler(); // check pending exit node reconnects and notify newts
}

View File

@@ -22,22 +22,7 @@ registry.registerPath({
request: {
params: deleteAccessTokenParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteAccessToken(

View File

@@ -31,7 +31,7 @@ export const generateAccessTokenBodySchema = z.strictObject({
});
export const generateAccssTokenParamsSchema = z.strictObject({
resourceId: z.coerce.number().int().positive()
resourceId: z.string().transform(Number).pipe(z.int().positive())
});
export type GenerateAccessTokenResponse = Omit<
@@ -54,22 +54,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function generateAccessToken(

View File

@@ -129,22 +129,7 @@ registry.registerPath({
}),
query: listAccessTokensSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
registry.registerPath({
@@ -158,22 +143,7 @@ registry.registerPath({
}),
query: listAccessTokensSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listAccessTokens(

View File

@@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { apiKeyOrg, apiKeys } from "@server/db";
import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
@@ -33,14 +32,6 @@ export type CreateOrgApiKeyResponse = {
lastChars: string;
createdAt: string;
};
const CreateOrgApiKeyResponseDataSchema = z.object({
apiKeyId: z.string(),
name: z.string(),
apiKey: z.string(),
lastChars: z.string(),
createdAt: z.string()
});
registry.registerPath({
method: "put",
@@ -57,16 +48,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateOrgApiKeyResponseDataSchema)
}
}
}
}
responses: {}
});
export async function createOrgApiKey(

View File

@@ -22,22 +22,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteApiKey(

View File

@@ -9,7 +9,6 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
@@ -45,19 +44,6 @@ export type ListApiKeyActionsResponse = {
pagination: { total: number; limit: number; offset: number };
};
const ListApiKeyActionsResponseDataSchema = z.object({
actions: z.array(
z.object({
actionId: z.string()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
@@ -67,18 +53,7 @@ registry.registerPath({
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListApiKeyActionsResponseDataSchema
)
}
}
}
}
responses: {}
});
export async function listApiKeyActions(

View File

@@ -9,7 +9,6 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const querySchema = z.object({
limit: z
@@ -49,23 +48,6 @@ export type ListOrgApiKeysResponse = {
pagination: { total: number; limit: number; offset: number };
};
const ListOrgApiKeysResponseDataSchema = z.object({
apiKeys: z.array(
z.object({
apiKeyId: z.string(),
orgId: z.string(),
lastChars: z.string(),
createdAt: z.string(),
name: z.string()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/api-keys",
@@ -75,18 +57,7 @@ registry.registerPath({
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListOrgApiKeysResponseDataSchema
)
}
}
}
}
responses: {}
});
export async function listOrgApiKeys(

View File

@@ -36,22 +36,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function setApiKeyActions(

View File

@@ -5,7 +5,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger";
import {
queryAccessAuditLogsQuery,
@@ -29,22 +28,7 @@ registry.registerPath({
}),
params: queryRequestAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function exportRequestAuditLogs(

View File

@@ -156,22 +156,7 @@ registry.registerPath({
query: queryAccessAuditLogsQuery,
params: queryRequestAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export type QueryRequestAnalyticsResponse = Awaited<ReturnType<typeof query>>;

View File

@@ -227,22 +227,7 @@ registry.registerPath({
query: queryAccessAuditLogsQuery,
params: queryRequestAuditLogsParams
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
async function queryUniqueFilterAttributes(

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
export const params = z.strictObject({
token: z.string(),
resourceId: z.coerce.number().int().positive()
resourceId: z.string().transform(Number).pipe(z.int().positive())
});
export type CheckResourceSessionParams = z.infer<typeof params>;

View File

@@ -51,22 +51,7 @@ export type LookupUserResponse = {
// request: {
// body: lookupBodySchema
// },
// responses: {
// 200: {
// description: "Successful response",
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
// status: z.number()
// })
// }
// }
// }
// }
// responses: {}
// });
export async function lookupUser(

View File

@@ -31,22 +31,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function applyJSONBlueprint(

View File

@@ -54,22 +54,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function applyYAMLBlueprint(

View File

@@ -7,12 +7,13 @@ import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import stoi from "@server/lib/stoi";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { BlueprintData } from "./types";
const getBlueprintSchema = z.strictObject({
blueprintId: z.coerce.number().int().positive(),
blueprintId: z.string().transform(stoi).pipe(z.int().positive()),
orgId: z.string()
});
@@ -56,22 +57,7 @@ registry.registerPath({
request: {
params: getBlueprintSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getBlueprint(

View File

@@ -74,22 +74,7 @@ registry.registerPath({
}),
query: listBluePrintsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listBlueprints(

View File

@@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const archiveClientSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
@@ -22,22 +22,7 @@ registry.registerPath({
request: {
params: archiveClientSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function archiveClient(

View File

@@ -13,7 +13,7 @@ import { sendTerminateClient } from "./terminate";
import { OlmErrorCodes } from "../olm/error";
const blockClientSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
@@ -24,22 +24,7 @@ registry.registerPath({
request: {
params: blockClientSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function blockClient(

View File

@@ -59,22 +59,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function createClient(

View File

@@ -60,22 +60,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function createUserClient(

View File

@@ -14,7 +14,7 @@ import { sendTerminateClient } from "./terminate";
import { OlmErrorCodes } from "../olm/error";
const deleteClientSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
@@ -25,22 +25,7 @@ registry.registerPath({
request: {
params: deleteClientSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteClient(

View File

@@ -253,22 +253,7 @@ registry.registerPath({
niceId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
registry.registerPath({
@@ -281,22 +266,7 @@ registry.registerPath({
clientId: z.number()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getClient(

View File

@@ -1,15 +1,20 @@
import {
clientLabels,
clients,
clientSitesAssociationsCache,
currentFingerprint,
db,
labels,
olms,
orgs,
roleClients,
sites,
userClients,
users
users,
type Label
} from "@server/db";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
@@ -169,6 +174,7 @@ type ClientWithSites = Awaited<ReturnType<typeof queryClientsBase>>[0] & {
siteNiceId: string | null;
}>;
olmUpdateAvailable?: boolean;
labels?: Array<Pick<Label, "labelId" | "name" | "color">>;
};
type OlmWithUpdateAvailable = ClientWithSites;
@@ -186,22 +192,7 @@ registry.registerPath({
query: listClientsSchema,
params: listClientsParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listClients(
@@ -270,6 +261,11 @@ export async function listClients(
(client) => client.clientId
);
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
orgId,
tierMatrix.labels
);
// Get client count with filter
const conditions = [
and(
@@ -303,18 +299,29 @@ export async function listClients(
}
if (query) {
conditions.push(
or(
like(
sql`LOWER(${clients.name})`,
"%" + query.toLowerCase() + "%"
),
like(
sql`LOWER(${clients.niceId})`,
"%" + query.toLowerCase() + "%"
const q = "%" + query.toLowerCase() + "%";
const queryList = [
like(sql`LOWER(${clients.name})`, q),
like(sql`LOWER(${clients.niceId})`, q)
];
if (isLabelFeatureEnabled) {
queryList.push(
inArray(
clients.clientId,
db
.select({ id: clientLabels.clientId })
.from(clientLabels)
.innerJoin(
labels,
eq(labels.labelId, clientLabels.labelId)
)
.where(like(sql`LOWER(${labels.name})`, q))
)
)
);
);
}
conditions.push(or(...queryList));
}
const baseQuery = queryClientsBase().where(and(...conditions));
@@ -341,6 +348,30 @@ export async function listClients(
const clientIds = clientsList.map((client) => client.clientId);
const siteAssociations = await getSiteAssociations(clientIds);
let labelsForClients: Array<{
labelId: number;
name: string;
color: string;
clientId: number;
}> = [];
if (isLabelFeatureEnabled && clientIds.length > 0) {
labelsForClients = await db
.select({
labelId: labels.labelId,
name: labels.name,
color: labels.color,
clientId: clientLabels.clientId
})
.from(labels)
.innerJoin(
clientLabels,
eq(clientLabels.labelId, labels.labelId)
)
.where(inArray(clientLabels.clientId, clientIds))
.orderBy(asc(clientLabels.clientLabelId));
}
// Group site associations by client ID
const sitesByClient = siteAssociations.reduce(
(acc, association) => {
@@ -368,7 +399,10 @@ export async function listClients(
const clientsWithSites = clientsList.map((client) => {
return {
...client,
sites: sitesByClient[client.clientId] || []
sites: sitesByClient[client.clientId] || [],
labels: labelsForClients.filter(
(l) => l.clientId === client.clientId
)
};
});

View File

@@ -213,22 +213,7 @@ registry.registerPath({
query: listUserDevicesSchema,
params: listUserDevicesParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function listUserDevices(

View File

@@ -6,7 +6,6 @@ import logger from "@server/logger";
import { generateId } from "@server/auth/sessions/app";
import { getNextAvailableClientSubnet } from "@server/lib/ip";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
@@ -15,12 +14,6 @@ export type PickClientDefaultsResponse = {
olmSecret: string;
subnet: string;
};
const PickClientDefaultsResponseDataSchema = z.object({
olmId: z.string(),
olmSecret: z.string(),
subnet: z.string()
});
const pickClientDefaultsSchema = z.strictObject({
orgId: z.string()
@@ -34,16 +27,7 @@ registry.registerPath({
request: {
params: pickClientDefaultsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(PickClientDefaultsResponseDataSchema)
}
}
}
}
responses: {}
});
export async function pickClientDefaults(

View File

@@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const unarchiveClientSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
@@ -22,22 +22,7 @@ registry.registerPath({
request: {
params: unarchiveClientSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function unarchiveClient(

View File

@@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const unblockClientSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
@@ -22,22 +22,7 @@ registry.registerPath({
request: {
params: unblockClientSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function unblockClient(

View File

@@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const updateClientParamsSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
clientId: z.string().transform(Number).pipe(z.int().positive())
});
const updateClientSchema = z.strictObject({
@@ -36,22 +36,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function updateClient(

View File

@@ -37,22 +37,7 @@ registry.registerPath({
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getDNSRecords(

View File

@@ -39,22 +39,7 @@ registry.registerPath({
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getDomain(

View File

@@ -9,7 +9,6 @@ import { eq, sql } from "drizzle-orm";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const listDomainsParamsSchema = z.strictObject({
orgId: z.string()
@@ -57,28 +56,6 @@ export type ListDomainsResponse = {
pagination: { total: number; limit: number; offset: number };
};
const ListDomainsResponseDataSchema = z.object({
domains: z.array(
z.object({
domainId: z.string(),
baseDomain: z.string(),
verified: z.boolean(),
type: z.string().nullable(),
failed: z.boolean(),
tries: z.number(),
configManaged: z.boolean(),
certResolver: z.string().nullable(),
preferWildcardCert: z.boolean().nullable(),
errorMessage: z.string().nullable()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/domains",
@@ -90,16 +67,7 @@ registry.registerPath({
}),
query: listDomainsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(ListDomainsResponseDataSchema)
}
}
}
}
responses: {}
});
export async function listDomains(

View File

@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db, domains, orgDomains } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -25,12 +24,6 @@ export type UpdateDomainResponse = {
certResolver: string | null;
preferWildcardCert: boolean | null;
};
const UpdateDomainResponseDataSchema = z.object({
domainId: z.string(),
certResolver: z.string().nullable(),
preferWildcardCert: z.boolean().nullable()
});
registry.registerPath({
method: "patch",
@@ -43,16 +36,7 @@ registry.registerPath({
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateDomainResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateOrgDomain(

View File

@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -23,8 +22,6 @@ const bodySchema = z.strictObject({
});
export type CreateIdpOrgPolicyResponse = {};
const CreateIdpOrgPolicyResponseDataSchema = z.object({});
registry.registerPath({
method: "put",
@@ -41,16 +38,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateIdpOrgPolicyResponseDataSchema)
}
}
}
}
responses: {}
});
export async function createIdpOrgPolicy(

View File

@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -34,11 +33,6 @@ export type CreateIdpResponse = {
idpId: number;
redirectUrl: string;
};
const CreateIdpResponseDataSchema = z.object({
idpId: z.number(),
redirectUrl: z.string()
});
registry.registerPath({
method: "put",
@@ -54,16 +48,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateIdpResponseDataSchema)
}
}
}
}
responses: {}
});
export async function createOidcIdp(

View File

@@ -25,22 +25,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteIdp(

View File

@@ -23,22 +23,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function deleteIdpOrgPolicy(

View File

@@ -38,22 +38,7 @@ registry.registerPath({
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
responses: {}
});
export async function getIdp(

View File

@@ -9,7 +9,6 @@ import { eq, sql } from "drizzle-orm";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const paramsSchema = z.object({
idpId: z.coerce.number<number>()
@@ -45,21 +44,6 @@ export type ListIdpOrgPoliciesResponse = {
pagination: { total: number; limit: number; offset: number };
};
const ListIdpOrgPoliciesResponseDataSchema = z.object({
policies: z.array(
z.object({
idpId: z.number(),
orgId: z.string(),
assignDefaultOrgRoleId: z.number().nullable()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/idp/{idpId}/org",
@@ -69,18 +53,7 @@ registry.registerPath({
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListIdpOrgPoliciesResponseDataSchema
)
}
}
}
}
responses: {}
});
export async function listIdpOrgPolicies(

View File

@@ -9,7 +9,6 @@ import { eq, sql } from "drizzle-orm";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const querySchema = z.strictObject({
limit: z
@@ -55,25 +54,6 @@ export type ListIdpsResponse = {
};
};
const ListIdpsResponseDataSchema = z.object({
idps: z.array(
z.object({
idpId: z.number(),
name: z.string(),
type: z.string(),
variant: z.string().nullable(),
orgCount: z.number(),
autoProvision: z.boolean().nullable(),
tags: z.string().nullable()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/idp",
@@ -82,16 +62,7 @@ registry.registerPath({
request: {
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(ListIdpsResponseDataSchema)
}
}
}
}
responses: {}
});
export async function listIdps(

View File

@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -22,8 +21,6 @@ const bodySchema = z.strictObject({
});
export type UpdateIdpOrgPolicyResponse = {};
const UpdateIdpOrgPolicyResponseDataSchema = z.object({});
registry.registerPath({
method: "post",
@@ -40,16 +37,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateIdpOrgPolicyResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateIdpOrgPolicy(

View File

@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -39,10 +38,6 @@ const bodySchema = z.strictObject({
export type UpdateIdpResponse = {
idpId: number;
};
const UpdateIdpResponseDataSchema = z.object({
idpId: z.number()
});
registry.registerPath({
method: "post",
@@ -59,16 +54,7 @@ registry.registerPath({
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateIdpResponseDataSchema)
}
}
}
}
responses: {}
});
export async function updateOidcIdp(

View File

@@ -0,0 +1,10 @@
import type { Label } from "@server/db";
import type { PaginatedResponse } from "@server/types/Pagination";
export type ListOrgLabelsResponse = PaginatedResponse<{
labels: Omit<Label, "orgId">[];
}>;
export type CreateOrEditLabelResponse = {
label: Label;
};

Some files were not shown because too many files have changed in this diff Show More