mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
chore(release): automatic release v1.45.3
This commit is contained in:
2
.github/workflows/automatic-approval.yml
vendored
2
.github/workflows/automatic-approval.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: actions/create-github-app-token@v2
|
||||
|
||||
12
.github/workflows/code-quality.yml
vendored
12
.github/workflows/code-quality.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
- name: Build docker image
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
- name: Copy env
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
|
||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Upload Crowdin translations
|
||||
uses: crowdin/github-action@v2
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
outputs:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.git_ref }}
|
||||
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
outputs:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.git_ref }}
|
||||
|
||||
|
||||
@@ -27,7 +27,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@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get Next Version
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
renovate-validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- run: |
|
||||
npx --yes --package renovate -- \
|
||||
renovate-config-validator --strict .github/renovate.json5
|
||||
|
||||
2
.github/workflows/on-release.yml
vendored
2
.github/workflows/on-release.yml
vendored
@@ -49,7 +49,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@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.obtainToken.outputs.token }}
|
||||
- name: Setup
|
||||
|
||||
2
.github/workflows/update-contributors.yml
vendored
2
.github/workflows/update-contributors.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
permission-pull-requests: write # required to create pr & enable automerge
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
|
||||
|
||||
@@ -27,7 +27,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@v5
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
- name: Setup
|
||||
|
||||
@@ -74,9 +74,9 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"glob": "^12.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"isomorphic-dompurify": "^2.33.0",
|
||||
"jotai": "^2.15.1",
|
||||
"jotai": "^2.15.2",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "16.0.7",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
@@ -104,7 +104,7 @@
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ import { groupCreateSchema } from "@homarr/validation/group";
|
||||
|
||||
export const InitGroup = () => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
const form = useZodForm(groupCreateSchema, {
|
||||
initialValues: {
|
||||
name: "",
|
||||
@@ -21,9 +25,6 @@ export const InitGroup = () => {
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError(error) {
|
||||
if (error.data?.code === "CONFLICT") {
|
||||
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") });
|
||||
|
||||
@@ -17,7 +17,11 @@ import { settingsInitSchema } from "@homarr/validation/settings";
|
||||
export const InitSettings = () => {
|
||||
const tSection = useScopedI18n("management.page.settings.section");
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
|
||||
|
||||
form.watch("analytics.enableGeneral", ({ value }) => {
|
||||
@@ -31,11 +35,7 @@ export const InitSettings = () => {
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
await mutateAsync(values);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,19 +26,19 @@ export const InitUserForm = () => {
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: tUser("notification.success.title"),
|
||||
message: tUser("notification.success.message"),
|
||||
});
|
||||
|
||||
await signIn("credentials", {
|
||||
void signIn("credentials", {
|
||||
name: values.username,
|
||||
password: values.password,
|
||||
redirect: false,
|
||||
}).then(async () => {
|
||||
await revalidatePathActionAsync("/init");
|
||||
});
|
||||
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -84,9 +84,6 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
|
||||
id: board.id,
|
||||
name: board.name,
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
});
|
||||
}, [board.id, board.name, openDuplicateModal]);
|
||||
|
||||
|
||||
@@ -86,7 +86,11 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
|
||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/integrations");
|
||||
},
|
||||
});
|
||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
||||
|
||||
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
|
||||
@@ -116,7 +120,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
app,
|
||||
},
|
||||
{
|
||||
async onSuccess(data) {
|
||||
onSuccess(data) {
|
||||
// We do it this way as we are unable to send a typesafe error through onError
|
||||
if (data?.error) {
|
||||
setError(data.error);
|
||||
@@ -132,7 +136,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
router.push("/manage/integrations");
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -15,7 +15,11 @@ interface RemoveCertificateProps {
|
||||
|
||||
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
|
||||
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||
},
|
||||
});
|
||||
const t = useI18n();
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -27,12 +31,11 @@ export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
||||
await mutateAsync(
|
||||
{ fileName },
|
||||
{
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.remove.notification.success.title"),
|
||||
message: t("certificate.action.remove.notification.success.message"),
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -15,7 +15,11 @@ interface RemoveHostnameActionIconProps {
|
||||
}
|
||||
|
||||
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
|
||||
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation();
|
||||
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
|
||||
},
|
||||
});
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
|
||||
@@ -26,8 +30,7 @@ export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) =
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async onConfirm() {
|
||||
await mutateAsync(input, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.removeHostname.notification.success.title"),
|
||||
message: t("certificate.action.removeHostname.notification.success.message"),
|
||||
|
||||
@@ -206,15 +206,16 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
|
||||
const t = useScopedI18n("docker.action");
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
|
||||
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation({
|
||||
async onSettled() {
|
||||
await utils.docker.getContainers.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const handleClickAsync = async () => {
|
||||
await mutateAsync(
|
||||
{ ids: props.selectedIds },
|
||||
{
|
||||
async onSettled() {
|
||||
await utils.docker.getContainers.invalidate();
|
||||
},
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t(`${props.action}.notification.success.title`),
|
||||
|
||||
@@ -18,7 +18,12 @@ interface UserProfileAvatarForm {
|
||||
}
|
||||
|
||||
export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
const { mutate } = clientApi.user.setProfileImage.useMutation();
|
||||
const { mutate } = clientApi.user.setProfileImage.useMutation({
|
||||
async onSuccess() {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
},
|
||||
});
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
@@ -38,9 +43,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
image: base64Url,
|
||||
},
|
||||
{
|
||||
async onSuccess() {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: tManageAvatar("changeImage.notification.success.message"),
|
||||
});
|
||||
@@ -74,9 +77,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
async onSuccess() {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: tManageAvatar("removeImage.notification.success.message"),
|
||||
});
|
||||
|
||||
@@ -25,7 +25,11 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
||||
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
|
||||
const { openModal } = useModalAction(UserSelectModal);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { mutateAsync } = clientApi.group.transferOwnership.useMutation();
|
||||
const { mutateAsync } = clientApi.group.transferOwnership.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
const handleTransfer = useCallback(() => {
|
||||
openModal(
|
||||
@@ -47,7 +51,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
||||
userId: id,
|
||||
},
|
||||
{
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
setInnerOwnerId(id);
|
||||
showSuccessNotification({
|
||||
title: tRoot("common.notification.transfer.success"),
|
||||
@@ -56,7 +60,6 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
||||
user: name,
|
||||
}),
|
||||
});
|
||||
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -56,16 +56,19 @@ export const GroupsTable = ({ groups, initialGroupIds, hasFilter }: GroupsTableP
|
||||
() => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index),
|
||||
[groupIds, initialGroupIds],
|
||||
);
|
||||
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation();
|
||||
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/users/groups");
|
||||
},
|
||||
});
|
||||
const handleSavePositionsAsync = async () => {
|
||||
await mutateAsync(
|
||||
{ positions: groupIds },
|
||||
{
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: t("group.action.changePosition.notification.success.message"),
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/users/groups");
|
||||
},
|
||||
onError() {
|
||||
showSuccessNotification({
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -45,19 +45,19 @@
|
||||
"@testcontainers/redis": "^11.9.0",
|
||||
"@turbo/gen": "^2.6.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.14",
|
||||
"@vitest/ui": "^4.0.14",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"@vitest/ui": "^4.0.15",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^27.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"semantic-release": "^25.0.2",
|
||||
"testcontainers": "^11.9.0",
|
||||
"turbo": "^2.6.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.14"
|
||||
"vitest": "^4.0.15"
|
||||
},
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"engines": {
|
||||
@@ -95,7 +95,7 @@
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||
"tmp@<=0.2.3": ">=0.2.5",
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.2.4"
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.2.6"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"@types/cookies": "0.9.2",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { userSignInSchema } from "@homarr/validation/user";
|
||||
import type { ldapSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
import { env } from "../../../env";
|
||||
import { LdapClient } from "../ldap-client";
|
||||
|
||||
export const authorizeWithLdapCredentialsAsync = async (
|
||||
db: Database,
|
||||
credentials: z.infer<typeof userSignInSchema>,
|
||||
credentials: z.infer<typeof ldapSignInSchema>,
|
||||
) => {
|
||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
||||
const client = new LdapClient();
|
||||
@@ -38,7 +38,14 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
attributes: [env.AUTH_LDAP_USERNAME_ATTRIBUTE, env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
||||
},
|
||||
})
|
||||
.then((entries) => entries.at(0));
|
||||
.then((entries) => {
|
||||
if (entries.length > 1) {
|
||||
logger.warn(`Multiple LDAP users found for ${credentials.name}, expected only one.`);
|
||||
throw new CredentialsSignin();
|
||||
}
|
||||
|
||||
return entries.at(0);
|
||||
});
|
||||
|
||||
if (!ldapUser) {
|
||||
logger.warn(`User ${credentials.name} not found in LDAP`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type Credentials from "@auth/core/providers/credentials";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { userSignInSchema } from "@homarr/validation/user";
|
||||
import { ldapSignInSchema, userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
||||
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
||||
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
|
||||
name: "Ldap",
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async authorize(credentials) {
|
||||
const data = await userSignInSchema.parseAsync(credentials);
|
||||
const data = await ldapSignInSchema.parseAsync(credentials);
|
||||
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
||||
},
|
||||
}) satisfies CredentialsConfiguration;
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ChangeEvent, FocusEvent } from "react";
|
||||
|
||||
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement>
|
||||
extends BasePropsFor<TOnChangeArg, TComponent> {
|
||||
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement> extends BasePropsFor<
|
||||
TOnChangeArg,
|
||||
TComponent
|
||||
> {
|
||||
value?: T;
|
||||
defaultValue?: T;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,19 @@ interface UploadMediaProps {
|
||||
|
||||
export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }: UploadMediaProps) => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
|
||||
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation({
|
||||
async onSuccess(mediaIds) {
|
||||
await onSuccess?.(
|
||||
mediaIds.map((id) => ({
|
||||
id,
|
||||
url: `/api/user-medias/${id}`,
|
||||
})),
|
||||
);
|
||||
},
|
||||
async onSettled() {
|
||||
await onSettled?.();
|
||||
},
|
||||
});
|
||||
|
||||
const handleFileUploadAsync = async (files: File[] | File | null) => {
|
||||
if (!files || (Array.isArray(files) && files.length === 0)) return;
|
||||
@@ -24,25 +36,16 @@ export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }
|
||||
const formData = new FormData();
|
||||
filesArray.forEach((file) => formData.append("files", file));
|
||||
await mutateAsync(formData, {
|
||||
async onSuccess(mediaIds) {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: t("media.action.upload.notification.success.message"),
|
||||
});
|
||||
await onSuccess?.(
|
||||
mediaIds.map((id) => ({
|
||||
id,
|
||||
url: `/api/user-medias/${id}`,
|
||||
})),
|
||||
);
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
message: t("media.action.upload.notification.error.message"),
|
||||
});
|
||||
},
|
||||
async onSettled() {
|
||||
await onSettled?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { z } from "zod/v4";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import { AppForm } from "@homarr/forms-collection";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -10,7 +9,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import type { appManageSchema } from "@homarr/validation/app";
|
||||
|
||||
interface QuickAddAppModalProps {
|
||||
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => MaybePromise<void>;
|
||||
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => void;
|
||||
}
|
||||
|
||||
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
|
||||
@@ -28,13 +27,13 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
|
||||
mutate(values, {
|
||||
async onSuccess(app) {
|
||||
onSuccess(app) {
|
||||
showSuccessNotification({
|
||||
title: tScoped("success.title"),
|
||||
message: tScoped("success.message"),
|
||||
});
|
||||
|
||||
await innerProps.onClose(app);
|
||||
innerProps.onClose(app);
|
||||
actions.closeModal();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -15,7 +15,6 @@ interface InnerProps {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
onSuccess: () => MaybePromise<void>;
|
||||
}
|
||||
|
||||
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
@@ -27,7 +26,11 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
|
||||
},
|
||||
});
|
||||
const boardNameStatus = useBoardNameStatus(form.values.name);
|
||||
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation();
|
||||
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -40,13 +43,12 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
|
||||
id: innerProps.board.id,
|
||||
},
|
||||
{
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
actions.closeModal();
|
||||
showSuccessNotification({
|
||||
title: t("board.action.duplicate.notification.success.title"),
|
||||
message: t("board.action.duplicate.notification.success.message"),
|
||||
});
|
||||
await innerProps.onSuccess();
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -65,7 +65,11 @@ export const ImportBoardModal = createModal(({ actions }) => {
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation();
|
||||
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
});
|
||||
const boardNameStatus = useBoardNameStatus(form.values.configuration.name);
|
||||
|
||||
const handleSubmitAsync = async (values: { file: File; configuration: OldmarrImportConfiguration }) => {
|
||||
@@ -74,9 +78,8 @@ export const ImportBoardModal = createModal(({ actions }) => {
|
||||
formData.set("configuration", JSON.stringify(values.configuration));
|
||||
|
||||
await mutateAsync(formData, {
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
actions.closeModal();
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
showSuccessNotification({
|
||||
title: tOldImport("notification.success.title"),
|
||||
message: tOldImport("notification.success.message"),
|
||||
|
||||
@@ -27,7 +27,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
|
||||
},
|
||||
},
|
||||
);
|
||||
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
|
||||
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation({
|
||||
async onSuccess() {
|
||||
await innerProps.onSuccess?.();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -35,12 +39,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
|
||||
const formData = new FormData();
|
||||
formData.set("file", values.file);
|
||||
await mutateAsync(formData, {
|
||||
async onSuccess() {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.create.notification.success.title"),
|
||||
message: t("certificate.action.create.notification.success.message"),
|
||||
});
|
||||
await innerProps.onSuccess?.();
|
||||
actions.closeModal();
|
||||
},
|
||||
onError() {
|
||||
|
||||
@@ -29,10 +29,10 @@ export const ImportTokenModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
mutate(
|
||||
{ checksum: innerProps.checksum, token: values.token },
|
||||
{
|
||||
async onSuccess(isValid) {
|
||||
onSuccess(isValid) {
|
||||
if (isValid) {
|
||||
actions.closeModal();
|
||||
await innerProps.onSuccessAsync(values.token);
|
||||
void innerProps.onSuccessAsync(values.token);
|
||||
} else {
|
||||
showErrorNotification({
|
||||
title: tTokenModal("notification.error.title"),
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@mantine/hooks": "^8.3.9",
|
||||
"@mantine/spotlight": "^8.3.9",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"jotai": "^2.15.1",
|
||||
"jotai": "^2.15.2",
|
||||
"next": "16.0.7",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "16.0.7",
|
||||
"next-intl": "4.5.6",
|
||||
"next-intl": "4.5.8",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
},
|
||||
|
||||
@@ -2232,7 +2232,7 @@
|
||||
"unknown": "Ukendt",
|
||||
"pending": "Afventende",
|
||||
"processing": "Behandler",
|
||||
"requested": "",
|
||||
"requested": "Anmodet",
|
||||
"partiallyAvailable": "Delvis",
|
||||
"available": "Tilgængelig",
|
||||
"blacklisted": "Blacklistet",
|
||||
|
||||
@@ -747,7 +747,7 @@
|
||||
},
|
||||
"statusCode": {
|
||||
"title": "Antwoord fout",
|
||||
"description": "",
|
||||
"description": "Onverwachte {statusCode} ({reason}) reactie van <url></url>. Controleer of de URL wijst naar de basis-URL van de integratie.",
|
||||
"otherDescription": "",
|
||||
"reason": {
|
||||
"badRequest": "Onjuist verzoek",
|
||||
@@ -2232,7 +2232,7 @@
|
||||
"unknown": "Onbekend",
|
||||
"pending": "In afwachting",
|
||||
"processing": "Verwerken",
|
||||
"requested": "",
|
||||
"requested": "Aangevraagd",
|
||||
"partiallyAvailable": "Gedeeltelijk",
|
||||
"available": "Beschikbaar",
|
||||
"blacklisted": "Geblacklist",
|
||||
|
||||
@@ -10,8 +10,10 @@ interface BaseSelectItem {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
|
||||
extends Pick<SelectProps, "label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder" | "clearable"> {
|
||||
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem> extends Pick<
|
||||
SelectProps,
|
||||
"label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder" | "clearable"
|
||||
> {
|
||||
data: TSelectItem[];
|
||||
description?: string;
|
||||
withAsterisk?: boolean;
|
||||
|
||||
@@ -69,6 +69,17 @@ export const userSignInSchema = z.object({
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ldapSignInSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
// Prevent special characters that could lead to LDAP injection attacks
|
||||
.regex(/^[^\\,+<>;"=)(*|!&]+$/, {
|
||||
message: "Invalid characters in ldap username",
|
||||
}),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const userRegistrationSchema = addConfirmPasswordRefinement(
|
||||
z.object({
|
||||
username: usernameSchema,
|
||||
|
||||
@@ -69,10 +69,10 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async onClose(createdAppId) {
|
||||
await refetch();
|
||||
form.setFieldValue(`options.${property}`, createdAppId);
|
||||
onClose(createdAppId) {
|
||||
void refetch().then(() => {
|
||||
form.setFieldValue(`options.${property}`, createdAppId);
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,9 +111,13 @@ const createColumns = (
|
||||
header: t("action.title"),
|
||||
Cell({ row }) {
|
||||
const utils = clientApi.useUtils();
|
||||
const { mutateAsync: startContainer } = clientApi.docker.startAll.useMutation();
|
||||
const { mutateAsync: stopContainer } = clientApi.docker.stopAll.useMutation();
|
||||
const { mutateAsync: restartContainer } = clientApi.docker.restartAll.useMutation();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const onSettled = async () => {
|
||||
await utils.docker.getContainers.invalidate();
|
||||
};
|
||||
const { mutateAsync: startContainer } = clientApi.docker.startAll.useMutation({ onSettled });
|
||||
const { mutateAsync: stopContainer } = clientApi.docker.stopAll.useMutation({ onSettled });
|
||||
const { mutateAsync: restartContainer } = clientApi.docker.restartAll.useMutation({ onSettled });
|
||||
|
||||
const handleActionAsync = async (action: "start" | "stop" | "restart") => {
|
||||
const mutation = action === "start" ? startContainer : action === "stop" ? stopContainer : restartContainer;
|
||||
@@ -121,9 +125,6 @@ const createColumns = (
|
||||
await mutation(
|
||||
{ ids: [row.original.id] },
|
||||
{
|
||||
async onSettled() {
|
||||
await utils.docker.getContainers.invalidate();
|
||||
},
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t(`action.${action}.notification.success.title`),
|
||||
|
||||
@@ -18,14 +18,17 @@ interface TextInput extends CommonInput<string> {
|
||||
validate?: z.ZodType<string>;
|
||||
}
|
||||
|
||||
interface MultiSelectInput<TOptions extends SelectOption[]>
|
||||
extends CommonInput<inferSelectOptionValue<TOptions[number]>[]> {
|
||||
interface MultiSelectInput<TOptions extends SelectOption[]> extends CommonInput<
|
||||
inferSelectOptionValue<TOptions[number]>[]
|
||||
> {
|
||||
options: TOptions;
|
||||
searchable?: boolean;
|
||||
}
|
||||
|
||||
export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier>
|
||||
extends Omit<CommonInput<TOptionValue[]>, "withDescription"> {
|
||||
export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier> extends Omit<
|
||||
CommonInput<TOptionValue[]>,
|
||||
"withDescription"
|
||||
> {
|
||||
AddButton: (props: { addItem: (item: TItem) => void; values: TOptionValue[] }) => React.ReactNode;
|
||||
ItemComponent: (props: {
|
||||
item: TItem;
|
||||
@@ -37,8 +40,9 @@ export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentif
|
||||
useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown };
|
||||
}
|
||||
|
||||
interface SelectInput<TOptions extends readonly SelectOption[]>
|
||||
extends CommonInput<inferSelectOptionValue<TOptions[number]>> {
|
||||
interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput<
|
||||
inferSelectOptionValue<TOptions[number]>
|
||||
> {
|
||||
options: TOptions;
|
||||
searchable?: boolean;
|
||||
}
|
||||
|
||||
433
pnpm-lock.yaml
generated
433
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^6.1.1",
|
||||
"typescript-eslint": "^8.46.2"
|
||||
"typescript-eslint": "^8.48.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||
"prettier": "^3.6.2"
|
||||
"prettier": "^3.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user