UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

682 lines (680 loc) • 25.7 kB
import { toZodSchema } from "../../../db/to-zod.mjs"; import "../../../db/index.mjs"; import { setSessionCookie } from "../../../cookies/index.mjs"; import { getSessionFromCtx } from "../../../api/routes/session.mjs"; import "../../../api/index.mjs"; import { getOrgAdapter } from "../adapter.mjs"; import { orgMiddleware, orgSessionMiddleware } from "../call.mjs"; import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs"; import { hasPermission } from "../has-permission.mjs"; import { teamSchema } from "../schema.mjs"; import { APIError } from "better-call"; import * as z from "zod"; import { createAuthEndpoint } from "@better-auth/core/api"; //#region src/plugins/organization/routes/crud-team.ts const teamBaseSchema = z.object({ name: z.string().meta({ description: "The name of the team. Eg: \"my-team\"" }), organizationId: z.string().meta({ description: "The organization ID which the team will be created in. Defaults to the active organization. Eg: \"organization-id\"" }).optional() }); const createTeam = (options) => { const additionalFieldsSchema = toZodSchema({ fields: options?.schema?.team?.additionalFields ?? {}, isClientSide: true }); return createAuthEndpoint("/organization/create-team", { method: "POST", body: z.object({ ...teamBaseSchema.shape, ...additionalFieldsSchema.shape }), use: [orgMiddleware], metadata: { $Infer: { body: {} }, openapi: { description: "Create a new team within an organization", responses: { "200": { description: "Team created successfully", content: { "application/json": { schema: { type: "object", properties: { id: { type: "string", description: "Unique identifier of the created team" }, name: { type: "string", description: "Name of the team" }, organizationId: { type: "string", description: "ID of the organization the team belongs to" }, createdAt: { type: "string", format: "date-time", description: "Timestamp when the team was created" }, updatedAt: { type: "string", format: "date-time", description: "Timestamp when the team was last updated" } }, required: [ "id", "name", "organizationId", "createdAt", "updatedAt" ] } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); const organizationId = ctx.body.organizationId || session?.session.activeOrganizationId; if (!session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED"); if (!organizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION }); const adapter = getOrgAdapter(ctx.context, options); if (session) { const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION }); if (!await hasPermission({ role: member.role, options: ctx.context.orgOptions, permissions: { team: ["create"] }, organizationId }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION }); } const existingTeams = await adapter.listTeams(organizationId); const maximum = typeof ctx.context.orgOptions.teams?.maximumTeams === "function" ? await ctx.context.orgOptions.teams?.maximumTeams({ organizationId, session }, ctx) : ctx.context.orgOptions.teams?.maximumTeams; if (maximum ? existingTeams.length >= maximum : false) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS }); const { name, organizationId: _, ...additionalFields } = ctx.body; const organization = await adapter.findOrganizationById(organizationId); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); let teamData = { name, organizationId, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...additionalFields }; if (options?.organizationHooks?.beforeCreateTeam) { const response = await options?.organizationHooks.beforeCreateTeam({ team: { name, organizationId, ...additionalFields }, user: session?.user, organization }); if (response && typeof response === "object" && "data" in response) teamData = { ...teamData, ...response.data }; } const createdTeam = await adapter.createTeam(teamData); if (options?.organizationHooks?.afterCreateTeam) await options?.organizationHooks.afterCreateTeam({ team: createdTeam, user: session?.user, organization }); return ctx.json(createdTeam); }); }; const removeTeamBodySchema = z.object({ teamId: z.string().meta({ description: `The team ID of the team to remove. Eg: "team-id"` }), organizationId: z.string().meta({ description: `The organization ID which the team falls under. If not provided, it will default to the user's active organization. Eg: "organization-id"` }).optional() }); const removeTeam = (options) => createAuthEndpoint("/organization/remove-team", { method: "POST", body: removeTeamBodySchema, use: [orgMiddleware], metadata: { openapi: { description: "Remove a team from an organization", responses: { "200": { description: "Team removed successfully", content: { "application/json": { schema: { type: "object", properties: { message: { type: "string", description: "Confirmation message indicating successful removal", enum: ["Team removed successfully."] } }, required: ["message"] } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); const organizationId = ctx.body.organizationId || session?.session.activeOrganizationId; if (!organizationId) return ctx.json(null, { status: 400, body: { message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION } }); if (!session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED"); const adapter = getOrgAdapter(ctx.context, options); if (session) { const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member || session.session?.activeTeamId === ctx.body.teamId) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM }); if (!await hasPermission({ role: member.role, options: ctx.context.orgOptions, permissions: { team: ["delete"] }, organizationId }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION }); } const team = await adapter.findTeamById({ teamId: ctx.body.teamId, organizationId }); if (!team || team.organizationId !== organizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND }); if (!ctx.context.orgOptions.teams?.allowRemovingAllTeams) { if ((await adapter.listTeams(organizationId)).length <= 1) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.UNABLE_TO_REMOVE_LAST_TEAM }); } const organization = await adapter.findOrganizationById(organizationId); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); if (options?.organizationHooks?.beforeDeleteTeam) await options?.organizationHooks.beforeDeleteTeam({ team, user: session?.user, organization }); await adapter.deleteTeam(team.id); if (options?.organizationHooks?.afterDeleteTeam) await options?.organizationHooks.afterDeleteTeam({ team, user: session?.user, organization }); return ctx.json({ message: "Team removed successfully." }); }); const updateTeam = (options) => { const additionalFieldsSchema = toZodSchema({ fields: options?.schema?.team?.additionalFields ?? {}, isClientSide: true }); return createAuthEndpoint("/organization/update-team", { method: "POST", body: z.object({ teamId: z.string().meta({ description: `The ID of the team to be updated. Eg: "team-id"` }), data: z.object({ ...teamSchema.shape, ...additionalFieldsSchema.shape }).partial() }), requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware], metadata: { $Infer: { body: {} }, openapi: { description: "Update an existing team in an organization", responses: { "200": { description: "Team updated successfully", content: { "application/json": { schema: { type: "object", properties: { id: { type: "string", description: "Unique identifier of the updated team" }, name: { type: "string", description: "Updated name of the team" }, organizationId: { type: "string", description: "ID of the organization the team belongs to" }, createdAt: { type: "string", format: "date-time", description: "Timestamp when the team was created" }, updatedAt: { type: "string", format: "date-time", description: "Timestamp when the team was last updated" } }, required: [ "id", "name", "organizationId", "createdAt", "updatedAt" ] } } } } } } } }, async (ctx) => { const session = ctx.context.session; const organizationId = ctx.body.data.organizationId || session.session.activeOrganizationId; if (!organizationId) return ctx.json(null, { status: 400, body: { message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION } }); const adapter = getOrgAdapter(ctx.context, options); const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM }); if (!await hasPermission({ role: member.role, options: ctx.context.orgOptions, permissions: { team: ["update"] }, organizationId }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM }); const team = await adapter.findTeamById({ teamId: ctx.body.teamId, organizationId }); if (!team || team.organizationId !== organizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND }); const { name, organizationId: __, ...additionalFields } = ctx.body.data; const organization = await adapter.findOrganizationById(organizationId); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); const updates = { name, ...additionalFields }; if (options?.organizationHooks?.beforeUpdateTeam) { const response = await options?.organizationHooks.beforeUpdateTeam({ team, updates, user: session.user, organization }); if (response && typeof response === "object" && "data" in response) { const modifiedUpdates = response.data; const updatedTeam$1 = await adapter.updateTeam(team.id, modifiedUpdates); if (options?.organizationHooks?.afterUpdateTeam) await options?.organizationHooks.afterUpdateTeam({ team: updatedTeam$1, user: session.user, organization }); return ctx.json(updatedTeam$1); } } const updatedTeam = await adapter.updateTeam(team.id, updates); if (options?.organizationHooks?.afterUpdateTeam) await options?.organizationHooks.afterUpdateTeam({ team: updatedTeam, user: session.user, organization }); return ctx.json(updatedTeam); }); }; const listOrganizationTeamsQuerySchema = z.optional(z.object({ organizationId: z.string().meta({ description: `The organization ID which the teams are under to list. Defaults to the users active organization. Eg: "organization-id"` }).optional() })); const listOrganizationTeams = (options) => createAuthEndpoint("/organization/list-teams", { method: "GET", query: listOrganizationTeamsQuerySchema, metadata: { openapi: { description: "List all teams in an organization", responses: { "200": { description: "Teams retrieved successfully", content: { "application/json": { schema: { type: "array", items: { type: "object", properties: { id: { type: "string", description: "Unique identifier of the team" }, name: { type: "string", description: "Name of the team" }, organizationId: { type: "string", description: "ID of the organization the team belongs to" }, createdAt: { type: "string", format: "date-time", description: "Timestamp when the team was created" }, updatedAt: { type: "string", format: "date-time", description: "Timestamp when the team was last updated" } }, required: [ "id", "name", "organizationId", "createdAt", "updatedAt" ] }, description: "Array of team objects within the organization" } } } } } } }, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware] }, async (ctx) => { const session = ctx.context.session; const organizationId = ctx.query?.organizationId || session?.session.activeOrganizationId; if (!organizationId) throw ctx.error("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION }); const adapter = getOrgAdapter(ctx.context, options); if (!await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: organizationId || "" })) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION }); const teams = await adapter.listTeams(organizationId); return ctx.json(teams); }); const setActiveTeamBodySchema = z.object({ teamId: z.string().meta({ description: "The team id to set as active. It can be null to unset the active team" }).nullable().optional() }); const setActiveTeam = (options) => createAuthEndpoint("/organization/set-active-team", { method: "POST", body: setActiveTeamBodySchema, requireHeaders: true, use: [orgSessionMiddleware, orgMiddleware], metadata: { openapi: { description: "Set the active team", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", description: "The team", $ref: "#/components/schemas/Team" } } } } } } } }, async (ctx) => { const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); const session = ctx.context.session; if (ctx.body.teamId === null) { if (!session.session.activeTeamId) return ctx.json(null); await setSessionCookie(ctx, { session: await adapter.setActiveTeam(session.session.token, null, ctx), user: session.user }); return ctx.json(null); } let teamId; if (!ctx.body.teamId) { const sessionTeamId = session.session.activeTeamId; if (!sessionTeamId) return ctx.json(null); else teamId = sessionTeamId; } else teamId = ctx.body.teamId; const team = await adapter.findTeamById({ teamId }); if (!team) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND }); if (!await adapter.findTeamMember({ teamId, userId: session.user.id })) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM }); await setSessionCookie(ctx, { session: await adapter.setActiveTeam(session.session.token, team.id, ctx), user: session.user }); return ctx.json(team); }); const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-teams", { method: "GET", metadata: { openapi: { description: "List all teams that the current user is a part of.", responses: { "200": { description: "Teams retrieved successfully", content: { "application/json": { schema: { type: "array", items: { type: "object", description: "The team", $ref: "#/components/schemas/Team" }, description: "Array of team objects within the organization" } } } } } } }, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware] }, async (ctx) => { const session = ctx.context.session; const teams = await getOrgAdapter(ctx.context, ctx.context.orgOptions).listTeamsByUser({ userId: session.user.id }); return ctx.json(teams); }); const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) })); const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team-members", { method: "GET", query: listTeamMembersQuerySchema, metadata: { openapi: { description: "List the members of the given team.", responses: { "200": { description: "Teams retrieved successfully", content: { "application/json": { schema: { type: "array", items: { type: "object", description: "The team member", properties: { id: { type: "string", description: "Unique identifier of the team member" }, userId: { type: "string", description: "The user ID of the team member" }, teamId: { type: "string", description: "The team ID of the team the team member is in" }, createdAt: { type: "string", format: "date-time", description: "Timestamp when the team member was created" } }, required: [ "id", "userId", "teamId", "createdAt" ] }, description: "Array of team member objects within the team" } } } } } } }, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware] }, async (ctx) => { const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); const teamId = ctx.query?.teamId || session?.session.activeTeamId; if (!teamId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM }); if (!await adapter.findTeamMember({ userId: session.user.id, teamId })) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM }); const members = await adapter.listTeamMembers({ teamId }); return ctx.json(members); }); const addTeamMemberBodySchema = z.object({ teamId: z.string().meta({ description: "The team the user should be a member of." }), userId: z.coerce.string().meta({ description: "The user Id which represents the user to be added as a member." }) }); const addTeamMember = (options) => createAuthEndpoint("/organization/add-team-member", { method: "POST", body: addTeamMemberBodySchema, metadata: { openapi: { description: "The newly created member", responses: { "200": { description: "Team member created successfully", content: { "application/json": { schema: { type: "object", description: "The team member", properties: { id: { type: "string", description: "Unique identifier of the team member" }, userId: { type: "string", description: "The user ID of the team member" }, teamId: { type: "string", description: "The team ID of the team the team member is in" }, createdAt: { type: "string", format: "date-time", description: "Timestamp when the team member was created" } }, required: [ "id", "userId", "teamId", "createdAt" ] } } } } } } }, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware] }, async (ctx) => { const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); if (!session.session.activeOrganizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION }); const currentMember = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: session.session.activeOrganizationId }); if (!currentMember) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); if (!await hasPermission({ role: currentMember.role, options: ctx.context.orgOptions, permissions: { member: ["update"] }, organizationId: session.session.activeOrganizationId }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER }); if (!await adapter.findMemberByOrgId({ userId: ctx.body.userId, organizationId: session.session.activeOrganizationId })) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); const team = await adapter.findTeamById({ teamId: ctx.body.teamId, organizationId: session.session.activeOrganizationId }); if (!team) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND }); const organization = await adapter.findOrganizationById(session.session.activeOrganizationId); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); const userBeingAdded = await ctx.context.internalAdapter.findUserById(ctx.body.userId); if (!userBeingAdded) throw new APIError("BAD_REQUEST", { message: "User not found" }); if (options?.organizationHooks?.beforeAddTeamMember) { const response = await options?.organizationHooks.beforeAddTeamMember({ teamMember: { teamId: ctx.body.teamId, userId: ctx.body.userId }, team, user: userBeingAdded, organization }); if (response && typeof response === "object" && "data" in response) {} } const teamMember = await adapter.findOrCreateTeamMember({ teamId: ctx.body.teamId, userId: ctx.body.userId }); if (options?.organizationHooks?.afterAddTeamMember) await options?.organizationHooks.afterAddTeamMember({ teamMember, team, user: userBeingAdded, organization }); return ctx.json(teamMember); }); const removeTeamMemberBodySchema = z.object({ teamId: z.string().meta({ description: "The team the user should be removed from." }), userId: z.coerce.string().meta({ description: "The user which should be removed from the team." }) }); const removeTeamMember = (options) => createAuthEndpoint("/organization/remove-team-member", { method: "POST", body: removeTeamMemberBodySchema, metadata: { openapi: { description: "Remove a member from a team", responses: { "200": { description: "Team member removed successfully", content: { "application/json": { schema: { type: "object", properties: { message: { type: "string", description: "Confirmation message indicating successful removal", enum: ["Team member removed successfully."] } }, required: ["message"] } } } } } } }, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware] }, async (ctx) => { const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); if (!session.session.activeOrganizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION }); const currentMember = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: session.session.activeOrganizationId }); if (!currentMember) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); if (!await hasPermission({ role: currentMember.role, options: ctx.context.orgOptions, permissions: { member: ["delete"] }, organizationId: session.session.activeOrganizationId }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER }); if (!await adapter.findMemberByOrgId({ userId: ctx.body.userId, organizationId: session.session.activeOrganizationId })) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); const team = await adapter.findTeamById({ teamId: ctx.body.teamId, organizationId: session.session.activeOrganizationId }); if (!team) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND }); const organization = await adapter.findOrganizationById(session.session.activeOrganizationId); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); const userBeingRemoved = await ctx.context.internalAdapter.findUserById(ctx.body.userId); if (!userBeingRemoved) throw new APIError("BAD_REQUEST", { message: "User not found" }); const teamMember = await adapter.findTeamMember({ teamId: ctx.body.teamId, userId: ctx.body.userId }); if (!teamMember) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM }); if (options?.organizationHooks?.beforeRemoveTeamMember) await options?.organizationHooks.beforeRemoveTeamMember({ teamMember, team, user: userBeingRemoved, organization }); await adapter.removeTeamMember({ teamId: ctx.body.teamId, userId: ctx.body.userId }); if (options?.organizationHooks?.afterRemoveTeamMember) await options?.organizationHooks.afterRemoveTeamMember({ teamMember, team, user: userBeingRemoved, organization }); return ctx.json({ message: "Team member removed successfully." }); }); //#endregion export { addTeamMember, createTeam, listOrganizationTeams, listTeamMembers, listUserTeams, removeTeam, removeTeamMember, setActiveTeam, updateTeam }; //# sourceMappingURL=crud-team.mjs.map