Compare commits

...

5 Commits

Author SHA1 Message Date
Manuel
3737543766 config: update version (#2219) 2024-12-08 20:53:35 +01:00
Manuel
85547277d1 fix: iframes javascript content (#2218) 2024-12-08 20:06:15 +01:00
Meier Lukas
31a7559b86 fix: two issues with board rename and iframes (#2215) 2024-12-07 22:21:40 +01:00
TyxTang
b59921b843 fix: Fix Jellyseerr Avatar Loading Issue (#2197)
fix: Fix Jellyseerr Avatar Loading Issue
feat: Add Fallback Image.
2024-11-27 22:17:48 +01:00
Marius Starke
95c126f2c6 fix: remove several occurrences of translation file prefix (#2188)
Co-authored-by: BuildTools <unconfigured@null.spigotmc.org>
2024-11-05 09:54:27 +01:00
11 changed files with 91 additions and 49 deletions

View File

@@ -11,7 +11,7 @@
"layout.manage.navigation.**", "layout.manage.navigation.**",
], ],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": "explicit"
}, },
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "0.15.7", "version": "0.15.9",
"description": "Homarr - A homepage for your server.", "description": "Homarr - A homepage for your server.",
"license": "MIT", "license": "MIT",
"repository": { "repository": {

View File

@@ -7,8 +7,8 @@
} }
}, },
"modal": { "modal": {
"text": "", "text": "The widet crashed unexpectitly. Please read the documentation and fix any typos.",
"label": "Your error", "label": "Occurred error",
"reportButton": "Report this error" "reportButton": "Report this error on GitHub"
} }
} }

View File

@@ -1,29 +1,33 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import Consola from 'consola';
import fs from 'fs'; import fs from 'fs';
import { z } from 'zod'; import { z } from 'zod';
import Consola from 'consola';
import { getDefaultBoardAsync } from '~/server/db/queries/userSettings'; import { getDefaultBoardAsync } from '~/server/db/queries/userSettings';
import { configExists } from '~/tools/config/configExists'; import { configExists } from '~/tools/config/configExists';
import { getConfig } from '~/tools/config/getConfig'; import { getConfig } from '~/tools/config/getConfig';
import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
import { writeConfig } from '~/tools/config/writeConfig';
import { generateDefaultApp } from '~/tools/shared/app'; import { generateDefaultApp } from '~/tools/shared/app';
import { configNameSchema } from '~/validations/boards';
import { adminProcedure, createTRPCRouter, protectedProcedure } from '../trpc'; import { adminProcedure, createTRPCRouter, protectedProcedure } from '../trpc';
import { writeConfig } from '~/tools/config/writeConfig';
import { configNameSchema } from '~/validations/boards';
export const boardRouter = createTRPCRouter({ export const boardRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
.meta({ openapi: { method: 'GET', path: '/boards/all', tags: ['board'] } }) .meta({ openapi: { method: 'GET', path: '/boards/all', tags: ['board'] } })
.input(z.void()) .input(z.void())
.output(z.array(z.object({ .output(
name: z.string(), z.array(
allowGuests: z.boolean(), z.object({
countApps: z.number().min(0), name: z.string(),
countWidgets: z.number().min(0), allowGuests: z.boolean(),
countCategories: z.number().min(0), countApps: z.number().min(0),
isDefaultForUser: z.boolean(), countWidgets: z.number().min(0),
}))) countCategories: z.number().min(0),
isDefaultForUser: z.boolean(),
})
)
)
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
@@ -44,7 +48,7 @@ export const boardRouter = createTRPCRouter({
countCategories: config.categories.length, countCategories: config.categories.length,
isDefaultForUser: name === defaultBoard, isDefaultForUser: name === defaultBoard,
}; };
}), })
); );
}), }),
addAppsForContainers: adminProcedure addAppsForContainers: adminProcedure
@@ -58,9 +62,9 @@ export const boardRouter = createTRPCRouter({
name: z.string(), name: z.string(),
icon: z.string().optional(), icon: z.string().optional(),
port: z.number().optional(), port: z.number().optional(),
}), })
), ),
}), })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
if (!configExists(input.boardName)) { if (!configExists(input.boardName)) {
@@ -101,12 +105,14 @@ export const boardRouter = createTRPCRouter({
const targetPath = `data/configs/${input.boardName}.json`; const targetPath = `data/configs/${input.boardName}.json`;
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8'); fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
}), }),
renameBoard: protectedProcedure renameBoard: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/boards/rename', tags: ['board'] } }) .meta({ openapi: { method: 'PUT', path: '/boards/rename', tags: ['board'] } })
.input(z.object({ .input(
oldName: z.string(), z.object({
newName: z.string().min(1), oldName: configNameSchema,
})) newName: configNameSchema,
})
)
.output(z.void()) .output(z.void())
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
if (input.oldName === 'default') { if (input.oldName === 'default') {
@@ -141,15 +147,19 @@ export const boardRouter = createTRPCRouter({
fs.unlinkSync(targetPath); fs.unlinkSync(targetPath);
Consola.info(`Deleted ${input.oldName} from file system`); Consola.info(`Deleted ${input.oldName} from file system`);
}), }),
duplicateBoard: protectedProcedure duplicateBoard: adminProcedure
.meta({ openapi: { method: 'POST', path: '/boards/duplicate', tags: ['board'] } }) .meta({ openapi: { method: 'POST', path: '/boards/duplicate', tags: ['board'] } })
.input(z.object({ .input(
boardName: z.string(), z.object({
})) boardName: z.string(),
})
)
.output(z.void()) .output(z.void())
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
if (!configExists(input.boardName)) { 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({ throw new TRPCError({
code: 'NOT_FOUND', code: 'NOT_FOUND',
message: 'Board not found', message: 'Board not found',
@@ -164,7 +174,7 @@ export const boardRouter = createTRPCRouter({
config.configProperties.name = targetName; config.configProperties.name = targetName;
writeConfig(config); 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', code: 'CONFLICT',
message: 'Board conflicts with an existing board', message: 'Board conflicts with an existing board',
}); });
} };
const generateDuplicateName = (baseName: string, increment: number) => { const generateDuplicateName = (baseName: string, increment: number) => {
const result = duplicationName.exec(baseName); const result = duplicationName.exec(baseName);
@@ -197,4 +207,4 @@ const generateDuplicateName = (baseName: string, increment: number) => {
} }
return `${baseName} (2)`; return `${baseName} (2)`;
} };

