mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-02 11:36:01 +01:00
fix: outdated config schema (#1769)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 2,
|
||||||
"configProperties": {
|
"configProperties": {
|
||||||
"name": "default"
|
"name": "default"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,31 @@ import { useState } from 'react';
|
|||||||
import { StepCreateAccount } from './step-create-account';
|
import { StepCreateAccount } from './step-create-account';
|
||||||
import { StepOnboardingFinished } from './step-onboarding-finished';
|
import { StepOnboardingFinished } from './step-onboarding-finished';
|
||||||
import { StepUpdatePathMappings } from './step-update-path-mappings';
|
import { StepUpdatePathMappings } from './step-update-path-mappings';
|
||||||
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
export const OnboardingSteps = ({ isUpdate }: { isUpdate: boolean }) => {
|
export const OnboardingSteps = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||||
|
const maximumSteps = isUpdate ? 3 : 2;
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const nextStep = () => setCurrentStep((current) => (current < 3 ? current + 1 : current));
|
|
||||||
|
const nextStep = () => setCurrentStep((current) => {
|
||||||
|
const newValue = (current < maximumSteps ? current + 1 : current);
|
||||||
|
|
||||||
|
if (currentStep + 1 >= maximumSteps) {
|
||||||
|
onFinishOnboarding();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
|
||||||
const prevStep = () => setCurrentStep((current) => (current > 0 ? current - 1 : current));
|
const prevStep = () => setCurrentStep((current) => (current > 0 ? current - 1 : current));
|
||||||
|
|
||||||
|
const { mutate: mutateConfigSchemaVersion } = api.config.updateConfigurationSchemaToLatest.useMutation();
|
||||||
|
|
||||||
|
const onFinishOnboarding = () => {
|
||||||
|
mutateConfigSchemaVersion();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="lg">
|
<Stack p="lg">
|
||||||
<Stepper
|
<Stepper
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { boardCustomizationSchema } from '~/validations/boards';
|
|||||||
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
||||||
|
|
||||||
import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
import { db } from '~/server/db';
|
||||||
|
import { users } from '~/server/db/schema';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
export const configNameSchema = z.string().regex(/^[a-zA-Z0-9-_]+$/);
|
export const configNameSchema = z.string().regex(/^[a-zA-Z0-9-_]+$/);
|
||||||
|
|
||||||
@@ -20,14 +23,14 @@ export const configRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (input.name.toLowerCase() === 'default') {
|
if (input.name.toLowerCase() === 'default') {
|
||||||
Consola.error("Rejected config deletion because default configuration can't be deleted");
|
Consola.error('Rejected config deletion because default configuration can\'t be deleted');
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'FORBIDDEN',
|
code: 'FORBIDDEN',
|
||||||
message: "Default config can't be deleted",
|
message: 'Default config can\'t be deleted',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ export const configRouter = createTRPCRouter({
|
|||||||
// If the target is not in the list of files, return an error
|
// If the target is not in the list of files, return an error
|
||||||
if (!matchedFile) {
|
if (!matchedFile) {
|
||||||
Consola.error(
|
Consola.error(
|
||||||
`Rejected config deletion request because config name '${input.name}' was not included in present configurations`
|
`Rejected config deletion request because config name '${input.name}' was not included in present configurations`,
|
||||||
);
|
);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
@@ -64,7 +67,7 @@ export const configRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
config: z.custom<ConfigType>((x) => !!x && typeof x === 'object'),
|
config: z.custom<ConfigType>((x) => !!x && typeof x === 'object'),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
Consola.info(`Saving updated configuration of '${input.name}' config.`);
|
Consola.info(`Saving updated configuration of '${input.name}' config.`);
|
||||||
@@ -96,16 +99,16 @@ export const configRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousApp = previousConfig.apps.find(
|
const previousApp = previousConfig.apps.find(
|
||||||
(previousApp) => previousApp.id === app.id
|
(previousApp) => previousApp.id === app.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const previousProperty = previousApp?.integration?.properties.find(
|
const previousProperty = previousApp?.integration?.properties.find(
|
||||||
(previousProperty) => previousProperty.field === property.field
|
(previousProperty) => previousProperty.field === property.field,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (property.value !== undefined && property.value !== null) {
|
if (property.value !== undefined && property.value !== null) {
|
||||||
Consola.info(
|
Consola.info(
|
||||||
'Detected credential change of private secret. Value will be overwritten in configuration'
|
'Detected credential change of private secret. Value will be overwritten in configuration',
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
field: property.field,
|
field: property.field,
|
||||||
@@ -165,7 +168,7 @@ export const configRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
if (!configExists(input.name)) {
|
if (!configExists(input.name)) {
|
||||||
@@ -223,4 +226,21 @@ export const configRouter = createTRPCRouter({
|
|||||||
const targetPath = path.join('data/configs', `${input.name}.json`);
|
const targetPath = path.join('data/configs', `${input.name}.json`);
|
||||||
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||||
}),
|
}),
|
||||||
|
// publicProcedure is not optimal, but should be fince, since there is no input and output data nor can you break the config
|
||||||
|
updateConfigurationSchemaToLatest: publicProcedure.mutation(async () => {
|
||||||
|
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
|
||||||
|
|
||||||
|
console.log('updating the schema version of', files.length, 'configurations');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const name = file.replace('.json', '');
|
||||||
|
const config = await getFrontendConfig(name);
|
||||||
|
|
||||||
|
config.schemaVersion = 2;
|
||||||
|
const targetPath = `data/configs/${name}.json`;
|
||||||
|
fs.writeFileSync(targetPath, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
|
||||||
|
console.log('updated', name, 'to schema version', config.schemaVersion);
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user