basic functionality

This commit is contained in:
miloschwartz
2026-06-30 21:03:19 -04:00
parent 31725eb3cc
commit f0efa4203b
44 changed files with 5048 additions and 1122 deletions

View File

@@ -218,6 +218,20 @@ export const labels = pgTable("labels", {
.notNull()
});
export const launcherViews = pgTable("launcherViews", {
viewId: serial("viewId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade"
}),
name: varchar("name").notNull(),
config: text("config").notNull(),
createdAt: varchar("createdAt").notNull(),
updatedAt: varchar("updatedAt").notNull()
});
export const siteLabels = pgTable(
"siteLabels",
{
@@ -1550,6 +1564,7 @@ export type RoundTripMessageTracker = InferSelectModel<
export type Network = InferSelectModel<typeof networks>;
export type StatusHistory = InferSelectModel<typeof statusHistory>;
export type Label = InferSelectModel<typeof labels>;
export type LauncherView = InferSelectModel<typeof launcherViews>;
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
export type UserPolicy = InferSelectModel<typeof userPolicies>;

View File

@@ -221,6 +221,20 @@ export const labels = sqliteTable("labels", {
.notNull()
});
export const launcherViews = sqliteTable("launcherViews", {
viewId: integer("viewId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
}),
name: text("name").notNull(),
config: text("config").notNull(),
createdAt: text("createdAt").notNull(),
updatedAt: text("updatedAt").notNull()
});
export const siteLabels = sqliteTable(
"siteLabels",
{
@@ -1549,6 +1563,7 @@ export type RoundTripMessageTracker = InferSelectModel<
>;
export type StatusHistory = InferSelectModel<typeof statusHistory>;
export type Label = InferSelectModel<typeof labels>;
export type LauncherView = InferSelectModel<typeof launcherViews>;
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
export type ResourcePolicyPincode = InferSelectModel<
typeof resourcePolicyPincode

View File

@@ -17,6 +17,7 @@ import * as idp from "./idp";
import * as blueprints from "./blueprints";
import * as apiKeys from "./apiKeys";
import * as logs from "./auditLogs";
import * as launcher from "./launcher";
import * as newt from "./newt";
import * as olm from "./olm";
import * as serverInfo from "./serverInfo";
@@ -455,6 +456,42 @@ authenticated.get(
resource.getUserResources
);
authenticated.get(
"/org/:orgId/launcher/groups",
verifyOrgAccess,
launcher.listLauncherGroups
);
authenticated.get(
"/org/:orgId/launcher/resources",
verifyOrgAccess,
launcher.listLauncherResources
);
authenticated.get(
"/org/:orgId/launcher/views",
verifyOrgAccess,
launcher.listLauncherViews
);
authenticated.post(
"/org/:orgId/launcher/views",
verifyOrgAccess,
launcher.createLauncherView
);
authenticated.put(
"/org/:orgId/launcher/views/:viewId",
verifyOrgAccess,
launcher.updateLauncherView
);
authenticated.delete(
"/org/:orgId/launcher/views/:viewId",
verifyOrgAccess,
launcher.deleteLauncherView
);
authenticated.get(
"/org/:orgId/user-resource-aliases",
verifyOrgAccess,

View File

@@ -13,6 +13,7 @@ import * as apiKeys from "./apiKeys";
import * as idp from "./idp";
import * as logs from "./auditLogs";
import * as siteResource from "./siteResource";
import * as launcher from "./launcher";
import {
verifyApiKey,
verifyApiKeyOrgAccess,
@@ -159,6 +160,42 @@ authenticated.get(
verifyApiKeyOrgAccess,
resource.getUserResources
);
authenticated.get(
"/org/:orgId/launcher/groups",
verifyApiKeyOrgAccess,
launcher.listLauncherGroups
);
authenticated.get(
"/org/:orgId/launcher/resources",
verifyApiKeyOrgAccess,
launcher.listLauncherResources
);
authenticated.get(
"/org/:orgId/launcher/views",
verifyApiKeyOrgAccess,
launcher.listLauncherViews
);
authenticated.post(
"/org/:orgId/launcher/views",
verifyApiKeyOrgAccess,
launcher.createLauncherView
);
authenticated.put(
"/org/:orgId/launcher/views/:viewId",
verifyApiKeyOrgAccess,
launcher.updateLauncherView
);
authenticated.delete(
"/org/:orgId/launcher/views/:viewId",
verifyApiKeyOrgAccess,
launcher.deleteLauncherView
);
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site-resource",

View File

@@ -0,0 +1,118 @@
import { db, launcherViews } from "@server/db";
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import moment from "moment";
import { fromZodError } from "zod-validation-error";
import { z } from "zod";
import {
isOrgAdminOrOwner,
verifyLauncherOrgMembership
} from "./launcherResourceAccess";
import { launcherViewConfigSchema } from "./types";
const createLauncherViewBodySchema = z.strictObject({
name: z.string().min(1).max(128),
config: launcherViewConfigSchema,
orgWide: z.boolean().optional().default(false)
});
export async function createLauncherView(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = getFirstString(req.params.orgId);
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
const parsed = createLauncherViewBodySchema.safeParse(req.body);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const { userRoleIds } = await verifyLauncherOrgMembership(
orgId,
userId
);
if (parsed.data.orgWide) {
const canManageOrgWide = await isOrgAdminOrOwner(
orgId,
userId,
userRoleIds
);
if (!canManageOrgWide) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Only administrators can create org-wide views"
)
);
}
}
const now = moment().toISOString();
const [created] = await db
.insert(launcherViews)
.values({
orgId,
userId: parsed.data.orgWide ? null : userId,
name: parsed.data.name,
config: JSON.stringify(parsed.data.config),
createdAt: now,
updatedAt: now
})
.returning();
return response(res, {
data: {
viewId: created.viewId,
orgId: created.orgId,
userId: created.userId,
name: created.name,
config: launcherViewConfigSchema.parse(
JSON.parse(created.config)
),
createdAt: created.createdAt,
updatedAt: created.updatedAt,
isOrgWide: created.userId == null
},
success: true,
error: false,
message: "Launcher view created successfully",
status: HttpCode.CREATED
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error creating launcher view:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}

View File

@@ -0,0 +1,97 @@
import { db, launcherViews } from "@server/db";
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import {
isOrgAdminOrOwner,
verifyLauncherOrgMembership
} from "./launcherResourceAccess";
export async function deleteLauncherView(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = getFirstString(req.params.orgId);
const viewId = Number.parseInt(
getFirstString(req.params.viewId) ?? "",
10
);
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId || !Number.isFinite(viewId)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid request parameters"
)
);
}
const { userRoleIds } = await verifyLauncherOrgMembership(
orgId,
userId
);
const [existing] = await db
.select()
.from(launcherViews)
.where(
and(
eq(launcherViews.viewId, viewId),
eq(launcherViews.orgId, orgId)
)
)
.limit(1);
if (!existing) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
);
}
const isPersonalView = existing.userId === userId;
const isOrgWideView = existing.userId == null;
const isAdmin = await isOrgAdminOrOwner(orgId, userId, userRoleIds);
if (!isPersonalView && !(isOrgWideView && isAdmin)) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have permission to delete this view"
)
);
}
await db.delete(launcherViews).where(eq(launcherViews.viewId, viewId));
return response(res, {
data: null,
success: true,
error: false,
message: "Launcher view deleted successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error deleting launcher view:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}

View File

@@ -0,0 +1,139 @@
export type SiteResourceDestinationInput = {
mode: "host" | "cidr" | "http" | "ssh";
destination: string | null;
destinationPort: number | null;
scheme: "http" | "https" | null;
};
export function resolveHttpHttpsDisplayPort(
mode: "http",
destinationPort: number | null
): number {
if (destinationPort != null) {
return destinationPort;
}
return 80;
}
export function formatSiteResourceDestinationDisplay(
row: SiteResourceDestinationInput
): string {
if (!row.destination) {
return "";
}
const { mode, destination, destinationPort, scheme } = row;
if (mode !== "http") {
return destination;
}
const port = resolveHttpHttpsDisplayPort(mode, destinationPort);
const downstreamScheme = scheme ?? "http";
const hostPart =
destination.includes(":") && !destination.startsWith("[")
? `[${destination}]`
: destination;
return `${downstreamScheme}://${hostPart}:${port}`;
}
export type PublicResourceAccessInput = {
mode: string;
fullDomain: string | null;
ssl: boolean;
proxyPort: number | null;
wildcard: boolean;
};
export type SiteResourceAccessInput = {
mode: string;
destination: string | null;
destinationPort: number | null;
scheme: "http" | "https" | null;
ssl: boolean;
fullDomain: string | null;
alias: string | null;
aliasAddress: string | null;
};
export type LauncherAccessFields = {
accessDisplay: string;
accessCopyValue: string;
accessUrl: string | null;
};
export function formatPublicResourceAccess(
resource: PublicResourceAccessInput
): LauncherAccessFields {
const browserModes = ["http", "ssh", "rdp", "vnc"];
if (!browserModes.includes(resource.mode)) {
const port = resource.proxyPort?.toString() ?? "";
return {
accessDisplay: port,
accessCopyValue: port,
accessUrl: null
};
}
if (!resource.fullDomain) {
return {
accessDisplay: "",
accessCopyValue: "",
accessUrl: null
};
}
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
return {
accessDisplay: url,
accessCopyValue: url,
accessUrl: resource.wildcard ? null : url
};
}
export function formatSiteResourceAccess(
resource: SiteResourceAccessInput
): LauncherAccessFields {
if (resource.alias) {
return {
accessDisplay: resource.alias,
accessCopyValue: resource.alias,
accessUrl: null
};
}
if (resource.mode === "http" && resource.fullDomain) {
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
return {
accessDisplay: url,
accessCopyValue: url,
accessUrl: url
};
}
const destination = formatSiteResourceDestinationDisplay({
mode: resource.mode as SiteResourceDestinationInput["mode"],
destination: resource.destination,
destinationPort: resource.destinationPort,
scheme: resource.scheme
});
if (destination) {
return {
accessDisplay: destination,
accessCopyValue: destination,
accessUrl: resource.mode === "http" ? destination : null
};
}
if (resource.aliasAddress) {
return {
accessDisplay: resource.aliasAddress,
accessCopyValue: resource.aliasAddress,
accessUrl: null
};
}
return {
accessDisplay: "",
accessCopyValue: "",
accessUrl: null
};
}

View File

@@ -0,0 +1,7 @@
export * from "./types";
export { listLauncherGroups } from "./listLauncherGroups";
export { listLauncherResources } from "./listLauncherResources";
export { listLauncherViews } from "./listLauncherViews";
export { createLauncherView } from "./createLauncherView";
export { updateLauncherView } from "./updateLauncherView";
export { deleteLauncherView } from "./deleteLauncherView";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { fromZodError } from "zod-validation-error";
import { listLauncherGroupsForUser } from "./launcherResourceAccess";
import { launcherListQuerySchema } from "./types";
export async function listLauncherGroups(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = getFirstString(req.params.orgId);
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
const parsed = launcherListQuerySchema.safeParse(req.query);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const { groups, total } = await listLauncherGroupsForUser(
orgId,
userId,
parsed.data
);
return response(res, {
data: {
groups,
pagination: {
total,
page: parsed.data.page,
pageSize: parsed.data.pageSize
}
},
success: true,
error: false,
message: "Launcher groups retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error listing launcher groups:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}

View File

@@ -0,0 +1,78 @@
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { fromZodError } from "zod-validation-error";
import { z } from "zod";
import { listLauncherResourcesForUser } from "./launcherResourceAccess";
import { launcherListQuerySchema } from "./types";
const listLauncherResourcesQuerySchema = launcherListQuerySchema.extend({
groupKey: z.string().min(1)
});
export async function listLauncherResources(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = getFirstString(req.params.orgId);
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
const parsed = listLauncherResourcesQuerySchema.safeParse(req.query);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const { resources, total } = await listLauncherResourcesForUser(
orgId,
userId,
parsed.data
);
return response(res, {
data: {
resources,
pagination: {
total,
page: parsed.data.page,
pageSize: parsed.data.pageSize
}
},
success: true,
error: false,
message: "Launcher resources retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error listing launcher resources:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}

View File

@@ -0,0 +1,83 @@
import { db, launcherViews } from "@server/db";
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode";
import { and, eq, isNull, or } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { launcherViewConfigSchema, type LauncherViewRecord } from "./types";
import { verifyLauncherOrgMembership } from "./launcherResourceAccess";
function mapViewRow(
row: typeof launcherViews.$inferSelect
): LauncherViewRecord {
return {
viewId: row.viewId,
orgId: row.orgId,
userId: row.userId,
name: row.name,
config: launcherViewConfigSchema.parse(JSON.parse(row.config)),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
isOrgWide: row.userId == null
};
}
export async function listLauncherViews(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = getFirstString(req.params.orgId);
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
await verifyLauncherOrgMembership(orgId, userId);
const rows = await db
.select()
.from(launcherViews)
.where(
and(
eq(launcherViews.orgId, orgId),
or(
eq(launcherViews.userId, userId),
isNull(launcherViews.userId)
)
)
);
return response(res, {
data: {
views: rows.map(mapViewRow)
},
success: true,
error: false,
message: "Launcher views retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error listing launcher views:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}

View File

@@ -0,0 +1,129 @@
import { z } from "zod";
export const LAUNCHER_UNLABELED_GROUP_KEY = "unlabeled";
export const launcherViewConfigSchema = z.object({
groupBy: z.enum(["site", "label"]).default("site"),
layout: z.enum(["grid", "list"]).default("grid"),
sortBy: z.literal("name").default("name"),
order: z.enum(["asc", "desc"]).default("asc"),
showLabels: z.boolean().default(true),
showSiteTags: z.boolean().default(true),
showRecents: z.boolean().default(false).optional(),
siteIds: z.array(z.number()).default([]),
labelIds: z.array(z.number()).default([]),
query: z.string().default("")
});
export type LauncherViewConfig = z.infer<typeof launcherViewConfigSchema>;
export const defaultLauncherViewConfig: LauncherViewConfig =
launcherViewConfigSchema.parse({});
export type LauncherLabel = {
labelId: number;
name: string;
color: string;
};
export type LauncherSiteInfo = {
siteId: number;
name: string;
type: string;
online?: boolean;
};
export type LauncherResource = {
launcherResourceKey: string;
resourceType: "public" | "site";
resourceId: number;
siteResourceId?: number;
name: string;
accessDisplay: string;
accessCopyValue: string;
accessUrl: string | null;
iconUrl: string | null;
enabled: boolean;
mode: string;
labels: LauncherLabel[];
site?: LauncherSiteInfo;
};
export type LauncherGroup = {
groupKey: string;
name: string;
groupType: "site" | "label";
itemCount: number;
siteType?: string;
siteOnline?: boolean;
labelColor?: string;
};
export type ListLauncherGroupsResponse = {
groups: LauncherGroup[];
pagination: {
total: number;
page: number;
pageSize: number;
};
};
export type ListLauncherResourcesResponse = {
resources: LauncherResource[];
pagination: {
total: number;
page: number;
pageSize: number;
};
};
export type LauncherViewRecord = {
viewId: number;
orgId: string;
userId: string | null;
name: string;
config: LauncherViewConfig;
createdAt: string;
updatedAt: string;
isOrgWide: boolean;
};
export type ListLauncherViewsResponse = {
views: LauncherViewRecord[];
};
export const launcherListQuerySchema = z.strictObject({
pageSize: z.coerce
.number()
.int()
.positive()
.optional()
.catch(20)
.default(20),
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
query: z.string().optional().default(""),
groupBy: z.enum(["site", "label"]).optional().default("site"),
groupKey: z.string().optional(),
siteIds: z.string().optional(),
labelIds: z.string().optional(),
sort_by: z.literal("name").optional().default("name"),
order: z.enum(["asc", "desc"]).optional().default("asc")
});
export type LauncherListQuery = z.infer<typeof launcherListQuerySchema>;
export function parseIdListParam(value: string | undefined): number[] {
if (!value?.trim()) {
return [];
}
return value
.split(",")
.map((part) => Number.parseInt(part.trim(), 10))
.filter((id) => Number.isFinite(id));
}
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
export type LauncherViewSelection =
| { type: "default" }
| { type: "saved"; viewId: number };

View File

@@ -0,0 +1,164 @@
import { db, launcherViews } from "@server/db";
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import moment from "moment";
import { fromZodError } from "zod-validation-error";
import { z } from "zod";
import {
isOrgAdminOrOwner,
verifyLauncherOrgMembership
} from "./launcherResourceAccess";
import { launcherViewConfigSchema } from "./types";
const updateLauncherViewBodySchema = z.strictObject({
name: z.string().min(1).max(128).optional(),
config: launcherViewConfigSchema.optional(),
orgWide: z.boolean().optional()
});
export async function updateLauncherView(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = getFirstString(req.params.orgId);
const viewId = Number.parseInt(
getFirstString(req.params.viewId) ?? "",
10
);
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId || !Number.isFinite(viewId)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid request parameters"
)
);
}
const parsed = updateLauncherViewBodySchema.safeParse(req.body);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const { userRoleIds } = await verifyLauncherOrgMembership(
orgId,
userId
);
const [existing] = await db
.select()
.from(launcherViews)
.where(
and(
eq(launcherViews.viewId, viewId),
eq(launcherViews.orgId, orgId)
)
)
.limit(1);
if (!existing) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
);
}
const isPersonalView = existing.userId === userId;
const isOrgWideView = existing.userId == null;
const isAdmin = await isOrgAdminOrOwner(orgId, userId, userRoleIds);
if (!isPersonalView && !(isOrgWideView && isAdmin)) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have permission to update this view"
)
);
}
if (parsed.data.orgWide === true && !isAdmin) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Only administrators can make views org-wide"
)
);
}
if (parsed.data.orgWide === false && isOrgWideView && !isAdmin) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Only administrators can change org-wide views"
)
);
}
const nextUserId =
parsed.data.orgWide === true
? null
: parsed.data.orgWide === false
? userId
: existing.userId;
const [updated] = await db
.update(launcherViews)
.set({
name: parsed.data.name ?? existing.name,
config: parsed.data.config
? JSON.stringify(parsed.data.config)
: existing.config,
userId: nextUserId,
updatedAt: moment().toISOString()
})
.where(eq(launcherViews.viewId, viewId))
.returning();
return response(res, {
data: {
viewId: updated.viewId,
orgId: updated.orgId,
userId: updated.userId,
name: updated.name,
config: launcherViewConfigSchema.parse(
JSON.parse(updated.config)
),
createdAt: updated.createdAt,
updatedAt: updated.updatedAt,
isOrgWide: updated.userId == null
},
success: true,
error: false,
message: "Launcher view updated successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error updating launcher view:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}