diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9a066dae06d..025acb66238 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -11,7 +11,7 @@
"layout.manage.navigation.**",
],
"editor.codeActionsOnSave": {
- "source.organizeImports": true
+ "source.organizeImports": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.patterns": {
diff --git a/package.json b/package.json
index bab2389e3fb..cf60568f2ef 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homarr",
- "version": "0.15.7",
+ "version": "0.15.8",
"description": "Homarr - A homepage for your server.",
"license": "MIT",
"repository": {
diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts
index 7426e763dd6..7971531c230 100644
--- a/src/server/api/routers/board.ts
+++ b/src/server/api/routers/board.ts
@@ -1,29 +1,33 @@
import { TRPCError } from '@trpc/server';
+import Consola from 'consola';
import fs from 'fs';
import { z } from 'zod';
-import Consola from 'consola';
import { getDefaultBoardAsync } from '~/server/db/queries/userSettings';
import { configExists } from '~/tools/config/configExists';
import { getConfig } from '~/tools/config/getConfig';
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
+import { writeConfig } from '~/tools/config/writeConfig';
import { generateDefaultApp } from '~/tools/shared/app';
+import { configNameSchema } from '~/validations/boards';
import { adminProcedure, createTRPCRouter, protectedProcedure } from '../trpc';
-import { writeConfig } from '~/tools/config/writeConfig';
-import { configNameSchema } from '~/validations/boards';
export const boardRouter = createTRPCRouter({
all: protectedProcedure
.meta({ openapi: { method: 'GET', path: '/boards/all', tags: ['board'] } })
.input(z.void())
- .output(z.array(z.object({
- name: z.string(),
- allowGuests: z.boolean(),
- countApps: z.number().min(0),
- countWidgets: z.number().min(0),
- countCategories: z.number().min(0),
- isDefaultForUser: z.boolean(),
- })))
+ .output(
+ z.array(
+ z.object({
+ name: z.string(),
+ allowGuests: z.boolean(),
+ countApps: z.number().min(0),
+ countWidgets: z.number().min(0),
+ countCategories: z.number().min(0),
+ isDefaultForUser: z.boolean(),
+ })
+ )
+ )
.query(async ({ ctx }) => {
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
@@ -44,7 +48,7 @@ export const boardRouter = createTRPCRouter({
countCategories: config.categories.length,
isDefaultForUser: name === defaultBoard,
};
- }),
+ })
);
}),
addAppsForContainers: adminProcedure
@@ -58,9 +62,9 @@ export const boardRouter = createTRPCRouter({
name: z.string(),
icon: z.string().optional(),
port: z.number().optional(),
- }),
+ })
),
- }),
+ })
)
.mutation(async ({ input }) => {
if (!configExists(input.boardName)) {
@@ -101,12 +105,14 @@ export const boardRouter = createTRPCRouter({
const targetPath = `data/configs/${input.boardName}.json`;
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
}),
- renameBoard: protectedProcedure
+ renameBoard: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/boards/rename', tags: ['board'] } })
- .input(z.object({
- oldName: z.string(),
- newName: z.string().min(1),
- }))
+ .input(
+ z.object({
+ oldName: configNameSchema,
+ newName: configNameSchema,
+ })
+ )
.output(z.void())
.mutation(async ({ input }) => {
if (input.oldName === 'default') {
@@ -141,15 +147,19 @@ export const boardRouter = createTRPCRouter({
fs.unlinkSync(targetPath);
Consola.info(`Deleted ${input.oldName} from file system`);
}),
- duplicateBoard: protectedProcedure
+ duplicateBoard: adminProcedure
.meta({ openapi: { method: 'POST', path: '/boards/duplicate', tags: ['board'] } })
- .input(z.object({
- boardName: z.string(),
- }))
+ .input(
+ z.object({
+ boardName: z.string(),
+ })
+ )
.output(z.void())
.mutation(async ({ input }) => {
if (!configExists(input.boardName)) {
- Consola.error(`Tried to duplicate ${input.boardName} but this configuration does not exist.`);
+ Consola.error(
+ `Tried to duplicate ${input.boardName} but this configuration does not exist.`
+ );
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Board not found',
@@ -164,7 +174,7 @@ export const boardRouter = createTRPCRouter({
config.configProperties.name = targetName;
writeConfig(config);
- Consola.info(`Wrote config to name '${targetName}'`)
+ Consola.info(`Wrote config to name '${targetName}'`);
}),
});
@@ -185,7 +195,7 @@ const attemptGenerateDuplicateName = (baseName: string, maxAttempts: number) =>
code: 'CONFLICT',
message: 'Board conflicts with an existing board',
});
-}
+};
const generateDuplicateName = (baseName: string, increment: number) => {
const result = duplicationName.exec(baseName);
@@ -197,4 +207,4 @@ const generateDuplicateName = (baseName: string, increment: number) => {
}
return `${baseName} (2)`;
-}
+};
diff --git a/src/server/api/routers/media-request.ts b/src/server/api/routers/media-request.ts
index 43c6cf22043..fe246c472f5 100644
--- a/src/server/api/routers/media-request.ts
+++ b/src/server/api/routers/media-request.ts
@@ -58,6 +58,7 @@ export const mediaRequestsRouter = createTRPCRouter({
name: genericItem.name,
userName: item.requestedBy.displayName,
userProfilePicture: constructAvatarUrl(appUrl, item.requestedBy.avatar),
+ fallbackUserProfilePicture: constructAvatarUrl(appUrl, item.requestedBy.avatar,'avatarproxy'),
userLink: `${appUrl}/users/${item.requestedBy.id}`,
userRequestCount: item.requestedBy.requestCount,
airDate: genericItem.airDate,
@@ -119,6 +120,7 @@ export const mediaRequestsRouter = createTRPCRouter({
id: user.id,
userName: user.displayName,
userProfilePicture: constructAvatarUrl(appUrl, user.avatar),
+ fallbackUserProfilePicture: constructAvatarUrl(appUrl, user.avatar,'avatarproxy'),
userLink: `${appUrl}/users/${user.id}`,
userRequestCount: user.requestCount,
};
@@ -137,14 +139,14 @@ export const mediaRequestsRouter = createTRPCRouter({
}),
});
-const constructAvatarUrl = (appUrl: string, avatar: string) => {
+const constructAvatarUrl = (appUrl: string, avatar: string, path?: string) => {
const isAbsolute = avatar.startsWith('http://') || avatar.startsWith('https://');
if (isAbsolute) {
return avatar;
}
- return `${appUrl}/${avatar}`;
+ return `${appUrl}/${path?.concat("/") ?? "" }${avatar}`;
};
const retrieveDetailsForItem = async (
diff --git a/src/validations/boards.ts b/src/validations/boards.ts
index be3347cc1a1..b66f2eb7ddc 100644
--- a/src/validations/boards.ts
+++ b/src/validations/boards.ts
@@ -1,8 +1,15 @@
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
import { z } from 'zod';
-import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
+import {
+ BackgroundImageAttachment,
+ BackgroundImageRepeat,
+ BackgroundImageSize,
+} from '~/types/settings';
-export const configNameSchema = z.string().regex(/^[a-zA-Z0-9-_\s()]+$/);
+export const configNameSchema = z
+ .string()
+ .regex(/^[a-zA-Z0-9-_\s()]+$/)
+ .min(1);
export const createBoardSchemaValidation = z.object({
name: configNameSchema,
diff --git a/src/widgets/dnshole/TimerModal.tsx b/src/widgets/dnshole/TimerModal.tsx
index 2ae41350227..f4f178c6e12 100644
--- a/src/widgets/dnshole/TimerModal.tsx
+++ b/src/widgets/dnshole/TimerModal.tsx
@@ -41,12 +41,12 @@ export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: Ti
setHours(0);
setMinutes(0);
}}
- title={t('modules/dns-hole-controls:durationModal.title')}
+ title={t('durationModal.title')}
>
- {t('modules/dns-hole-controls:durationModal.hours')}
+ {t('durationModal.hours')}
- {t('modules/dns-hole-controls:durationModal.minutes')}
+ {t('durationModal.minutes')}
- {t('modules/dns-hole-controls:durationModal.unlimited')}
+ {t('durationModal.unlimited')}
diff --git a/src/widgets/iframe/IFrameTile.tsx b/src/widgets/iframe/IFrameTile.tsx
index 86b32c89c1b..ab3f1790d71 100644
--- a/src/widgets/iframe/IFrameTile.tsx
+++ b/src/widgets/iframe/IFrameTile.tsx
@@ -116,6 +116,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
return (