chore: update prettier configuration for print width (#519)
* feat: update prettier configuration for print width * chore: apply code formatting to entire repository * fix: remove build files * fix: format issue --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -22,60 +22,52 @@ export const appRouter = createTRPCRouter({
|
||||
orderBy: asc(apps.name),
|
||||
});
|
||||
}),
|
||||
byId: publicProcedure
|
||||
.input(validation.app.byId)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const app = await ctx.db.query.apps.findFirst({
|
||||
where: eq(apps.id, input.id),
|
||||
byId: publicProcedure.input(validation.app.byId).query(async ({ ctx, input }) => {
|
||||
const app = await ctx.db.query.apps.findFirst({
|
||||
where: eq(apps.id, input.id),
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "App not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!app) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "App not found",
|
||||
});
|
||||
}
|
||||
return app;
|
||||
}),
|
||||
create: publicProcedure.input(validation.app.manage).mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.insert(apps).values({
|
||||
id: createId(),
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
iconUrl: input.iconUrl,
|
||||
href: input.href,
|
||||
});
|
||||
}),
|
||||
update: publicProcedure.input(validation.app.edit).mutation(async ({ ctx, input }) => {
|
||||
const app = await ctx.db.query.apps.findFirst({
|
||||
where: eq(apps.id, input.id),
|
||||
});
|
||||
|
||||
return app;
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(validation.app.manage)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.insert(apps).values({
|
||||
id: createId(),
|
||||
if (!app) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "App not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(apps)
|
||||
.set({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
iconUrl: input.iconUrl,
|
||||
href: input.href,
|
||||
});
|
||||
}),
|
||||
update: publicProcedure
|
||||
.input(validation.app.edit)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const app = await ctx.db.query.apps.findFirst({
|
||||
where: eq(apps.id, input.id),
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "App not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(apps)
|
||||
.set({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
iconUrl: input.iconUrl,
|
||||
href: input.href,
|
||||
})
|
||||
.where(eq(apps.id, input.id));
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(validation.app.byId)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.delete(apps).where(eq(apps.id, input.id));
|
||||
}),
|
||||
})
|
||||
.where(eq(apps.id, input.id));
|
||||
}),
|
||||
delete: publicProcedure.input(validation.app.byId).mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.delete(apps).where(eq(apps.id, input.id));
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -16,50 +16,34 @@ import {
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||
import {
|
||||
createSectionSchema,
|
||||
sharedItemSchema,
|
||||
validation,
|
||||
z,
|
||||
} from "@homarr/validation";
|
||||
import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation";
|
||||
|
||||
import { zodUnionFromArray } from "../../../validation/src/enums";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
permissionRequiredProcedure,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
getAllBoards: publicProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const permissionsOfCurrentUserWhenPresent =
|
||||
await ctx.db.query.boardUserPermissions.findMany({
|
||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||
});
|
||||
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
|
||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||
});
|
||||
|
||||
const permissionsOfCurrentUserGroupsWhenPresent =
|
||||
await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
with: {
|
||||
group: {
|
||||
with: {
|
||||
boardPermissions: {},
|
||||
},
|
||||
const permissionsOfCurrentUserGroupsWhenPresent = await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
with: {
|
||||
group: {
|
||||
with: {
|
||||
boardPermissions: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const boardIds = permissionsOfCurrentUserWhenPresent
|
||||
.map((permission) => permission.boardId)
|
||||
.concat(
|
||||
permissionsOfCurrentUserGroupsWhenPresent
|
||||
.map((groupMember) =>
|
||||
groupMember.group.boardPermissions.map(
|
||||
(permission) => permission.boardId,
|
||||
),
|
||||
)
|
||||
.map((groupMember) => groupMember.group.boardPermissions.map((permission) => permission.boardId))
|
||||
.flat(),
|
||||
);
|
||||
|
||||
@@ -89,9 +73,7 @@ export const boardRouter = createTRPCRouter({
|
||||
permissionsOfCurrentUserGroupsWhenPresent.length >= 1
|
||||
? inArray(
|
||||
boardGroupPermissions.groupId,
|
||||
permissionsOfCurrentUserGroupsWhenPresent.map(
|
||||
(groupMember) => groupMember.groupId,
|
||||
),
|
||||
permissionsOfCurrentUserGroupsWhenPresent.map((groupMember) => groupMember.groupId),
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
@@ -129,61 +111,33 @@ export const boardRouter = createTRPCRouter({
|
||||
});
|
||||
});
|
||||
}),
|
||||
renameBoard: protectedProcedure
|
||||
.input(validation.board.rename)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"full-access",
|
||||
);
|
||||
renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full-access");
|
||||
|
||||
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
|
||||
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
|
||||
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
.set({ name: input.name })
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
|
||||
}),
|
||||
changeBoardVisibility: protectedProcedure
|
||||
.input(validation.board.changeVisibility)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"full-access",
|
||||
);
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full-access");
|
||||
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
.set({ isPublic: input.visibility === "public" })
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
deleteBoard: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"full-access",
|
||||
);
|
||||
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full-access");
|
||||
|
||||
await ctx.db.delete(boards).where(eq(boards.id, input.id));
|
||||
}),
|
||||
setHomeBoard: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"board-view",
|
||||
);
|
||||
await ctx.db.delete(boards).where(eq(boards.id, input.id));
|
||||
}),
|
||||
setHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "board-view");
|
||||
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({ homeBoardId: input.id })
|
||||
.where(eq(users.id, ctx.session.user.id));
|
||||
}),
|
||||
await ctx.db.update(users).set({ homeBoardId: input.id }).where(eq(users.id, ctx.session.user.id));
|
||||
}),
|
||||
getHomeBoard: publicProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const user = userId
|
||||
@@ -192,39 +146,21 @@ export const boardRouter = createTRPCRouter({
|
||||
})
|
||||
: null;
|
||||
|
||||
const boardWhere = user?.homeBoardId
|
||||
? eq(boards.id, user.homeBoardId)
|
||||
: eq(boards.name, "home");
|
||||
const boardWhere = user?.homeBoardId ? eq(boards.id, user.homeBoardId) : eq(boards.name, "home");
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(
|
||||
ctx.db,
|
||||
boardWhere,
|
||||
ctx.session?.user.id ?? null,
|
||||
);
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
}),
|
||||
getBoardByName: publicProcedure
|
||||
.input(validation.board.byName)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const boardWhere = eq(boards.name, input.name);
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
||||
getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => {
|
||||
const boardWhere = eq(boards.name, input.name);
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(
|
||||
ctx.db,
|
||||
boardWhere,
|
||||
ctx.session?.user.id ?? null,
|
||||
);
|
||||
}),
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
}),
|
||||
savePartialBoardSettings: protectedProcedure
|
||||
.input(
|
||||
validation.board.savePartialSettings.and(z.object({ id: z.string() })),
|
||||
)
|
||||
.input(validation.board.savePartialSettings.and(z.object({ id: z.string() })))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"board-change",
|
||||
);
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "board-change");
|
||||
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
@@ -254,271 +190,222 @@ export const boardRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
saveBoard: protectedProcedure
|
||||
.input(validation.board.save)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"board-change",
|
||||
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "board-change");
|
||||
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
const dbBoard = await getFullBoardWithWhereAsync(transaction, eq(boards.id, input.id), ctx.session.user.id);
|
||||
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
await transaction.insert(sections).values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
position: section.position,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const inputItems = input.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
const dbItems = dbBoard.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
const dbBoard = await getFullBoardWithWhereAsync(
|
||||
transaction,
|
||||
eq(boards.id, input.id),
|
||||
ctx.session.user.id,
|
||||
const addedItems = filterAddedItems(inputItems, dbItems);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
await transaction.insert(items).values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
sectionId: item.sectionId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const addedSections = filterAddedItems(
|
||||
input.sections,
|
||||
dbBoard.sections,
|
||||
);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
await transaction.insert(sections).values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
position: section.position,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const inputItems = input.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
const dbItems = dbBoard.sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ ...item, sectionId: section.id })),
|
||||
);
|
||||
|
||||
const addedItems = filterAddedItems(inputItems, dbItems);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
await transaction.insert(items).values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
sectionId: item.sectionId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const inputIntegrationRelations = inputItems.flatMap(
|
||||
({ integrations, id: itemId }) =>
|
||||
integrations.map((integration) => ({
|
||||
integrationId: integration.id,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbItems.flatMap(
|
||||
({ integrations, id: itemId }) =>
|
||||
integrations.map((integration) => ({
|
||||
integrationId: integration.id,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
.update(items)
|
||||
.set({
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
sectionId: item.sectionId,
|
||||
})
|
||||
.where(eq(items.id, item.id));
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(
|
||||
input.sections,
|
||||
dbBoard.sections,
|
||||
);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find(
|
||||
(dbSection) => dbSection.id === section.id,
|
||||
);
|
||||
await transaction
|
||||
.update(sections)
|
||||
.set({
|
||||
position: section.position,
|
||||
name:
|
||||
prev?.kind === "category" && "name" in section
|
||||
? section.name
|
||||
: null,
|
||||
})
|
||||
.where(eq(sections.id, section.id));
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
await transaction
|
||||
.delete(integrationItems)
|
||||
.where(
|
||||
and(
|
||||
eq(integrationItems.itemId, relation.itemId),
|
||||
eq(integrationItems.integrationId, relation.integrationId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const removedItems = filterRemovedItems(inputItems, dbItems);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
await transaction.delete(items).where(inArray(items.id, itemIds));
|
||||
}
|
||||
|
||||
const removedSections = filterRemovedItems(
|
||||
input.sections,
|
||||
dbBoard.sections,
|
||||
);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction
|
||||
.delete(sections)
|
||||
.where(inArray(sections.id, sectionIds));
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
getBoardPermissions: protectedProcedure
|
||||
.input(validation.board.permissions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"full-access",
|
||||
const inputIntegrationRelations = inputItems.flatMap(({ integrations, id: itemId }) =>
|
||||
integrations.map((integration) => ({
|
||||
integrationId: integration.id,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbItems.flatMap(({ integrations, id: itemId }) =>
|
||||
integrations.map((integration) => ({
|
||||
integrationId: integration.id,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
||||
where: inArray(
|
||||
groupPermissions.permission,
|
||||
getPermissionsWithParents([
|
||||
"board-view-all",
|
||||
"board-modify-all",
|
||||
"board-full-access",
|
||||
]),
|
||||
),
|
||||
columns: {
|
||||
groupId: false,
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
.update(items)
|
||||
.set({
|
||||
kind: item.kind,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
sectionId: item.sectionId,
|
||||
})
|
||||
.where(eq(items.id, item.id));
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
await transaction
|
||||
.update(sections)
|
||||
.set({
|
||||
position: section.position,
|
||||
name: prev?.kind === "category" && "name" in section ? section.name : null,
|
||||
})
|
||||
.where(eq(sections.id, section.id));
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
await transaction
|
||||
.delete(integrationItems)
|
||||
.where(
|
||||
and(
|
||||
eq(integrationItems.itemId, relation.itemId),
|
||||
eq(integrationItems.integrationId, relation.integrationId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const removedItems = filterRemovedItems(inputItems, dbItems);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
await transaction.delete(items).where(inArray(items.id, itemIds));
|
||||
}
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction.delete(sections).where(inArray(sections.id, sectionIds));
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full-access");
|
||||
|
||||
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
||||
where: inArray(
|
||||
groupPermissions.permission,
|
||||
getPermissionsWithParents(["board-view-all", "board-modify-all", "board-full-access"]),
|
||||
),
|
||||
columns: {
|
||||
groupId: false,
|
||||
},
|
||||
with: {
|
||||
group: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
with: {
|
||||
},
|
||||
});
|
||||
|
||||
const userPermissions = await ctx.db.query.boardUserPermissions.findMany({
|
||||
where: eq(boardUserPermissions.boardId, input.id),
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dbGroupBoardPermission = await ctx.db.query.boardGroupPermissions.findMany({
|
||||
where: eq(boardGroupPermissions.boardId, input.id),
|
||||
with: {
|
||||
group: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
userPermissions: userPermissions
|
||||
.map(({ user, permission }) => ({
|
||||
user,
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
return (permissionA.user.name ?? "").localeCompare(permissionB.user.name ?? "");
|
||||
}),
|
||||
groupPermissions: dbGroupBoardPermission
|
||||
.map(({ group, permission }) => ({
|
||||
group: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userPermissions = await ctx.db.query.boardUserPermissions.findMany({
|
||||
where: eq(boardUserPermissions.boardId, input.id),
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dbGroupBoardPermission =
|
||||
await ctx.db.query.boardGroupPermissions.findMany({
|
||||
where: eq(boardGroupPermissions.boardId, input.id),
|
||||
with: {
|
||||
group: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
userPermissions: userPermissions
|
||||
.map(({ user, permission }) => ({
|
||||
user,
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
return (permissionA.user.name ?? "").localeCompare(
|
||||
permissionB.user.name ?? "",
|
||||
);
|
||||
}),
|
||||
groupPermissions: dbGroupBoardPermission
|
||||
.map(({ group, permission }) => ({
|
||||
group: {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
},
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
saveUserBoardPermissions: protectedProcedure
|
||||
.input(validation.board.savePermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"full-access",
|
||||
);
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full-access");
|
||||
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(boardUserPermissions)
|
||||
.where(eq(boardUserPermissions.boardId, input.id));
|
||||
await transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.id));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -534,16 +421,10 @@ export const boardRouter = createTRPCRouter({
|
||||
saveGroupBoardPermissions: protectedProcedure
|
||||
.input(validation.board.savePermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(
|
||||
ctx,
|
||||
eq(boards.id, input.id),
|
||||
"full-access",
|
||||
);
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full-access");
|
||||
|
||||
await ctx.db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(boardGroupPermissions)
|
||||
.where(eq(boardGroupPermissions.boardId, input.id));
|
||||
await transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.id));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -558,11 +439,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}),
|
||||
});
|
||||
|
||||
const noBoardWithSimilarNameAsync = async (
|
||||
db: Database,
|
||||
name: string,
|
||||
ignoredIds: string[] = [],
|
||||
) => {
|
||||
const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => {
|
||||
const boards = await db.query.boards.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
@@ -571,9 +448,7 @@ const noBoardWithSimilarNameAsync = async (
|
||||
});
|
||||
|
||||
const board = boards.find(
|
||||
(board) =>
|
||||
board.name.toLowerCase() === name.toLowerCase() &&
|
||||
!ignoredIds.includes(board.id),
|
||||
(board) => board.name.toLowerCase() === name.toLowerCase() && !ignoredIds.includes(board.id),
|
||||
);
|
||||
|
||||
if (board) {
|
||||
@@ -584,11 +459,7 @@ const noBoardWithSimilarNameAsync = async (
|
||||
}
|
||||
};
|
||||
|
||||
const getFullBoardWithWhereAsync = async (
|
||||
db: Database,
|
||||
where: SQL<unknown>,
|
||||
userId: string | null,
|
||||
) => {
|
||||
const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
});
|
||||
@@ -622,10 +493,7 @@ const getFullBoardWithWhereAsync = async (
|
||||
},
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(
|
||||
boardGroupPermissions.groupId,
|
||||
groupsOfCurrentUser.map((group) => group.groupId).concat(""),
|
||||
),
|
||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -660,9 +528,7 @@ const forKind = <T extends WidgetKind>(kind: T) =>
|
||||
options: z.record(z.unknown()),
|
||||
});
|
||||
|
||||
const outputItemSchema = zodUnionFromArray(
|
||||
widgetKinds.map((kind) => forKind(kind)),
|
||||
).and(sharedItemSchema);
|
||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||
|
||||
const parseSection = (section: unknown) => {
|
||||
const result = createSectionSchema(outputItemSchema).safeParse(section);
|
||||
@@ -672,26 +538,11 @@ const parseSection = (section: unknown) => {
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const filterAddedItems = <TInput extends { id: string }>(
|
||||
inputArray: TInput[],
|
||||
dbArray: TInput[],
|
||||
) =>
|
||||
inputArray.filter(
|
||||
(inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id),
|
||||
);
|
||||
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
||||
|
||||
const filterRemovedItems = <TInput extends { id: string }>(
|
||||
inputArray: TInput[],
|
||||
dbArray: TInput[],
|
||||
) =>
|
||||
dbArray.filter(
|
||||
(dbItem) => !inputArray.some((inputItem) => dbItem.id === inputItem.id),
|
||||
);
|
||||
const filterRemovedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||
dbArray.filter((dbItem) => !inputArray.some((inputItem) => dbItem.id === inputItem.id));
|
||||
|
||||
const filterUpdatedItems = <TInput extends { id: string }>(
|
||||
inputArray: TInput[],
|
||||
dbArray: TInput[],
|
||||
) =>
|
||||
inputArray.filter((inputItem) =>
|
||||
dbArray.some((dbItem) => dbItem.id === inputItem.id),
|
||||
);
|
||||
const filterUpdatedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||
inputArray.filter((inputItem) => dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
||||
|
||||
@@ -4,11 +4,7 @@ import type { Session } from "@homarr/auth";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import type { Database, SQL } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import {
|
||||
boardGroupPermissions,
|
||||
boardUserPermissions,
|
||||
groupMembers,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import { boardGroupPermissions, boardUserPermissions, groupMembers } from "@homarr/db/schema/sqlite";
|
||||
import type { BoardPermission } from "@homarr/definitions";
|
||||
|
||||
/**
|
||||
@@ -38,10 +34,7 @@ export const throwIfActionForbiddenAsync = async (
|
||||
where: eq(boardUserPermissions.userId, session?.user.id ?? ""),
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(
|
||||
boardGroupPermissions.groupId,
|
||||
groupsOfCurrentUser.map((group) => group.groupId).concat(""),
|
||||
),
|
||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -50,8 +43,7 @@ export const throwIfActionForbiddenAsync = async (
|
||||
notAllowed();
|
||||
}
|
||||
|
||||
const { hasViewAccess, hasChangeAccess, hasFullAccess } =
|
||||
constructBoardPermissions(board, session);
|
||||
const { hasViewAccess, hasChangeAccess, hasFullAccess } = constructBoardPermissions(board, session);
|
||||
|
||||
if (hasFullAccess) {
|
||||
return; // As full access is required and user has full access, allow
|
||||
|
||||
@@ -2,98 +2,86 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
||||
import {
|
||||
groupMembers,
|
||||
groupPermissions,
|
||||
groups,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const groupRouter = createTRPCRouter({
|
||||
getPaginated: protectedProcedure
|
||||
.input(validation.group.paginated)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search
|
||||
? like(groups.name, `%${input.search.trim()}%`)
|
||||
: undefined;
|
||||
const groupCount = await ctx.db
|
||||
.select({
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(groups)
|
||||
.where(whereQuery);
|
||||
getPaginated: protectedProcedure.input(validation.group.paginated).query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
||||
const groupCount = await ctx.db
|
||||
.select({
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(groups)
|
||||
.where(whereQuery);
|
||||
|
||||
const dbGroups = await ctx.db.query.groups.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
const dbGroups = await ctx.db.query.groups.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
limit: input.pageSize,
|
||||
offset: (input.page - 1) * input.pageSize,
|
||||
where: whereQuery,
|
||||
});
|
||||
},
|
||||
limit: input.pageSize,
|
||||
offset: (input.page - 1) * input.pageSize,
|
||||
where: whereQuery,
|
||||
});
|
||||
|
||||
return {
|
||||
items: dbGroups.map((group) => ({
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
})),
|
||||
totalCount: groupCount[0]!.count,
|
||||
};
|
||||
}),
|
||||
getById: protectedProcedure
|
||||
.input(validation.group.byId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const group = await ctx.db.query.groups.findFirst({
|
||||
where: eq(groups.id, input.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Group not found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
return {
|
||||
items: dbGroups.map((group) => ({
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
permissions: group.permissions.map(
|
||||
(permission) => permission.permission,
|
||||
),
|
||||
};
|
||||
}),
|
||||
})),
|
||||
totalCount: groupCount[0]!.count,
|
||||
};
|
||||
}),
|
||||
getById: protectedProcedure.input(validation.group.byId).query(async ({ input, ctx }) => {
|
||||
const group = await ctx.db.query.groups.findFirst({
|
||||
where: eq(groups.id, input.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Group not found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
permissions: group.permissions.map((permission) => permission.permission),
|
||||
};
|
||||
}),
|
||||
selectable: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await ctx.db.query.groups.findMany({
|
||||
columns: {
|
||||
@@ -102,120 +90,92 @@ export const groupRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
createGroup: protectedProcedure
|
||||
.input(validation.group.create)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName);
|
||||
createGroup: protectedProcedure.input(validation.group.create).mutation(async ({ input, ctx }) => {
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName);
|
||||
|
||||
const id = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id,
|
||||
const id = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id,
|
||||
name: normalizedName,
|
||||
ownerId: ctx.session.user.id,
|
||||
});
|
||||
|
||||
return id;
|
||||
}),
|
||||
updateGroup: protectedProcedure.input(validation.group.update).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
name: normalizedName,
|
||||
ownerId: ctx.session.user.id,
|
||||
});
|
||||
})
|
||||
.where(eq(groups.id, input.id));
|
||||
}),
|
||||
savePermissions: protectedProcedure.input(validation.group.savePermissions).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
return id;
|
||||
}),
|
||||
updateGroup: protectedProcedure
|
||||
.input(validation.group.update)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
await ctx.db.delete(groupPermissions).where(eq(groupPermissions.groupId, input.groupId));
|
||||
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
name: normalizedName,
|
||||
})
|
||||
.where(eq(groups.id, input.id));
|
||||
}),
|
||||
savePermissions: protectedProcedure
|
||||
.input(validation.group.savePermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db
|
||||
.delete(groupPermissions)
|
||||
.where(eq(groupPermissions.groupId, input.groupId));
|
||||
|
||||
await ctx.db.insert(groupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: input.groupId,
|
||||
permission,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
transferOwnership: protectedProcedure
|
||||
.input(validation.group.groupUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
ownerId: input.userId,
|
||||
})
|
||||
.where(eq(groups.id, input.groupId));
|
||||
}),
|
||||
deleteGroup: protectedProcedure
|
||||
.input(validation.group.byId)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
await ctx.db.delete(groups).where(eq(groups.id, input.id));
|
||||
}),
|
||||
addMember: protectedProcedure
|
||||
.input(validation.group.groupUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(groups.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
await ctx.db.insert(groupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: input.groupId,
|
||||
userId: input.userId,
|
||||
});
|
||||
}),
|
||||
removeMember: protectedProcedure
|
||||
.input(validation.group.groupUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
permission,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
transferOwnership: protectedProcedure.input(validation.group.groupUser).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db
|
||||
.delete(groupMembers)
|
||||
.where(
|
||||
and(
|
||||
eq(groupMembers.groupId, input.groupId),
|
||||
eq(groupMembers.userId, input.userId),
|
||||
),
|
||||
);
|
||||
}),
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
ownerId: input.userId,
|
||||
})
|
||||
.where(eq(groups.id, input.groupId));
|
||||
}),
|
||||
deleteGroup: protectedProcedure.input(validation.group.byId).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
await ctx.db.delete(groups).where(eq(groups.id, input.id));
|
||||
}),
|
||||
addMember: protectedProcedure.input(validation.group.groupUser).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(groups.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
groupId: input.groupId,
|
||||
userId: input.userId,
|
||||
});
|
||||
}),
|
||||
removeMember: protectedProcedure.input(validation.group.groupUser).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db
|
||||
.delete(groupMembers)
|
||||
.where(and(eq(groupMembers.groupId, input.groupId), eq(groupMembers.userId, input.userId)));
|
||||
}),
|
||||
});
|
||||
|
||||
const normalizeName = (name: string) => name.trim();
|
||||
|
||||
const checkSimilarNameAndThrowAsync = async (
|
||||
db: Database,
|
||||
name: string,
|
||||
ignoreId?: string,
|
||||
) => {
|
||||
const checkSimilarNameAndThrowAsync = async (db: Database, name: string, ignoreId?: string) => {
|
||||
const similar = await db.query.groups.findFirst({
|
||||
where: and(
|
||||
like(groups.name, `${name}`),
|
||||
not(eq(groups.id, ignoreId ?? "")),
|
||||
),
|
||||
where: and(like(groups.name, `${name}`), not(eq(groups.id, ignoreId ?? ""))),
|
||||
});
|
||||
|
||||
if (similar) {
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import { count } from "@homarr/db";
|
||||
import {
|
||||
apps,
|
||||
boards,
|
||||
groups,
|
||||
integrations,
|
||||
invites,
|
||||
users,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema/sqlite";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const homeRouter = createTRPCRouter({
|
||||
getStats: protectedProcedure.query(async ({ ctx }) => {
|
||||
return {
|
||||
countBoards:
|
||||
(await ctx.db.select({ count: count() }).from(boards))[0]?.count ?? 0,
|
||||
countUsers:
|
||||
(await ctx.db.select({ count: count() }).from(users))[0]?.count ?? 0,
|
||||
countGroups:
|
||||
(await ctx.db.select({ count: count() }).from(groups))[0]?.count ?? 0,
|
||||
countInvites:
|
||||
(await ctx.db.select({ count: count() }).from(invites))[0]?.count ?? 0,
|
||||
countIntegrations:
|
||||
(await ctx.db.select({ count: count() }).from(integrations))[0]
|
||||
?.count ?? 0,
|
||||
countApps:
|
||||
(await ctx.db.select({ count: count() }).from(apps))[0]?.count ?? 0,
|
||||
countBoards: (await ctx.db.select({ count: count() }).from(boards))[0]?.count ?? 0,
|
||||
countUsers: (await ctx.db.select({ count: count() }).from(users))[0]?.count ?? 0,
|
||||
countGroups: (await ctx.db.select({ count: count() }).from(groups))[0]?.count ?? 0,
|
||||
countInvites: (await ctx.db.select({ count: count() }).from(invites))[0]?.count ?? 0,
|
||||
countIntegrations: (await ctx.db.select({ count: count() }).from(integrations))[0]?.count ?? 0,
|
||||
countApps: (await ctx.db.select({ count: count() }).from(apps))[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -5,28 +5,22 @@ import { validation } from "@homarr/validation";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
export const iconsRouter = createTRPCRouter({
|
||||
findIcons: publicProcedure
|
||||
.input(validation.icons.findIcons)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return {
|
||||
icons: await ctx.db.query.iconRepositories.findMany({
|
||||
with: {
|
||||
icons: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
where:
|
||||
input.searchText?.length ?? 0 > 0
|
||||
? like(icons.name, `%${input.searchText}%`)
|
||||
: undefined,
|
||||
limit: 5,
|
||||
findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => {
|
||||
return {
|
||||
icons: await ctx.db.query.iconRepositories.findMany({
|
||||
with: {
|
||||
icons: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
where: input.searchText?.length ?? 0 > 0 ? like(icons.name, `%${input.searchText}%`) : undefined,
|
||||
limit: 5,
|
||||
},
|
||||
}),
|
||||
countIcons:
|
||||
(await ctx.db.select({ count: count() }).from(icons))[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}),
|
||||
countIcons: (await ctx.db.select({ count: count() }).from(icons))[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -5,11 +5,7 @@ import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import {
|
||||
getAllSecretKindOptions,
|
||||
integrationKinds,
|
||||
integrationSecretKindObject,
|
||||
} from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, integrationKinds, integrationSecretKindObject } from "@homarr/definitions";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
@@ -26,231 +22,198 @@ export const integrationRouter = createTRPCRouter({
|
||||
}))
|
||||
.sort(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) -
|
||||
integrationKinds.indexOf(integrationB.kind),
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
}),
|
||||
byId: publicProcedure
|
||||
.input(validation.integration.byId)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
secrets: {
|
||||
columns: {
|
||||
kind: true,
|
||||
value: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
byId: publicProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
secrets: {
|
||||
columns: {
|
||||
kind: true,
|
||||
value: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Integration not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Integration not found",
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
url: integration.url,
|
||||
secrets: integration.secrets.map((secret) => ({
|
||||
kind: secret.kind,
|
||||
// Only return the value if the secret is public, so for example the username
|
||||
value: integrationSecretKindObject[secret.kind].isPublic ? decryptSecret(secret.value) : null,
|
||||
updatedAt: secret.updatedAt,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
create: publicProcedure.input(validation.integration.create).mutation(async ({ ctx, input }) => {
|
||||
const integrationId = createId();
|
||||
await ctx.db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
kind: input.kind,
|
||||
});
|
||||
|
||||
return {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
url: integration.url,
|
||||
secrets: integration.secrets.map((secret) => ({
|
||||
kind: secret.kind,
|
||||
// Only return the value if the secret is public, so for example the username
|
||||
value: integrationSecretKindObject[secret.kind].isPublic
|
||||
? decryptSecret(secret.value)
|
||||
: null,
|
||||
updatedAt: secret.updatedAt,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(validation.integration.create)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integrationId = createId();
|
||||
await ctx.db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
for (const secret of input.secrets) {
|
||||
await ctx.db.insert(integrationSecrets).values({
|
||||
kind: secret.kind,
|
||||
value: encryptSecret(secret.value),
|
||||
updatedAt: new Date(),
|
||||
integrationId,
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: publicProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
secrets: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Integration not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(integrations)
|
||||
.set({
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
kind: input.kind,
|
||||
});
|
||||
})
|
||||
.where(eq(integrations.id, input.id));
|
||||
|
||||
for (const secret of input.secrets) {
|
||||
await ctx.db.insert(integrationSecrets).values({
|
||||
kind: secret.kind,
|
||||
value: encryptSecret(secret.value),
|
||||
updatedAt: new Date(),
|
||||
integrationId,
|
||||
});
|
||||
const decryptedSecrets = integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
}));
|
||||
|
||||
const changedSecrets = input.secrets.filter(
|
||||
(secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
secret.value !== null && // only update secrets that have a value
|
||||
!decryptedSecrets.find((dSecret) => dSecret.kind === secret.kind && dSecret.value === secret.value),
|
||||
);
|
||||
|
||||
if (changedSecrets.length > 0) {
|
||||
for (const changedSecret of changedSecrets) {
|
||||
const secretInput = {
|
||||
integrationId: input.id,
|
||||
value: changedSecret.value,
|
||||
kind: changedSecret.kind,
|
||||
};
|
||||
if (!decryptedSecrets.some((secret) => secret.kind === changedSecret.kind)) {
|
||||
await addSecretAsync(ctx.db, secretInput);
|
||||
} else {
|
||||
await updateSecretAsync(ctx.db, secretInput);
|
||||
}
|
||||
}
|
||||
}),
|
||||
update: publicProcedure
|
||||
.input(validation.integration.update)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
}
|
||||
}),
|
||||
delete: publicProcedure.input(validation.integration.delete).mutation(async ({ ctx, input }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Integration not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
|
||||
}),
|
||||
testConnection: publicProcedure.input(validation.integration.testConnection).mutation(async ({ ctx, input }) => {
|
||||
const secrets = input.secrets.filter((secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
Boolean(secret.value),
|
||||
);
|
||||
|
||||
// Find any matching secret kinds
|
||||
let secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) =>
|
||||
secretKinds.every((secretKind) => secrets.some((secret) => secret.kind === secretKind)),
|
||||
);
|
||||
|
||||
if (!secretKinds && input.id === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
|
||||
if (!secretKinds && input.id !== null) {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
secrets: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Integration not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(integrations)
|
||||
.set({
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
})
|
||||
.where(eq(integrations.id, input.id));
|
||||
|
||||
const decryptedSecrets = integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
}));
|
||||
|
||||
const changedSecrets = input.secrets.filter(
|
||||
(secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
secret.value !== null && // only update secrets that have a value
|
||||
!decryptedSecrets.find(
|
||||
(dSecret) =>
|
||||
dSecret.kind === secret.kind && dSecret.value === secret.value,
|
||||
),
|
||||
);
|
||||
|
||||
if (changedSecrets.length > 0) {
|
||||
for (const changedSecret of changedSecrets) {
|
||||
const secretInput = {
|
||||
integrationId: input.id,
|
||||
value: changedSecret.value,
|
||||
kind: changedSecret.kind,
|
||||
};
|
||||
if (
|
||||
!decryptedSecrets.some(
|
||||
(secret) => secret.kind === changedSecret.kind,
|
||||
)
|
||||
) {
|
||||
await addSecretAsync(ctx.db, secretInput);
|
||||
} else {
|
||||
await updateSecretAsync(ctx.db, secretInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(validation.integration.delete)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Integration not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
|
||||
}),
|
||||
testConnection: publicProcedure
|
||||
.input(validation.integration.testConnection)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const secrets = input.secrets.filter(
|
||||
(secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
Boolean(secret.value),
|
||||
);
|
||||
|
||||
// Find any matching secret kinds
|
||||
let secretKinds = getAllSecretKindOptions(input.kind).find(
|
||||
(secretKinds) =>
|
||||
secretKinds.every((secretKind) =>
|
||||
secrets.some((secret) => secret.kind === secretKind),
|
||||
),
|
||||
);
|
||||
|
||||
if (!secretKinds && input.id === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
const decryptedSecrets = integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
}));
|
||||
|
||||
if (!secretKinds && input.id !== null) {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
secrets: true,
|
||||
},
|
||||
});
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
const decryptedSecrets = integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
}));
|
||||
|
||||
// Add secrets that are not defined in the input from the database
|
||||
for (const dbSecret of decryptedSecrets) {
|
||||
if (!secrets.find((secret) => secret.kind === dbSecret.kind)) {
|
||||
secrets.push({
|
||||
kind: dbSecret.kind,
|
||||
value: dbSecret.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) =>
|
||||
secretKinds.every((secretKind) =>
|
||||
secrets.some((secret) => secret.kind === secretKind),
|
||||
),
|
||||
);
|
||||
|
||||
if (!secretKinds) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
// Add secrets that are not defined in the input from the database
|
||||
for (const dbSecret of decryptedSecrets) {
|
||||
if (!secrets.find((secret) => secret.kind === dbSecret.kind)) {
|
||||
secrets.push({
|
||||
kind: dbSecret.kind,
|
||||
value: dbSecret.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: actually test the connection
|
||||
// Probably by calling a function on the integration class
|
||||
// getIntegration(input.kind).testConnection(secrets)
|
||||
// getIntegration(kind: IntegrationKind): Integration
|
||||
// interface Integration {
|
||||
// testConnection(): Promise<void>;
|
||||
// }
|
||||
}),
|
||||
secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) =>
|
||||
secretKinds.every((secretKind) => secrets.some((secret) => secret.kind === secretKind)),
|
||||
);
|
||||
|
||||
if (!secretKinds) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: actually test the connection
|
||||
// Probably by calling a function on the integration class
|
||||
// getIntegration(input.kind).testConnection(secrets)
|
||||
// getIntegration(kind: IntegrationKind): Integration
|
||||
// interface Integration {
|
||||
// testConnection(): Promise<void>;
|
||||
// }
|
||||
}),
|
||||
});
|
||||
|
||||
const algorithm = "aes-256-cbc"; //Using AES encryption
|
||||
const key = Buffer.from(
|
||||
"1d71cceced68159ba59a277d056a66173613052cbeeccbfbd15ab1c909455a4d",
|
||||
"hex",
|
||||
); // TODO: generate with const data = crypto.randomBytes(32).toString('hex')
|
||||
const key = Buffer.from("1d71cceced68159ba59a277d056a66173613052cbeeccbfbd15ab1c909455a4d", "hex"); // TODO: generate with const data = crypto.randomBytes(32).toString('hex')
|
||||
|
||||
//Encrypting text
|
||||
export function encryptSecret(text: string): `${string}.${string}` {
|
||||
const initializationVector = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(
|
||||
algorithm,
|
||||
Buffer.from(key),
|
||||
initializationVector,
|
||||
);
|
||||
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), initializationVector);
|
||||
let encrypted = cipher.update(text);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return `${encrypted.toString("hex")}.${initializationVector.toString("hex")}`;
|
||||
@@ -261,11 +224,7 @@ function decryptSecret(value: `${string}.${string}`) {
|
||||
const [data, dataIv] = value.split(".") as [string, string];
|
||||
const initializationVector = Buffer.from(dataIv, "hex");
|
||||
const encryptedText = Buffer.from(data, "hex");
|
||||
const decipher = crypto.createDecipheriv(
|
||||
algorithm,
|
||||
Buffer.from(key),
|
||||
initializationVector,
|
||||
);
|
||||
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), initializationVector);
|
||||
let decrypted = decipher.update(encryptedText);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString();
|
||||
@@ -283,12 +242,7 @@ const updateSecretAsync = async (db: Database, input: UpdateSecretInput) => {
|
||||
value: encryptSecret(input.value),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(integrationSecrets.integrationId, input.integrationId),
|
||||
eq(integrationSecrets.kind, input.kind),
|
||||
),
|
||||
);
|
||||
.where(and(eq(integrationSecrets.integrationId, input.integrationId), eq(integrationSecrets.kind, input.kind)));
|
||||
};
|
||||
|
||||
interface AddSecretInput {
|
||||
|
||||
@@ -8,11 +8,7 @@ export const locationRouter = createTRPCRouter({
|
||||
.input(validation.location.searchCity.input)
|
||||
.output(validation.location.searchCity.output)
|
||||
.query(async ({ input }) => {
|
||||
const res = await fetch(
|
||||
`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`,
|
||||
);
|
||||
return (await res.json()) as z.infer<
|
||||
typeof validation.location.searchCity.output
|
||||
>;
|
||||
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
|
||||
return (await res.json()) as z.infer<typeof validation.location.searchCity.output>;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -14,9 +14,7 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
|
||||
const data = {} as ServerSettings;
|
||||
defaultServerSettingsKeys.forEach((key) => {
|
||||
const settingValue = settings.find(
|
||||
(setting) => setting.settingKey === key,
|
||||
)?.value;
|
||||
const settingValue = settings.find((setting) => setting.settingKey === key)?.value;
|
||||
if (!settingValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,10 +155,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.map(({ name }) => name)).toStrictEqual([
|
||||
"public",
|
||||
"private2",
|
||||
]);
|
||||
expect(result.map(({ name }) => name)).toStrictEqual(["public", "private2"]);
|
||||
});
|
||||
|
||||
test.each([["board-view"], ["board-change"]] satisfies [BoardPermission][])(
|
||||
@@ -221,10 +218,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.map(({ name }) => name)).toStrictEqual([
|
||||
"public",
|
||||
"private1",
|
||||
]);
|
||||
expect(result.map(({ name }) => name)).toStrictEqual(["public", "private1"]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -277,10 +271,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.map(({ name }) => name)).toStrictEqual([
|
||||
"public",
|
||||
"private1",
|
||||
]);
|
||||
expect(result.map(({ name }) => name)).toStrictEqual(["public", "private1"]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -356,11 +347,7 @@ describe("rename board should rename board", () => {
|
||||
});
|
||||
expect(dbBoard).toBeDefined();
|
||||
expect(dbBoard?.name).toBe("newName");
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"full-access",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
});
|
||||
|
||||
test("should throw error when similar board name exists", async () => {
|
||||
@@ -384,13 +371,10 @@ describe("rename board should rename board", () => {
|
||||
});
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.renameBoard({ id: boardId, name: "Newname" });
|
||||
const actAsync = async () => await caller.renameBoard({ id: boardId, name: "Newname" });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrowError(
|
||||
"Board with similar name already exists",
|
||||
);
|
||||
await expect(actAsync()).rejects.toThrowError("Board with similar name already exists");
|
||||
});
|
||||
|
||||
test("should throw error when board not found", async () => {
|
||||
@@ -399,8 +383,7 @@ describe("rename board should rename board", () => {
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.renameBoard({ id: "nonExistentBoardId", name: "newName" });
|
||||
const actAsync = async () => await caller.renameBoard({ id: "nonExistentBoardId", name: "newName" });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||
@@ -439,11 +422,7 @@ describe("changeBoardVisibility should change board visibility", () => {
|
||||
});
|
||||
expect(dbBoard).toBeDefined();
|
||||
expect(dbBoard?.isPublic).toBe(visibility === "public");
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"full-access",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -473,11 +452,7 @@ describe("deleteBoard should delete board", () => {
|
||||
where: eq(boards.id, boardId),
|
||||
});
|
||||
expect(dbBoard).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"full-access",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
});
|
||||
|
||||
test("should throw error when board not found", async () => {
|
||||
@@ -486,8 +461,7 @@ describe("deleteBoard should delete board", () => {
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.deleteBoard({ id: "nonExistentBoardId" });
|
||||
const actAsync = async () => await caller.deleteBoard({ id: "nonExistentBoardId" });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||
@@ -511,40 +485,29 @@ describe("getHomeBoard should return home board", () => {
|
||||
name: "home",
|
||||
...fullBoardProps,
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-view",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-view");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBoardByName should return board by name", () => {
|
||||
it.each([["default"], ["something"]])(
|
||||
"should return board by name %s when present",
|
||||
async (name) => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
it.each([["default"], ["something"]])("should return board by name %s when present", async (name) => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const fullBoardProps = await createFullBoardAsync(db, name);
|
||||
const fullBoardProps = await createFullBoardAsync(db, name);
|
||||
|
||||
// Act
|
||||
const result = await caller.getBoardByName({ name });
|
||||
// Act
|
||||
const result = await caller.getBoardByName({ name });
|
||||
|
||||
// Assert
|
||||
expectInputToBeFullBoardWithName(result, {
|
||||
name,
|
||||
...fullBoardProps,
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-view",
|
||||
);
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
expectInputToBeFullBoardWithName(result, {
|
||||
name,
|
||||
...fullBoardProps,
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-view");
|
||||
});
|
||||
|
||||
it("should throw error when not present", async () => {
|
||||
// Arrange
|
||||
@@ -553,8 +516,7 @@ describe("getBoardByName should return board by name", () => {
|
||||
await createFullBoardAsync(db, "default");
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.getBoardByName({ name: "nonExistentBoard" });
|
||||
const actAsync = async () => await caller.getBoardByName({ name: "nonExistentBoard" });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||
@@ -611,9 +573,7 @@ describe("savePartialBoardSettings should save general settings", () => {
|
||||
expect(dbBoard?.metaTitle).toBe(newMetaTitle);
|
||||
expect(dbBoard?.logoImageUrl).toBe(newLogoImageUrl);
|
||||
expect(dbBoard?.faviconImageUrl).toBe(newFaviconImageUrl);
|
||||
expect(dbBoard?.backgroundImageAttachment).toBe(
|
||||
newBackgroundImageAttachment,
|
||||
);
|
||||
expect(dbBoard?.backgroundImageAttachment).toBe(newBackgroundImageAttachment);
|
||||
expect(dbBoard?.backgroundImageRepeat).toBe(newBackgroundImageRepeat);
|
||||
expect(dbBoard?.backgroundImageSize).toBe(newBackgroundImageSize);
|
||||
expect(dbBoard?.backgroundImageUrl).toBe(newBackgroundImageUrl);
|
||||
@@ -623,11 +583,7 @@ describe("savePartialBoardSettings should save general settings", () => {
|
||||
expect(dbBoard?.primaryColor).toBe(newPrimaryColor);
|
||||
expect(dbBoard?.secondaryColor).toBe(newSecondaryColor);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
|
||||
it("should throw error when board not found", async () => {
|
||||
@@ -682,21 +638,14 @@ describe("saveBoard should save full board", () => {
|
||||
expect(definedBoard.sections.length).toBe(1);
|
||||
expect(definedBoard.sections[0]?.id).not.toBe(sectionId);
|
||||
expect(section).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
it("should remove item when not present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const { boardId, itemId, sectionId } = await createFullBoardAsync(
|
||||
db,
|
||||
"default",
|
||||
);
|
||||
const { boardId, itemId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
|
||||
await caller.saveBoard({
|
||||
id: boardId,
|
||||
@@ -742,11 +691,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(firstSection.items.length).toBe(1);
|
||||
expect(firstSection.items[0]?.id).not.toBe(itemId);
|
||||
expect(item).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
it("should remove integration reference when not present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
@@ -759,8 +704,7 @@ describe("saveBoard should save full board", () => {
|
||||
url: "http://localhost:3000",
|
||||
} as const;
|
||||
|
||||
const { boardId, itemId, integrationId, sectionId } =
|
||||
await createFullBoardAsync(db, "default");
|
||||
const { boardId, itemId, integrationId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
await db.insert(integrations).values(anotherIntegration);
|
||||
|
||||
await caller.saveBoard({
|
||||
@@ -813,71 +757,61 @@ describe("saveBoard should save full board", () => {
|
||||
expect(firstItem.integrations.length).toBe(1);
|
||||
expect(firstItem.integrations[0]?.integrationId).not.toBe(integrationId);
|
||||
expect(integration).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
it.each([
|
||||
[{ kind: "empty" as const }],
|
||||
[{ kind: "category" as const, name: "My first category" }],
|
||||
])("should add section when present in input", async (partialSection) => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
it.each([[{ kind: "empty" as const }], [{ kind: "category" as const, name: "My first category" }]])(
|
||||
"should add section when present in input",
|
||||
async (partialSection) => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
|
||||
const newSectionId = createId();
|
||||
await caller.saveBoard({
|
||||
id: boardId,
|
||||
sections: [
|
||||
{
|
||||
id: newSectionId,
|
||||
position: 1,
|
||||
items: [],
|
||||
...partialSection,
|
||||
const newSectionId = createId();
|
||||
await caller.saveBoard({
|
||||
id: boardId,
|
||||
sections: [
|
||||
{
|
||||
id: newSectionId,
|
||||
position: 1,
|
||||
items: [],
|
||||
...partialSection,
|
||||
},
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const board = await db.query.boards.findFirst({
|
||||
where: eq(boards.id, boardId),
|
||||
with: {
|
||||
sections: true,
|
||||
},
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const board = await db.query.boards.findFirst({
|
||||
where: eq(boards.id, boardId),
|
||||
with: {
|
||||
sections: true,
|
||||
},
|
||||
});
|
||||
const section = await db.query.sections.findFirst({
|
||||
where: eq(sections.id, newSectionId),
|
||||
});
|
||||
|
||||
const section = await db.query.sections.findFirst({
|
||||
where: eq(sections.id, newSectionId),
|
||||
});
|
||||
|
||||
const definedBoard = expectToBeDefined(board);
|
||||
expect(definedBoard.sections.length).toBe(2);
|
||||
const addedSection = expectToBeDefined(
|
||||
definedBoard.sections.find((section) => section.id === newSectionId),
|
||||
);
|
||||
expect(addedSection).toBeDefined();
|
||||
expect(addedSection.id).toBe(newSectionId);
|
||||
expect(addedSection.kind).toBe(partialSection.kind);
|
||||
expect(addedSection.position).toBe(1);
|
||||
if ("name" in partialSection) {
|
||||
expect(addedSection.name).toBe(partialSection.name);
|
||||
}
|
||||
expect(section).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
});
|
||||
const definedBoard = expectToBeDefined(board);
|
||||
expect(definedBoard.sections.length).toBe(2);
|
||||
const addedSection = expectToBeDefined(definedBoard.sections.find((section) => section.id === newSectionId));
|
||||
expect(addedSection).toBeDefined();
|
||||
expect(addedSection.id).toBe(newSectionId);
|
||||
expect(addedSection.kind).toBe(partialSection.kind);
|
||||
expect(addedSection.position).toBe(1);
|
||||
if ("name" in partialSection) {
|
||||
expect(addedSection.name).toBe(partialSection.name);
|
||||
}
|
||||
expect(section).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
},
|
||||
);
|
||||
it("should add item when present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
@@ -928,25 +862,17 @@ describe("saveBoard should save full board", () => {
|
||||
expect(definedBoard.sections.length).toBe(1);
|
||||
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||
expect(firstSection.items.length).toBe(1);
|
||||
const addedItem = expectToBeDefined(
|
||||
firstSection.items.find((item) => item.id === newItemId),
|
||||
);
|
||||
const addedItem = expectToBeDefined(firstSection.items.find((item) => item.id === newItemId));
|
||||
expect(addedItem).toBeDefined();
|
||||
expect(addedItem.id).toBe(newItemId);
|
||||
expect(addedItem.kind).toBe("clock");
|
||||
expect(addedItem.options).toBe(
|
||||
SuperJSON.stringify({ is24HourFormat: true }),
|
||||
);
|
||||
expect(addedItem.options).toBe(SuperJSON.stringify({ is24HourFormat: true }));
|
||||
expect(addedItem.height).toBe(1);
|
||||
expect(addedItem.width).toBe(1);
|
||||
expect(addedItem.xOffset).toBe(3);
|
||||
expect(addedItem.yOffset).toBe(2);
|
||||
expect(item).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
it("should add integration reference when present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
@@ -959,10 +885,7 @@ describe("saveBoard should save full board", () => {
|
||||
url: "http://plex.local",
|
||||
} as const;
|
||||
|
||||
const { boardId, itemId, sectionId } = await createFullBoardAsync(
|
||||
db,
|
||||
"default",
|
||||
);
|
||||
const { boardId, itemId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
await db.insert(integrations).values(integration);
|
||||
|
||||
await caller.saveBoard({
|
||||
@@ -1011,17 +934,11 @@ describe("saveBoard should save full board", () => {
|
||||
expect(definedBoard.sections.length).toBe(1);
|
||||
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||
expect(firstSection.items.length).toBe(1);
|
||||
const firstItem = expectToBeDefined(
|
||||
firstSection.items.find((item) => item.id === itemId),
|
||||
);
|
||||
const firstItem = expectToBeDefined(firstSection.items.find((item) => item.id === itemId));
|
||||
expect(firstItem.integrations.length).toBe(1);
|
||||
expect(firstItem.integrations[0]?.integrationId).toBe(integration.id);
|
||||
expect(integrationItem).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
it("should update section when present in input", async () => {
|
||||
const db = createDb();
|
||||
@@ -1066,16 +983,12 @@ describe("saveBoard should save full board", () => {
|
||||
|
||||
const definedBoard = expectToBeDefined(board);
|
||||
expect(definedBoard.sections.length).toBe(2);
|
||||
const firstSection = expectToBeDefined(
|
||||
definedBoard.sections.find((section) => section.id === sectionId),
|
||||
);
|
||||
const firstSection = expectToBeDefined(definedBoard.sections.find((section) => section.id === sectionId));
|
||||
expect(firstSection.id).toBe(sectionId);
|
||||
expect(firstSection.kind).toBe("empty");
|
||||
expect(firstSection.position).toBe(1);
|
||||
expect(firstSection.name).toBe(null);
|
||||
const secondSection = expectToBeDefined(
|
||||
definedBoard.sections.find((section) => section.id === newSectionId),
|
||||
);
|
||||
const secondSection = expectToBeDefined(definedBoard.sections.find((section) => section.id === newSectionId));
|
||||
expect(secondSection.id).toBe(newSectionId);
|
||||
expect(secondSection.kind).toBe("category");
|
||||
expect(secondSection.position).toBe(0);
|
||||
@@ -1086,10 +999,7 @@ describe("saveBoard should save full board", () => {
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const { boardId, itemId, sectionId } = await createFullBoardAsync(
|
||||
db,
|
||||
"default",
|
||||
);
|
||||
const { boardId, itemId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
|
||||
await caller.saveBoard({
|
||||
id: boardId,
|
||||
@@ -1129,24 +1039,15 @@ describe("saveBoard should save full board", () => {
|
||||
expect(definedBoard.sections.length).toBe(1);
|
||||
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||
expect(firstSection.items.length).toBe(1);
|
||||
const firstItem = expectToBeDefined(
|
||||
firstSection.items.find((item) => item.id === itemId),
|
||||
);
|
||||
const firstItem = expectToBeDefined(firstSection.items.find((item) => item.id === itemId));
|
||||
expect(firstItem.id).toBe(itemId);
|
||||
expect(firstItem.kind).toBe("clock");
|
||||
expect(
|
||||
SuperJSON.parse<{ is24HourFormat: boolean }>(firstItem.options)
|
||||
.is24HourFormat,
|
||||
).toBe(false);
|
||||
expect(SuperJSON.parse<{ is24HourFormat: boolean }>(firstItem.options).is24HourFormat).toBe(false);
|
||||
expect(firstItem.height).toBe(3);
|
||||
expect(firstItem.width).toBe(2);
|
||||
expect(firstItem.xOffset).toBe(7);
|
||||
expect(firstItem.yOffset).toBe(5);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"board-change",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
});
|
||||
it("should fail when board not found", async () => {
|
||||
const db = createDb();
|
||||
@@ -1216,9 +1117,7 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
const result = await caller.getBoardPermissions({ id: boardId });
|
||||
|
||||
// Assert
|
||||
expect(result.groupPermissions).toEqual([
|
||||
{ group: { id: groupId, name: "group1" }, permission: "board-view" },
|
||||
]);
|
||||
expect(result.groupPermissions).toEqual([{ group: { id: groupId, name: "group1" }, permission: "board-view" }]);
|
||||
expect(result.userPermissions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
@@ -1231,14 +1130,8 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result.inherited).toEqual([
|
||||
{ group: { id: groupId, name: "group1" }, permission: "admin" },
|
||||
]);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"full-access",
|
||||
);
|
||||
expect(result.inherited).toEqual([{ group: { id: groupId, name: "group1" }, permission: "admin" }]);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1279,11 +1172,7 @@ describe("saveUserBoardPermissions should save user board permissions", () => {
|
||||
where: eq(boardUserPermissions.userId, user1),
|
||||
});
|
||||
expect(dbUserPermission).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"full-access",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1330,11 +1219,7 @@ describe("saveGroupBoardPermissions should save group board permissions", () =>
|
||||
where: eq(boardGroupPermissions.groupId, groupId),
|
||||
});
|
||||
expect(dbGroupPermission).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"full-access",
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,10 +9,7 @@ import { throwIfActionForbiddenAsync } from "../../board/board-access";
|
||||
|
||||
const defaultCreatorId = createId();
|
||||
|
||||
const expectActToBeAsync = async (
|
||||
act: () => Promise<void>,
|
||||
success: boolean,
|
||||
) => {
|
||||
const expectActToBeAsync = async (act: () => Promise<void>, success: boolean) => {
|
||||
if (!success) {
|
||||
await expect(act()).rejects.toThrow("Board not found");
|
||||
return;
|
||||
@@ -29,161 +26,124 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
||||
["full-access" as const, true],
|
||||
["board-change" as const, true],
|
||||
["board-view" as const, true],
|
||||
])(
|
||||
"with permission %s should return %s when hasFullAccess is true",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: true,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
])("with permission %s should return %s when hasFullAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: true,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
// Act
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, boardId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, true],
|
||||
["board-view" as const, true],
|
||||
])(
|
||||
"with permission %s should return %s when hasChangeAccess is true",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: true,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
])("with permission %s should return %s when hasChangeAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: true,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
// Act
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, boardId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, false],
|
||||
["board-view" as const, true],
|
||||
])(
|
||||
"with permission %s should return %s when hasViewAccess is true",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: true,
|
||||
});
|
||||
])("with permission %s should return %s when hasViewAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: true,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
// Act
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, boardId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, false],
|
||||
["board-view" as const, false],
|
||||
])(
|
||||
"with permission %s should return %s when hasViewAccess is false",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
])("with permission %s should return %s when hasViewAccess is false", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
// Act
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, boardId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test("should throw when board is not found", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, createId()),
|
||||
"full-access",
|
||||
);
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, createId()), "full-access");
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow("Board not found");
|
||||
|
||||
@@ -2,12 +2,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import {
|
||||
groupMembers,
|
||||
groupPermissions,
|
||||
groups,
|
||||
users,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { groupRouter } from "../group";
|
||||
@@ -103,9 +98,7 @@ describe("paginated should return a list of groups with pagination", () => {
|
||||
expect(item?.members.length).toBe(1);
|
||||
const userKeys = Object.keys(item?.members[0] ?? {});
|
||||
expect(userKeys.length).toBe(4);
|
||||
expect(
|
||||
["id", "name", "email", "image"].some((key) => userKeys.includes(key)),
|
||||
);
|
||||
expect(["id", "name", "email", "image"].some((key) => userKeys.includes(key)));
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -178,9 +171,7 @@ describe("byId should return group by id including members and permissions", ()
|
||||
|
||||
const userKeys = Object.keys(result?.members[0] ?? {});
|
||||
expect(userKeys.length).toBe(4);
|
||||
expect(
|
||||
["id", "name", "email", "image"].some((key) => userKeys.includes(key)),
|
||||
);
|
||||
expect(["id", "name", "email", "image"].some((key) => userKeys.includes(key)));
|
||||
expect(result.permissions.length).toBe(1);
|
||||
expect(result.permissions[0]).toBe("admin");
|
||||
});
|
||||
@@ -249,98 +240,88 @@ describe("create should create group in database", () => {
|
||||
["test", "Test "],
|
||||
["test", "test"],
|
||||
["test", " TeSt"],
|
||||
])(
|
||||
"with similar name %s it should fail to create %s",
|
||||
async (similarName, nameToCreate) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = groupRouter.createCaller({ db, session: defaultSession });
|
||||
])("with similar name %s it should fail to create %s", async (similarName, nameToCreate) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = groupRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: similarName,
|
||||
});
|
||||
await db.insert(groups).values({
|
||||
id: createId(),
|
||||
name: similarName,
|
||||
});
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.createGroup({ name: nameToCreate });
|
||||
// Act
|
||||
const actAsync = async () => await caller.createGroup({ name: nameToCreate });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("similar name");
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("similar name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("update should update name with value that is no duplicate", () => {
|
||||
test.each([
|
||||
["first", "second ", "second"],
|
||||
["first", " first", "first"],
|
||||
])(
|
||||
"update should update name from %s to %s normalized",
|
||||
async (initialValue, updateValue, expectedValue) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = groupRouter.createCaller({ db, session: defaultSession });
|
||||
])("update should update name from %s to %s normalized", async (initialValue, updateValue, expectedValue) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = groupRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const groupId = createId();
|
||||
await db.insert(groups).values([
|
||||
{
|
||||
id: groupId,
|
||||
name: initialValue,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: "Third",
|
||||
},
|
||||
]);
|
||||
const groupId = createId();
|
||||
await db.insert(groups).values([
|
||||
{
|
||||
id: groupId,
|
||||
name: initialValue,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: "Third",
|
||||
},
|
||||
]);
|
||||
|
||||
// Act
|
||||
// Act
|
||||
await caller.updateGroup({
|
||||
id: groupId,
|
||||
name: updateValue,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const value = await db.query.groups.findFirst({
|
||||
where: eq(groups.id, groupId),
|
||||
});
|
||||
expect(value?.name).toBe(expectedValue);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["Second ", "second"],
|
||||
[" seCond", "second"],
|
||||
])("with similar name %s it should fail to update %s", async (updateValue, initialDuplicate) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = groupRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const groupId = createId();
|
||||
await db.insert(groups).values([
|
||||
{
|
||||
id: groupId,
|
||||
name: "Something",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: initialDuplicate,
|
||||
},
|
||||
]);
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.updateGroup({
|
||||
id: groupId,
|
||||
name: updateValue,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const value = await db.query.groups.findFirst({
|
||||
where: eq(groups.id, groupId),
|
||||
});
|
||||
expect(value?.name).toBe(expectedValue);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["Second ", "second"],
|
||||
[" seCond", "second"],
|
||||
])(
|
||||
"with similar name %s it should fail to update %s",
|
||||
async (updateValue, initialDuplicate) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = groupRouter.createCaller({ db, session: defaultSession });
|
||||
|
||||
const groupId = createId();
|
||||
await db.insert(groups).values([
|
||||
{
|
||||
id: groupId,
|
||||
name: "Something",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
name: initialDuplicate,
|
||||
},
|
||||
]);
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.updateGroup({
|
||||
id: groupId,
|
||||
name: updateValue,
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("similar name");
|
||||
},
|
||||
);
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("similar name");
|
||||
});
|
||||
|
||||
test("with non existing id it should throw not found error", async () => {
|
||||
// Arrange
|
||||
@@ -392,10 +373,7 @@ describe("savePermissions should save permissions for group", () => {
|
||||
});
|
||||
|
||||
expect(permissions.length).toBe(2);
|
||||
expect(permissions.map(({ permission }) => permission)).toEqual([
|
||||
"integration-use-all",
|
||||
"board-full-access",
|
||||
]);
|
||||
expect(permissions.map(({ permission }) => permission)).toEqual(["integration-use-all", "board-full-access"]);
|
||||
});
|
||||
|
||||
test("with non existing group it should throw not found error", async () => {
|
||||
|
||||
@@ -118,17 +118,11 @@ describe("byId should return an integration by id", () => {
|
||||
|
||||
const result = await caller.byId({ id: "1" });
|
||||
expect(result.secrets.length).toBe(3);
|
||||
const username = expectToBeDefined(
|
||||
result.secrets.find((secret) => secret.kind === "username"),
|
||||
);
|
||||
const username = expectToBeDefined(result.secrets.find((secret) => secret.kind === "username"));
|
||||
expect(username.value).not.toBeNull();
|
||||
const password = expectToBeDefined(
|
||||
result.secrets.find((secret) => secret.kind === "password"),
|
||||
);
|
||||
const password = expectToBeDefined(result.secrets.find((secret) => secret.kind === "password"));
|
||||
expect(password.value).toBeNull();
|
||||
const apiKey = expectToBeDefined(
|
||||
result.secrets.find((secret) => secret.kind === "apiKey"),
|
||||
);
|
||||
const apiKey = expectToBeDefined(result.secrets.find((secret) => secret.kind === "apiKey"));
|
||||
expect(apiKey.value).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -200,9 +194,7 @@ describe("update should update an integration", () => {
|
||||
integrationId,
|
||||
updatedAt: lastWeek,
|
||||
};
|
||||
await db
|
||||
.insert(integrationSecrets)
|
||||
.values([usernameToInsert, passwordToInsert]);
|
||||
await db.insert(integrationSecrets).values([usernameToInsert, passwordToInsert]);
|
||||
|
||||
const input = {
|
||||
id: integrationId,
|
||||
@@ -231,15 +223,9 @@ describe("update should update an integration", () => {
|
||||
expect(dbIntegration!.url).toBe(input.url);
|
||||
|
||||
expect(dbSecrets.length).toBe(3);
|
||||
const username = expectToBeDefined(
|
||||
dbSecrets.find((secret) => secret.kind === "username"),
|
||||
);
|
||||
const password = expectToBeDefined(
|
||||
dbSecrets.find((secret) => secret.kind === "password"),
|
||||
);
|
||||
const apiKey = expectToBeDefined(
|
||||
dbSecrets.find((secret) => secret.kind === "apiKey"),
|
||||
);
|
||||
const username = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "username"));
|
||||
const password = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "password"));
|
||||
const apiKey = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "apiKey"));
|
||||
expect(username.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(password.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(apiKey.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
@@ -327,26 +313,23 @@ describe("testConnection should test the connection to an integration", () => {
|
||||
{ kind: "password" as const, value: "Password123!" },
|
||||
],
|
||||
],
|
||||
])(
|
||||
"should fail when a required secret is missing when creating %s integration",
|
||||
async (kind, secrets) => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
])("should fail when a required secret is missing when creating %s integration", async (kind, secrets) => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: null,
|
||||
kind,
|
||||
url: `http://${kind}.local`,
|
||||
secrets,
|
||||
};
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: null,
|
||||
kind,
|
||||
url: `http://${kind}.local`,
|
||||
secrets,
|
||||
};
|
||||
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||
},
|
||||
);
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
|
||||
@@ -5,10 +5,7 @@ import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/db";
|
||||
import { serverSettings } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import {
|
||||
defaultServerSettings,
|
||||
defaultServerSettingsKeys,
|
||||
} from "@homarr/server-settings";
|
||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
|
||||
import { serverSettingsRouter } from "../serverSettings";
|
||||
|
||||
|
||||
@@ -218,10 +218,7 @@ describe("editProfile shoud update user", () => {
|
||||
});
|
||||
|
||||
// assert
|
||||
const user = await db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.id, id));
|
||||
const user = await db.select().from(schema.users).where(eq(schema.users.id, id));
|
||||
|
||||
expect(user).toHaveLength(1);
|
||||
expect(user[0]).toStrictEqual({
|
||||
@@ -261,10 +258,7 @@ describe("editProfile shoud update user", () => {
|
||||
});
|
||||
|
||||
// assert
|
||||
const user = await db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.id, id));
|
||||
const user = await db.select().from(schema.users).where(eq(schema.users.id, id));
|
||||
|
||||
expect(user).toHaveLength(1);
|
||||
expect(user[0]).toStrictEqual({
|
||||
|
||||
@@ -11,55 +11,46 @@ import { validation, z } from "@homarr/validation";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
initUser: publicProcedure
|
||||
.input(validation.user.init)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const firstUser = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
initUser: publicProcedure.input(validation.user.init).mutation(async ({ ctx, input }) => {
|
||||
const firstUser = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (firstUser) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User already exists",
|
||||
});
|
||||
}
|
||||
|
||||
if (firstUser) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User already exists",
|
||||
});
|
||||
}
|
||||
await createUserAsync(ctx.db, input);
|
||||
}),
|
||||
register: publicProcedure.input(validation.user.registrationApi).mutation(async ({ ctx, input }) => {
|
||||
const inviteWhere = and(eq(invites.id, input.inviteId), eq(invites.token, input.token));
|
||||
const dbInvite = await ctx.db.query.invites.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
expirationDate: true,
|
||||
},
|
||||
where: inviteWhere,
|
||||
});
|
||||
|
||||
await createUserAsync(ctx.db, input);
|
||||
}),
|
||||
register: publicProcedure
|
||||
.input(validation.user.registrationApi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const inviteWhere = and(
|
||||
eq(invites.id, input.inviteId),
|
||||
eq(invites.token, input.token),
|
||||
);
|
||||
const dbInvite = await ctx.db.query.invites.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
expirationDate: true,
|
||||
},
|
||||
where: inviteWhere,
|
||||
if (!dbInvite || dbInvite.expirationDate < new Date()) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Invalid invite",
|
||||
});
|
||||
}
|
||||
|
||||
if (!dbInvite || dbInvite.expirationDate < new Date()) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Invalid invite",
|
||||
});
|
||||
}
|
||||
|
||||
await createUserAsync(ctx.db, input);
|
||||
// Delete invite as it's used
|
||||
await ctx.db.delete(invites).where(inviteWhere);
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(validation.user.create)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await createUserAsync(ctx.db, input);
|
||||
}),
|
||||
await createUserAsync(ctx.db, input);
|
||||
// Delete invite as it's used
|
||||
await ctx.db.delete(invites).where(inviteWhere);
|
||||
}),
|
||||
create: publicProcedure.input(validation.user.create).mutation(async ({ ctx, input }) => {
|
||||
await createUserAsync(ctx.db, input);
|
||||
}),
|
||||
setProfileImage: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@@ -74,10 +65,7 @@ export const userRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Only admins can change other users profile images
|
||||
if (
|
||||
ctx.session.user.id !== input.userId &&
|
||||
!ctx.session.user.permissions.includes("admin")
|
||||
) {
|
||||
if (ctx.session.user.id !== input.userId && !ctx.session.user.permissions.includes("admin")) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to change other users profile images",
|
||||
@@ -126,112 +114,103 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
getById: publicProcedure
|
||||
.input(z.object({ userId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
getById: publicProcedure.input(z.object({ userId: z.string() })).query(async ({ input, ctx }) => {
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
},
|
||||
where: eq(users.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}),
|
||||
editProfile: publicProcedure.input(validation.user.editProfile).mutation(async ({ input, ctx }) => {
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
columns: { email: true },
|
||||
where: eq(users.id, input.id),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
const emailDirty = input.email && user.email !== input.email;
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
name: input.name,
|
||||
email: emailDirty === true ? input.email : undefined,
|
||||
emailVerified: emailDirty === true ? null : undefined,
|
||||
})
|
||||
.where(eq(users.id, input.id));
|
||||
}),
|
||||
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
|
||||
await ctx.db.delete(users).where(eq(users.id, input));
|
||||
}),
|
||||
changePassword: protectedProcedure.input(validation.user.changePasswordApi).mutation(async ({ ctx, input }) => {
|
||||
const user = ctx.session.user;
|
||||
// Only admins can change other users' passwords
|
||||
if (!user.permissions.includes("admin") && user.id !== input.userId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Admins can change the password of other users without providing the previous password
|
||||
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
|
||||
|
||||
if (isPreviousPasswordRequired) {
|
||||
const dbUser = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
password: true,
|
||||
salt: true,
|
||||
},
|
||||
where: eq(users.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!dbUser) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}),
|
||||
editProfile: publicProcedure
|
||||
.input(validation.user.editProfile)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
columns: { email: true },
|
||||
where: eq(users.id, input.id),
|
||||
});
|
||||
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
|
||||
const isValid = previousPasswordHash === dbUser.password;
|
||||
|
||||
if (!user) {
|
||||
if (!isValid) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
code: "FORBIDDEN",
|
||||
message: "Invalid password",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const emailDirty = input.email && user.email !== input.email;
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
name: input.name,
|
||||
email: emailDirty === true ? input.email : undefined,
|
||||
emailVerified: emailDirty === true ? null : undefined,
|
||||
})
|
||||
.where(eq(users.id, input.id));
|
||||
}),
|
||||
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
|
||||
await ctx.db.delete(users).where(eq(users.id, input));
|
||||
const salt = await createSaltAsync();
|
||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
password: hashedPassword,
|
||||
})
|
||||
.where(eq(users.id, input.userId));
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.input(validation.user.changePasswordApi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = ctx.session.user;
|
||||
// Only admins can change other users' passwords
|
||||
if (!user.permissions.includes("admin") && user.id !== input.userId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Admins can change the password of other users without providing the previous password
|
||||
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
|
||||
|
||||
if (isPreviousPasswordRequired) {
|
||||
const dbUser = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
password: true,
|
||||
salt: true,
|
||||
},
|
||||
where: eq(users.id, input.userId),
|
||||
});
|
||||
|
||||
if (!dbUser) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
const previousPasswordHash = await hashPasswordAsync(
|
||||
input.previousPassword,
|
||||
dbUser.salt ?? "",
|
||||
);
|
||||
const isValid = previousPasswordHash === dbUser.password;
|
||||
|
||||
if (!isValid) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Invalid password",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const salt = await createSaltAsync();
|
||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
password: hashedPassword,
|
||||
})
|
||||
.where(eq(users.id, input.userId));
|
||||
}),
|
||||
setMessage: publicProcedure.input(z.string()).mutation(async ({ input }) => {
|
||||
await exampleChannel.publishAsync({ message: input });
|
||||
}),
|
||||
@@ -244,10 +223,7 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
});
|
||||
|
||||
const createUserAsync = async (
|
||||
db: Database,
|
||||
input: z.infer<typeof validation.user.create>,
|
||||
) => {
|
||||
const createUserAsync = async (db: Database, input: z.infer<typeof validation.user.create>) => {
|
||||
const salt = await createSaltAsync();
|
||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||
|
||||
|
||||
@@ -27,17 +27,11 @@ import { ZodError } from "@homarr/validation";
|
||||
*
|
||||
* @see https://trpc.io/docs/server/context
|
||||
*/
|
||||
export const createTRPCContext = (opts: {
|
||||
headers: Headers;
|
||||
session: Session | null;
|
||||
}) => {
|
||||
export const createTRPCContext = (opts: { headers: Headers; session: Session | null }) => {
|
||||
const session = opts.session;
|
||||
const source = opts.headers.get("x-trpc-source") ?? "unknown";
|
||||
|
||||
logger.info(
|
||||
`tRPC request from ${source} by user '${session?.user.id}'`,
|
||||
session?.user,
|
||||
);
|
||||
logger.info(`tRPC request from ${source} by user '${session?.user.id}'`, session?.user);
|
||||
|
||||
return {
|
||||
session,
|
||||
|
||||
Reference in New Issue
Block a user