View File

@@ -58,6 +58,7 @@ export const mediaRequestsRouter = createTRPCRouter({
name: genericItem.name, name: genericItem.name,
userName: item.requestedBy.displayName, userName: item.requestedBy.displayName,
userProfilePicture: constructAvatarUrl(appUrl, item.requestedBy.avatar), userProfilePicture: constructAvatarUrl(appUrl, item.requestedBy.avatar),
fallbackUserProfilePicture: constructAvatarUrl(appUrl, item.requestedBy.avatar,'avatarproxy'),
userLink: `${appUrl}/users/${item.requestedBy.id}`, userLink: `${appUrl}/users/${item.requestedBy.id}`,
userRequestCount: item.requestedBy.requestCount, userRequestCount: item.requestedBy.requestCount,
airDate: genericItem.airDate, airDate: genericItem.airDate,
@@ -119,6 +120,7 @@ export const mediaRequestsRouter = createTRPCRouter({
id: user.id, id: user.id,
userName: user.displayName, userName: user.displayName,
userProfilePicture: constructAvatarUrl(appUrl, user.avatar), userProfilePicture: constructAvatarUrl(appUrl, user.avatar),
fallbackUserProfilePicture: constructAvatarUrl(appUrl, user.avatar,'avatarproxy'),
userLink: `${appUrl}/users/${user.id}`, userLink: `${appUrl}/users/${user.id}`,
userRequestCount: user.requestCount, 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://'); const isAbsolute = avatar.startsWith('http://') || avatar.startsWith('https://');
if (isAbsolute) { if (isAbsolute) {
return avatar; return avatar;
} }
return `${appUrl}/${avatar}`; return `${appUrl}/${path?.concat("/") ?? "" }${avatar}`;
}; };
const retrieveDetailsForItem = async ( const retrieveDetailsForItem = async (

View File

@@ -1,8 +1,15 @@
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core'; import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
import { z } from 'zod'; 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({ export const createBoardSchemaValidation = z.object({
name: configNameSchema, name: configNameSchema,

View File

@@ -41,12 +41,12 @@ export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: Ti
setHours(0); setHours(0);
setMinutes(0); setMinutes(0);
}} }}
title={t('modules/dns-hole-controls:durationModal.title')} title={t('durationModal.title')}
> >
<Flex direction="column" align="center" justify="center"> <Flex direction="column" align="center" justify="center">
<Stack align="flex-end"> <Stack align="flex-end">
<Group spacing={5}> <Group spacing={5}>
<Text>{t('modules/dns-hole-controls:durationModal.hours')}</Text> <Text>{t('durationModal.hours')}</Text>
<ActionIcon <ActionIcon
size={35} size={35}
variant="default" variant="default"
@@ -73,7 +73,7 @@ export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: Ti
</ActionIcon> </ActionIcon>
</Group> </Group>
<Group spacing={5}> <Group spacing={5}>
<Text>{t('modules/dns-hole-controls:durationModal.minutes')}</Text> <Text>{t('durationModal.minutes')}</Text>
<ActionIcon <ActionIcon
size={35} size={35}
variant="default" variant="default"
@@ -101,7 +101,7 @@ export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: Ti
</Group> </Group>
</Stack> </Stack>
<Text ta="center" c="dimmed" my={5}> <Text ta="center" c="dimmed" my={5}>
{t('modules/dns-hole-controls:durationModal.unlimited')} {t('durationModal.unlimited')}
</Text> </Text>
<Button <Button
variant="light" variant="light"
@@ -116,7 +116,7 @@ export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: Ti
close(); close();
}} }}
> >
{t('modules/dns-hole-controls:durationModal.set')} {t('durationModal.set')}
</Button> </Button>
</Flex> </Flex>
</Modal> </Modal>

