Compare commits

..
Author SHA1 Message Date
ajnart 96bb33299e style: auto-fix lint and format 2026-07-06 04:32:49 +00:00
ajnart 39a1a3695e fix: address coderabbit review on #6214
- Remove invalid null return from session callback (must return session object)
- Prevent admin from disabling their own account (guard with isSelf check)
- Hide disable UI when viewing own profile
- Add user existence check to changeDisabled endpoint
- Add OpenAPI metadata to changeDisabled mutation
2026-07-06 06:31:42 +02:00
ajnart fbac0c8752 refactor: remove ternary operators for better readability
Replace ternary operators with if-statements and helper functions in:
- DisableUserButton component
- user list disabled status column
- EditUserPage disable item configuration
2026-07-06 02:59:02 +02:00
ajnart 26c83d8853 style: auto-fix lint and format 2026-07-06 00:57:10 +00:00
ajnart 4ff2c9a7fb feat(users): add ability to disable users without deleting them
- Add disabled boolean field to user schema (PostgreSQL, MySQL, SQLite)
- Block disabled users from logging in via any auth provider (credentials, LDAP, OIDC)
- Check disabled status in session callback to prevent session replay
- Add changeDisabled API endpoint (admin-only)
- Include disabled field in getById and getAll user endpoints
- Add disable/enable user toggle in admin UI with status badge in user list
2026-07-06 02:55:39 +02:00
52 changed files with 971 additions and 862 deletions

No files matched your search

