From 6f7327b77478c567b478c2c61b0a09400e426678 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 27 Jul 2024 11:38:51 +0200 Subject: [PATCH] feat: restrict non credential provider interactions (#871) * wip: add provider field to sqlite user table * feat: disable invites when credentials provider is not used * wip: add migration for provider field in user table with sqlite * wip: remove fields that can not be modified by non credential users * wip: make username, mail and avatar disabled instead of hidden * wip: external users membership of group cannot be managed manually * feat: add alerts to inform about disabled fields and managing group members * wip: add mysql migration for provider on user table * chore: fix format issues * chore: address pull request feedback * fix: build issue * fix: deepsource issues * fix: tests not working * feat: restrict login to specific auth providers * chore: address pull request feedback * fix: deepsource issue --- .../app/[locale]/auth/invite/[id]/page.tsx | 3 + .../nextjs/src/app/[locale]/manage/layout.tsx | 2 + apps/nextjs/src/app/[locale]/manage/page.tsx | 42 +- .../_components/_profile-avatar-form.tsx | 42 +- .../general/_components/_profile-form.tsx | 29 +- .../manage/users/[userId]/general/page.tsx | 25 +- .../[locale]/manage/users/[userId]/layout.tsx | 14 +- .../manage/users/[userId]/security/page.tsx | 4 + .../manage/users/groups/[id]/members/page.tsx | 28 +- .../[locale]/manage/users/invites/page.tsx | 7 + .../src/components/layout/navigation.tsx | 19 +- packages/api/src/router/group.ts | 1 + packages/api/src/router/invite.ts | 4 + packages/api/src/router/invite/checks.ts | 12 + packages/api/src/router/test/group.spec.ts | 4 +- packages/api/src/router/test/invite.spec.ts | 9 + packages/api/src/router/test/user.spec.ts | 13 + packages/api/src/router/user.ts | 87 +- packages/auth/providers/check-provider.ts | 9 + .../authorization/basic-authorization.ts | 4 +- .../authorization/ldap-authorization.ts | 35 +- .../credentials/credentials-provider.ts | 3 +- packages/auth/providers/oidc/oidc-provider.ts | 1 + .../providers/test/ldap-authorization.spec.ts | 77 +- packages/auth/server.ts | 1 + .../db/migrations/mysql/0005_soft_microbe.sql | 1 + .../migrations/mysql/meta/0005_snapshot.json | 1328 +++++++++++++++++ .../db/migrations/mysql/meta/_journal.json | 7 + .../db/migrations/sqlite/0005_lean_random.sql | 1 + .../migrations/sqlite/meta/0005_snapshot.json | 1271 ++++++++++++++++ .../db/migrations/sqlite/meta/_journal.json | 7 + packages/db/schema/mysql.ts | 2 + packages/db/schema/sqlite.ts | 2 + packages/definitions/src/auth.ts | 2 + packages/definitions/src/index.ts | 1 + packages/translation/src/lang/en.ts | 8 +- 36 files changed, 2989 insertions(+), 116 deletions(-) create mode 100644 packages/api/src/router/invite/checks.ts create mode 100644 packages/auth/providers/check-provider.ts create mode 100644 packages/db/migrations/mysql/0005_soft_microbe.sql create mode 100644 packages/db/migrations/mysql/meta/0005_snapshot.json create mode 100644 packages/db/migrations/sqlite/0005_lean_random.sql create mode 100644 packages/db/migrations/sqlite/meta/0005_snapshot.json create mode 100644 packages/definitions/src/auth.ts diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx index 989406228..1624b50b5 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx @@ -2,6 +2,7 @@ import { notFound } from "next/navigation"; import { Card, Center, Stack, Text, Title } from "@mantine/core"; import { auth } from "@homarr/auth/next"; +import { isProviderEnabled } from "@homarr/auth/server"; import { and, db, eq } from "@homarr/db"; import { invites } from "@homarr/db/schema/sqlite"; import { getScopedI18n } from "@homarr/translation/server"; @@ -19,6 +20,8 @@ interface InviteUsagePageProps { } export default async function InviteUsagePage({ params, searchParams }: InviteUsagePageProps) { + if (!isProviderEnabled("credentials")) notFound(); + const session = await auth(); if (session) notFound(); diff --git a/apps/nextjs/src/app/[locale]/manage/layout.tsx b/apps/nextjs/src/app/[locale]/manage/layout.tsx index bcb5990e0..694017e41 100644 --- a/apps/nextjs/src/app/[locale]/manage/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/layout.tsx @@ -22,6 +22,7 @@ import { IconUsersGroup, } from "@tabler/icons-react"; +import { isProviderEnabled } from "@homarr/auth/server"; import { getScopedI18n } from "@homarr/translation/server"; import { MainHeader } from "~/components/layout/header"; @@ -65,6 +66,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) { label: t("items.users.items.invites"), icon: IconMailForward, href: "/manage/users/invites", + hidden: !isProviderEnabled("credentials"), }, { label: t("items.users.items.groups"), diff --git a/apps/nextjs/src/app/[locale]/manage/page.tsx b/apps/nextjs/src/app/[locale]/manage/page.tsx index 4ee83be77..8e25ffb92 100644 --- a/apps/nextjs/src/app/[locale]/manage/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/page.tsx @@ -3,6 +3,7 @@ import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core"; import { IconArrowRight } from "@tabler/icons-react"; import { api } from "@homarr/api/server"; +import { isProviderEnabled } from "@homarr/auth/server"; import { getScopedI18n } from "@homarr/translation/server"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; @@ -14,6 +15,7 @@ interface LinkProps { subtitle: string; count: number; href: string; + hidden?: boolean; } export async function generateMetadata() { @@ -42,6 +44,7 @@ export default async function ManagementPage() { title: t("statistic.createUser"), }, { + hidden: !isProviderEnabled("credentials"), count: statistics.countInvites, href: "/manage/users/invites", subtitle: t("statisticLabel.authentication"), @@ -72,24 +75,27 @@ export default async function ManagementPage() { - {links.map((link, index) => ( - - - - - {link.count} - - - - {link.subtitle} - - {link.title} - - - - - - ))} + {links.map( + (link) => + !link.hidden && ( + + + + + {link.count} + + + + {link.subtitle} + + {link.title} + + + + + + ), + )} ); diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx index 4ad991bd6..7a61bc3de 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx @@ -93,24 +93,38 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => { }); }, [mutate, user.id, openConfirmModal, tManageAvatar]); + const isCredentialsUser = user.provider === "credentials"; + return ( - + - + - + {isCredentialsUser && ( + + )} diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx index 44ba9ddb7..0921ccd61 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx @@ -51,8 +51,12 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => { }, }); + // Only credentials users can edit their profile + const isProviderCredentials = user.provider === "credentials"; + const handleSubmit = useCallback( (values: FormType) => { + if (!isProviderCredentials) return; mutate({ ...values, id: user.id, @@ -64,14 +68,25 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => { return (
- - + + - - - + {isProviderCredentials && ( + + + + )}
); diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx index 3dfcfca59..20099f17c 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx @@ -1,5 +1,6 @@ import { notFound } from "next/navigation"; -import { Box, Group, Stack, Title } from "@mantine/core"; +import { Alert, Box, Group, Stack, Title } from "@mantine/core"; +import { IconExclamationCircle } from "@tabler/icons-react"; import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; @@ -53,8 +54,14 @@ export default async function EditUserPage({ params }: Props) { notFound(); } + const isCredentialsUser = user.provider === "credentials"; + return ( + }> + {t("management.page.user.fieldsDisabledExternalProvider")} + + {tGeneral("title")} @@ -67,13 +74,15 @@ export default async function EditUserPage({ params }: Props) { - - } - /> - + {isCredentialsUser && ( + + } + /> + + )} ); } diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/layout.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/layout.tsx index 4821ac090..f63aa1a6d 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/layout.tsx @@ -28,6 +28,8 @@ export default async function Layout({ children, params }: PropsWithChildren } /> - } - /> + {isCredentialsUser && ( + } + /> + )} diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx index 0145520de..99061405d 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx @@ -28,6 +28,10 @@ export default async function UserSecurityPage({ params }: Props) { notFound(); } + if (user.provider !== "credentials") { + notFound(); + } + return ( {tSecurity("title")} diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx index e5f0fc604..adad026ee 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx @@ -1,8 +1,11 @@ import Link from "next/link"; -import { Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core"; +import { Alert, Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core"; +import { IconExclamationCircle } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; +import { env } from "@homarr/auth/env.mjs"; +import { isProviderEnabled } from "@homarr/auth/server"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { SearchInput, UserAvatar } from "@homarr/ui"; @@ -28,9 +31,22 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD group.members.filter((member) => member.name?.toLowerCase().includes(searchParams.search!.trim().toLowerCase())) : group.members; + const providerTypes = isProviderEnabled("credentials") + ? env.AUTH_PROVIDERS.length > 1 + ? "mixed" + : "credentials" + : "external"; + return ( {tMembers("title")} + + {providerTypes !== "credentials" && ( + }> + {t(`group.memberNotice.${providerTypes}`)} + + )} + - member.id)} /> + {isProviderEnabled("credentials") && ( + member.id)} /> + )} {filteredMembers.length === 0 && (
@@ -60,7 +78,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD } interface RowProps { - member: RouterOutputs["group"]["getPaginated"]["items"][number]["members"][number]; + member: RouterOutputs["group"]["getById"]["members"][number]; groupId: string; } @@ -70,13 +88,13 @@ const Row = ({ member, groupId }: RowProps) => { - + {member.name} - + {member.provider === "credentials" && } ); diff --git a/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx index 1e7b8de1e..4044fb5b5 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx @@ -1,9 +1,16 @@ +import { notFound } from "next/navigation"; + import { api } from "@homarr/api/server"; +import { isProviderEnabled } from "@homarr/auth/server"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { InviteListComponent } from "./_components/invite-list"; export default async function InvitesOverviewPage() { + if (!isProviderEnabled("credentials")) { + notFound(); + } + const initialInvites = await api.invite.getAll(); return ( <> diff --git a/apps/nextjs/src/components/layout/navigation.tsx b/apps/nextjs/src/components/layout/navigation.tsx index c9a53aaad..d360e105a 100644 --- a/apps/nextjs/src/components/layout/navigation.tsx +++ b/apps/nextjs/src/components/layout/navigation.tsx @@ -22,18 +22,24 @@ export const MainNavigation = ({ headerSection, footerSection, links }: MainNavi component={ScrollArea} > {links.map((link, index) => { + if (link.hidden) { + return null; + } + const { icon: TablerIcon, ...props } = link; const Icon = ; let clientLink: ClientNavigationLink; if ("items" in props) { clientLink = { ...props, - items: props.items.map((item) => { - return { - ...item, - icon: , - }; - }), + items: props.items + .filter((item) => !item.hidden) + .map((item) => { + return { + ...item, + icon: , + }; + }), } as ClientNavigationLink; } else { clientLink = props as ClientNavigationLink; @@ -49,6 +55,7 @@ export const MainNavigation = ({ headerSection, footerSection, links }: MainNavi interface CommonNavigationLinkProps { label: string; icon: TablerIcon; + hidden?: boolean; } interface NavigationLinkHref extends CommonNavigationLinkProps { diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index 3d0cfe168..7a47e7d3e 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -57,6 +57,7 @@ export const groupRouter = createTRPCRouter({ name: true, email: true, image: true, + provider: true, }, }, }, diff --git a/packages/api/src/router/invite.ts b/packages/api/src/router/invite.ts index cc6894115..9285ea4d2 100644 --- a/packages/api/src/router/invite.ts +++ b/packages/api/src/router/invite.ts @@ -6,9 +6,11 @@ import { invites } from "@homarr/db/schema/sqlite"; import { z } from "@homarr/validation"; import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { throwIfCredentialsDisabled } from "./invite/checks"; export const inviteRouter = createTRPCRouter({ getAll: protectedProcedure.query(async ({ ctx }) => { + throwIfCredentialsDisabled(); const dbInvites = await ctx.db.query.invites.findMany({ orderBy: asc(invites.expirationDate), columns: { @@ -32,6 +34,7 @@ export const inviteRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); const id = createId(); const token = randomBytes(20).toString("hex"); @@ -54,6 +57,7 @@ export const inviteRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); const dbInvite = await ctx.db.query.invites.findFirst({ where: eq(invites.id, input.id), }); diff --git a/packages/api/src/router/invite/checks.ts b/packages/api/src/router/invite/checks.ts new file mode 100644 index 000000000..330faaae3 --- /dev/null +++ b/packages/api/src/router/invite/checks.ts @@ -0,0 +1,12 @@ +import { TRPCError } from "@trpc/server"; + +import { env } from "@homarr/auth/env.mjs"; + +export const throwIfCredentialsDisabled = () => { + if (!env.AUTH_PROVIDERS.includes("credentials")) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Credentials provider is disabled", + }); + } +}; diff --git a/packages/api/src/router/test/group.spec.ts b/packages/api/src/router/test/group.spec.ts index 29921ea68..51c11e70e 100644 --- a/packages/api/src/router/test/group.spec.ts +++ b/packages/api/src/router/test/group.spec.ts @@ -170,8 +170,8 @@ describe("byId should return group by id including members and permissions", () expect(result.members.length).toBe(1); const userKeys = Object.keys(result.members[0] ?? {}); - expect(userKeys.length).toBe(4); - expect(["id", "name", "email", "image"].some((key) => userKeys.includes(key))); + expect(userKeys.length).toBe(5); + expect(["id", "name", "email", "image", "provider"].some((key) => userKeys.includes(key))); expect(result.permissions.length).toBe(1); expect(result.permissions[0]).toBe("admin"); }); diff --git a/packages/api/src/router/test/invite.spec.ts b/packages/api/src/router/test/invite.spec.ts index bcd8c3675..9c39d5bb2 100644 --- a/packages/api/src/router/test/invite.spec.ts +++ b/packages/api/src/router/test/invite.spec.ts @@ -22,6 +22,15 @@ vi.mock("@homarr/auth", async () => { return { ...mod, auth: () => ({}) as Session }; }); +// Mock the env module to return the credentials provider +vi.mock("@homarr/auth/env.mjs", () => { + return { + env: { + AUTH_PROVIDERS: ["credentials"], + }, + }; +}); + describe("all should return all existing invites without sensitive informations", () => { test("invites should not contain sensitive informations", async () => { // Arrange diff --git a/packages/api/src/router/test/user.spec.ts b/packages/api/src/router/test/user.spec.ts index 59c58436f..084674566 100644 --- a/packages/api/src/router/test/user.spec.ts +++ b/packages/api/src/router/test/user.spec.ts @@ -13,6 +13,15 @@ vi.mock("@homarr/auth", async () => { return { ...mod, auth: () => ({}) as Session }; }); +// Mock the env module to return the credentials provider +vi.mock("@homarr/auth/env.mjs", () => { + return { + env: { + AUTH_PROVIDERS: ["credentials"], + }, + }; +}); + describe("initUser should initialize the first user", () => { it("should throw an error if a user already exists", async () => { const db = createDb(); @@ -230,6 +239,7 @@ describe("editProfile shoud update user", () => { password: null, image: null, homeBoardId: null, + provider: "credentials", }); }); @@ -270,6 +280,7 @@ describe("editProfile shoud update user", () => { password: null, image: null, homeBoardId: null, + provider: "credentials", }); }); }); @@ -294,6 +305,7 @@ describe("delete should delete user", () => { password: null, salt: null, homeBoardId: null, + provider: "ldap" as const, }, { id: userToDelete, @@ -314,6 +326,7 @@ describe("delete should delete user", () => { password: null, salt: null, homeBoardId: null, + provider: "oidc" as const, }, ]; diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index b82c8f14f..af838445f 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -4,12 +4,17 @@ import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import type { Database } from "@homarr/db"; import { and, createId, eq, schema } from "@homarr/db"; import { groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema/sqlite"; +import type { SupportedAuthProvider } from "@homarr/definitions"; +import { logger } from "@homarr/log"; import { validation, z } from "@homarr/validation"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { throwIfCredentialsDisabled } from "./invite/checks"; export const userRouter = createTRPCRouter({ initUser: publicProcedure.input(validation.user.init).mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); + const firstUser = await ctx.db.query.users.findFirst({ columns: { id: true, @@ -40,6 +45,7 @@ export const userRouter = createTRPCRouter({ }); }), register: publicProcedure.input(validation.user.registrationApi).mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); const inviteWhere = and(eq(invites.id, input.inviteId), eq(invites.token, input.token)); const dbInvite = await ctx.db.query.invites.findFirst({ columns: { @@ -56,7 +62,7 @@ export const userRouter = createTRPCRouter({ }); } - await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username); + await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username); await createUserAsync(ctx.db, input); @@ -64,7 +70,8 @@ export const userRouter = createTRPCRouter({ await ctx.db.delete(invites).where(inviteWhere); }), create: publicProcedure.input(validation.user.create).mutation(async ({ ctx, input }) => { - await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username); + throwIfCredentialsDisabled(); + await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username); await createUserAsync(ctx.db, input); }), @@ -93,6 +100,7 @@ export const userRouter = createTRPCRouter({ columns: { id: true, image: true, + provider: true, }, where: eq(users.id, input.userId), }); @@ -104,6 +112,13 @@ export const userRouter = createTRPCRouter({ }); } + if (user.provider !== "credentials") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Profile image can not be changed for users with external providers", + }); + } + await ctx.db .update(users) .set({ @@ -112,13 +127,14 @@ export const userRouter = createTRPCRouter({ .where(eq(users.id, input.userId)); }), getAll: publicProcedure.query(async ({ ctx }) => { - return ctx.db.query.users.findMany({ + return await ctx.db.query.users.findMany({ columns: { id: true, name: true, email: true, emailVerified: true, image: true, + provider: true, }, }); }), @@ -139,6 +155,7 @@ export const userRouter = createTRPCRouter({ email: true, emailVerified: true, image: true, + provider: true, }, where: eq(users.id, input.userId), }); @@ -154,7 +171,7 @@ export const userRouter = createTRPCRouter({ }), editProfile: publicProcedure.input(validation.user.editProfile).mutation(async ({ input, ctx }) => { const user = await ctx.db.query.users.findFirst({ - columns: { email: true }, + columns: { email: true, provider: true }, where: eq(users.id, input.id), }); @@ -165,7 +182,14 @@ export const userRouter = createTRPCRouter({ }); } - await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.name, input.id); + if (user.provider !== "credentials") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Username and email can not be changed for users with external providers", + }); + } + + await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.name, input.id); const emailDirty = input.email && user.email !== input.email; await ctx.db @@ -190,26 +214,38 @@ export const userRouter = createTRPCRouter({ }); } + const dbUser = await ctx.db.query.users.findFirst({ + columns: { + id: true, + password: true, + salt: true, + provider: true, + }, + where: eq(users.id, input.userId), + }); + + if (!dbUser) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + + if (dbUser.provider !== "credentials") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Password can not be changed for users with external providers", + }); + } + // Admins can change the password of other users without providing the previous password const isPreviousPasswordRequired = ctx.session.user.id === input.userId; + logger.info( + `User ${user.id} is changing password for user ${input.userId}, previous password is required: ${isPreviousPasswordRequired}`, + ); + if (isPreviousPasswordRequired) { - const dbUser = await ctx.db.query.users.findFirst({ - columns: { - id: true, - password: true, - salt: true, - }, - where: eq(users.id, input.userId), - }); - - if (!dbUser) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } - const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? ""); const isValid = previousPasswordHash === dbUser.password; @@ -249,9 +285,14 @@ const createUserAsync = async (db: Database, input: z.infer { +const checkUsernameAlreadyTakenAndThrowAsync = async ( + db: Database, + provider: SupportedAuthProvider, + username: string, + ignoreId?: string, +) => { const user = await db.query.users.findFirst({ - where: eq(users.name, username.toLowerCase()), + where: and(eq(users.name, username.toLowerCase()), eq(users.provider, provider)), }); if (!user) return; diff --git a/packages/auth/providers/check-provider.ts b/packages/auth/providers/check-provider.ts new file mode 100644 index 000000000..fd483465b --- /dev/null +++ b/packages/auth/providers/check-provider.ts @@ -0,0 +1,9 @@ +import type { SupportedAuthProvider } from "@homarr/definitions"; + +import { env } from "../env.mjs"; + +export const isProviderEnabled = (provider: SupportedAuthProvider) => { + // The question mark is placed there because isProviderEnabled is called during static build of about page + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return env.AUTH_PROVIDERS?.includes(provider); +}; diff --git a/packages/auth/providers/credentials/authorization/basic-authorization.ts b/packages/auth/providers/credentials/authorization/basic-authorization.ts index 5113b83af..a4debe69c 100644 --- a/packages/auth/providers/credentials/authorization/basic-authorization.ts +++ b/packages/auth/providers/credentials/authorization/basic-authorization.ts @@ -1,7 +1,7 @@ import bcrypt from "bcrypt"; import type { Database } from "@homarr/db"; -import { eq } from "@homarr/db"; +import { and, eq } from "@homarr/db"; import { users } from "@homarr/db/schema/sqlite"; import { logger } from "@homarr/log"; import type { validation, z } from "@homarr/validation"; @@ -11,7 +11,7 @@ export const authorizeWithBasicCredentialsAsync = async ( credentials: z.infer, ) => { const user = await db.query.users.findFirst({ - where: eq(users.name, credentials.name), + where: and(eq(users.name, credentials.name), eq(users.provider, "credentials")), }); if (!user?.password) { diff --git a/packages/auth/providers/credentials/authorization/ldap-authorization.ts b/packages/auth/providers/credentials/authorization/ldap-authorization.ts index e78c1663a..c286468d6 100644 --- a/packages/auth/providers/credentials/authorization/ldap-authorization.ts +++ b/packages/auth/providers/credentials/authorization/ldap-authorization.ts @@ -1,7 +1,8 @@ -import type { Adapter } from "@auth/core/adapters"; import { CredentialsSignin } from "@auth/core/errors"; -import { createId } from "@homarr/db"; +import type { Database } from "@homarr/db"; +import { and, createId, eq } from "@homarr/db"; +import { users } from "@homarr/db/schema/sqlite"; import { logger } from "@homarr/log"; import type { validation } from "@homarr/validation"; import { z } from "@homarr/validation"; @@ -10,7 +11,7 @@ import { env } from "../../../env.mjs"; import { LdapClient } from "../ldap-client"; export const authorizeWithLdapCredentialsAsync = async ( - adapter: Adapter, + db: Database, credentials: z.infer, ) => { logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`); @@ -89,18 +90,30 @@ export const authorizeWithLdapCredentialsAsync = async ( await client.disconnectAsync(); // Create or update user in the database - let user = await adapter.getUserByEmail?.(mailResult.data); + let user = await db.query.users.findFirst({ + columns: { + id: true, + name: true, + image: true, + email: true, + emailVerified: true, + provider: true, + }, + where: and(eq(users.email, mailResult.data), eq(users.provider, "ldap")), + }); if (!user) { logger.info(`User ${credentials.name} not found in the database. Creating...`); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - user = await adapter.createUser!({ + user = { id: createId(), name: credentials.name, email: mailResult.data, emailVerified: new Date(), // assume email is verified - }); + image: null, + provider: "ldap", + }; + await db.insert(users).values(user); logger.info(`User ${credentials.name} created successfully.`); } @@ -108,11 +121,9 @@ export const authorizeWithLdapCredentialsAsync = async ( if (user.name !== credentials.name) { logger.warn(`User ${credentials.name} found in the database but with different name. Updating...`); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - user = await adapter.updateUser!({ - id: user.id, - name: credentials.name, - }); + user.name = credentials.name; + + await db.update(users).set({ name: user.name }).where(eq(users.id, user.id)); logger.info(`User ${credentials.name} updated successfully.`); } diff --git a/packages/auth/providers/credentials/credentials-provider.ts b/packages/auth/providers/credentials/credentials-provider.ts index 2af81345a..7acda2749 100644 --- a/packages/auth/providers/credentials/credentials-provider.ts +++ b/packages/auth/providers/credentials/credentials-provider.ts @@ -3,7 +3,6 @@ import type Credentials from "@auth/core/providers/credentials"; import type { Database } from "@homarr/db"; import { validation } from "@homarr/validation"; -import { adapter } from "../../adapter"; import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization"; import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization"; @@ -32,7 +31,7 @@ export const createCredentialsConfiguration = (db: Database) => const data = await validation.user.signIn.parseAsync(credentials); if (data.credentialType === "ldap") { - return await authorizeWithLdapCredentialsAsync(adapter, data).catch(() => null); + return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null); } return await authorizeWithBasicCredentialsAsync(db, data); diff --git a/packages/auth/providers/oidc/oidc-provider.ts b/packages/auth/providers/oidc/oidc-provider.ts index 32cc43968..925e7d1dd 100644 --- a/packages/auth/providers/oidc/oidc-provider.ts +++ b/packages/auth/providers/oidc/oidc-provider.ts @@ -32,6 +32,7 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig { // Act const act = () => - authorizeWithLdapCredentialsAsync(null as unknown as Adapter, { + authorizeWithLdapCredentialsAsync(null as unknown as Database, { name: "test", password: "test", credentialType: "ldap", @@ -55,7 +54,7 @@ describe("authorizeWithLdapCredentials", () => { // Act const act = () => - authorizeWithLdapCredentialsAsync(null as unknown as Adapter, { + authorizeWithLdapCredentialsAsync(null as unknown as Database, { name: "test", password: "test", credentialType: "ldap", @@ -85,7 +84,7 @@ describe("authorizeWithLdapCredentials", () => { // Act const act = () => - authorizeWithLdapCredentialsAsync(null as unknown as Adapter, { + authorizeWithLdapCredentialsAsync(null as unknown as Database, { name: "test", password: "test", credentialType: "ldap", @@ -118,7 +117,7 @@ describe("authorizeWithLdapCredentials", () => { // Act const act = () => - authorizeWithLdapCredentialsAsync(null as unknown as Adapter, { + authorizeWithLdapCredentialsAsync(null as unknown as Database, { name: "test", password: "test", credentialType: "ldap", @@ -132,7 +131,6 @@ describe("authorizeWithLdapCredentials", () => { test("should authorize user with correct credentials and create user", async () => { // Arrange const db = createDb(); - const adapter = DrizzleAdapter(db); const spy = vi.spyOn(ldapClient, "LdapClient"); spy.mockImplementation( () => @@ -151,7 +149,7 @@ describe("authorizeWithLdapCredentials", () => { ); // Act - const result = await authorizeWithLdapCredentialsAsync(adapter, { + const result = await authorizeWithLdapCredentialsAsync(db, { name: "test", password: "test", credentialType: "ldap", @@ -166,13 +164,68 @@ describe("authorizeWithLdapCredentials", () => { expect(dbUser?.id).toBe(result.id); expect(dbUser?.email).toBe("test@gmail.com"); expect(dbUser?.emailVerified).not.toBeNull(); + expect(dbUser?.provider).toBe("ldap"); + }); + + test("should authorize user with correct credentials and create user with same email when credentials user already exists", async () => { + // Arrange + const db = createDb(); + const spy = vi.spyOn(ldapClient, "LdapClient"); + const salt = await createSaltAsync(); + spy.mockImplementation( + () => + ({ + bindAsync: vi.fn(() => Promise.resolve()), + searchAsync: vi.fn(() => + Promise.resolve([ + { + dn: "test", + mail: "test@gmail.com", + }, + ]), + ), + disconnectAsync: vi.fn(), + }) as unknown as ldapClient.LdapClient, + ); + await db.insert(users).values({ + id: createId(), + name: "test", + salt, + password: await hashPasswordAsync("test", salt), + email: "test@gmail.com", + provider: "credentials", + }); + + // Act + const result = await authorizeWithLdapCredentialsAsync(db, { + name: "test", + password: "test", + credentialType: "ldap", + }); + + // Assert + expect(result.name).toBe("test"); + const dbUser = await db.query.users.findFirst({ + where: and(eq(users.name, "test"), eq(users.provider, "ldap")), + }); + expect(dbUser).toBeDefined(); + expect(dbUser?.id).toBe(result.id); + expect(dbUser?.email).toBe("test@gmail.com"); + expect(dbUser?.emailVerified).not.toBeNull(); + expect(dbUser?.provider).toBe("ldap"); + + const credentialsUser = await db.query.users.findFirst({ + where: and(eq(users.name, "test"), eq(users.provider, "credentials")), + }); + + expect(credentialsUser).toBeDefined(); + expect(credentialsUser?.id).not.toBe(result.id); }); test("should authorize user with correct credentials and update name", async () => { // Arrange const userId = createId(); const db = createDb(); - const adapter = DrizzleAdapter(db); const salt = await createSaltAsync(); await db.insert(users).values({ id: userId, @@ -180,10 +233,11 @@ describe("authorizeWithLdapCredentials", () => { salt, password: await hashPasswordAsync("test", salt), email: "test@gmail.com", + provider: "ldap", }); // Act - const result = await authorizeWithLdapCredentialsAsync(adapter, { + const result = await authorizeWithLdapCredentialsAsync(db, { name: "test", password: "test", credentialType: "ldap", @@ -200,5 +254,6 @@ describe("authorizeWithLdapCredentials", () => { expect(dbUser?.id).toBe(userId); expect(dbUser?.name).toBe("test"); expect(dbUser?.email).toBe("test@gmail.com"); + expect(dbUser?.provider).toBe("ldap"); }); }); diff --git a/packages/auth/server.ts b/packages/auth/server.ts index 89166ddb1..ae11f0f60 100644 --- a/packages/auth/server.ts +++ b/packages/auth/server.ts @@ -1 +1,2 @@ export { hasQueryAccessToIntegrationsAsync } from "./permissions/integration-query-permissions"; +export { isProviderEnabled } from "./providers/check-provider"; diff --git a/packages/db/migrations/mysql/0005_soft_microbe.sql b/packages/db/migrations/mysql/0005_soft_microbe.sql new file mode 100644 index 000000000..c8736cf74 --- /dev/null +++ b/packages/db/migrations/mysql/0005_soft_microbe.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD `provider` varchar(64) DEFAULT 'credentials' NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/mysql/meta/0005_snapshot.json b/packages/db/migrations/mysql/meta/0005_snapshot.json new file mode 100644 index 000000000..123e68089 --- /dev/null +++ b/packages/db/migrations/mysql/meta/0005_snapshot.json @@ -0,0 +1,1328 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "50ab4b07-6f46-438b-806d-27827f138a01", + "prevId": "4af9bcc1-5573-4e00-8629-3c8de4c3a826", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": ["board_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": ["board_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('fixed')" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('no-repeat')" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('cover')" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fa5252')" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fd7e14')" + }, + "opacity": { + "name": "opacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"] + } + } + }, + "groupMember": { + "name": "groupMember", + "columns": { + "groupId": { + "name": "groupId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_groupId_group_id_fk": { + "name": "groupMember_groupId_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_userId_user_id_fk": { + "name": "groupMember_userId_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_groupId_userId_pk": { + "name": "groupMember_groupId_userId_pk", + "columns": ["groupId", "userId"] + } + }, + "uniqueConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "groupId": { + "name": "groupId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_groupId_group_id_fk": { + "name": "groupPermission_groupId_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "iconRepository_id": { + "name": "iconRepository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iconRepository_slug": { + "name": "iconRepository_slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "iconRepository_iconRepository_id": { + "name": "iconRepository_iconRepository_id", + "columns": ["iconRepository_id"] + } + }, + "uniqueConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "icon_id": { + "name": "icon_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_name": { + "name": "icon_name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_checksum": { + "name": "icon_checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iconRepository_id": { + "name": "iconRepository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_iconRepository_id_iconRepository_iconRepository_id_fk": { + "name": "icon_iconRepository_id_iconRepository_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["iconRepository_id"], + "columnsTo": ["iconRepository_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "icon_icon_id": { + "name": "icon_icon_id", + "columns": ["icon_id"] + } + }, + "uniqueConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk", + "columns": ["integration_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": ["item_id", "integration_id"] + } + }, + "uniqueConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": ["integration_id", "kind"] + } + }, + "uniqueConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": ["integration_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"] + } + } + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serverSetting_key": { + "name": "serverSetting_key", + "columns": ["key"] + } + }, + "uniqueConstraints": { + "serverSetting_key_unique": { + "name": "serverSetting_key_unique", + "columns": ["key"] + } + } + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_sessionToken": { + "name": "session_sessionToken", + "columns": ["sessionToken"] + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "homeBoardId": { + "name": "homeBoardId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_homeBoardId_board_id_fk": { + "name": "user_homeBoardId_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["homeBoardId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {} + } + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json index dac0c8477..ead0f991c 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1720113913876, "tag": "0004_noisy_giant_girl", "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1722068832607, + "tag": "0005_soft_microbe", + "breakpoints": true } ] } diff --git a/packages/db/migrations/sqlite/0005_lean_random.sql b/packages/db/migrations/sqlite/0005_lean_random.sql new file mode 100644 index 000000000..c74916745 --- /dev/null +++ b/packages/db/migrations/sqlite/0005_lean_random.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD `provider` text DEFAULT 'credentials' NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/0005_snapshot.json b/packages/db/migrations/sqlite/meta/0005_snapshot.json new file mode 100644 index 000000000..90a6c7d32 --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0005_snapshot.json @@ -0,0 +1,1271 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ca6ab27e-e943-4d66-ace2-ee3a3a70d52a", + "prevId": "17473af6-8220-4f3b-970f-1cfe9e9f2ebe", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": ["provider", "providerAccountId"], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "columns": ["board_id", "group_id", "permission"], + "name": "boardGroupPermission_board_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "columns": ["board_id", "permission", "user_id"], + "name": "boardUserPermission_board_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + } + }, + "indexes": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "groupId": { + "name": "groupId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_groupId_group_id_fk": { + "name": "groupMember_groupId_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_userId_user_id_fk": { + "name": "groupMember_userId_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_groupId_userId_pk": { + "columns": ["groupId", "userId"], + "name": "groupMember_groupId_userId_pk" + } + }, + "uniqueConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "groupId": { + "name": "groupId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_groupId_group_id_fk": { + "name": "groupPermission_groupId_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "iconRepository_id": { + "name": "iconRepository_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "iconRepository_slug": { + "name": "iconRepository_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "icon_id": { + "name": "icon_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_name": { + "name": "icon_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_checksum": { + "name": "icon_checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iconRepository_id": { + "name": "iconRepository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_iconRepository_id_iconRepository_iconRepository_id_fk": { + "name": "icon_iconRepository_id_iconRepository_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["iconRepository_id"], + "columnsTo": ["iconRepository_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "columns": ["group_id", "integration_id", "permission"], + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "columns": ["integration_id", "item_id"], + "name": "integration_item_item_id_integration_id_pk" + } + }, + "uniqueConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "columns": ["integration_id", "kind"], + "name": "integrationSecret_integration_id_kind_pk" + } + }, + "uniqueConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "columns": ["integration_id", "permission", "user_id"], + "name": "integrationUserPermission_integration_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": { + "serverSetting_key_unique": { + "name": "serverSetting_key_unique", + "columns": ["key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "homeBoardId": { + "name": "homeBoardId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_homeBoardId_board_id_fk": { + "name": "user_homeBoardId_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["homeBoardId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": ["identifier", "token"], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index 4e36deafc..59fbb7ccc 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1720036615408, "tag": "0004_peaceful_red_ghost", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1722014142492, + "tag": "0005_lean_random", + "breakpoints": true } ] } diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index 824e269b5..d5312f503 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -13,6 +13,7 @@ import type { IntegrationPermission, IntegrationSecretKind, SectionKind, + SupportedAuthProvider, WidgetKind, } from "@homarr/definitions"; import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions"; @@ -25,6 +26,7 @@ export const users = mysqlTable("user", { image: text("image"), password: text("password"), salt: text("salt"), + provider: varchar("provider", { length: 64 }).$type().default("credentials").notNull(), homeBoardId: varchar("homeBoardId", { length: 64 }).references((): AnyMySqlColumn => boards.id, { onDelete: "set null", }), diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index dd72e42d5..2945f95cd 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -15,6 +15,7 @@ import type { IntegrationPermission, IntegrationSecretKind, SectionKind, + SupportedAuthProvider, WidgetKind, } from "@homarr/definitions"; @@ -26,6 +27,7 @@ export const users = sqliteTable("user", { image: text("image"), password: text("password"), salt: text("salt"), + provider: text("provider").$type().default("credentials").notNull(), homeBoardId: text("homeBoardId").references((): AnySQLiteColumn => boards.id, { onDelete: "set null", }), diff --git a/packages/definitions/src/auth.ts b/packages/definitions/src/auth.ts new file mode 100644 index 000000000..129d1135c --- /dev/null +++ b/packages/definitions/src/auth.ts @@ -0,0 +1,2 @@ +export const supportedAuthProviders = ["credentials", "oidc", "ldap"] as const; +export type SupportedAuthProvider = (typeof supportedAuthProviders)[number]; diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts index 42ce3cc29..140b2d924 100644 --- a/packages/definitions/src/index.ts +++ b/packages/definitions/src/index.ts @@ -4,3 +4,4 @@ export * from "./section"; export * from "./widget"; export * from "./permissions"; export * from "./docker"; +export * from "./auth"; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index f46d6b7ba..7fb97c109 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -195,6 +195,10 @@ export default { }, }, }, + memberNotice: { + mixed: "Some members are from external providers and cannot be managed here", + external: "All members are from external providers and cannot be managed here", + }, action: { create: { label: "New group", @@ -1334,6 +1338,8 @@ export default { }, user: { back: "Back to users", + fieldsDisabledExternalProvider: + "Certain fields are disabled because they are managed by an external authentication provider.", setting: { general: { title: "General", @@ -1379,7 +1385,7 @@ export default { }, }, invite: { - title: "Manager user invites", + title: "Manage user invites", action: { new: { title: "New invite",