UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

444 lines (442 loc) • 19.4 kB
import { toZodSchema } from "../../../db/to-zod.mjs"; import "../../../db/index.mjs"; import { setSessionCookie } from "../../../cookies/index.mjs"; import { getSessionFromCtx, requestOnlySessionMiddleware } 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 { APIError } from "better-call"; import * as z from "zod"; import { createAuthEndpoint } from "@better-auth/core/api"; //#region src/plugins/organization/routes/crud-org.ts const baseOrganizationSchema = z.object({ name: z.string().min(1).meta({ description: "The name of the organization" }), slug: z.string().min(1).meta({ description: "The slug of the organization" }), userId: z.coerce.string().meta({ description: "The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: \"user-id\"" }).optional(), logo: z.string().meta({ description: "The logo of the organization" }).optional(), metadata: z.record(z.string(), z.any()).meta({ description: "The metadata of the organization" }).optional(), keepCurrentActiveOrganization: z.boolean().meta({ description: "Whether to keep the current active organization active after creating a new one. Eg: true" }).optional() }); const createOrganization = (options) => { const additionalFieldsSchema = toZodSchema({ fields: options?.schema?.organization?.additionalFields || {}, isClientSide: true }); return createAuthEndpoint("/organization/create", { method: "POST", body: z.object({ ...baseOrganizationSchema.shape, ...additionalFieldsSchema.shape }), use: [orgMiddleware], metadata: { $Infer: { body: {} }, openapi: { description: "Create an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", description: "The organization that was created", $ref: "#/components/schemas/Organization" } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED"); let user = session?.user || null; if (!user) { if (!ctx.body.userId) throw new APIError("UNAUTHORIZED"); user = await ctx.context.internalAdapter.findUserById(ctx.body.userId); } if (!user) return ctx.json(null, { status: 401 }); const options$1 = ctx.context.orgOptions; const canCreateOrg = typeof options$1?.allowUserToCreateOrganization === "function" ? await options$1.allowUserToCreateOrganization(user) : options$1?.allowUserToCreateOrganization === void 0 ? true : options$1.allowUserToCreateOrganization; const isSystemAction = !session && ctx.body.userId; if (!canCreateOrg && !isSystemAction) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION }); const adapter = getOrgAdapter(ctx.context, options$1); const userOrganizations = await adapter.listOrganizations(user.id); if (typeof options$1.organizationLimit === "number" ? userOrganizations.length >= options$1.organizationLimit : typeof options$1.organizationLimit === "function" ? await options$1.organizationLimit(user) : false) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS }); if (await adapter.findOrganizationBySlug(ctx.body.slug)) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS }); let { keepCurrentActiveOrganization: _, userId: __, ...orgData } = ctx.body; if (options$1.organizationCreation?.beforeCreate) { const response = await options$1.organizationCreation.beforeCreate({ organization: { ...orgData, createdAt: /* @__PURE__ */ new Date() }, user }, ctx.request); if (response && typeof response === "object" && "data" in response) orgData = { ...ctx.body, ...response.data }; } if (options$1?.organizationHooks?.beforeCreateOrganization) { const response = await options$1?.organizationHooks.beforeCreateOrganization({ organization: orgData, user }); if (response && typeof response === "object" && "data" in response) orgData = { ...ctx.body, ...response.data }; } const organization = await adapter.createOrganization({ organization: { ...orgData, createdAt: /* @__PURE__ */ new Date() } }); let member; let teamMember = null; let data = { userId: user.id, organizationId: organization.id, role: ctx.context.orgOptions.creatorRole || "owner" }; if (options$1?.organizationHooks?.beforeAddMember) { const response = await options$1?.organizationHooks.beforeAddMember({ member: { userId: user.id, organizationId: organization.id, role: ctx.context.orgOptions.creatorRole || "owner" }, user, organization }); if (response && typeof response === "object" && "data" in response) data = { ...data, ...response.data }; } member = await adapter.createMember(data); if (options$1?.organizationHooks?.afterAddMember) await options$1?.organizationHooks.afterAddMember({ member, user, organization }); if (options$1?.teams?.enabled && options$1.teams.defaultTeam?.enabled !== false) { let teamData = { organizationId: organization.id, name: `${organization.name}`, createdAt: /* @__PURE__ */ new Date() }; if (options$1?.organizationHooks?.beforeCreateTeam) { const response = await options$1?.organizationHooks.beforeCreateTeam({ team: { organizationId: organization.id, name: `${organization.name}` }, user, organization }); if (response && typeof response === "object" && "data" in response) teamData = { ...teamData, ...response.data }; } const defaultTeam = await options$1.teams.defaultTeam?.customCreateDefaultTeam?.(organization, ctx) || await adapter.createTeam(teamData); teamMember = await adapter.findOrCreateTeamMember({ teamId: defaultTeam.id, userId: user.id }); if (options$1?.organizationHooks?.afterCreateTeam) await options$1?.organizationHooks.afterCreateTeam({ team: defaultTeam, user, organization }); } if (options$1.organizationCreation?.afterCreate) await options$1.organizationCreation.afterCreate({ organization, user, member }, ctx.request); if (options$1?.organizationHooks?.afterCreateOrganization) await options$1?.organizationHooks.afterCreateOrganization({ organization, user, member }); if (ctx.context.session && !ctx.body.keepCurrentActiveOrganization) await adapter.setActiveOrganization(ctx.context.session.session.token, organization.id, ctx); if (teamMember && ctx.context.session && !ctx.body.keepCurrentActiveOrganization) await adapter.setActiveTeam(ctx.context.session.session.token, teamMember.teamId, ctx); return ctx.json({ ...organization, metadata: organization.metadata && typeof organization.metadata === "string" ? JSON.parse(organization.metadata) : organization.metadata, members: [member] }); }); }; const checkOrganizationSlugBodySchema = z.object({ slug: z.string().meta({ description: "The organization slug to check. Eg: \"my-org\"" }) }); const checkOrganizationSlug = (options) => createAuthEndpoint("/organization/check-slug", { method: "POST", body: checkOrganizationSlugBodySchema, use: [requestOnlySessionMiddleware, orgMiddleware] }, async (ctx) => { if (!await getOrgAdapter(ctx.context, options).findOrganizationBySlug(ctx.body.slug)) return ctx.json({ status: true }); throw new APIError("BAD_REQUEST", { message: "slug is taken" }); }); const baseUpdateOrganizationSchema = z.object({ name: z.string().min(1).meta({ description: "The name of the organization" }).optional(), slug: z.string().min(1).meta({ description: "The slug of the organization" }).optional(), logo: z.string().meta({ description: "The logo of the organization" }).optional(), metadata: z.record(z.string(), z.any()).meta({ description: "The metadata of the organization" }).optional() }); const updateOrganization = (options) => { const additionalFieldsSchema = toZodSchema({ fields: options?.schema?.organization?.additionalFields || {}, isClientSide: true }); return createAuthEndpoint("/organization/update", { method: "POST", body: z.object({ data: z.object({ ...additionalFieldsSchema.shape, ...baseUpdateOrganizationSchema.shape }).partial(), organizationId: z.string().meta({ description: "The organization ID. Eg: \"org-id\"" }).optional() }), requireHeaders: true, use: [orgMiddleware], metadata: { $Infer: { body: {} }, openapi: { description: "Update an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", description: "The updated organization", $ref: "#/components/schemas/Organization" } } } } } } } }, async (ctx) => { const session = await ctx.context.getSession(ctx); if (!session) throw new APIError("UNAUTHORIZED", { message: "User not found" }); const organizationId = ctx.body.organizationId || session.session.activeOrganizationId; if (!organizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); const adapter = getOrgAdapter(ctx.context, options); const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); if (!await hasPermission({ permissions: { organization: ["update"] }, role: member.role, options: ctx.context.orgOptions, organizationId }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION }); if (typeof ctx.body.data.slug === "string") { const existingOrganization = await adapter.findOrganizationBySlug(ctx.body.data.slug); if (existingOrganization && existingOrganization.id !== organizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_SLUG_ALREADY_TAKEN }); } if (options?.organizationHooks?.beforeUpdateOrganization) { const response = await options.organizationHooks.beforeUpdateOrganization({ organization: ctx.body.data, user: session.user, member }); if (response && typeof response === "object" && "data" in response) ctx.body.data = { ...ctx.body.data, ...response.data }; } const updatedOrg = await adapter.updateOrganization(organizationId, ctx.body.data); if (options?.organizationHooks?.afterUpdateOrganization) await options.organizationHooks.afterUpdateOrganization({ organization: updatedOrg, user: session.user, member }); return ctx.json(updatedOrg); }); }; const deleteOrganizationBodySchema = z.object({ organizationId: z.string().meta({ description: "The organization id to delete" }) }); const deleteOrganization = (options) => { return createAuthEndpoint("/organization/delete", { method: "POST", body: deleteOrganizationBodySchema, requireHeaders: true, use: [orgMiddleware], metadata: { openapi: { description: "Delete an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "string", description: "The organization id that was deleted" } } } } } } } }, async (ctx) => { if (ctx.context.orgOptions.organizationDeletion?.disabled || ctx.context.orgOptions.disableOrganizationDeletion) { if (ctx.context.orgOptions.organizationDeletion?.disabled) ctx.context.logger.info("`organizationDeletion.disabled` is deprecated. Use `disableOrganizationDeletion` instead"); throw new APIError("NOT_FOUND", { message: "Organization deletion is disabled" }); } const session = await ctx.context.getSession(ctx); if (!session) throw new APIError("UNAUTHORIZED", { status: 401 }); const organizationId = ctx.body.organizationId; if (!organizationId) return ctx.json(null, { status: 400, body: { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND } }); const adapter = getOrgAdapter(ctx.context, options); const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); if (!await hasPermission({ role: member.role, permissions: { organization: ["delete"] }, organizationId, options: ctx.context.orgOptions }, ctx)) throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION }); if (organizationId === session.session.activeOrganizationId) /** * If the organization is deleted, we set the active organization to null */ await adapter.setActiveOrganization(session.session.token, null, ctx); const org = await adapter.findOrganizationById(organizationId); if (!org) throw new APIError("BAD_REQUEST"); if (options?.organizationHooks?.beforeDeleteOrganization) await options.organizationHooks.beforeDeleteOrganization({ organization: org, user: session.user }); await adapter.deleteOrganization(organizationId); if (options?.organizationHooks?.afterDeleteOrganization) await options.organizationHooks.afterDeleteOrganization({ organization: org, user: session.user }); return ctx.json(org); }); }; const getFullOrganizationQuerySchema = z.optional(z.object({ organizationId: z.string().meta({ description: "The organization id to get" }).optional(), organizationSlug: z.string().meta({ description: "The organization slug to get" }).optional(), membersLimit: z.number().or(z.string().transform((val) => parseInt(val))).meta({ description: "The limit of members to get. By default, it uses the membershipLimit option which defaults to 100." }).optional() })); const getFullOrganization = (options) => createAuthEndpoint("/organization/get-full-organization", { method: "GET", query: getFullOrganizationQuerySchema, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware], metadata: { openapi: { operationId: "getOrganization", description: "Get the full organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", description: "The organization", $ref: "#/components/schemas/Organization" } } } } } } } }, async (ctx) => { const session = ctx.context.session; const organizationId = ctx.query?.organizationSlug || ctx.query?.organizationId || session.session.activeOrganizationId; if (!organizationId) return ctx.json(null, { status: 200 }); const adapter = getOrgAdapter(ctx.context, options); const organization = await adapter.findFullOrganization({ organizationId, isSlug: !!ctx.query?.organizationSlug, includeTeams: ctx.context.orgOptions.teams?.enabled, membersLimit: ctx.query?.membersLimit }); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); if (!await adapter.checkMembership({ userId: session.user.id, organizationId: organization.id })) { await adapter.setActiveOrganization(session.session.token, null, ctx); throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); } return ctx.json(organization); }); const setActiveOrganizationBodySchema = z.object({ organizationId: z.string().meta({ description: "The organization id to set as active. It can be null to unset the active organization. Eg: \"org-id\"" }).nullable().optional(), organizationSlug: z.string().meta({ description: "The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: \"org-slug\"" }).optional() }); const setActiveOrganization = (options) => { return createAuthEndpoint("/organization/set-active", { method: "POST", body: setActiveOrganizationBodySchema, use: [orgSessionMiddleware, orgMiddleware], requireHeaders: true, metadata: { openapi: { operationId: "setActiveOrganization", description: "Set the active organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", description: "The organization", $ref: "#/components/schemas/Organization" } } } } } } } }, async (ctx) => { const adapter = getOrgAdapter(ctx.context, options); const session = ctx.context.session; let organizationId = ctx.body.organizationId; const organizationSlug = ctx.body.organizationSlug; if (organizationId === null) { if (!session.session.activeOrganizationId) return ctx.json(null); await setSessionCookie(ctx, { session: await adapter.setActiveOrganization(session.session.token, null, ctx), user: session.user }); return ctx.json(null); } if (!organizationId && !organizationSlug) { const sessionOrgId = session.session.activeOrganizationId; if (!sessionOrgId) return ctx.json(null); organizationId = sessionOrgId; } if (organizationSlug && !organizationId) { const organization$1 = await adapter.findOrganizationBySlug(organizationSlug); if (!organization$1) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); organizationId = organization$1.id; } if (!organizationId) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); if (!await adapter.checkMembership({ userId: session.user.id, organizationId })) { await adapter.setActiveOrganization(session.session.token, null, ctx); throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION }); } const organization = await adapter.findOrganizationById(organizationId); if (!organization) throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); await setSessionCookie(ctx, { session: await adapter.setActiveOrganization(session.session.token, organization.id, ctx), user: session.user }); return ctx.json(organization); }); }; const listOrganizations = (options) => createAuthEndpoint("/organization/list", { method: "GET", use: [orgMiddleware, orgSessionMiddleware], requireHeaders: true, metadata: { openapi: { description: "List all organizations", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Organization" } } } } } } } } }, async (ctx) => { const organizations = await getOrgAdapter(ctx.context, options).listOrganizations(ctx.context.session.user.id); return ctx.json(organizations); }); //#endregion export { checkOrganizationSlug, createOrganization, deleteOrganization, getFullOrganization, listOrganizations, setActiveOrganization, updateOrganization }; //# sourceMappingURL=crud-org.mjs.map