Merge branch 'dev' into ajnart/fix-duplicate-users

This commit is contained in:
Meier Lukas
2024-05-18 13:49:25 +02:00
committed by GitHub
62 changed files with 308 additions and 235 deletions

View File

@@ -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")}

View File

@@ -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();
}, },
}); });

View File

@@ -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;

View File

@@ -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 });
}, },
}); });

View File

@@ -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;

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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")}

View File

@@ -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")}

View File

@@ -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 (

View File

@@ -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 ({

View File

@@ -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),
)} )}
> >

View File

@@ -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";

View File

@@ -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({

View File

@@ -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");
}); });
}, },

View File

@@ -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");
}); });
}, },

View File

@@ -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,

View File

@@ -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");
}, },
}); });

View File

@@ -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;

View File

@@ -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({

View File

@@ -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 />

View File

@@ -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 />

View File

@@ -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);
}, },

View File

@@ -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);

View 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

View 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 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({

View File

@@ -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"),

View File

@@ -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", {

View File

@@ -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(
{ {

View File

@@ -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`,
); );
}, },

View File

@@ -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`,
);
}, },
}); });
}, [ }, [

View File

@@ -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"),

View File

@@ -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")));
} }

View File

@@ -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);
}; };

View File

@@ -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;
}); });

View File

@@ -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();
}); });

View File

@@ -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:

View File

@@ -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);
} }
}; };

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),

View File

@@ -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");
}); });
}); });

View File

@@ -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,
}); });

View File

@@ -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);
}, },
); );

View File

@@ -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");
}); });
}); });

View File

@@ -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");
}); });
}); });

View File

@@ -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");
}); });
}); });

View File

@@ -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");
}, },
); );
}); });

View File

@@ -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();

View File

@@ -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),
}, },
}; };
}; };

View File

@@ -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";

View File

@@ -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);

View File

@@ -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({

View File

@@ -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);
}; };

View File

@@ -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(),
}; };

View File

@@ -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"]);
}); });

View File

@@ -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);
}); });

View File

@@ -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));
}, },
}; };
}; };

View File

@@ -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 };

View File

@@ -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",