View File

@@ -5,6 +5,20 @@ import { useTranslation } from 'next-i18next';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
function sanitizeUrl(url: string) {
let parsedUrl: URL;
try {
parsedUrl = new URL(url);
} catch (e) {
return 'about:blank';
}
if (['http:', 'https:'].includes(parsedUrl.protocol)) {
return parsedUrl.href;
} else {
throw new Error(`Protocol '${parsedUrl.protocol}' is not supported. Use HTTP or HTTPS.`);
}
}
const definition = defineWidget({ const definition = defineWidget({
id: 'iframe', id: 'iframe',
icon: IconBrowser, icon: IconBrowser,
@@ -117,7 +131,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
<Container h="100%" w="100%" maw="initial" mah="initial" p={0}> <Container h="100%" w="100%" maw="initial" mah="initial" p={0}>
<iframe <iframe
className={classes.iframe} className={classes.iframe}
src={widget.properties.embedUrl} src={sanitizeUrl(widget.properties.embedUrl)}
title="widget iframe" title="widget iframe"
allow={allowedPermissions.join(' ')} allow={allowedPermissions.join(' ')}
> >

View File

@@ -11,6 +11,7 @@ import {
Stack, Stack,
Text, Text,
Tooltip, Tooltip,
Avatar,
useMantineTheme, useMantineTheme,
} from '@mantine/core'; } from '@mantine/core';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
@@ -170,14 +171,17 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
</Flex> </Flex>
<Stack justify="center"> <Stack justify="center">
<Flex gap="xs"> <Flex gap="xs">
<Image <Avatar
src={item.userProfilePicture} src={item.userProfilePicture}
width={25} size={25}
height={25}
alt="requester avatar" alt="requester avatar"
radius="xl" radius="xl"
withPlaceholder >
/> <Image
src={item.fallbackUserProfilePicture}
alt="requester avatar"
/>
</Avatar>
<Anchor <Anchor
href={item.userLink} href={item.userLink}
target={widget.properties.openInNewTab ? '_blank' : '_self'} target={widget.properties.openInNewTab ? '_blank' : '_self'}

View File

@@ -7,6 +7,7 @@ import {
Stack, Stack,
Text, Text,
Tooltip, Tooltip,
Image,
useMantineTheme, useMantineTheme,
} from '@mantine/core'; } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useElementSize } from '@mantine/hooks';
@@ -163,7 +164,9 @@ function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
/> />
</Tooltip.Floating> </Tooltip.Floating>
)} )}
<Avatar radius="xl" size={45} src={user.userProfilePicture} alt="user avatar" /> <Avatar radius="xl" size={45} src={user.userProfilePicture} alt="user avatar" >
<Image src={user.fallbackUserProfilePicture} alt="user avatar" />
</Avatar>
<Stack spacing={0} style={{ flex: 1 }}> <Stack spacing={0} style={{ flex: 1 }}>
<Text>{user.userName}</Text> <Text>{user.userName}</Text>
<Text size="xs"> <Text size="xs">

View File

@@ -7,6 +7,7 @@ export type MediaRequest = {
name: string; name: string;
userName: string; userName: string;
userProfilePicture: string; userProfilePicture: string;
fallbackUserProfilePicture: string;
userLink: string; userLink: string;
userRequestCount: number; userRequestCount: number;
airDate?: string; airDate?: string;
@@ -22,6 +23,7 @@ export type Users = {
id: number; id: number;
userName: string; userName: string;
userProfilePicture: string; userProfilePicture: string;
fallbackUserProfilePicture: string;
userLink: string; userLink: string;
userRequestCount: number; userRequestCount: number;
}; };