mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
✨ Add board customization page
This commit is contained in:
16
src/server/api/helper.ts
Normal file
16
src/server/api/helper.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createServerSideHelpers } from '@trpc/react-query/server';
|
||||
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { rootRouter } from './root';
|
||||
import { createTRPCContext } from './trpc';
|
||||
|
||||
export const createTrpcServersideHelpers = async (
|
||||
props: Pick<GetServerSidePropsContext, 'req' | 'res'>
|
||||
) =>
|
||||
createServerSideHelpers({
|
||||
router: rootRouter,
|
||||
ctx: await createTRPCContext(props as CreateNextContextOptions),
|
||||
transformer: superjson,
|
||||
});
|
||||
@@ -1,13 +1,19 @@
|
||||
import { MantineTheme } from '@mantine/core';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import Consola from 'consola';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
import { configExists } from '~/tools/config/configExists';
|
||||
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
|
||||
import { BackendConfigType, ConfigType } from '~/types/config';
|
||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
||||
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
||||
|
||||
import { getConfig } from '../../../tools/config/getConfig';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const configNameSchema = z.string().regex(/^[a-zA-Z0-9-_]+$/);
|
||||
|
||||
export const configRouter = createTRPCRouter({
|
||||
all: publicProcedure.query(async () => {
|
||||
@@ -17,10 +23,10 @@ export const configRouter = createTRPCRouter({
|
||||
// Strip the .json extension from the file name
|
||||
return files.map((file) => file.replace('.json', ''));
|
||||
}),
|
||||
delete: publicProcedure
|
||||
delete: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
name: configNameSchema,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
@@ -60,10 +66,10 @@ export const configRouter = createTRPCRouter({
|
||||
message: 'Configuration deleted with success',
|
||||
};
|
||||
}),
|
||||
save: publicProcedure
|
||||
save: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
name: configNameSchema,
|
||||
config: z.custom<ConfigType>((x) => !!x && typeof x === 'object'),
|
||||
})
|
||||
)
|
||||
@@ -124,6 +130,8 @@ export const configRouter = createTRPCRouter({
|
||||
},
|
||||
})),
|
||||
],
|
||||
// Settings can only be changed in the configuration file
|
||||
settings: previousConfig.settings,
|
||||
};
|
||||
|
||||
newConfig = {
|
||||
@@ -160,4 +168,59 @@ export const configRouter = createTRPCRouter({
|
||||
message: 'Configuration saved with success',
|
||||
};
|
||||
}),
|
||||
byName: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: configNameSchema,
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
if (!configExists(input.name)) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Configuration not found',
|
||||
});
|
||||
}
|
||||
|
||||
return await getFrontendConfig(input.name);
|
||||
}),
|
||||
saveCusomization: adminProcedure
|
||||
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
|
||||
.mutation(async ({ input }) => {
|
||||
const previousConfig = getConfig(input.name);
|
||||
const newConfig = {
|
||||
...previousConfig,
|
||||
settings: {
|
||||
...previousConfig.settings,
|
||||
customization: {
|
||||
...previousConfig.settings.customization,
|
||||
appOpacity: input.appearance.opacity,
|
||||
backgroundImageUrl: input.appearance.backgroundSrc,
|
||||
colors: {
|
||||
primary: input.appearance.primaryColor,
|
||||
secondary: input.appearance.secondaryColor,
|
||||
shade: input.appearance.shade as MantineTheme['primaryShade'],
|
||||
},
|
||||
customCss: input.appearance.customCss,
|
||||
faviconUrl: input.pageMetadata.faviconSrc,
|
||||
gridstack: {
|
||||
columnCountSmall: input.gridstack.sm,
|
||||
columnCountMedium: input.gridstack.md,
|
||||
columnCountLarge: input.gridstack.lg,
|
||||
},
|
||||
layout: {
|
||||
...previousConfig.settings.customization.layout,
|
||||
enabledLeftSidebar: input.layout.leftSidebarEnabled,
|
||||
enabledRightSidebar: input.layout.rightSidebarEnabled,
|
||||
enabledPing: input.layout.pingsEnabled,
|
||||
},
|
||||
logoImageUrl: input.pageMetadata.logoSrc,
|
||||
metaTitle: input.pageMetadata.metaTitle,
|
||||
pageTitle: input.pageMetadata.pageTitle,
|
||||
},
|
||||
},
|
||||
} satisfies BackendConfigType;
|
||||
const targetPath = path.join('data/configs', `${input.name}.json`);
|
||||
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -128,3 +128,30 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
|
||||
|
||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session?.user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
if (!ctx.session?.user.isAdmin) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN' });
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Admin (authenticated) procedure
|
||||
*
|
||||
* If you want a query or mutation to ONLY be accessible to logged in admins, use this. It verifies
|
||||
* the session is valid, guarantees `ctx.session.user` is not null and the user is an admin.
|
||||
*
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const adminProcedure = t.procedure.use(enforceUserIsAdmin);
|
||||
|
||||
Reference in New Issue
Block a user