-1
View File
@@ -33,7 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.69.2
- 1.69.0
- 1.68.0
- 1.67.0
+1 -1
View File
@@ -11,7 +11,7 @@ outputs:
runs:
using: "composite"
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Docker Buildx
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 || github.actor_id == 175486441
steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
+7 -7
View File
@@ -31,7 +31,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -92,7 +92,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -106,7 +106,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -124,7 +124,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Set up Docker Buildx
@@ -153,7 +153,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Upload Crowdin translations
uses: crowdin/github-action@v2
@@ -26,7 +26,7 @@ jobs:
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
@@ -57,7 +57,7 @@ jobs:
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v7
- uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
@@ -119,7 +119,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -158,7 +158,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Build docs
@@ -29,7 +29,7 @@ jobs:
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: dev
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: dev
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Validate prebuilt dependencies for amd64
id: validate-amd64
uses: ./.github/actions/prebuilt-debian
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Validate prebuilt dependencies for arm64
id: validate-arm64
uses: ./.github/actions/prebuilt-debian
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- run: |
npx --yes --package renovate -- \
renovate-config-validator --strict .github/renovate.json5
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
token: ${{ steps.obtainToken.outputs.token }}
ref: dev
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
@@ -28,7 +28,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup
-6
View File
@@ -1,9 +1,3 @@
## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06)
### Bug Fixes
* **layout:** raise AppShellHeader z-index above Indicator dots ([687edb9](https://github.com/homarr-labs/homarr/commit/687edb9226307cb83986097abb24fe7f672e1778)), closes [#6194](https://github.com/homarr-labs/homarr/issues/6194)
## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04)
### Bug Fixes
+1 -1
View File
@@ -58,7 +58,7 @@ import { immichServerStatsWidget } from '../../widgets/immich-server-stats';
Additional permissions may be required depending on your Immich version. If you encounter authorization errors, grant any missing permissions requested by the server.
</span>
),
),Ï
'Create the integration in Homarr and copy the API key to there'
]
}]} />
+1 -1
View File
@@ -43,7 +43,7 @@
"random-word-slugs": "^0.1.7",
"react": "catalog:",
"react-dom": "catalog:",
"swiper": "^14.0.1",
"swiper": "^12.2.0",
"tailwindcss": "^4.3.2"
},
"devDependencies": {
+2 -1
View File
@@ -1,7 +1,8 @@
@import url("https://api.fontshare.com/v2/css?f[]=work-sans@2,1&display=swap");
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
@import url(https://api.fontshare.com/v2/css?f[]=work-sans@2,1&display=swap);
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
@theme {
+4 -5
View File
@@ -1,6 +1,9 @@
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"target": "ES2022",
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -9,14 +12,10 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@site/*": ["./*"]
},
},
"include": [
"**/*.ts",
+14 -12
View File
@@ -5,9 +5,21 @@ import "@homarr/common/env";
import "@homarr/core/infrastructure/logs/env";
import "@homarr/docker/env";
import { readFileSync } from "fs";
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const homarrVersion = (() => {
for (const candidate of ["../../package.json", "./package.json"]) {
try {
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
if (pkg.name === "homarr") return pkg.version as string;
} catch {}
}
return "unknown";
})();
// Package path does not work... so we need to use relative path
const withNextIntl = createNextIntlPlugin({
requestConfig: "../../packages/translation/src/request.ts",
@@ -15,7 +27,7 @@ const withNextIntl = createNextIntlPlugin({
const nextConfig: NextConfig = {
env: {
HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown",
HOMARR_VERSION: homarrVersion,
},
output: "standalone",
reactStrictMode: true,
@@ -31,19 +43,9 @@ const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
webpackMemoryOptimizations: true,
},
turbopack: {
// ponytail: known Turbopack NFT warning from path.join(process.cwd(), …) in
// src/app/api/backup/{route,shared}.ts. No working placement in 16.2.x
// (see vercel/next.js#95125). Suppress until upstream fix lands.
ignoreIssue: [
{
path: "**/*",
title: "Encountered unexpected file in NFT list",
},
],
},
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: {
localPatterns: [
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "HOMARR_VERSION=$(node -p \"require('../../package.json').version\") pnpm with-env next build",
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"format": "oxfmt .",
@@ -0,0 +1,90 @@
"use client";
import { useTransition } from "react";
import { Button, Tooltip } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
interface DisableUserButtonProps {
user: RouterOutputs["user"]["getById"];
isSelf?: boolean;
}
export const DisableUserButton = ({ user, isSelf = false }: DisableUserButtonProps) => {
const t = useI18n();
const [isPending, startTransition] = useTransition();
const changeDisabledMutation = clientApi.user.changeDisabled.useMutation();
const isDisabled = user.disabled;
const getSuccessMessage = () => {
if (isDisabled) {
return {
title: t("user.action.enable.success", { defaultValue: "User enabled" }),
message: t("user.action.enable.successDescription", { defaultValue: "User has been enabled" }),
};
}
return {
title: t("user.action.disable.success", { defaultValue: "User disabled" }),
message: t("user.action.disable.successDescription", { defaultValue: "User has been disabled" }),
};
};
const handleToggle = () => {
if (isSelf) return;
startTransition(async () => {
try {
await changeDisabledMutation.mutateAsync({
userId: user.id,
disabled: !isDisabled,
});
showSuccessNotification(getSuccessMessage());
} catch {
showErrorNotification({
title: t("common.notification.update.error"),
message: t("common.notification.update.error"),
});
}
});
};
const getTooltipLabel = () => {
if (isDisabled) {
return t("user.action.enable.description", { defaultValue: "Enable this user" });
}
return t("user.action.disable.description", { defaultValue: "Disable this user" });
};
const getButtonLabel = () => {
if (isDisabled) {
return t("user.action.enable.label", { defaultValue: "Enable user" });
}
return t("user.action.disable.label", { defaultValue: "Disable user" });
};
const getButtonColor = () => (isDisabled ? "green" : "yellow");
return (
<Tooltip
label={
isSelf
? t("user.action.disableSelf.disabled", { defaultValue: "Cannot disable your own account" })
: getTooltipLabel()
}
position="top"
>
<Button
onClick={handleToggle}
loading={isPending || changeDisabledMutation.isPending}
color={getButtonColor()}
variant="light"
disabled={isSelf}
>
{getButtonLabel()}
</Button>
</Tooltip>
);
};
@@ -11,6 +11,7 @@ import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { DisableUserButton } from "./_components/_disable-user-button";
import { UserGeneralSettingsForm } from "./_components/_general-settings-form";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { ResetTours } from "./_components/_reset-tours";
@@ -61,6 +62,19 @@ export default async function EditUserPage(props: Props) {
const searchEngines = await api.searchEngine.getSelectable();
const isSelf = session?.user.id === user.id;
const isCredentialsUser = user.provider === "credentials";
const isDisabled = user.disabled;
const getDisableItemLabel = () => {
if (isDisabled) return t("user.action.enable.label");
return t("user.action.disable.label");
};
const getDisableItemDescription = () => {
if (isDisabled) {
return t("management.page.user.action.enable.description", { defaultValue: "Re-enable access for this user" });
}
return t("management.page.user.action.disable.description", { defaultValue: "Prevent this user from logging in" });
};
return (
<Stack>
@@ -96,6 +110,13 @@ export default async function EditUserPage(props: Props) {
)}
<DangerZoneRoot>
{!isSelf && (
<DangerZoneItem
label={getDisableItemLabel()}
description={getDisableItemDescription()}
action={<DisableUserButton user={user} isSelf={isSelf} />}
/>
)}
<DangerZoneItem
label={t("user.action.delete.label")}
description={t("user.action.delete.description")}
@@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
import { Anchor, Group, Text, Tooltip } from "@mantine/core";
import { Anchor, Badge, Group, Text, Tooltip } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table";
@@ -52,6 +52,15 @@ export const UserListComponent = ({ initialUserList }: UserListComponentProps) =
</Group>
),
},
{
accessorKey: "disabled",
header: t("user.field.status.label", { defaultValue: "Status" }),
size: 100,
Cell: ({ row }) => {
if (!row.original.disabled) return null;
return <Badge color="red">{t("user.field.status.disabled", { defaultValue: "Disabled" })}</Badge>;
},
},
],
[t],
);
+8 -10
View File
@@ -1,13 +1,11 @@
import fs from "fs";
import path from "path";
export const findMigrationsFolder = () => {
const cwd = process.cwd();
const migrationsFolderCandidates = [
path.join(cwd, "db/migrations/sqlite"),
path.join(cwd, "packages/db/migrations/sqlite"),
path.resolve(cwd, "../../db/migrations/sqlite"),
"/app/db/migrations/sqlite",
];
return migrationsFolderCandidates.find((candidate) => fs.existsSync(candidate));
};
const migrationsFolderCandidates = [
path.join(process.cwd(), "db/migrations/sqlite"),
path.join(process.cwd(), "packages/db/migrations/sqlite"),
path.resolve(process.cwd(), "../../db/migrations/sqlite"),
"/app/db/migrations/sqlite",
];
export const findMigrationsFolder = () => migrationsFolderCandidates.find((candidate) => fs.existsSync(candidate));
+1 -1
View File
@@ -18,7 +18,7 @@ interface Props {
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
return (
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
<AppShellHeader maw="100vw" style={{ overflowX: "hidden" }}>
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
{hasNavigation && <ClientBurger />}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "homarr-prebuilt-debian",
"private": true,
"dependencies": {
"better-sqlite3": "^12.11.1"
"better-sqlite3": "^12.10.0"
},
"engines": {
"node": ">=24.18.0",
+5 -5
View File
@@ -9,16 +9,16 @@ importers:
.:
dependencies:
better-sqlite3:
specifier: ^12.11.1
version: 12.11.1
specifier: ^12.10.0
version: 12.10.0
packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-sqlite3@12.11.1:
resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==}
better-sqlite3@12.10.0:
resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
bindings@1.5.0:
@@ -148,7 +148,7 @@ snapshots:
base64-js@1.5.1: {}
better-sqlite3@12.11.1:
better-sqlite3@12.10.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.69.2",
"version": "1.69.1",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -43,7 +43,6 @@
"@semantic-release/release-notes-generator": "catalog:",
"@testcontainers/redis": "catalog:",
"@turbo/gen": "catalog:",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
+43
View File
@@ -232,6 +232,7 @@ export const userRouter = createTRPCRouter({
email: true,
emailVerified: true,
image: true,
disabled: true,
}),
),
)
@@ -252,6 +253,7 @@ export const userRouter = createTRPCRouter({
email: true,
emailVerified: true,
image: true,
disabled: true,
},
});
}),
@@ -341,6 +343,7 @@ export const userRouter = createTRPCRouter({
emailVerified: true,
image: true,
provider: true,
disabled: true,
homeBoardId: true,
mobileHomeBoardId: true,
firstDayOfWeek: true,
@@ -377,6 +380,7 @@ export const userRouter = createTRPCRouter({
emailVerified: true,
image: true,
provider: true,
disabled: true,
homeBoardId: true,
mobileHomeBoardId: true,
firstDayOfWeek: true,
@@ -777,6 +781,45 @@ export const userRouter = createTRPCRouter({
completedBoardTour: user?.completedBoardTour ?? false,
};
}),
changeDisabled: permissionRequiredProcedure
.requiresPermission("admin")
.input(
z.object({
userId: z.string(),
disabled: z.boolean(),
}),
)
.output(z.void())
.meta({
openapi: {
method: "PATCH",
path: "/api/users/disabled",
tags: ["users"],
protect: true,
},
})
.mutation(async ({ input, ctx }) => {
const dbUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
},
where: eq(users.id, input.userId),
});
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await ctx.db
.update(users)
.set({
disabled: input.disabled,
})
.where(eq(users.id, input.userId));
}),
});
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
@@ -23,6 +23,11 @@ export const authorizeWithBasicCredentialsAsync = async (
return null;
}
if (user.disabled) {
logger.warn("User is disabled", { userName: user.name });
return null;
}
logger.info("User is trying to log in. Checking password...", { userName: user.name });
const isValidPassword = await comparePasswordsAsync(credentials.password, user.password);
@@ -107,6 +107,7 @@ export const authorizeWithLdapCredentialsAsync = async (
email: true,
emailVerified: true,
provider: true,
disabled: true,
},
where: and(eq(users.email, mailResult.data), eq(users.provider, "ldap")),
});
@@ -130,6 +131,11 @@ export const authorizeWithLdapCredentialsAsync = async (
logger.info("User created successfully", { userName: credentials.name });
}
if (user.disabled) {
logger.warn("User is disabled", { userName: credentials.name });
throw new CredentialsSignin("User is disabled");
}
return {
id: user.id,
name: credentials.name,
+1 -1
View File
@@ -6,5 +6,5 @@ export function parseCookies(cookieString: string) {
}
export function setClientCookie(name: string, value: string, options: SerializeOptions = {}) {
document.cookie = stringifySetCookie({ name, value, ...options });
document.cookie = stringifySetCookie(name, value, options);
}
@@ -25,7 +25,7 @@ export class AxiosHttpErrorHandler extends HttpErrorHandler {
return new RequestError(requestErrorInput, {
cause: error,
}) as AnyRequestError;
});
}
handleResponseError(error: unknown): ResponseError | undefined {
if (!(error instanceof AxiosError)) return undefined;
@@ -20,7 +20,7 @@ export class FetchHttpErrorHandler extends HttpErrorHandler {
const result = matchErrorCode(error.cause.code);
if (!result) return undefined;
return new RequestError(result, { cause: error }) as AnyRequestError;
return new RequestError(result, { cause: error });
}
/**
@@ -59,7 +59,7 @@ export const matchErrorCode = (code: string): AnyRequestErrorInput | undefined =
type: key,
reason: found[0] as RequestErrorReason<typeof key>,
code: code as RequestErrorCode,
} as AnyRequestErrorInput;
};
}
return undefined;
@@ -30,7 +30,7 @@ export class NodeFetchHttpErrorHandler extends HttpErrorHandler {
return new RequestError(requestErrorInput, {
cause: error,
}) as AnyRequestError;
});
}
/**
@@ -0,0 +1 @@
ALTER TABLE `user` ADD `disabled` boolean DEFAULT false NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "disabled" boolean DEFAULT false NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "disabled" integer DEFAULT 0 NOT NULL;
+1
View File
@@ -72,6 +72,7 @@ export const users = mysqlTable("user", {
image: text(),
password: text(),
provider: varchar({ length: 64 }).$type<SupportedAuthProvider>().default("credentials").notNull(),
disabled: boolean().default(false).notNull(),
homeBoardId: varchar({ length: 64 }).references((): AnyMySqlColumn => boards.id, {
onDelete: "set null",
}),
+1
View File
@@ -71,6 +71,7 @@ export const users = pgTable("user", {
image: text(),
password: text(),
provider: varchar({ length: 64 }).$type<SupportedAuthProvider>().default("credentials").notNull(),
disabled: boolean().default(false).notNull(),
homeBoardId: varchar({ length: 64 }).references((): AnyPgColumn => boards.id, {
onDelete: "set null",
}),
+1
View File
@@ -54,6 +54,7 @@ export const users = sqliteTable("user", {
image: text(),
password: text(),
provider: text().$type<SupportedAuthProvider>().default("credentials").notNull(),
disabled: int({ mode: "boolean" }).default(false).notNull(),
homeBoardId: text().references((): AnySQLiteColumn => boards.id, {
onDelete: "set null",
}),
@@ -72,14 +72,7 @@ export const AppForm = ({
}
form.initialize(toFormValues(initialValues));
// `form` is a new object every render (Mantine useForm returns a fresh literal),
// so it must not be a dep here — it would re-run every render and, since
// form.initialize() always calls clearErrors() (new {} object) internally,
// trigger an infinite update loop. initialValuesKey (a primitive string)
// already captures the only case this effect exists for: re-initializing
// when initialValues actually changes (e.g. navigating between edit pages).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValuesKey]);
}, [initialValuesKey, form, initialValues]);
// Debounce the name value with 200ms delay
const [debouncedName] = useDebouncedValue(form.values.name, 200);
+33 -33
View File
@@ -6,9 +6,9 @@
"subtitle": "",
"description": "",
"action": {
"scratch": "Sākt no jauna",
"scratch": "",
"importOldmarr": "",
"restoreSqlite": "SQLite rezerves kopija",
"restoreSqlite": "",
"restoreSqliteDescription": "",
"backToOptions": ""
}
@@ -26,19 +26,19 @@
}
},
"importSettings": {
"title": "Importēt iestatījumus",
"title": "",
"description": ""
},
"boardSelection": {
"title": "",
"description": "",
"action": {
"selectAll": "Atzīmēt visu",
"selectAll": "",
"unselectAll": ""
}
},
"summary": {
"title": "Importēšanas kopsavilkums",
"title": "",
"description": "",
"action": {
"import": ""
@@ -46,7 +46,7 @@
"entities": {
"apps": "Lietotnes",
"boards": "Dēļi",
"integrations": "Integrācijas",
"integrations": "",
"credentialUsers": ""
}
},
@@ -67,15 +67,15 @@
}
},
"user": {
"title": "Administratora lietotājs",
"title": "",
"subtitle": "",
"notification": {
"success": {
"title": "Lietotājs izveidots",
"title": "",
"message": ""
},
"error": {
"title": "Lietotāja izveide neizdevās"
"title": ""
}
}
},
@@ -94,19 +94,19 @@
"subtitle": ""
},
"integrations": {
"title": "Integrācijas",
"title": "",
"subtitle": "",
"description": "",
"action": {
"continue": "Turpināt",
"skip": "Pagaidām izlaist šo soli"
"continue": "",
"skip": ""
},
"configure": {
"progress": "{current} no {total}",
"skip": "Izlaist šo integrāciju",
"progress": "",
"skip": "",
"finishing": "",
"error": {
"title": "Neizdevās iestatīt!",
"title": "",
"message": ""
},
"helpers": {
@@ -115,7 +115,7 @@
}
},
"urlTemplate": {
"label": "Bāzes URL",
"label": "",
"placeholder": "",
"hint": "",
"warning": "",
@@ -136,14 +136,14 @@
}
},
"finish": {
"title": "Pabeigt Iestatīšanu",
"title": "",
"subtitle": "",
"description": "",
"action": {
"goToBoard": "",
"createBoard": "",
"inviteUser": "Ielūgt citus lietotājus",
"docs": "Lasīt dokumentāciju"
"inviteUser": "",
"docs": ""
}
}
},
@@ -158,7 +158,7 @@
"subtitle": ""
},
"invite": {
"title": "Pievienoties Homarr",
"title": "",
"subtitle": "",
"description": ""
},
@@ -177,20 +177,20 @@
},
"password": {
"label": "Parole",
"suggestionsTitle": "Paroles ieteikumi",
"suggestionsTitle": "",
"suggestion": {
"length": "Vismaz 8 rakstzīmes",
"length": "",
"lowercase": "Ietver mazo burtu",
"uppercase": "Ietver lielo burtu",
"number": "Ietver numuru",
"special": "Iekļauj speciālo simbolu"
"special": ""
}
},
"passwordConfirm": {
"label": "Apstipriniet paroli"
},
"previousPassword": {
"label": "Iepriekšējā parole"
"label": ""
},
"homeBoard": {
"label": ""
@@ -218,10 +218,10 @@
"notification": {
"success": {
"title": "",
"message": "Jūs esat veiksmīgi pieteicies"
"message": ""
},
"error": {
"title": "Pieteikšanās neizdevās",
"title": "",
"message": ""
}
},
@@ -245,13 +245,13 @@
},
"create": "Izveidot lietotāju",
"changePassword": {
"label": "Mainīt paroli",
"label": "",
"notification": {
"success": {
"message": "Parole veiksmīgi nomainīta."
"message": ""
},
"error": {
"message": "Nav iespējams nomainīt paroli"
"message": ""
}
}
},
@@ -595,12 +595,12 @@
}
},
"defaultGroup": {
"name": "Noklusētā grupa",
"name": "",
"description": ""
}
},
"app": {
"search": "Atrast aplikāciju",
"search": "",
"page": {
"list": {
"title": "Lietotnes",
@@ -689,7 +689,7 @@
"list": {
"title": "",
"noResults": "",
"loading": "Notiek ielāde..."
"loading": ""
},
"create": {
"title": "",
@@ -700,7 +700,7 @@
}
},
"action": {
"create": "Izveidot",
"create": "",
"import": "",
"save": "",
"delete": "",
+12 -12
View File
@@ -1599,7 +1599,7 @@
"menu": {
"label": {
"settings": "Ustawienia",
"refresh": "Odśwież",
"refresh": "",
"options": "Opcje",
"actions": "Akcje"
}
@@ -1904,7 +1904,7 @@
"noStation": "Nie wybrano stacji",
"option": {
"baseUrl": {
"label": "Szukaj adresu URL"
"label": ""
},
"station": {
"label": "Stacja"
@@ -2791,8 +2791,8 @@
"description": "Wyświetl datę zdjęcia oraz bieżący indeks na dole pokazu slajdów"
},
"randomizePhotos": {
"label": "Losowe zdjęcia",
"description": "Pokaż zdjęcia w losowej kolejności, zamiast zawsze rozpoczynać się od tej samej kolejności albumu."
"label": "",
"description": ""
}
},
"description": "Pokaz slajdów zdjęć z Twojego albumu w Immich",
@@ -3158,7 +3158,7 @@
"label": "Dostawca"
},
"providerUrl": {
"label": "URL dostawcy"
"label": ""
},
"identifier": {
"label": "Identyfikator",
@@ -3209,11 +3209,11 @@
"tooltip": "Nie udało się rozpoznać dostawcy. Ustaw go ręcznie po zaimportowaniu obrazów."
},
"tokens": {
"label": "Tokeny dostawcy",
"token": "Token",
"configured": "Token skonfigurowany",
"notConfigured": "Brak ustawionych tokenów",
"save": "Zapisz"
"label": "",
"token": "",
"configured": "",
"notConfigured": "",
"save": ""
}
}
},
@@ -3236,9 +3236,9 @@
"invalidIdentifier": "Nieprawidłowy identyfikator",
"noMatchingVersion": "Nie znaleziono pasującej wersji",
"noReleasesFound": "Nie znaleziono wydań!",
"noProviderSelected": "Nie wybrano dostawcy",
"noProviderSelected": "",
"noProviderResponse": "Brak odpowiedzi od dostawcy",
"unexpected": "Nieoczekiwany błąd dostawcy"
"unexpected": ""
}
}
},
+1 -1
View File
@@ -3437,7 +3437,7 @@
"description": ""
},
"maxNowPlayingItems": {
"label": "Максимум елементів для відтворення",
"label": "",
"description": ""
},
"showLibraryCount": {
+644 -696
View File
File diff suppressed because it is too large. Load diff
+29 -33
View File
@@ -14,7 +14,7 @@ catalog:
"@dnd-kit/utilities": ^3.2.2
"@drizzle-team/brocli": ^0.12.0
"@extractus/feed-extractor": 7.2.1
"@gfazioli/mantine-onboarding-tour": ^4.0.6
'@gfazioli/mantine-onboarding-tour': ^4.0.6
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
"@homarr/node-unifi": ^2.6.0
@@ -45,11 +45,11 @@ catalog:
"@semantic-release/release-notes-generator": ^14.1.1
"@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.44.0
"@tanstack/query-async-storage-persister": ^5.101.2
"@tanstack/react-query": ^5.101.2
"@tanstack/react-query-devtools": ^5.101.2
"@tanstack/react-query-next-experimental": ^5.101.2
"@tanstack/react-query-persist-client": ^5.101.2
"@tanstack/query-async-storage-persister": ^5.101.0
"@tanstack/react-query": ^5.101.0
"@tanstack/react-query-devtools": ^5.101.0
"@tanstack/react-query-next-experimental": ^5.101.0
"@tanstack/react-query-persist-client": ^5.101.0
"@testcontainers/mysql": ^12.0.4
"@testcontainers/postgresql": ^12.0.4
"@testcontainers/redis": ^12.0.4
@@ -69,12 +69,12 @@ catalog:
"@tiptap/extension-underline": 3.27.1
"@tiptap/react": ^3.27.1
"@tiptap/starter-kit": ^3.27.1
"@trpc/client": ^11.18.0
"@trpc/next": ^11.18.0
"@trpc/react-query": ^11.18.0
"@trpc/server": ^11.18.0
"@trpc/tanstack-react-query": ^11.18.0
"@turbo/gen": ^2.10.2
"@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0
"@trpc/server": ^11.17.0
"@trpc/tanstack-react-query": ^11.17.0
"@turbo/gen": ^2.10.0
"@types/adm-zip": 0.5.8
"@types/bcrypt": 6.0.0
"@types/better-sqlite3": 7.6.13
@@ -89,29 +89,29 @@ catalog:
"@types/prismjs": ^1.26.6
"@types/react": 19.2.17
"@types/react-dom": 19.2.3
"@scalar/api-reference-react": ^0.9.50
"@scalar/api-reference-react": ^0.9.49
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.396.3
posthog-node: ^5.39.1
posthog-js: ^1.396.2
posthog-node: ^4.18.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9
"@vitest/ui": ^4.1.9
"@xterm/addon-canvas": ^0.7.0
"@xterm/addon-fit": 0.11.0
"@xterm/xterm": ^6.0.0
adm-zip: 0.5.18
adm-zip: 0.5.17
jszip: ^3.10.1
sql.js: ^1.14.1
babel-plugin-react-compiler: ^1.0.0
bcrypt: ^6.0.0
better-sqlite3: ^12.11.1
better-sqlite3: ^12.10.0
chroma-js: ^3.2.0
clsx: ^2.1.1
concurrently: ^10.0.3
conventional-changelog-conventionalcommits: ^10.0.0
cookie: 2.0.0
conventional-changelog-conventionalcommits: ^9.3.1
cookie: 1.1.1
cookies: ^0.9.1
croner: 10.0.1
cross-env: ^10.1.0
@@ -148,11 +148,11 @@ catalog:
mysql2: 3.22.5
next: 16.2.9
next-auth: 5.0.0-beta.31
next-intl: 4.13.1
next-intl: 4.13.0
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.57.0
oxlint: ^1.72.0
oxfmt: ^0.56.0
oxlint: ^1.71.0
pg: ^8.22.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
@@ -164,7 +164,7 @@ catalog:
react-markdown: ^10.1.0
react-quill-new: ^3.8.3
react-simple-code-editor: ^0.14.1
recharts: ^3.9.1
recharts: ^3.9.0
sass: ^1.101.0
semantic-release: ^25.0.5
semver-parser: ^4.2.1
@@ -174,11 +174,11 @@ catalog:
timezones-ical-library: ^2.2.1
trpc-to-mcp: ^1.3.2
trpc-to-openapi: ^3.3.0
tsdav: ^2.3.0
tsdav: ^2.2.2
tsx: 4.20.4
turbo: ^2.10.2
turbo: ^2.10.0
typescript: ^6.0.3
vite: ^8.1.2
vite: ^8.1.0
undici: 7.28.0
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.9
@@ -191,7 +191,6 @@ catalog:
zod: ^4.4.3
zod-form-data: ^3.0.2
zod-validation-error: ^5.0.0
catalogMode: strict
nodeLinker: hoisted
strictPeerDependencies: false
@@ -208,32 +207,29 @@ minimumReleaseAgeExclude:
- form-data@4.0.6
# Renovate security update: undici@7.28.0
- undici@7.28.0
# Renovate security update: esbuild@0.28.1
- esbuild@0.28.1
overrides:
"@babel/helpers@<7.26.10": ">=7.29.7"
"@babel/runtime@<7.26.10": ">=7.29.7"
"axios@>=1.0.0 <1.8.2": ">=1.18.1"
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.7"
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.7"
"esbuild@<=0.24.2": ">=0.28.1"
"esbuild@<=0.24.2": ">=0.28.0"
"form-data@>=4.0.0 <4.0.4": ">=4.0.6"
"hono@<4.6.5": ">=4.12.27"
"linkifyjs@<4.3.2": ">=4.3.3"
"nanoid@>=4.0.0 <5.0.9": ">=5.1.16"
"prismjs@<1.30.0": ">=1.30.0"
"proxmox-api>undici": "7.28.0"
"proxmox-api>undici": "7.27.1"
"react-is": "^19.2.7"
"rollup@>=4.0.0 <4.22.4": ">=4.62.2"
"sha.js@<=2.4.11": ">=2.4.12"
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.3"
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.3"
"tmp@<=0.2.3": ">=0.2.7"
"vite@>=5.0.0 <=5.4.18": ">=8.1.2"
"vite@>=5.0.0 <=5.4.18": ">=8.1.0"
patchedDependencies:
"@types/node-unifi": "patches/@types__node-unifi.patch"
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
allowUnusedPatches: true
allowBuilds:
"@tree-sitter-grammars/tree-sitter-yaml": true