Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3737543766 | ||
|
|
85547277d1 | ||
|
|
31a7559b86 | ||
|
|
b59921b843 | ||
|
|
95c126f2c6 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -244,4 +244,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)`;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(' ')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user