diff --git a/.changeset/curvy-flowers-grin.md b/.changeset/curvy-flowers-grin.md new file mode 100644 index 000000000..2c0ae5f9a --- /dev/null +++ b/.changeset/curvy-flowers-grin.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Remove organization functions as better-auth can handle them now server-side as well diff --git a/.changeset/polite-otters-follow.md b/.changeset/polite-otters-follow.md new file mode 100644 index 000000000..5eedcca2c --- /dev/null +++ b/.changeset/polite-otters-follow.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": patch +--- + +Fix type of FdmAuth by including plugins and other settings diff --git a/.changeset/twelve-chicken-poke.md b/.changeset/twelve-chicken-poke.md new file mode 100644 index 000000000..c9d0912da --- /dev/null +++ b/.changeset/twelve-chicken-poke.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": patch +--- + +Use Better-Auth functions for organizations instead of fdm-core functions diff --git a/fdm-app/app/lib/auth.server.ts b/fdm-app/app/lib/auth.server.ts index d7c7bde6b..89eb06f77 100644 --- a/fdm-app/app/lib/auth.server.ts +++ b/fdm-app/app/lib/auth.server.ts @@ -33,11 +33,11 @@ if (serverConfig.mail) { create: { ...auth.options.databaseHooks?.user?.create, after: async ( - user: ExtendedUser, + user: any, context?: GenericEndpointContext, ) => { if (originalUserCreateAfter) { - await originalUserCreateAfter(user, context) + await originalUserCreateAfter(user) } try { const email = await renderWelcomeEmail(user) @@ -75,10 +75,11 @@ export async function getSession(request: Request): Promise { } // Determine userName - let displayUserName = user.displayUsername - if (!displayUserName) { - displayUserName = createDisplayUsername(user.firstname, user.surname) - } + const displayUserName = + user.displayUsername || + createDisplayUsername(user.firstname, user.surname) || + user.name || + user.email // Expand session const sessionWithUserName = { diff --git a/fdm-app/app/routes/organization.$slug.tsx b/fdm-app/app/routes/organization.$slug.tsx index 968d3f000..31c8d8b5b 100644 --- a/fdm-app/app/routes/organization.$slug.tsx +++ b/fdm-app/app/routes/organization.$slug.tsx @@ -1,12 +1,5 @@ -import { - cancelPendingInvitation, - getOrganization, - getPendingInvitationsForOrganization, - getUsersInOrganization, - inviteUserToOrganization, - removeUserFromOrganization, - updateRoleOfUserAtOrganization, -} from "@svenvw/fdm-core" +import type { User } from "better-auth" +import type { Invitation, Member, Organization } from "better-auth/plugins" import { formatDistanceToNow } from "date-fns" import { nl } from "date-fns/locale" import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router" @@ -32,10 +25,9 @@ import { SelectValue, } from "~/components/ui/select" import { Separator } from "~/components/ui/separator" -import { getSession } from "~/lib/auth.server" +import { auth, getSession } from "~/lib/auth.server" import { renderInvitationEmail, sendEmail } from "~/lib/email.server" import { handleActionError, handleLoaderError } from "~/lib/error" -import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" export async function loader({ request, params }: LoaderFunctionArgs) { @@ -44,27 +36,58 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } const session = await getSession(request) - const organization = await getOrganization( - fdm, - params.slug, - session.user.id, - ) + const organizations = await auth.api.listOrganizations({ + headers: request.headers, + }) + + const organization = organizations.find((org) => org.slug === params.slug) if (!organization) { throw handleLoaderError("not found: organization") } // Get members of organization - const members = await getUsersInOrganization(fdm, params.slug) + const membersListResponse = await auth.api.listMembers({ + headers: request.headers, + query: { + organizationId: organization.id, + }, + }) + const members = membersListResponse.members + + // Determine permissions + const currentUserMember = members.find((m) => m.userId === session.user.id) + const role = currentUserMember?.role || "viewer" + const permissions = { + canEdit: role === "owner" || role === "admin", + canDelete: role === "owner", + canInvite: role === "owner" || role === "admin", + canUpdateRoleUser: role === "owner" || role === "admin", + canRemoveUser: role === "owner" || role === "admin", + } // Get pending invitations of organization - const invitations = await getPendingInvitationsForOrganization( - fdm, - organization.id, - ) + let invitations: Invitation[] = [] + if (permissions.canInvite) { + const invitationsListResponse = await auth.api.listInvitations({ + headers: request.headers, + query: { + organizationId: organization.id, + }, + }) + invitations = ( + Array.isArray(invitationsListResponse) + ? invitationsListResponse + : [] + ).filter((inv) => inv.status === "pending") + } return { - organization: organization, + organization: { + ...organization, + permissions, + description: organization.metadata?.description || "", + }, invitations: invitations, members: members, } @@ -103,10 +126,9 @@ export default function OrganizationIndex() {
{members.map((member) => ( ))}
@@ -142,11 +164,8 @@ export default function OrganizationIndex() {
{invitations.map((invitation) => ( ))}
@@ -161,18 +180,20 @@ export default function OrganizationIndex() { ) } +type MemberWithUser = Member & { + user: { + id: string + name: string + email: string + image?: string | null + } +} + const MemberRow = ({ member, permissions, }: { - member: { - id: string - firstname: string - surname: string - username: string - role: string - image: string - } + member: MemberWithUser permissions: { canEdit: boolean canDelete: boolean @@ -181,20 +202,17 @@ const MemberRow = ({ canRemoveUser: boolean } }) => { - const initials = member.firstname.charAt(0) + member.surname.charAt(0) + const initials = (member.user.name || "?").charAt(0).toUpperCase() return ( -
+
- + {initials}

- {member.firstname} {member.surname} + {member.user.name}

{!permissions.canUpdateRoleUser ? (

@@ -214,13 +232,7 @@ const MemberAction = ({ member, permissions, }: { - member: { - firstname: string - surname: string - username: string - role: string - image: string - } + member: MemberWithUser permissions: { canEdit: boolean canDelete: boolean @@ -231,15 +243,15 @@ const MemberAction = ({ }) => { return (

- + {permissions.canRemoveUser ? ( @@ -264,27 +276,13 @@ const MemberAction = ({ ) } -const InvitationRow = ({ - invitation, -}: { - invitation: { - email: string - role: string - expires_at: Date - inviter_firstname: string - inviter_surname: string - invitation_id: string - } -}) => { +const InvitationRow = ({ invitation }: { invitation: Invitation }) => { return ( -
+
- {invitation.email.charAt(0).toUpperCase()} + {(invitation.email || "?").charAt(0).toUpperCase()}
@@ -299,21 +297,19 @@ const InvitationRow = ({

Verloopt{" "} - {formatDistanceToNow(invitation.expires_at, { + {formatDistanceToNow(new Date(invitation.expiresAt), { addSuffix: true, locale: nl, })}

-

- {`Uitgenodigd door: ${invitation.inviter_firstname} ${invitation.inviter_surname}`} -

+
{organizations.length === 0 ? ( -
+

Je bent nog geen lid van een organisatie @@ -80,25 +66,20 @@ export default function OrganizationsIndex() {

) : (
- {organizations.map((org: OrganizationType) => ( - + {organizations.map((org) => ( +
- {org.name} - - {org.role} - + {org.name}

- {org.description} + {org.metadata?.description ?? + "Geen beschrijving"}

diff --git a/fdm-app/app/routes/organization.invitations.$invitation_id.respond.tsx b/fdm-app/app/routes/organization.invitations.$invitation_id.respond.tsx index d0fbf7255..df86969a0 100644 --- a/fdm-app/app/routes/organization.invitations.$invitation_id.respond.tsx +++ b/fdm-app/app/routes/organization.invitations.$invitation_id.respond.tsx @@ -1,8 +1,3 @@ -import { - acceptInvitation, - getPendingInvitation, - rejectInvitation, -} from "@svenvw/fdm-core" import { useEffect } from "react" import { type ActionFunctionArgs, @@ -14,7 +9,7 @@ import { useSearchParams, useSubmit, } from "react-router" -import { redirectWithSuccess } from "remix-toast" +import { dataWithError, redirectWithSuccess } from "remix-toast" import z from "zod" import { Button } from "~/components/ui/button" import { @@ -25,8 +20,7 @@ import { CardTitle, } from "~/components/ui/card" import { Separator } from "~/components/ui/separator" -import { getSession } from "~/lib/auth.server" -import { fdm } from "~/lib/fdm.server" +import { auth, getSession } from "~/lib/auth.server" import { extractFormValuesFromRequest } from "~/lib/form" import type { Route } from "../+types/root" @@ -34,36 +28,32 @@ export async function loader({ request, params }: LoaderFunctionArgs) { await getSession(request) // Check for valid invitation id - if (!params.invitation_id) { - throw failBadRequest("Bad Request: invitation id missing") + const invitationId = params.invitation_id + if (!invitationId) { + throw data("Invitation not found", { + status: 404, + statusText: "Invitation not found", + }) } - - // Check for valid invitation - try { - const invitation = await getPendingInvitation(fdm, params.invitation_id) - - return { - invitationId: invitation.invitation_id, - inviterFirstName: invitation.inviter_firstname, - inviterSurname: invitation.inviter_surname, - organizationSlug: invitation.organization_slug, - organizationName: invitation.organization_name, - role: invitation.role, - } - } catch (_e) { - throw data("Invitation not found", 404) + const invitation = await auth.api.getInvitation({ + query: { + id: invitationId, + }, + headers: request.headers, + }) + + if (!invitation) { + throw data("Uitnodiging niet gevonden", { + status: 404, + statusText: "Invitation not found", + }) } + + return invitation } export default function Respond() { - const { - invitationId, - inviterFirstName, - inviterSurname, - organizationSlug, - organizationName, - role, - } = useLoaderData() + const invitation = useLoaderData() const [searchParams] = useSearchParams() const intentRaw = searchParams.get("intent") @@ -75,22 +65,20 @@ export default function Respond() { if (intent && intent === "accept") { submit( { - invitation_id: invitationId, intent: "accept", - organization_slug: organizationSlug, }, { method: "POST" }, ) } - }, [intent, submit, invitationId, organizationSlug]) + }, [intent, submit]) - if (intent !== "accept" && intent !== "reject") { - throw failBadRequest(`Invalid intent: ${intent}`) + if (intent !== "accept" && intent !== "reject" && intent !== "do_nothing") { + throw new Error(`Invalid intent: ${intent}`) } if (intent === "accept") { return ( -

+

Uitnodiging wordt geaccepteerd...

) @@ -105,12 +93,12 @@ export default function Respond() {

- {`${inviterFirstName} ${inviterSurname} heeft je uitgenodigd om lid te worden van de organisatie `} - {`${organizationName}.`} + {`${invitation.inviterEmail} heeft je uitgenodigd om lid te worden van de organisatie `} + {`${invitation.organizationName}.`}

Je bent uitgenodigd als{" "} - {role} + {invitation.role}

Weet je zeker dat je deze uitnodiging wilt afwijzen? @@ -121,7 +109,7 @@ export default function Respond() {

{invitations.length === 0 ? ( -
+

Je hebt op dit moment geen uitnodigingen open @@ -95,15 +88,14 @@ export default function OrganizationsIndex() { ) : (
{invitations.map((invitation) => ( - + - {invitation.organization_name} + {invitation.organizationName} Uitgenodigd door{" "} - {invitation.inviter_firstname}{" "} - {invitation.inviter_surname} + {invitation.inviterEmail} @@ -117,7 +109,7 @@ export default function OrganizationsIndex() {

Verloopt{" "} {formatDistanceToNow( - invitation.expires_at, + new Date(invitation.expiresAt), { addSuffix: true, locale: nl, @@ -128,7 +120,7 @@ export default function OrganizationsIndex() {