diff --git a/.github/workflows/renovate-automatic-approval.yml b/.github/workflows/renovate-automatic-approval.yml index 1185fd800..4f8020308 100644 --- a/.github/workflows/renovate-automatic-approval.yml +++ b/.github/workflows/renovate-automatic-approval.yml @@ -6,6 +6,7 @@ on: jobs: approve-renovate-prs: runs-on: ubuntu-latest + if: github.actor_id == 158783068 # Id of renovate bot see https://api.github.com/users/homarr-renovate%5Bbot%5D steps: - name: Checkout code uses: actions/checkout@v4 @@ -21,6 +22,4 @@ jobs: env: GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }} run: | - for pr in $(gh pr list --author homarr-renovate[bot] --json number --jq .[].number); do - gh pr review $pr --approve --body "Automatically approved by GitHub Action" - done + gh pr review ${{github.event.pull_request.number}} --approve --body "Automatically approved by GitHub Action" diff --git a/Dockerfile b/Dockerfile index 845d978c0..4cb3283da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN turbo prune @homarr/nextjs --docker --out-dir ./next-out RUN turbo prune @homarr/tasks --docker --out-dir ./tasks-out RUN turbo prune @homarr/websocket --docker --out-dir ./websocket-out RUN turbo prune @homarr/db --docker --out-dir ./migration-out +RUN turbo prune @homarr/cli --docker --out-dir ./cli-out # Add lockfile and package.json's of isolated subworkspace FROM base AS installer @@ -34,6 +35,10 @@ COPY --from=builder /app/migration-out/json/ . COPY --from=builder /app/migration-out/pnpm-lock.yaml ./pnpm-lock.yaml RUN corepack enable pnpm && pnpm install +COPY --from=builder /app/cli-out/json/ . +COPY --from=builder /app/cli-out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN corepack enable pnpm && pnpm install + COPY --from=builder /app/next-out/json/ . COPY --from=builder /app/next-out/pnpm-lock.yaml ./pnpm-lock.yaml RUN corepack enable pnpm && pnpm install @@ -45,6 +50,7 @@ COPY --from=builder /app/tasks-out/full/ . COPY --from=builder /app/websocket-out/full/ . COPY --from=builder /app/next-out/full/ . COPY --from=builder /app/migration-out/full/ . +COPY --from=builder /app/cli-out/full/ . # Copy static data as it is not part of the build COPY static-data ./static-data @@ -55,15 +61,23 @@ RUN corepack enable pnpm && pnpm build FROM base AS runner WORKDIR /app -RUN apk add --no-cache redis +RUN apk add --no-cache redis bash RUN mkdir /appdata RUN mkdir /appdata/db RUN mkdir /appdata/redis VOLUME /appdata -# Don't run production as root + + RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs + +# Enable homarr cli +COPY --from=installer --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs +RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr +RUN chmod +x /usr/bin/homarr + +# Don't run production as root RUN chown -R nextjs:nodejs /appdata USER nextjs diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 9fbf6f904..8b958f050 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -32,17 +32,17 @@ "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0", - "@mantine/colors-generator": "^7.11.2", - "@mantine/core": "^7.11.2", - "@mantine/hooks": "^7.11.2", - "@mantine/modals": "^7.11.2", - "@mantine/tiptap": "^7.11.2", + "@mantine/colors-generator": "^7.12.0", + "@mantine/core": "^7.12.0", + "@mantine/hooks": "^7.12.0", + "@mantine/modals": "^7.12.0", + "@mantine/tiptap": "^7.12.0", "@homarr/server-settings": "workspace:^0.1.0", "@t3-oss/env-nextjs": "^0.11.0", - "@tanstack/react-query": "^5.51.21", - "@tanstack/react-query-devtools": "^5.51.21", - "@tanstack/react-query-next-experimental": "5.51.21", - "@tabler/icons-react": "^3.11.0", + "@tanstack/react-query": "^5.51.23", + "@tanstack/react-query-devtools": "^5.51.23", + "@tanstack/react-query-next-experimental": "5.51.23", + "@tabler/icons-react": "^3.12.0", "@trpc/client": "next", "@trpc/next": "next", "@trpc/react-query": "next", @@ -56,7 +56,7 @@ "dotenv": "^16.4.5", "flag-icons": "^7.2.3", "glob": "^11.0.0", - "jotai": "^2.9.1", + "jotai": "^2.9.2", "mantine-react-table": "2.0.0-beta.6", "next": "^14.2.5", "postcss-preset-mantine": "^1.17.0", @@ -74,7 +74,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/chroma-js": "2.4.4", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@types/prismjs": "^1.26.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/apps/nextjs/src/app/[locale]/_client-providers/session.tsx b/apps/nextjs/src/app/[locale]/_client-providers/session.tsx index d4e0538db..249299bbb 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/session.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/session.tsx @@ -1,14 +1,46 @@ "use client"; +import { createContext, useContext, useEffect } from "react"; import type { PropsWithChildren } from "react"; +import dayjs from "dayjs"; import type { Session } from "@homarr/auth"; -import { SessionProvider } from "@homarr/auth/client"; +import { SessionProvider, signIn } from "@homarr/auth/client"; -interface AuthProviderProps { +interface AuthProviderProps extends AuthContextProps { session: Session | null; } -export const AuthProvider = ({ children, session }: PropsWithChildren) => { - return {children}; +export const AuthProvider = ({ children, session, logoutUrl }: PropsWithChildren) => { + useLoginRedirectOnSessionExpiry(session); + + return ( + + {children} + + ); +}; + +interface AuthContextProps { + logoutUrl: string | undefined; +} + +const AuthContext = createContext(null); + +export const useAuthContext = () => { + const context = useContext(AuthContext); + + if (!context) throw new Error("useAuthContext must be used within an AuthProvider"); + + return context; +}; + +const useLoginRedirectOnSessionExpiry = (session: Session | null) => { + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + if (!session) return () => {}; + //setTimeout doesn't allow for a number higher than 2147483647 (2³¹-1 , or roughly 24 days) + const timeout = setTimeout(() => void signIn(), Math.min(dayjs(session.expires).diff(), 2147483647)); + return () => clearTimeout(timeout); + }, [session]); }; diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx index 7823d050d..fb8472bd4 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx @@ -10,7 +10,7 @@ import { LoginForm } from "./_login-form"; interface LoginProps { searchParams: { - redirectAfterLogin?: string; + callbackUrl?: string; }; } @@ -18,7 +18,7 @@ export default async function Login({ searchParams }: LoginProps) { const session = await auth(); if (session) { - redirect(searchParams.redirectAfterLogin ?? "/"); + redirect(searchParams.callbackUrl ?? "/"); } const t = await getScopedI18n("user.page.login"); @@ -40,7 +40,7 @@ export default async function Login({ searchParams }: LoginProps) { providers={env.AUTH_PROVIDERS} oidcClientName={env.AUTH_OIDC_CLIENT_NAME} isOidcAutoLoginEnabled={env.AUTH_OIDC_AUTO_LOGIN} - callbackUrl={searchParams.redirectAfterLogin ?? "/"} + callbackUrl={searchParams.callbackUrl ?? "/"} /> diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index 7b3a8d657..223ebf9be 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -8,6 +8,7 @@ import "~/styles/scroll-area.scss"; import { ColorSchemeScript, createTheme, MantineProvider } from "@mantine/core"; +import { env } from "@homarr/auth/env.mjs"; import { auth } from "@homarr/auth/next"; import { ModalProvider } from "@homarr/modals"; import { Notifications } from "@homarr/notifications"; @@ -56,7 +57,7 @@ export default function Layout(props: { children: React.ReactNode; params: { loc const StackedProvider = composeWrappers([ async (innerProps) => { const session = await auth(); - return ; + return ; }, (innerProps) => , (innerProps) => , diff --git a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx index 2e6983b6e..7ddf2816a 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx @@ -4,6 +4,7 @@ import { IconApps, IconPencil } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; +import { parseAppHrefWithVariablesServer } from "@homarr/common/server"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { ManageContainer } from "~/components/manage/manage-container"; @@ -69,8 +70,8 @@ const AppCard = async ({ app }: AppCardProps) => { )} {app.href && ( - - {app.href} + + {parseAppHrefWithVariablesServer(app.href)} )} diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx index 1c59db412..ed30fb23d 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx @@ -30,6 +30,8 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => { onSuccess: async (values) => { await mutateAsync({ name: values.name, + columnCount: values.columnCount, + isPublic: values.isPublic, }); }, boardNames, diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index caea75a79..e5ca818de 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -2,7 +2,7 @@ import type { MantineColor } from "@mantine/core"; import { Avatar, Badge, Box, Button, Group, Text } from "@mantine/core"; -import { IconPlayerPlay, IconPlayerStop, IconRotateClockwise, IconTrash } from "@tabler/icons-react"; +import { IconPlayerPlay, IconPlayerStop, IconRefresh, IconRotateClockwise, IconTrash } from "@tabler/icons-react"; import type { MRT_ColumnDef } from "mantine-react-table"; import { MantineReactTable } from "mantine-react-table"; @@ -46,10 +46,12 @@ const createColumns = ( accessorKey: "image", header: t("docker.field.containerImage.label"), maxSize: 200, - Cell({ renderedCellValue }) { + Cell({ renderedCellValue, cell }) { return ( - {renderedCellValue} + + {renderedCellValue} + ); }, @@ -93,6 +95,35 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers" }, initialState: { density: "xs", showGlobalFilter: true }, + renderTopToolbarCustomActions: () => { + const utils = clientApi.useUtils(); + const { mutate, isPending } = clientApi.docker.invalidate.useMutation({ + async onSuccess() { + await utils.docker.getContainers.invalidate(); + showSuccessNotification({ + title: tDocker("action.refresh.notification.success.title"), + message: tDocker("action.refresh.notification.success.message"), + }); + }, + onError() { + showErrorNotification({ + title: tDocker("action.refresh.notification.error.title"), + message: tDocker("action.refresh.notification.error.message"), + }); + }, + }); + + return ( + + ); + }, renderToolbarAlertBannerContent: ({ groupedAlert, table }) => { return ( diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/page.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/page.tsx index c9054efe2..7af012d53 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/page.tsx @@ -1,12 +1,19 @@ +import { notFound } from "next/navigation"; import { Stack, Title } from "@mantine/core"; import { api } from "@homarr/api/server"; +import { auth } from "@homarr/auth/next"; import { getScopedI18n } from "@homarr/translation/server"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { DockerTable } from "./docker-table"; export default async function DockerPage() { + const session = await auth(); + if (!session?.user || !session.user.permissions.includes("admin")) { + notFound(); + } + const { containers, timestamp } = await api.docker.getContainers(); const tDocker = await getScopedI18n("docker"); diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx new file mode 100644 index 000000000..bbc8f308c --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { Button, Group, Select, Stack } from "@mantine/core"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { useZodForm } from "@homarr/form"; +import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; +import { useI18n } from "@homarr/translation/client"; +import type { z } from "@homarr/validation"; +import { validation } from "@homarr/validation"; + +import { revalidatePathActionAsync } from "~/app/revalidatePathAction"; + +interface ChangeHomeBoardFormProps { + user: RouterOutputs["user"]["getById"]; + boardsData: { value: string; label: string }[]; +} + +export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => { + const t = useI18n(); + const { mutate, isPending } = clientApi.user.changeHomeBoardId.useMutation({ + async onSettled() { + await revalidatePathActionAsync(`/manage/users/${user.id}`); + }, + onSuccess(_, variables) { + form.setInitialValues({ + homeBoardId: variables.homeBoardId, + }); + showSuccessNotification({ + message: t("user.action.changeHomeBoard.notification.success.message"), + }); + }, + onError() { + showErrorNotification({ + message: t("user.action.changeHomeBoard.notification.error.message"), + }); + }, + }); + const form = useZodForm(validation.user.changeHomeBoard, { + initialValues: { + homeBoardId: user.homeBoardId ?? "", + }, + }); + + const handleSubmit = (values: FormType) => { + mutate({ + userId: user.id, + ...values, + }); + }; + + return ( +
+ +