Merge branch 'dev' into ajnart/fix-duplicate-users
This commit is contained in:
@@ -35,7 +35,7 @@ export const LoginForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (values: FormType) => {
|
const handleSubmitAsync = async (values: FormType) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
await signIn("credentials", {
|
await signIn("credentials", {
|
||||||
@@ -66,7 +66,9 @@ export const LoginForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
|
||||||
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("field.username.label")}
|
label={t("field.username.label")}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { api } from "@homarr/api/server";
|
|||||||
import { createBoardContentPage } from "../_creator";
|
import { createBoardContentPage } from "../_creator";
|
||||||
|
|
||||||
export default createBoardContentPage<{ locale: string }>({
|
export default createBoardContentPage<{ locale: string }>({
|
||||||
async getInitialBoard() {
|
async getInitialBoardAsync() {
|
||||||
return await api.board.getDefaultBoard();
|
return await api.board.getDefaultBoard();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import definition from "./_definition";
|
import definition from "./_definition";
|
||||||
|
|
||||||
const { generateMetadata, page } = definition;
|
const { generateMetadataAsync: generateMetadata, page } = definition;
|
||||||
|
|
||||||
export default page;
|
export default page;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { api } from "@homarr/api/server";
|
|||||||
import { createBoardContentPage } from "../_creator";
|
import { createBoardContentPage } from "../_creator";
|
||||||
|
|
||||||
export default createBoardContentPage<{ locale: string; name: string }>({
|
export default createBoardContentPage<{ locale: string; name: string }>({
|
||||||
async getInitialBoard({ name }) {
|
async getInitialBoardAsync({ name }) {
|
||||||
return await api.board.getBoardByName({ name });
|
return await api.board.getBoardByName({ name });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import definition from "./_definition";
|
import definition from "./_definition";
|
||||||
|
|
||||||
const { generateMetadata, page } = definition;
|
const { generateMetadataAsync: generateMetadata, page } = definition;
|
||||||
|
|
||||||
export default page;
|
export default page;
|
||||||
|
|
||||||
|
|||||||
@@ -14,24 +14,24 @@ import { BoardContentHeaderActions } from "./_header-actions";
|
|||||||
export type Params = Record<string, unknown>;
|
export type Params = Record<string, unknown>;
|
||||||
|
|
||||||
interface Props<TParams extends Params> {
|
interface Props<TParams extends Params> {
|
||||||
getInitialBoard: (params: TParams) => Promise<Board>;
|
getInitialBoardAsync: (params: TParams) => Promise<Board>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createBoardContentPage = <
|
export const createBoardContentPage = <
|
||||||
TParams extends Record<string, unknown>,
|
TParams extends Record<string, unknown>,
|
||||||
>({
|
>({
|
||||||
getInitialBoard,
|
getInitialBoardAsync: getInitialBoard,
|
||||||
}: Props<TParams>) => {
|
}: Props<TParams>) => {
|
||||||
return {
|
return {
|
||||||
layout: createBoardLayout({
|
layout: createBoardLayout({
|
||||||
headerActions: <BoardContentHeaderActions />,
|
headerActions: <BoardContentHeaderActions />,
|
||||||
getInitialBoard,
|
getInitialBoardAsync: getInitialBoard,
|
||||||
isBoardContentPage: true,
|
isBoardContentPage: true,
|
||||||
}),
|
}),
|
||||||
page: () => {
|
page: () => {
|
||||||
return <ClientBoard />;
|
return <ClientBoard />;
|
||||||
},
|
},
|
||||||
generateMetadata: async ({
|
generateMetadataAsync: async ({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: TParams;
|
params: TParams;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { editModeAtom } from "~/components/board/editMode";
|
import { editModeAtom } from "~/components/board/editMode";
|
||||||
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
|
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
|
||||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||||
@@ -131,7 +131,7 @@ const EditModeMenu = () => {
|
|||||||
message: t("notification.success.message"),
|
message: t("notification.success.message"),
|
||||||
});
|
});
|
||||||
void utils.board.getBoardByName.invalidate({ name: board.name });
|
void utils.board.getBoardByName.invalidate({ name: board.name });
|
||||||
void revalidatePathAction(`/boards/${board.name}`);
|
void revalidatePathActionAsync(`/boards/${board.name}`);
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createBoardLayout } from "../_layout-creator";
|
|||||||
|
|
||||||
export default createBoardLayout<{ locale: string; name: string }>({
|
export default createBoardLayout<{ locale: string; name: string }>({
|
||||||
headerActions: <BoardOtherHeaderActions />,
|
headerActions: <BoardOtherHeaderActions />,
|
||||||
async getInitialBoard({ name }) {
|
async getInitialBoardAsync({ name }) {
|
||||||
return await api.board.getBoardByName({ name });
|
return await api.board.getBoardByName({ name });
|
||||||
},
|
},
|
||||||
isBoardContentPage: false,
|
isBoardContentPage: false,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const GroupSelectModal = createModal<InnerProps>(
|
|||||||
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
|
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const form = useForm<GroupSelectFormType>();
|
const form = useForm<GroupSelectFormType>();
|
||||||
const handleSubmit = async (values: GroupSelectFormType) => {
|
const handleSubmitAsync = async (values: GroupSelectFormType) => {
|
||||||
const currentGroup = groups?.find((group) => group.id === values.groupId);
|
const currentGroup = groups?.find((group) => group.id === values.groupId);
|
||||||
if (!currentGroup) return;
|
if (!currentGroup) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -38,7 +38,9 @@ export const GroupSelectModal = createModal<InnerProps>(
|
|||||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
|
||||||
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Select
|
<Select
|
||||||
{...form.getInputProps("groupId")}
|
{...form.getInputProps("groupId")}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const UserSelectModal = createModal<InnerProps>(
|
|||||||
const { data: users, isPending } = clientApi.user.selectable.useQuery();
|
const { data: users, isPending } = clientApi.user.selectable.useQuery();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const form = useForm<UserSelectFormType>();
|
const form = useForm<UserSelectFormType>();
|
||||||
const handleSubmit = async (values: UserSelectFormType) => {
|
const handleSubmitAsync = async (values: UserSelectFormType) => {
|
||||||
const currentUser = users?.find((user) => user.id === values.userId);
|
const currentUser = users?.find((user) => user.id === values.userId);
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -48,7 +48,9 @@ export const UserSelectModal = createModal<InnerProps>(
|
|||||||
const currentUser = users?.find((user) => user.id === form.values.userId);
|
const currentUser = users?.find((user) => user.id === form.values.userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
|
||||||
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Select
|
<Select
|
||||||
{...form.getInputProps("userId")}
|
{...form.getInputProps("userId")}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import type { TranslationObject } from "@homarr/translation";
|
|||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
import { getBoardPermissions } from "~/components/board/permissions/server";
|
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
|
||||||
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
|
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
|
||||||
import { AccessSettingsContent } from "./_access";
|
import { AccessSettingsContent } from "./_access";
|
||||||
import { BackgroundSettingsContent } from "./_background";
|
import { BackgroundSettingsContent } from "./_background";
|
||||||
@@ -45,10 +45,10 @@ interface Props {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBoardAndPermissions = async (params: Props["params"]) => {
|
const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
|
||||||
try {
|
try {
|
||||||
const board = await api.board.getBoardByName({ name: params.name });
|
const board = await api.board.getBoardByName({ name: params.name });
|
||||||
const { hasFullAccess } = await getBoardPermissions(board);
|
const { hasFullAccess } = await getBoardPermissionsAsync(board);
|
||||||
const permissions = hasFullAccess
|
const permissions = hasFullAccess
|
||||||
? await api.board.getBoardPermissions({ id: board.id })
|
? await api.board.getBoardPermissions({ id: board.id })
|
||||||
: {
|
: {
|
||||||
@@ -73,8 +73,8 @@ export default async function BoardSettingsPage({
|
|||||||
params,
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { board, permissions } = await getBoardAndPermissions(params);
|
const { board, permissions } = await getBoardAndPermissionsAsync(params);
|
||||||
const { hasFullAccess } = await getBoardPermissions(board);
|
const { hasFullAccess } = await getBoardPermissionsAsync(board);
|
||||||
const t = await getScopedI18n("board.setting");
|
const t = await getScopedI18n("board.setting");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import { BoardMantineProvider } from "./(content)/_theme";
|
|||||||
|
|
||||||
interface CreateBoardLayoutProps<TParams extends Params> {
|
interface CreateBoardLayoutProps<TParams extends Params> {
|
||||||
headerActions: JSX.Element;
|
headerActions: JSX.Element;
|
||||||
getInitialBoard: (params: TParams) => Promise<Board>;
|
getInitialBoardAsync: (params: TParams) => Promise<Board>;
|
||||||
isBoardContentPage: boolean;
|
isBoardContentPage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createBoardLayout = <TParams extends Params>({
|
export const createBoardLayout = <TParams extends Params>({
|
||||||
headerActions,
|
headerActions,
|
||||||
getInitialBoard,
|
getInitialBoardAsync: getInitialBoard,
|
||||||
isBoardContentPage,
|
isBoardContentPage,
|
||||||
}: CreateBoardLayoutProps<TParams>) => {
|
}: CreateBoardLayoutProps<TParams>) => {
|
||||||
const Layout = async ({
|
const Layout = async ({
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const InitUserForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (values: FormType) => {
|
const handleSubmitAsync = async (values: FormType) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
@@ -51,7 +51,7 @@ export const InitUserForm = () => {
|
|||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit(
|
onSubmit={form.onSubmit(
|
||||||
(values) => void handleSubmit(values),
|
(values) => void handleSubmitAsync(values),
|
||||||
(err) => console.log(err),
|
(err) => console.log(err),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
|
import "@homarr/ui/styles.css";
|
||||||
import "@homarr/notifications/styles.css";
|
import "@homarr/notifications/styles.css";
|
||||||
import "@homarr/spotlight/styles.css";
|
import "@homarr/spotlight/styles.css";
|
||||||
import "@mantine/core/styles.css";
|
|
||||||
|
|
||||||
import { ColorSchemeScript, createTheme, MantineProvider } from "@mantine/core";
|
import { ColorSchemeScript, createTheme, MantineProvider } from "@mantine/core";
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "../../../revalidatePathAction";
|
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
|
||||||
|
|
||||||
interface AppDeleteButtonProps {
|
interface AppDeleteButtonProps {
|
||||||
app: RouterOutputs["app"]["all"][number];
|
app: RouterOutputs["app"]["all"][number];
|
||||||
@@ -37,7 +37,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
|
|||||||
title: t("notification.success.title"),
|
title: t("notification.success.title"),
|
||||||
message: t("notification.success.message"),
|
message: t("notification.success.message"),
|
||||||
});
|
});
|
||||||
void revalidatePathAction("/manage/apps");
|
void revalidatePathActionAsync("/manage/apps");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { TranslationFunction } from "@homarr/translation";
|
|||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { validation, z } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { AppForm } from "../../_form";
|
import { AppForm } from "../../_form";
|
||||||
|
|
||||||
interface AppEditFormProps {
|
interface AppEditFormProps {
|
||||||
@@ -30,7 +30,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
|
|||||||
title: t("success.title"),
|
title: t("success.title"),
|
||||||
message: t("success.message"),
|
message: t("success.message"),
|
||||||
});
|
});
|
||||||
void revalidatePathAction("/manage/apps").then(() => {
|
void revalidatePathActionAsync("/manage/apps").then(() => {
|
||||||
router.push("/manage/apps");
|
router.push("/manage/apps");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { TranslationFunction } from "@homarr/translation";
|
|||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { validation, z } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { AppForm } from "../_form";
|
import { AppForm } from "../_form";
|
||||||
|
|
||||||
export const AppNewForm = () => {
|
export const AppNewForm = () => {
|
||||||
@@ -25,7 +25,7 @@ export const AppNewForm = () => {
|
|||||||
title: t("success.title"),
|
title: t("success.title"),
|
||||||
message: t("success.message"),
|
message: t("success.message"),
|
||||||
});
|
});
|
||||||
void revalidatePathAction("/manage/apps").then(() => {
|
void revalidatePathActionAsync("/manage/apps").then(() => {
|
||||||
router.push("/manage/apps");
|
router.push("/manage/apps");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { useConfirmModal } from "@homarr/modals";
|
import { useConfirmModal } from "@homarr/modals";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||||
|
|
||||||
const iconProps = {
|
const iconProps = {
|
||||||
@@ -42,7 +42,7 @@ export const BoardCardMenuDropdown = ({
|
|||||||
|
|
||||||
const { mutateAsync, isPending } = clientApi.board.deleteBoard.useMutation({
|
const { mutateAsync, isPending } = clientApi.board.deleteBoard.useMutation({
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await revalidatePathAction("/manage/boards");
|
await revalidatePathActionAsync("/manage/boards");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@ export const BoardCardMenuDropdown = ({
|
|||||||
children: t("delete.confirm.description", {
|
children: t("delete.confirm.description", {
|
||||||
name: board.name,
|
name: board.name,
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
id: board.id,
|
id: board.id,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
||||||
|
|
||||||
interface CreateBoardButtonProps {
|
interface CreateBoardButtonProps {
|
||||||
@@ -21,7 +21,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
|||||||
|
|
||||||
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
|
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await revalidatePathAction("/manage/boards");
|
await revalidatePathActionAsync("/manage/boards");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { api } from "@homarr/api/server";
|
|||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
import { UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
import { getBoardPermissions } from "~/components/board/permissions/server";
|
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
|
||||||
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
||||||
import { CreateBoardButton } from "./_components/create-board-button";
|
import { CreateBoardButton } from "./_components/create-board-button";
|
||||||
|
|
||||||
@@ -53,7 +53,8 @@ interface BoardCardProps {
|
|||||||
|
|
||||||
const BoardCard = async ({ board }: BoardCardProps) => {
|
const BoardCard = async ({ board }: BoardCardProps) => {
|
||||||
const t = await getScopedI18n("management.page.board");
|
const t = await getScopedI18n("management.page.board");
|
||||||
const { hasChangeAccess: isMenuVisible } = await getBoardPermissions(board);
|
const { hasChangeAccess: isMenuVisible } =
|
||||||
|
await getBoardPermissionsAsync(board);
|
||||||
const visibility = board.isPublic ? "public" : "private";
|
const visibility = board.isPublic ? "public" : "private";
|
||||||
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
|
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "../../../revalidatePathAction";
|
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
|
||||||
|
|
||||||
interface DeleteIntegrationActionButtonProps {
|
interface DeleteIntegrationActionButtonProps {
|
||||||
count: number;
|
count: number;
|
||||||
@@ -49,7 +49,7 @@ export const DeleteIntegrationActionButton = ({
|
|||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
router.replace("/manage/integrations");
|
router.replace("/manage/integrations");
|
||||||
}
|
}
|
||||||
void revalidatePathAction("/manage/integrations");
|
void revalidatePathActionAsync("/manage/integrations");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import type { z } from "@homarr/validation";
|
import type { z } from "@homarr/validation";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { SecretCard } from "../../_integration-secret-card";
|
import { SecretCard } from "../../_integration-secret-card";
|
||||||
import { IntegrationSecretInput } from "../../_integration-secret-inputs";
|
import { IntegrationSecretInput } from "../../_integration-secret-inputs";
|
||||||
import {
|
import {
|
||||||
@@ -66,7 +66,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
integration.secrets.map((secret) => [secret.kind, secret]),
|
integration.secrets.map((secret) => [secret.kind, secret]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async (values: FormType) => {
|
const handleSubmitAsync = async (values: FormType) => {
|
||||||
if (isDirty) return;
|
if (isDirty) return;
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
@@ -83,7 +83,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
title: t("integration.page.edit.notification.success.title"),
|
title: t("integration.page.edit.notification.success.title"),
|
||||||
message: t("integration.page.edit.notification.success.message"),
|
message: t("integration.page.edit.notification.success.message"),
|
||||||
});
|
});
|
||||||
void revalidatePathAction("/manage/integrations").then(() =>
|
void revalidatePathActionAsync("/manage/integrations").then(() =>
|
||||||
router.push("/manage/integrations"),
|
router.push("/manage/integrations"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -98,7 +98,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
|
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TestConnectionNoticeAlert />
|
<TestConnectionNoticeAlert />
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
TestConnectionNoticeAlert,
|
TestConnectionNoticeAlert,
|
||||||
useTestConnectionDirty,
|
useTestConnectionDirty,
|
||||||
} from "../_integration-test-connection";
|
} from "../_integration-test-connection";
|
||||||
import { revalidatePathAction } from "../../../../revalidatePathAction";
|
import { revalidatePathActionAsync } from "../../../../revalidatePathAction";
|
||||||
|
|
||||||
interface NewIntegrationFormProps {
|
interface NewIntegrationFormProps {
|
||||||
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
|
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
|
||||||
@@ -67,7 +67,7 @@ export const NewIntegrationForm = ({
|
|||||||
});
|
});
|
||||||
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
||||||
|
|
||||||
const handleSubmit = async (values: FormType) => {
|
const handleSubmitAsync = async (values: FormType) => {
|
||||||
if (isDirty) return;
|
if (isDirty) return;
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@ export const NewIntegrationForm = ({
|
|||||||
title: t("integration.page.create.notification.success.title"),
|
title: t("integration.page.create.notification.success.title"),
|
||||||
message: t("integration.page.create.notification.success.message"),
|
message: t("integration.page.create.notification.success.message"),
|
||||||
});
|
});
|
||||||
void revalidatePathAction("/manage/integrations").then(() =>
|
void revalidatePathActionAsync("/manage/integrations").then(() =>
|
||||||
router.push("/manage/integrations"),
|
router.push("/manage/integrations"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -95,7 +95,7 @@ export const NewIntegrationForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((value) => void handleSubmit(value))}>
|
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TestConnectionNoticeAlert />
|
<TestConnectionNoticeAlert />
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { useConfirmModal } from "@homarr/modals";
|
import { useConfirmModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface DeleteUserButtonProps {
|
interface DeleteUserButtonProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -21,7 +21,7 @@ export const DeleteUserButton = ({ user }: DeleteUserButtonProps) => {
|
|||||||
const { mutateAsync: mutateUserDeletionAsync } =
|
const { mutateAsync: mutateUserDeletionAsync } =
|
||||||
clientApi.user.delete.useMutation({
|
clientApi.user.delete.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await revalidatePathAction("/manage/users").then(() =>
|
await revalidatePathActionAsync("/manage/users").then(() =>
|
||||||
router.push("/manage/users"),
|
router.push("/manage/users"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -33,6 +33,7 @@ export const DeleteUserButton = ({ user }: DeleteUserButtonProps) => {
|
|||||||
openConfirmModal({
|
openConfirmModal({
|
||||||
title: t("user.action.delete.label"),
|
title: t("user.action.delete.label"),
|
||||||
children: t("user.action.delete.confirm", { username: user.name }),
|
children: t("user.action.delete.confirm", { username: user.name }),
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await mutateUserDeletionAsync(user.id);
|
await mutateUserDeletionAsync(user.id);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface UserProfileAvatarForm {
|
interface UserProfileAvatarForm {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -44,7 +44,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
{
|
{
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
// Revalidate all as the avatar is used in multiple places
|
// Revalidate all as the avatar is used in multiple places
|
||||||
await revalidatePathAction("/");
|
await revalidatePathActionAsync("/");
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: tManageAvatar(
|
message: tManageAvatar(
|
||||||
"changeImage.notification.success.message",
|
"changeImage.notification.success.message",
|
||||||
@@ -87,7 +87,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
{
|
{
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
// Revalidate all as the avatar is used in multiple places
|
// Revalidate all as the avatar is used in multiple places
|
||||||
await revalidatePathAction("/");
|
await revalidatePathActionAsync("/");
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: tManageAvatar(
|
message: tManageAvatar(
|
||||||
"removeImage.notification.success.message",
|
"removeImage.notification.success.message",
|
||||||
@@ -161,7 +161,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileToBase64Async = (file: File): Promise<string> =>
|
const fileToBase64Async = async (file: File): Promise<string> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface UserProfileFormProps {
|
interface UserProfileFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -32,7 +32,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
|
|||||||
});
|
});
|
||||||
const { mutate, isPending } = clientApi.user.editProfile.useMutation({
|
const { mutate, isPending } = clientApi.user.editProfile.useMutation({
|
||||||
async onSettled() {
|
async onSettled() {
|
||||||
await revalidatePathAction("/manage/users");
|
await revalidatePathActionAsync("/manage/users");
|
||||||
},
|
},
|
||||||
onSuccess(_, variables) {
|
onSuccess(_, variables) {
|
||||||
// Reset form initial values to reset dirty state
|
// Reset form initial values to reset dirty state
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface ChangePasswordFormProps {
|
interface ChangePasswordFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -24,7 +24,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.user.changePassword.useMutation({
|
const { mutate, isPending } = clientApi.user.changePassword.useMutation({
|
||||||
async onSettled() {
|
async onSettled() {
|
||||||
await revalidatePathAction(`/manage/users/${user.id}`);
|
await revalidatePathActionAsync(`/manage/users/${user.id}`);
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface DeleteGroupProps {
|
interface DeleteGroupProps {
|
||||||
group: {
|
group: {
|
||||||
@@ -34,6 +34,7 @@ export const DeleteGroup = ({ group }: DeleteGroupProps) => {
|
|||||||
children: tDelete("confirm", {
|
children: tDelete("confirm", {
|
||||||
name: group.name,
|
name: group.name,
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
@@ -41,7 +42,7 @@ export const DeleteGroup = ({ group }: DeleteGroupProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
void revalidatePathAction("/manage/users/groups");
|
void revalidatePathActionAsync("/manage/users/groups");
|
||||||
router.push("/manage/users/groups");
|
router.push("/manage/users/groups");
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: tRoot("common.notification.delete.success"),
|
title: tRoot("common.notification.delete.success"),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface RenameGroupFormProps {
|
interface RenameGroupFormProps {
|
||||||
group: {
|
group: {
|
||||||
@@ -38,7 +38,7 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
void revalidatePathAction(`/users/groups/${group.id}`);
|
void revalidatePathActionAsync(`/users/groups/${group.id}`);
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("common.notification.update.success"),
|
title: t("common.notification.update.success"),
|
||||||
message: t("group.action.update.notification.success.message", {
|
message: t("group.action.update.notification.success.message", {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const TransferGroupOwnership = ({
|
|||||||
name: group.name,
|
name: group.name,
|
||||||
username: name,
|
username: name,
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useModalAction } from "@homarr/modals";
|
|||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { UserSelectModal } from "~/app/[locale]/boards/[name]/settings/_access/user-select-modal";
|
import { UserSelectModal } from "~/app/[locale]/boards/[name]/settings/_access/user-select-modal";
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface AddGroupMemberProps {
|
interface AddGroupMemberProps {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
@@ -26,12 +26,13 @@ export const AddGroupMember = ({
|
|||||||
const handleAddMember = useCallback(() => {
|
const handleAddMember = useCallback(() => {
|
||||||
openModal(
|
openModal(
|
||||||
{
|
{
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async onSelect({ id }) {
|
async onSelect({ id }) {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
userId: id,
|
userId: id,
|
||||||
groupId,
|
groupId,
|
||||||
});
|
});
|
||||||
await revalidatePathAction(
|
await revalidatePathActionAsync(
|
||||||
`/manage/users/groups/${groupId}}/members`,
|
`/manage/users/groups/${groupId}}/members`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { useConfirmModal } from "@homarr/modals";
|
import { useConfirmModal } from "@homarr/modals";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
interface RemoveGroupMemberProps {
|
interface RemoveGroupMemberProps {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
@@ -29,12 +29,15 @@ export const RemoveGroupMember = ({
|
|||||||
children: tRemoveMember("confirm", {
|
children: tRemoveMember("confirm", {
|
||||||
user: user.name ?? "",
|
user: user.name ?? "",
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
groupId,
|
groupId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
await revalidatePathAction(`/manage/users/groups/${groupId}/members`);
|
await revalidatePathActionAsync(
|
||||||
|
`/manage/users/groups/${groupId}/members`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
export const AddGroup = () => {
|
export const AddGroup = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -44,7 +44,7 @@ const AddGroupModal = createModal<void>(({ actions }) => {
|
|||||||
mutate(values, {
|
mutate(values, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
actions.closeModal();
|
actions.closeModal();
|
||||||
void revalidatePathAction("/manage/users/groups");
|
void revalidatePathActionAsync("/manage/users/groups");
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("common.notification.create.success"),
|
title: t("common.notification.create.success"),
|
||||||
message: t("group.action.create.notification.success.message"),
|
message: t("group.action.create.notification.success.message"),
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function revalidatePathAction(path: string) {
|
export async function revalidatePathActionAsync(path: string) {
|
||||||
return new Promise((resolve) => resolve(revalidatePath(path, "page")));
|
return new Promise((resolve) => resolve(revalidatePath(path, "page")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { auth } from "@homarr/auth/next";
|
|||||||
import type { BoardPermissionsProps } from "@homarr/auth/shared";
|
import type { BoardPermissionsProps } from "@homarr/auth/shared";
|
||||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||||
|
|
||||||
export const getBoardPermissions = async (board: BoardPermissionsProps) => {
|
export const getBoardPermissionsAsync = async (
|
||||||
|
board: BoardPermissionsProps,
|
||||||
|
) => {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
return constructBoardPermissions(board, session);
|
return constructBoardPermissions(board, session);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,14 +87,15 @@ export const iconsUpdaterJob = createCronJob(EVERY_WEEK, {
|
|||||||
if (newIcons.length >= 1) {
|
if (newIcons.length >= 1) {
|
||||||
await transaction.insert(icons).values(newIcons);
|
await transaction.insert(icons).values(newIcons);
|
||||||
}
|
}
|
||||||
await transaction.delete(icons).where(
|
if (deadIcons.length >= 1) {
|
||||||
deadIcons.length >= 1
|
await transaction.delete(icons).where(
|
||||||
? inArray(
|
inArray(
|
||||||
icons.checksum,
|
icons.checksum,
|
||||||
deadIcons.map((icon) => icon.checksum),
|
deadIcons.map((icon) => icon.checksum),
|
||||||
)
|
),
|
||||||
: undefined,
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
countDeleted += deadIcons.length;
|
countDeleted += deadIcons.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { EVERY_MINUTE } from "../lib/cron-job/constants";
|
import { EVERY_MINUTE } from "../lib/cron-job/constants";
|
||||||
import { createCronJob } from "../lib/cron-job/creator";
|
import { createCronJob } from "../lib/cron-job/creator";
|
||||||
import { queueWorker } from "../lib/queue/worker";
|
import { queueWorkerAsync } from "../lib/queue/worker";
|
||||||
|
|
||||||
// This job processes queues, it runs every minute.
|
// This job processes queues, it runs every minute.
|
||||||
export const queuesJob = createCronJob(EVERY_MINUTE).withCallback(async () => {
|
export const queuesJob = createCronJob(EVERY_MINUTE).withCallback(async () => {
|
||||||
await queueWorker();
|
await queueWorkerAsync();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const createQueueClient = <TQueues extends Queues>(queues: TQueues) => {
|
|||||||
const queue = queueRegistry.get(name);
|
const queue = queueRegistry.get(name);
|
||||||
if (!queue) return;
|
if (!queue) return;
|
||||||
|
|
||||||
await queueChannel.add({
|
await queueChannel.addAsync({
|
||||||
name,
|
name,
|
||||||
data,
|
data,
|
||||||
executionDate:
|
executionDate:
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import { queueRegistry } from "~/queues";
|
|||||||
* This function reads all the queue executions that are due and processes them.
|
* This function reads all the queue executions that are due and processes them.
|
||||||
* Those executions are stored in the redis queue channel.
|
* Those executions are stored in the redis queue channel.
|
||||||
*/
|
*/
|
||||||
export const queueWorker = async () => {
|
export const queueWorkerAsync = async () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const executions = await queueChannel.filter((item) => {
|
const executions = await queueChannel.filterAsync((item) => {
|
||||||
return item.executionDate < now;
|
return item.executionDate < now;
|
||||||
});
|
});
|
||||||
for (const execution of executions) {
|
for (const execution of executions) {
|
||||||
const queue = queueRegistry.get(execution.name);
|
const queue = queueRegistry.get(execution.name);
|
||||||
if (!queue) continue;
|
if (!queue) continue;
|
||||||
await queue.callback(execution.data);
|
await queue.callback(execution.data);
|
||||||
await queueChannel.markAsDone(execution._id);
|
await queueChannel.markAsDoneAsync(execution._id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const wss = new WebSocketServer({
|
|||||||
const handler = applyWSSHandler({
|
const handler = applyWSSHandler({
|
||||||
wss,
|
wss,
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
|
// ignore error on next line because the createContext must be set with this name
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
createContext: async ({ req }) => {
|
createContext: async ({ req }) => {
|
||||||
try {
|
try {
|
||||||
const headers = Object.entries(req.headers).map(
|
const headers = Object.entries(req.headers).map(
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
"full-access",
|
"full-access",
|
||||||
);
|
);
|
||||||
|
|
||||||
await noBoardWithSimilarName(ctx.db, input.name, [input.id]);
|
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(boards)
|
.update(boards)
|
||||||
@@ -164,7 +164,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const boardWhere = eq(boards.name, "default");
|
const boardWhere = eq(boards.name, "default");
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
||||||
|
|
||||||
return await getFullBoardWithWhere(
|
return await getFullBoardWithWhereAsync(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
boardWhere,
|
boardWhere,
|
||||||
ctx.session?.user.id ?? null,
|
ctx.session?.user.id ?? null,
|
||||||
@@ -176,7 +176,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const boardWhere = eq(boards.name, input.name);
|
const boardWhere = eq(boards.name, input.name);
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
|
||||||
|
|
||||||
return await getFullBoardWithWhere(
|
return await getFullBoardWithWhereAsync(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
boardWhere,
|
boardWhere,
|
||||||
ctx.session?.user.id ?? null,
|
ctx.session?.user.id ?? null,
|
||||||
@@ -229,7 +229,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
await ctx.db.transaction(async (transaction) => {
|
await ctx.db.transaction(async (transaction) => {
|
||||||
const dbBoard = await getFullBoardWithWhere(
|
const dbBoard = await getFullBoardWithWhereAsync(
|
||||||
transaction,
|
transaction,
|
||||||
eq(boards.id, input.id),
|
eq(boards.id, input.id),
|
||||||
ctx.session.user.id,
|
ctx.session.user.id,
|
||||||
@@ -523,7 +523,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const noBoardWithSimilarName = async (
|
const noBoardWithSimilarNameAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
name: string,
|
name: string,
|
||||||
ignoredIds: string[] = [],
|
ignoredIds: string[] = [],
|
||||||
@@ -549,7 +549,7 @@ const noBoardWithSimilarName = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFullBoardWithWhere = async (
|
const getFullBoardWithWhereAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
where: SQL<unknown>,
|
where: SQL<unknown>,
|
||||||
userId: string | null,
|
userId: string | null,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.input(validation.group.create)
|
.input(validation.group.create)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const normalizedName = normalizeName(input.name);
|
const normalizedName = normalizeName(input.name);
|
||||||
await checkSimilarNameAndThrow(ctx.db, normalizedName);
|
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName);
|
||||||
|
|
||||||
const id = createId();
|
const id = createId();
|
||||||
await ctx.db.insert(groups).values({
|
await ctx.db.insert(groups).values({
|
||||||
@@ -123,7 +123,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
|
|
||||||
const normalizedName = normalizeName(input.name);
|
const normalizedName = normalizeName(input.name);
|
||||||
await checkSimilarNameAndThrow(ctx.db, normalizedName, input.id);
|
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id);
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(groups)
|
.update(groups)
|
||||||
@@ -206,7 +206,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const normalizeName = (name: string) => name.trim();
|
const normalizeName = (name: string) => name.trim();
|
||||||
|
|
||||||
const checkSimilarNameAndThrow = async (
|
const checkSimilarNameAndThrowAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
name: string,
|
name: string,
|
||||||
ignoreId?: string,
|
ignoreId?: string,
|
||||||
|
|||||||
@@ -139,9 +139,9 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
(secret) => secret.kind === changedSecret.kind,
|
(secret) => secret.kind === changedSecret.kind,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await addSecret(ctx.db, secretInput);
|
await addSecretAsync(ctx.db, secretInput);
|
||||||
} else {
|
} else {
|
||||||
await updateSecret(ctx.db, secretInput);
|
await updateSecretAsync(ctx.db, secretInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ interface UpdateSecretInput {
|
|||||||
value: string;
|
value: string;
|
||||||
kind: IntegrationSecretKind;
|
kind: IntegrationSecretKind;
|
||||||
}
|
}
|
||||||
const updateSecret = async (db: Database, input: UpdateSecretInput) => {
|
const updateSecretAsync = async (db: Database, input: UpdateSecretInput) => {
|
||||||
await db
|
await db
|
||||||
.update(integrationSecrets)
|
.update(integrationSecrets)
|
||||||
.set({
|
.set({
|
||||||
@@ -296,7 +296,7 @@ interface AddSecretInput {
|
|||||||
value: string;
|
value: string;
|
||||||
kind: IntegrationSecretKind;
|
kind: IntegrationSecretKind;
|
||||||
}
|
}
|
||||||
const addSecret = async (db: Database, input: AddSecretInput) => {
|
const addSecretAsync = async (db: Database, input: AddSecretInput) => {
|
||||||
await db.insert(integrationSecrets).values({
|
await db.insert(integrationSecrets).values({
|
||||||
kind: input.kind,
|
kind: input.kind,
|
||||||
value: encryptSecret(input.value),
|
value: encryptSecret(input.value),
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ describe("byId should return an app by id", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () => await caller.byId({ id: "2" });
|
const actAsync = async () => await caller.byId({ id: "2" });
|
||||||
await expect(act()).rejects.toThrow("App not found");
|
await expect(actAsync()).rejects.toThrow("App not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ describe("update should update an app", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.update({
|
await caller.update({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
name: "Mantine",
|
name: "Mantine",
|
||||||
@@ -182,7 +182,7 @@ describe("update should update an app", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
href: null,
|
href: null,
|
||||||
});
|
});
|
||||||
await expect(act()).rejects.toThrow("App not found");
|
await expect(actAsync()).rejects.toThrow("App not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const defaultSession = {
|
|||||||
// Mock the auth module to return an empty session
|
// Mock the auth module to return an empty session
|
||||||
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||||
|
|
||||||
const createRandomUser = async (db: Database) => {
|
const createRandomUserAsync = async (db: Database) => {
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -51,8 +51,8 @@ describe("getAllBoards should return all boards accessable to the current user",
|
|||||||
const db = createDb();
|
const db = createDb();
|
||||||
const caller = boardRouter.createCaller({ db, session: null });
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
const user2 = await createRandomUser(db);
|
const user2 = await createRandomUserAsync(db);
|
||||||
|
|
||||||
await db.insert(boards).values([
|
await db.insert(boards).values([
|
||||||
{
|
{
|
||||||
@@ -91,8 +91,8 @@ describe("getAllBoards should return all boards accessable to the current user",
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
const user2 = await createRandomUser(db);
|
const user2 = await createRandomUserAsync(db);
|
||||||
|
|
||||||
await db.insert(boards).values([
|
await db.insert(boards).values([
|
||||||
{
|
{
|
||||||
@@ -122,8 +122,8 @@ describe("getAllBoards should return all boards accessable to the current user",
|
|||||||
const db = createDb();
|
const db = createDb();
|
||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
const user2 = await createRandomUser(db);
|
const user2 = await createRandomUserAsync(db);
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
});
|
});
|
||||||
@@ -170,8 +170,8 @@ describe("getAllBoards should return all boards accessable to the current user",
|
|||||||
session: defaultSession,
|
session: defaultSession,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
const user2 = await createRandomUser(db);
|
const user2 = await createRandomUserAsync(db);
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
});
|
});
|
||||||
@@ -237,8 +237,8 @@ describe("getAllBoards should return all boards accessable to the current user",
|
|||||||
session: defaultSession,
|
session: defaultSession,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
const user2 = await createRandomUser(db);
|
const user2 = await createRandomUserAsync(db);
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
});
|
});
|
||||||
@@ -322,10 +322,10 @@ describe("createBoard should create a new board", () => {
|
|||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () => await caller.createBoard({ name: "newBoard" });
|
const actAsync = async () => await caller.createBoard({ name: "newBoard" });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrowError("Permission denied");
|
await expect(actAsync()).rejects.toThrowError("Permission denied");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,11 +383,11 @@ describe("rename board should rename board", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.renameBoard({ id: boardId, name: "Newname" });
|
await caller.renameBoard({ id: boardId, name: "Newname" });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrowError(
|
await expect(actAsync()).rejects.toThrowError(
|
||||||
"Board with similar name already exists",
|
"Board with similar name already exists",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -398,11 +398,11 @@ describe("rename board should rename board", () => {
|
|||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.renameBoard({ id: "nonExistentBoardId", name: "newName" });
|
await caller.renameBoard({ id: "nonExistentBoardId", name: "newName" });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrowError("Board not found");
|
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -485,11 +485,11 @@ describe("deleteBoard should delete board", () => {
|
|||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.deleteBoard({ id: "nonExistentBoardId" });
|
await caller.deleteBoard({ id: "nonExistentBoardId" });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrowError("Board not found");
|
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -552,11 +552,11 @@ describe("getBoardByName should return board by name", () => {
|
|||||||
await createFullBoardAsync(db, "default");
|
await createFullBoardAsync(db, "default");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.getBoardByName({ name: "nonExistentBoard" });
|
await caller.getBoardByName({ name: "nonExistentBoard" });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrowError("Board not found");
|
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -633,7 +633,7 @@ describe("savePartialBoardSettings should save general settings", () => {
|
|||||||
const db = createDb();
|
const db = createDb();
|
||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.savePartialBoardSettings({
|
await caller.savePartialBoardSettings({
|
||||||
pageTitle: "newPageTitle",
|
pageTitle: "newPageTitle",
|
||||||
metaTitle: "newMetaTitle",
|
metaTitle: "newMetaTitle",
|
||||||
@@ -642,7 +642,7 @@ describe("savePartialBoardSettings should save general settings", () => {
|
|||||||
id: "nonExistentBoardId",
|
id: "nonExistentBoardId",
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(act()).rejects.toThrowError("Board not found");
|
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1151,13 +1151,13 @@ describe("saveBoard should save full board", () => {
|
|||||||
const db = createDb();
|
const db = createDb();
|
||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.saveBoard({
|
await caller.saveBoard({
|
||||||
id: "nonExistentBoardId",
|
id: "nonExistentBoardId",
|
||||||
sections: [],
|
sections: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(act()).rejects.toThrowError("Board not found");
|
await expect(actAsync()).rejects.toThrowError("Board not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1168,8 +1168,8 @@ describe("getBoardPermissions should return board permissions", () => {
|
|||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
const user2 = await createRandomUser(db);
|
const user2 = await createRandomUserAsync(db);
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
});
|
});
|
||||||
@@ -1250,7 +1250,7 @@ describe("saveUserBoardPermissions should save user board permissions", () => {
|
|||||||
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
const caller = boardRouter.createCaller({ db, session: defaultSession });
|
||||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||||
|
|
||||||
const user1 = await createRandomUser(db);
|
const user1 = await createRandomUserAsync(db);
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultCreatorId,
|
id: defaultCreatorId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { throwIfActionForbiddenAsync } from "../../board/board-access";
|
|||||||
|
|
||||||
const defaultCreatorId = createId();
|
const defaultCreatorId = createId();
|
||||||
|
|
||||||
const expectActToBe = async (act: () => Promise<void>, success: boolean) => {
|
const expectActToBeAsync = async (
|
||||||
|
act: () => Promise<void>,
|
||||||
|
success: boolean,
|
||||||
|
) => {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await expect(act()).rejects.toThrow("Board not found");
|
await expect(act()).rejects.toThrow("Board not found");
|
||||||
return;
|
return;
|
||||||
@@ -55,7 +58,7 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expectActToBe(act, expectedResult);
|
await expectActToBeAsync(act, expectedResult);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expectActToBe(act, expectedResult);
|
await expectActToBeAsync(act, expectedResult);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -129,7 +132,7 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expectActToBe(act, expectedResult);
|
await expectActToBeAsync(act, expectedResult);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -166,7 +169,7 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expectActToBe(act, expectedResult);
|
await expectActToBeAsync(act, expectedResult);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -196,10 +196,10 @@ describe("byId should return group by id including members and permissions", ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () => await caller.getById({ id: "1" });
|
const actAsync = async () => await caller.getById({ id: "1" });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Group not found");
|
await expect(actAsync()).rejects.toThrow("Group not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,13 +235,13 @@ describe("create should create group in database", () => {
|
|||||||
const longName = "a".repeat(65);
|
const longName = "a".repeat(65);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.createGroup({
|
await caller.createGroup({
|
||||||
name: longName,
|
name: longName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("too_big");
|
await expect(actAsync()).rejects.toThrow("too_big");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
@@ -262,10 +262,11 @@ describe("create should create group in database", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () => await caller.createGroup({ name: nameToCreate });
|
const actAsync = async () =>
|
||||||
|
await caller.createGroup({ name: nameToCreate });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("similar name");
|
await expect(actAsync()).rejects.toThrow("similar name");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -330,14 +331,14 @@ describe("update should update name with value that is no duplicate", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.updateGroup({
|
await caller.updateGroup({
|
||||||
id: groupId,
|
id: groupId,
|
||||||
name: updateValue,
|
name: updateValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("similar name");
|
await expect(actAsync()).rejects.toThrow("similar name");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -408,14 +409,14 @@ describe("savePermissions should save permissions for group", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.savePermissions({
|
await caller.savePermissions({
|
||||||
groupId: createId(),
|
groupId: createId(),
|
||||||
permissions: ["integration-create", "board-full-access"],
|
permissions: ["integration-create", "board-full-access"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Group not found");
|
await expect(actAsync()).rejects.toThrow("Group not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -468,14 +469,14 @@ describe("transferOwnership should transfer ownership of group", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.transferOwnership({
|
await caller.transferOwnership({
|
||||||
groupId: createId(),
|
groupId: createId(),
|
||||||
userId: createId(),
|
userId: createId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Group not found");
|
await expect(actAsync()).rejects.toThrow("Group not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -520,13 +521,13 @@ describe("deleteGroup should delete group", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.deleteGroup({
|
await caller.deleteGroup({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Group not found");
|
await expect(actAsync()).rejects.toThrow("Group not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -580,14 +581,14 @@ describe("addMember should add member to group", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.addMember({
|
await caller.addMember({
|
||||||
groupId: createId(),
|
groupId: createId(),
|
||||||
userId: createId(),
|
userId: createId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Group not found");
|
await expect(actAsync()).rejects.toThrow("Group not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -644,14 +645,14 @@ describe("removeMember should remove member from group", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.removeMember({
|
await caller.removeMember({
|
||||||
groupId: createId(),
|
groupId: createId(),
|
||||||
userId: createId(),
|
userId: createId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Group not found");
|
await expect(actAsync()).rejects.toThrow("Group not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ describe("byId should return an integration by id", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () => await caller.byId({ id: "2" });
|
const actAsync = async () => await caller.byId({ id: "2" });
|
||||||
await expect(act()).rejects.toThrow("Integration not found");
|
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should only return the public secret values", async () => {
|
it("should only return the public secret values", async () => {
|
||||||
@@ -258,14 +258,14 @@ describe("update should update an integration", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.update({
|
await caller.update({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
name: "Pi Hole",
|
name: "Pi Hole",
|
||||||
url: "http://hole.local",
|
url: "http://hole.local",
|
||||||
secrets: [],
|
secrets: [],
|
||||||
});
|
});
|
||||||
await expect(act()).rejects.toThrow("Integration not found");
|
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,8 +343,8 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
secrets,
|
secrets,
|
||||||
};
|
};
|
||||||
|
|
||||||
const act = async () => await caller.testConnection(input);
|
const actAsync = async () => await caller.testConnection(input);
|
||||||
await expect(act()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -373,8 +373,8 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
secrets,
|
secrets,
|
||||||
};
|
};
|
||||||
|
|
||||||
const act = async () => await caller.testConnection(input);
|
const actAsync = async () => await caller.testConnection(input);
|
||||||
await expect(act()).resolves.toBeUndefined();
|
await expect(actAsync()).resolves.toBeUndefined();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -395,8 +395,8 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const act = async () => await caller.testConnection(input);
|
const actAsync = async () => await caller.testConnection(input);
|
||||||
await expect(act()).resolves.toBeUndefined();
|
await expect(actAsync()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be successful when overriding one of the secrets for an existing nzbGet integration", async () => {
|
it("should be successful when overriding one of the secrets for an existing nzbGet integration", async () => {
|
||||||
@@ -439,8 +439,8 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const act = async () => await caller.testConnection(input);
|
const actAsync = async () => await caller.testConnection(input);
|
||||||
await expect(act()).resolves.toBeUndefined();
|
await expect(actAsync()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail when a required secret is missing for an existing nzbGet integration", async () => {
|
it("should fail when a required secret is missing for an existing nzbGet integration", async () => {
|
||||||
@@ -477,8 +477,8 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const act = async () => await caller.testConnection(input);
|
const actAsync = async () => await caller.testConnection(input);
|
||||||
await expect(act()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail when the updating integration does not exist", async () => {
|
it("should fail when the updating integration does not exist", async () => {
|
||||||
@@ -488,7 +488,7 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.testConnection({
|
await caller.testConnection({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
kind: "nzbGet",
|
kind: "nzbGet",
|
||||||
@@ -498,6 +498,6 @@ describe("testConnection should test the connection to an integration", () => {
|
|||||||
{ kind: "password", value: "Password123!" },
|
{ kind: "password", value: "Password123!" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
await expect(act()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -183,9 +183,9 @@ describe("delete should remove invite by id", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () => await caller.deleteInvite({ id: createId() });
|
const actAsync = async () => await caller.deleteInvite({ id: createId() });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("not found");
|
await expect(actAsync()).rejects.toThrow("not found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ describe("initUser should initialize the first user", () => {
|
|||||||
password: "test",
|
password: "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.initUser({
|
await caller.initUser({
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "12345678",
|
password: "12345678",
|
||||||
confirmPassword: "12345678",
|
confirmPassword: "12345678",
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(act()).rejects.toThrow("User already exists");
|
await expect(actAsync()).rejects.toThrow("User already exists");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create a user if none exists", async () => {
|
it("should create a user if none exists", async () => {
|
||||||
@@ -66,14 +66,14 @@ describe("initUser should initialize the first user", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.initUser({
|
await caller.initUser({
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "12345678",
|
password: "12345678",
|
||||||
confirmPassword: "12345679",
|
confirmPassword: "12345679",
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(act()).rejects.toThrow("Passwords do not match");
|
await expect(actAsync()).rejects.toThrow("Passwords do not match");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not create a user if the password is too short", async () => {
|
it("should not create a user if the password is too short", async () => {
|
||||||
@@ -83,14 +83,14 @@ describe("initUser should initialize the first user", () => {
|
|||||||
session: null,
|
session: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.initUser({
|
await caller.initUser({
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "1234567",
|
password: "1234567",
|
||||||
confirmPassword: "1234567",
|
confirmPassword: "1234567",
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(act()).rejects.toThrow("too_small");
|
await expect(actAsync()).rejects.toThrow("too_small");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ describe("register should create a user with valid invitation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const act = async () =>
|
const actAsync = async () =>
|
||||||
await caller.register({
|
await caller.register({
|
||||||
inviteId,
|
inviteId,
|
||||||
token: inviteToken,
|
token: inviteToken,
|
||||||
@@ -186,7 +186,7 @@ describe("register should create a user with valid invitation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(act()).rejects.toThrow("Invalid invite");
|
await expect(actAsync()).rejects.toThrow("Invalid invite");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
import { createSalt, hashPassword } from "@homarr/auth";
|
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, createId, eq, schema } from "@homarr/db";
|
import { and, createId, eq, schema } from "@homarr/db";
|
||||||
import { invites, users } from "@homarr/db/schema/sqlite";
|
import { invites, users } from "@homarr/db/schema/sqlite";
|
||||||
@@ -27,7 +27,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await createUser(ctx.db, input);
|
await createUserAsync(ctx.db, input);
|
||||||
}),
|
}),
|
||||||
register: publicProcedure
|
register: publicProcedure
|
||||||
.input(validation.user.registrationApi)
|
.input(validation.user.registrationApi)
|
||||||
@@ -53,7 +53,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
|
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
|
||||||
|
|
||||||
await createUser(ctx.db, input);
|
await createUserAsync(ctx.db, input);
|
||||||
|
await createUserAsync(ctx.db, input);
|
||||||
// Delete invite as it's used
|
// Delete invite as it's used
|
||||||
await ctx.db.delete(invites).where(inviteWhere);
|
await ctx.db.delete(invites).where(inviteWhere);
|
||||||
}),
|
}),
|
||||||
@@ -62,8 +63,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
|
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
|
||||||
|
|
||||||
await createUser(ctx.db, input);
|
await createUserAsync(ctx.db, input);
|
||||||
}),
|
}),
|
||||||
setProfileImage: protectedProcedure
|
setProfileImage: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -219,7 +220,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousPasswordHash = await hashPassword(
|
const previousPasswordHash = await hashPasswordAsync(
|
||||||
input.previousPassword,
|
input.previousPassword,
|
||||||
dbUser.salt ?? "",
|
dbUser.salt ?? "",
|
||||||
);
|
);
|
||||||
@@ -233,8 +234,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const salt = await createSalt();
|
const salt = await createSaltAsync();
|
||||||
const hashedPassword = await hashPassword(input.password, salt);
|
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
@@ -243,7 +244,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
.where(eq(users.id, input.userId));
|
.where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
setMessage: publicProcedure.input(z.string()).mutation(async ({ input }) => {
|
setMessage: publicProcedure.input(z.string()).mutation(async ({ input }) => {
|
||||||
await exampleChannel.publish({ message: input });
|
await exampleChannel.publishAsync({ message: input });
|
||||||
}),
|
}),
|
||||||
test: publicProcedure.subscription(() => {
|
test: publicProcedure.subscription(() => {
|
||||||
return observable<{ message: string }>((emit) => {
|
return observable<{ message: string }>((emit) => {
|
||||||
@@ -254,12 +255,12 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createUser = async (
|
const createUserAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
input: z.infer<typeof validation.user.create>,
|
input: z.infer<typeof validation.user.create>,
|
||||||
) => {
|
) => {
|
||||||
const salt = await createSalt();
|
const salt = await createSaltAsync();
|
||||||
const hashedPassword = await hashPassword(input.password, salt);
|
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||||
|
|
||||||
const username = input.username.toLowerCase();
|
const username = input.username.toLowerCase();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
sessionTokenCookieName,
|
sessionTokenCookieName,
|
||||||
} from "./session";
|
} from "./session";
|
||||||
|
|
||||||
export const getCurrentUserPermissions = async (
|
export const getCurrentUserPermissionsAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
userId: string,
|
userId: string,
|
||||||
) => {
|
) => {
|
||||||
@@ -47,7 +47,7 @@ export const createSessionCallback = (
|
|||||||
...session.user,
|
...session.user,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
permissions: await getCurrentUserPermissions(db, user.id),
|
permissions: await getCurrentUserPermissionsAsync(db, user.id),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,7 @@ export * from "./security";
|
|||||||
export const createHandlers = (isCredentialsRequest: boolean) =>
|
export const createHandlers = (isCredentialsRequest: boolean) =>
|
||||||
createConfiguration(isCredentialsRequest);
|
createConfiguration(isCredentialsRequest);
|
||||||
|
|
||||||
export { getSessionFromToken, sessionTokenCookieName } from "./session";
|
export {
|
||||||
|
getSessionFromTokenAsync as getSessionFromToken,
|
||||||
|
sessionTokenCookieName,
|
||||||
|
} from "./session";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const createCredentialsConfiguration = (db: Database) =>
|
|||||||
type: "password",
|
type: "password",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
const data = await validation.user.signIn.parseAsync(credentials);
|
const data = await validation.user.signIn.parseAsync(credentials);
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import { createId } from "@homarr/db";
|
|||||||
import { users } from "@homarr/db/schema/sqlite";
|
import { users } from "@homarr/db/schema/sqlite";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
import { createSalt, hashPassword } from "../../security";
|
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||||
import { createCredentialsConfiguration } from "../credentials";
|
import { createCredentialsConfiguration } from "../credentials";
|
||||||
|
|
||||||
describe("Credentials authorization", () => {
|
describe("Credentials authorization", () => {
|
||||||
it("should authorize user with correct credentials", async () => {
|
it("should authorize user with correct credentials", async () => {
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
const salt = await createSalt();
|
const salt = await createSaltAsync();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
name: "test",
|
name: "test",
|
||||||
password: await hashPassword("test", salt),
|
password: await hashPasswordAsync("test", salt),
|
||||||
salt,
|
salt,
|
||||||
});
|
});
|
||||||
const result = await createCredentialsConfiguration(db).authorize({
|
const result = await createCredentialsConfiguration(db).authorize({
|
||||||
@@ -38,11 +38,11 @@ describe("Credentials authorization", () => {
|
|||||||
it(`should not authorize user with incorrect credentials (${password})`, async () => {
|
it(`should not authorize user with incorrect credentials (${password})`, async () => {
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
const salt = await createSalt();
|
const salt = await createSaltAsync();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
name: "test",
|
name: "test",
|
||||||
password: await hashPassword("test", salt),
|
password: await hashPasswordAsync("test", salt),
|
||||||
salt,
|
salt,
|
||||||
});
|
});
|
||||||
const result = await createCredentialsConfiguration(db).authorize({
|
const result = await createCredentialsConfiguration(db).authorize({
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
export const createSalt = async () => {
|
export const createSaltAsync = async () => {
|
||||||
return bcrypt.genSalt(10);
|
return bcrypt.genSalt(10);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hashPassword = async (password: string, salt: string) => {
|
export const hashPasswordAsync = async (password: string, salt: string) => {
|
||||||
return bcrypt.hash(password, salt);
|
return bcrypt.hash(password, salt);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Session } from "@auth/core/types";
|
|||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
|
|
||||||
import { getCurrentUserPermissions } from "./callbacks";
|
import { getCurrentUserPermissionsAsync } from "./callbacks";
|
||||||
|
|
||||||
export const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
export const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||||
export const sessionTokenCookieName = "next-auth.session-token";
|
export const sessionTokenCookieName = "next-auth.session-token";
|
||||||
@@ -16,7 +16,7 @@ export const generateSessionToken = () => {
|
|||||||
return randomUUID();
|
return randomUUID();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSessionFromToken = async (
|
export const getSessionFromTokenAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
token: string | undefined,
|
token: string | undefined,
|
||||||
): Promise<Session | null> => {
|
): Promise<Session | null> => {
|
||||||
@@ -48,7 +48,7 @@ export const getSessionFromToken = async (
|
|||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
...session.user,
|
...session.user,
|
||||||
permissions: await getCurrentUserPermissions(db, session.user.id),
|
permissions: await getCurrentUserPermissionsAsync(db, session.user.id),
|
||||||
},
|
},
|
||||||
expires: session.expires.toISOString(),
|
expires: session.expires.toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import * as definitions from "@homarr/definitions";
|
|||||||
import {
|
import {
|
||||||
createSessionCallback,
|
createSessionCallback,
|
||||||
createSignInCallback,
|
createSignInCallback,
|
||||||
getCurrentUserPermissions,
|
getCurrentUserPermissionsAsync,
|
||||||
} from "../callbacks";
|
} from "../callbacks";
|
||||||
|
|
||||||
describe("getCurrentUserPermissions", () => {
|
describe("getCurrentUserPermissions", () => {
|
||||||
@@ -30,7 +30,7 @@ describe("getCurrentUserPermissions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userId = "1";
|
const userId = "1";
|
||||||
const result = await getCurrentUserPermissions(db, userId);
|
const result = await getCurrentUserPermissionsAsync(db, userId);
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
test("should return permissions for user", async () => {
|
test("should return permissions for user", async () => {
|
||||||
@@ -56,7 +56,7 @@ describe("getCurrentUserPermissions", () => {
|
|||||||
permission: "admin",
|
permission: "admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await getCurrentUserPermissions(db, mockId);
|
const result = await getCurrentUserPermissionsAsync(db, mockId);
|
||||||
expect(result).toEqual(["board-create"]);
|
expect(result).toEqual(["board-create"]);
|
||||||
expect(getPermissionsWithChildrenMock).toHaveBeenCalledWith(["admin"]);
|
expect(getPermissionsWithChildrenMock).toHaveBeenCalledWith(["admin"]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { createSalt, hashPassword } from "../security";
|
import { createSaltAsync, hashPasswordAsync } from "../security";
|
||||||
|
|
||||||
describe("createSalt should return a salt", () => {
|
describe("createSalt should return a salt", () => {
|
||||||
it("should return a salt", async () => {
|
it("should return a salt", async () => {
|
||||||
const result = await createSalt();
|
const result = await createSaltAsync();
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.length).toBeGreaterThan(25);
|
expect(result.length).toBeGreaterThan(25);
|
||||||
});
|
});
|
||||||
it("should return a different salt each time", async () => {
|
it("should return a different salt each time", async () => {
|
||||||
const result1 = await createSalt();
|
const result1 = await createSaltAsync();
|
||||||
const result2 = await createSalt();
|
const result2 = await createSaltAsync();
|
||||||
expect(result1).not.toEqual(result2);
|
expect(result1).not.toEqual(result2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -18,8 +18,8 @@ describe("createSalt should return a salt", () => {
|
|||||||
describe("hashPassword should return a hash", () => {
|
describe("hashPassword should return a hash", () => {
|
||||||
it("should return a hash", async () => {
|
it("should return a hash", async () => {
|
||||||
const password = "password";
|
const password = "password";
|
||||||
const salt = await createSalt();
|
const salt = await createSaltAsync();
|
||||||
const result = await hashPassword(password, salt);
|
const result = await hashPasswordAsync(password, salt);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.length).toBeGreaterThan(55);
|
expect(result.length).toBeGreaterThan(55);
|
||||||
expect(result).not.toEqual(password);
|
expect(result).not.toEqual(password);
|
||||||
@@ -27,20 +27,20 @@ describe("hashPassword should return a hash", () => {
|
|||||||
it("should return a different hash each time", async () => {
|
it("should return a different hash each time", async () => {
|
||||||
const password = "password";
|
const password = "password";
|
||||||
const password2 = "another password";
|
const password2 = "another password";
|
||||||
const salt = await createSalt();
|
const salt = await createSaltAsync();
|
||||||
|
|
||||||
const result1 = await hashPassword(password, salt);
|
const result1 = await hashPasswordAsync(password, salt);
|
||||||
const result2 = await hashPassword(password2, salt);
|
const result2 = await hashPasswordAsync(password2, salt);
|
||||||
|
|
||||||
expect(result1).not.toEqual(result2);
|
expect(result1).not.toEqual(result2);
|
||||||
});
|
});
|
||||||
it("should return a different hash for the same password with different salts", async () => {
|
it("should return a different hash for the same password with different salts", async () => {
|
||||||
const password = "password";
|
const password = "password";
|
||||||
const salt1 = await createSalt();
|
const salt1 = await createSaltAsync();
|
||||||
const salt2 = await createSalt();
|
const salt2 = await createSaltAsync();
|
||||||
|
|
||||||
const result1 = await hashPassword(password, salt1);
|
const result1 = await hashPasswordAsync(password, salt1);
|
||||||
const result2 = await hashPassword(password, salt2);
|
const result2 = await hashPasswordAsync(password, salt2);
|
||||||
|
|
||||||
expect(result1).not.toEqual(result2);
|
expect(result1).not.toEqual(result2);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const createSubPubChannel = <TData>(name: string) => {
|
|||||||
* Publish data to the channel with last data saved.
|
* Publish data to the channel with last data saved.
|
||||||
* @param data data to be published
|
* @param data data to be published
|
||||||
*/
|
*/
|
||||||
publish: async (data: TData) => {
|
publishAsync: async (data: TData) => {
|
||||||
await lastDataClient.set(lastChannelName, superjson.stringify(data));
|
await lastDataClient.set(lastChannelName, superjson.stringify(data));
|
||||||
await publisher.publish(channelName, superjson.stringify(data));
|
await publisher.publish(channelName, superjson.stringify(data));
|
||||||
},
|
},
|
||||||
@@ -64,11 +64,11 @@ type WithId<TItem> = TItem & { _id: string };
|
|||||||
*/
|
*/
|
||||||
export const createQueueChannel = <TItem>(name: string) => {
|
export const createQueueChannel = <TItem>(name: string) => {
|
||||||
const queueChannelName = `queue:${name}`;
|
const queueChannelName = `queue:${name}`;
|
||||||
const getData = async () => {
|
const getDataAsync = async () => {
|
||||||
const data = await queueClient.get(queueChannelName);
|
const data = await queueClient.get(queueChannelName);
|
||||||
return data ? superjson.parse<WithId<TItem>[]>(data) : [];
|
return data ? superjson.parse<WithId<TItem>[]>(data) : [];
|
||||||
};
|
};
|
||||||
const setData = async (data: WithId<TItem>[]) => {
|
const setDataAsync = async (data: WithId<TItem>[]) => {
|
||||||
await queueClient.set(queueChannelName, superjson.stringify(data));
|
await queueClient.set(queueChannelName, superjson.stringify(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,22 +77,22 @@ export const createQueueChannel = <TItem>(name: string) => {
|
|||||||
* Add a new queue execution.
|
* Add a new queue execution.
|
||||||
* @param data data to be stored in the queue execution to run it later
|
* @param data data to be stored in the queue execution to run it later
|
||||||
*/
|
*/
|
||||||
add: async (data: TItem) => {
|
addAsync: async (data: TItem) => {
|
||||||
const items = await getData();
|
const items = await getDataAsync();
|
||||||
items.push({ _id: createId(), ...data });
|
items.push({ _id: createId(), ...data });
|
||||||
await setData(items);
|
await setDataAsync(items);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all queue executions.
|
* Get all queue executions.
|
||||||
*/
|
*/
|
||||||
all: getData,
|
all: getDataAsync,
|
||||||
/**
|
/**
|
||||||
* Get a queue execution by its id.
|
* Get a queue execution by its id.
|
||||||
* @param id id of the queue execution (stored under _id key)
|
* @param id id of the queue execution (stored under _id key)
|
||||||
* @returns queue execution or undefined if not found
|
* @returns queue execution or undefined if not found
|
||||||
*/
|
*/
|
||||||
byId: async (id: string) => {
|
byIdAsync: async (id: string) => {
|
||||||
const items = await getData();
|
const items = await getDataAsync();
|
||||||
return items.find((item) => item._id === id);
|
return items.find((item) => item._id === id);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -100,17 +100,17 @@ export const createQueueChannel = <TItem>(name: string) => {
|
|||||||
* @param filter callback function that returns true if the item should be included in the result
|
* @param filter callback function that returns true if the item should be included in the result
|
||||||
* @returns filtered queue executions
|
* @returns filtered queue executions
|
||||||
*/
|
*/
|
||||||
filter: async (filter: (item: WithId<TItem>) => boolean) => {
|
filterAsync: async (filter: (item: WithId<TItem>) => boolean) => {
|
||||||
const items = await getData();
|
const items = await getDataAsync();
|
||||||
return items.filter(filter);
|
return items.filter(filter);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Marks an queue execution as done, by deleting it.
|
* Marks an queue execution as done, by deleting it.
|
||||||
* @param id id of the queue execution (stored under _id key)
|
* @param id id of the queue execution (stored under _id key)
|
||||||
*/
|
*/
|
||||||
markAsDone: async (id: string) => {
|
markAsDoneAsync: async (id: string) => {
|
||||||
const items = await getData();
|
const items = await getDataAsync();
|
||||||
await setData(items.filter((item) => item._id !== id));
|
await setDataAsync(items.filter((item) => item._id !== id));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { api } from "@homarr/api/server";
|
|||||||
|
|
||||||
import type { WidgetProps } from "../definition";
|
import type { WidgetProps } from "../definition";
|
||||||
|
|
||||||
export default async function getServerData({ options }: WidgetProps<"app">) {
|
export default async function getServerDataAsync({
|
||||||
|
options,
|
||||||
|
}: WidgetProps<"app">) {
|
||||||
try {
|
try {
|
||||||
const app = await api.app.byId({ id: options.appId });
|
const app = await api.app.byId({ id: options.appId });
|
||||||
return { app };
|
return { app };
|
||||||
|
|||||||
@@ -40,6 +40,48 @@ const config = {
|
|||||||
{ checksVoidReturn: { attributes: false } },
|
{ checksVoidReturn: { attributes: false } },
|
||||||
],
|
],
|
||||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||||
|
"no-restricted-syntax": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
selector: "FunctionDeclaration[async=false][id.name=/Async$/]",
|
||||||
|
message: "Function ending in 'Async' must be declared async",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"FunctionDeclaration[async=true][id.name=/^[a-z].*$/][id.name=/ ^(?!generateMetadata$)[a-z].*$/][id.name!=/Async$/]",
|
||||||
|
message:
|
||||||
|
"Async function name must end in 'Async' (function declaration)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "MethodDefinition[value.async=false][key.name=/Async$/]",
|
||||||
|
message: "Method ending in 'Async' must be declared async",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "MethodDefinition[value.async=true][key.name!=/Async$/]",
|
||||||
|
message: "Async method name must end in 'Async'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"Property[value.type=/FunctionExpression$/][value.async=false][key.name=/Async$/]",
|
||||||
|
message: "Function ending in 'Async' must be declared async",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"Property[value.type=/FunctionExpression$/][value.async=true][key.name!=/^on(Success|Settled)$/][key.name!=/Async$/]",
|
||||||
|
message: "Async function name must end in 'Async' (property)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"VariableDeclarator[init.type=/FunctionExpression$/][init.async=false][id.name=/Async$/]",
|
||||||
|
message: "Function ending in 'Async' must be declared async",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"VariableDeclarator[init.type=/FunctionExpression$/][init.async=true][id.name=/^[a-z].*$/][id.name!=/Async$/]",
|
||||||
|
message:
|
||||||
|
"Async function name must end in 'Async' (variable declarator)",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
"**/.eslintrc.cjs",
|
"**/.eslintrc.cjs",
|
||||||
|
|||||||
Reference in New Issue
Block a user