feat(organization): multi-team support

This commit is contained in:
Bereket Engida
2025-07-19 12:07:35 -07:00
parent 8e4f9b6c6f
commit 28c846a7cc
15 changed files with 1383 additions and 128 deletions
+138 -8
View File
@@ -1151,6 +1151,80 @@ type removeTeam = {
```
</APIMethod>
#### Set Active Team
Sets the given team as the current active team. If `teamId` is `null` the current active team is unset.
<APIMethod path="/organization/set-active-team" method="POST">
```ts
type setActiveTeam = {
/**
* The team ID of the team to set as the current active team.
*/
teamId?: string = "team-id"
}
```
</APIMethod>
#### List User Teams
List all teams that the current user is a part of.
<APIMethod path="/organization/list-user-teams" method="GET">
```ts
type listUserTeams = {
}
```
</APIMethod>
#### List Team Members
List the members of the given team.
<APIMethod path="/organization/list-team-members" method="POST">
```ts
type listTeamMembers = {
/**
* The team whose members we should return. If this is not provided the members of the current active team get returned.
*/
teamId?: string = "team-id"
}
```
</APIMethod>
#### Add Team Member
Add a member to a team.
<APIMethod path="/organization/add-team-member" method="POST">
```ts
type addTeamMember = {
/**
* The team the user should be a member of.
*/
teamId: string = "team-id"
/**
* The user Id which represents the user to be added as a member.
*/
userId: string = "user-id"
}
```
</APIMethod>
#### Remove Team Member
Remove a member from a team.
<APIMethod path="/organization/remove-team-member" method="POST">
```ts
type removeTeamMember = {
/**
* The team the user should be removed from.
*/
teamId: string = "team-id"
/**
* The user which should be removed from the team.
*/
userId: string = "user-id"
}
```
</APIMethod>
### Team Permissions
Teams follow the organization's permission system. To manage teams, users need the following permissions:
@@ -1212,7 +1286,9 @@ The invited member will be added to the specified team upon accepting the invita
### Database Schema
When teams are enabled, a new `team` table is added with the following structure:
When teams are enabled, new `team` and `teamMember` tables are added to the database.
Table Name: `team`
<DatabaseTable
fields={[
@@ -1241,8 +1317,39 @@ When teams are enabled, a new `team` table is added with the following structure
{
name: "updatedAt",
type: "Date",
description: "Timestamp of when the team was last updated"
}
isOptional: true,
description: "Timestamp of when the team was created"
},
]}
/>
Table Name: `teamMember`
<DatabaseTable
fields={[
{
name: "id",
type: "string",
description: "Unique identifier for each team member",
isPrimaryKey: true
},
{
name: "teamId",
type: "string",
description: "Unique identifier for each team",
isForeignKey: true
},
{
name: "userId",
type: "string",
description: "The ID of the user",
isForeignKey: true
},
{
name: "createdAt",
type: "Date",
description: "Timestamp of when the team member was created"
},
]}
/>
@@ -1385,7 +1492,7 @@ Table Name: `invitation`
Table Name: `session`
You need to add one more field to the session table to store the active organization ID.
You need to add two more fields to the session table to store the active organization ID and the active team ID.
<DatabaseTable
fields={[
@@ -1395,6 +1502,12 @@ You need to add one more field to the session table to store the active organiza
description: "The ID of the active organization",
isOptional: true
},
{
name: "activeTeamId",
type: "string",
description: "The ID of the active team",
isOptional: true
},
]}
/>
@@ -1435,20 +1548,37 @@ Table Name: `team`
]}
/>
Table Name: `member`
Table Name: `teamMember`
<DatabaseTable
fields={[
{
name: "id",
type: "string",
description: "Unique identifier for each team member",
isPrimaryKey: true
},
{
name: "teamId",
type: "string",
description: "The ID of the team",
isOptional: true
description: "Unique identifier for each team",
isForeignKey: true
},
{
name: "userId",
type: "string",
description: "The ID of the user",
isForeignKey: true
},
{
name: "createdAt",
type: "Date",
description: "Timestamp of when the team member was created"
},
]}
/>
Table Name: `invitation`
Table Name: `invitation`
<DatabaseTable
fields={[
@@ -12,6 +12,7 @@ import type {
OrganizationInput,
Team,
TeamInput,
TeamMember,
} from "./schema";
import { BetterAuthError } from "../../error";
import type { AuthContext } from "../../init";
@@ -444,7 +445,7 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
includeTeamMembers?: IncludeMembers;
}): Promise<
| (InferTeam<O> &
(IncludeMembers extends true ? { members: InferMember<O>[] } : {}))
(IncludeMembers extends true ? { members: TeamMember[] } : {}))
| null
> => {
const team = await adapter.findOne<InferTeam<O>>({
@@ -467,10 +468,11 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
if (!team) {
return null;
}
let members: InferMember<O>[] = [];
let members: TeamMember[] = [];
if (includeTeamMembers) {
members = await adapter.findMany<InferMember<O>>({
model: "member",
members = await adapter.findMany<TeamMember>({
model: "teamMember",
where: [
{
field: "teamId",
@@ -484,8 +486,9 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
members,
};
}
return team as InferTeam<O> &
(IncludeMembers extends true ? { members: InferMember<O>[] } : {});
(IncludeMembers extends true ? { members: TeamMember[] } : {});
},
updateTeam: async (
teamId: string,
@@ -570,6 +573,130 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
return invitation;
},
setActiveTeam: async (sessionToken: string, teamId: string | null) => {
const session = await context.internalAdapter.updateSession(
sessionToken,
{
activeTeamId: teamId,
},
);
return session as Session;
},
listTeamMembers: async (data: {
teamId: string;
}) => {
const members = await adapter.findMany<TeamMember>({
model: "teamMember",
where: [
{
field: "teamId",
value: data.teamId,
},
],
});
return members;
},
listTeamsByUser: async (data: {
userId: string;
}) => {
const members = await adapter.findMany<TeamMember>({
model: "teamMember",
where: [
{
field: "userId",
value: data.userId,
},
],
});
const teams = await adapter.findMany<Team>({
model: "team",
where: [
{
field: "id",
operator: "in",
value: members.map((m) => m.teamId),
},
],
});
return teams;
},
findTeamMember: async (data: {
teamId: string;
userId: string;
}) => {
const member = await adapter.findOne<TeamMember>({
model: "teamMember",
where: [
{
field: "teamId",
value: data.teamId,
},
{
field: "userId",
value: data.userId,
},
],
});
return member;
},
findOrCreateTeamMember: async (data: {
teamId: string;
userId: string;
}) => {
const member = await adapter.findOne<TeamMember>({
model: "teamMember",
where: [
{
field: "teamId",
value: data.teamId,
},
{
field: "userId",
value: data.userId,
},
],
});
if (member) return member;
return await adapter.create<Omit<TeamMember, "id">, TeamMember>({
model: "teamMember",
data: {
teamId: data.teamId,
userId: data.userId,
createdAt: new Date(),
},
});
},
removeTeamMember: async (data: {
teamId: string;
userId: string;
}) => {
await adapter.delete({
model: "teamMember",
where: [
{
field: "teamId",
value: data.teamId,
},
{
field: "userId",
value: data.userId,
},
],
});
},
findInvitationsByTeamId: async (teamId: string) => {
const invitations = await adapter.findMany<InferInvitation<O>>({
model: "invitation",
@@ -597,7 +724,7 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
email: string;
role: string;
organizationId: string;
teamId?: string;
teamIds: string[];
} & Record<string, any>; // This represents the additionalFields for the invitation
user: User;
}) => {
@@ -606,6 +733,7 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
options?.invitationExpiresIn || defaultExpiration,
"sec",
);
const invite = await adapter.create<
Omit<InvitationInput, "id">,
InferInvitation<O>
@@ -616,6 +744,7 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
expiresAt,
inviterId: user.id,
...invitation,
teamId: invitation.teamIds.join(","),
},
});
@@ -5,7 +5,7 @@ import type { Role } from "../access";
import type { OrganizationOptions } from "./types";
import type { defaultRoles } from "./access/statement";
export const orgMiddleware = createAuthMiddleware(async (ctx) => {
export const orgMiddleware = createAuthMiddleware(async () => {
return {} as {
orgOptions: OrganizationOptions;
roles: typeof defaultRoles & {
@@ -13,6 +13,7 @@ export const orgMiddleware = createAuthMiddleware(async (ctx) => {
};
getSession: (context: GenericEndpointContext) => Promise<{
session: Session & {
activeTeamId?: string;
activeOrganizationId?: string;
};
user: User;
@@ -31,6 +32,7 @@ export const orgSessionMiddleware = createAuthMiddleware(
async (ctx) => {
const session = ctx.context.session as {
session: Session & {
activeTeamId?: string;
activeOrganizationId?: string;
};
user: User;
@@ -55,4 +55,12 @@ export const ORGANIZATION_ERROR_CODES = {
"You are not allowed to delete this team",
INVITATION_LIMIT_REACHED: "Invitation limit reached",
TEAM_MEMBER_LIMIT_REACHED: "Team member limit reached",
USER_IS_NOT_A_MEMBER_OF_THE_TEAM: "User is not a member of the team",
YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM:
"You are not allowed to list the members of this team",
YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM: "You do not have an active team",
YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER:
"You are not allowed to create a new member",
YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER:
"You are not allowed to remove a team member",
} as const;
@@ -1327,6 +1327,9 @@ describe("Additional Fields", async () => {
teamRequiredField: string;
teamOptionalField?: string;
}[],
teamMember: [] as {
id: string;
}[],
};
const orgOptions = {
@@ -1429,7 +1432,6 @@ describe("Additional Fields", async () => {
userId: string;
role: string;
createdAt: Date;
teamId?: string | undefined;
} & {
memberRequiredField: string;
} & {
@@ -39,12 +39,18 @@ import {
listOrganizationTeams,
removeTeam,
updateTeam,
setActiveTeam,
listUserTeams,
listTeamMembers,
addTeamMember,
removeTeamMember,
} from "./routes/crud-team";
import type {
InferInvitation,
InferMember,
InferOrganization,
Team,
TeamMember,
} from "./schema";
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import { defaultRoles, defaultStatements } from "./access";
@@ -431,6 +437,86 @@ export const organization = <O extends OrganizationOptions>(
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update-team)
*/
updateTeam: updateTeam(options as O),
/**
* ### Endpoint
*
* POST `/organization/set-active-team`
*
* ### API Methods
*
* **server:**
* `auth.api.setActiveTeam`
*
* **client:**
* `authClient.organization.setActiveTeam`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-set-active-team)
*/
setActiveTeam: setActiveTeam(options as O),
/**
* ### Endpoint
*
* POST `/organization/list-user-teams`
*
* ### API Methods
*
* **server:**
* `auth.api.listUserTeams`
*
* **client:**
* `authClient.organization.listUserTeams`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-set-active-team)
*/
listUserTeams: listUserTeams(options as O),
/**
* ### Endpoint
*
* POST `/organization/list-team-members`
*
* ### API Methods
*
* **server:**
* `auth.api.listTeamMembers`
*
* **client:**
* `authClient.organization.listTeamMembers`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-set-active-team)
*/
listTeamMembers: listTeamMembers(options as O),
/**
* ### Endpoint
*
* POST `/organization/add-team-member`
*
* ### API Methods
*
* **server:**
* `auth.api.addTeamMember`
*
* **client:**
* `authClient.organization.addTeamMember`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-add-team-member)
*/
addTeamMember: addTeamMember(options as O),
/**
* ### Endpoint
*
* POST `/organization/remove-team-member`
*
* ### API Methods
*
* **server:**
* `auth.api.removeTeamMember`
*
* **client:**
* `authClient.organization.removeTeamMember`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-remove-team-member)
*/
removeTeamMember: removeTeamMember(options as O),
};
if (teamSupport) {
endpoints = {
@@ -475,6 +561,30 @@ export const organization = <O extends OrganizationOptions>(
...(options?.schema?.team?.additionalFields || {}),
},
},
teamMember: {
modelName: options?.schema?.teamMember?.modelName,
fields: {
teamId: {
type: "string",
required: true,
fieldName: options?.schema?.teamMember?.fields?.teamId,
},
userId: {
type: "string",
required: true,
references: {
model: "user",
field: "id",
},
fieldName: options?.schema?.teamMember?.fields?.userId,
},
createdAt: {
type: "date",
required: false,
fieldName: options?.schema?.teamMember?.fields?.createdAt,
},
},
},
} satisfies AuthPluginSchema)
: undefined;
@@ -637,6 +747,15 @@ export const organization = <O extends OrganizationOptions>(
required: false,
fieldName: options?.schema?.session?.fields?.activeOrganizationId,
},
...(teamSupport
? {
activeTeamId: {
type: "string",
required: false,
fieldName: options?.schema?.session?.fields?.activeTeamId,
},
}
: {}),
},
},
organization: {
@@ -700,16 +819,6 @@ export const organization = <O extends OrganizationOptions>(
defaultValue: "member",
fieldName: options?.schema?.member?.fields?.role,
},
...(teamSupport
? {
teamId: {
type: "string",
required: false,
sortable: true,
fieldName: options?.schema?.member?.fields?.teamId,
},
}
: {}),
createdAt: {
type: "date",
required: true,
@@ -783,6 +892,7 @@ export const organization = <O extends OrganizationOptions>(
Invitation: {} as InferInvitation<O>,
Member: {} as InferMember<O>,
Team: teamSupport ? ({} as Team) : ({} as any),
TeamMember: teamSupport ? ({} as TeamMember) : ({} as any),
ActiveOrganization: {} as Awaited<
ReturnType<ReturnType<typeof getFullOrganization<O>>>
>,
@@ -9,6 +9,7 @@ import { parseRoles } from "../organization";
import { type OrganizationOptions } from "../types";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { hasPermission } from "../has-permission";
import { setSessionCookie } from "../../../cookies";
import {
toZodSchema,
type InferAdditionalFieldsFromPluginOptions,
@@ -19,6 +20,7 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
fields: option?.schema?.invitation?.additionalFields || {},
isClientSide: true,
});
const baseSchema = z.object({
email: z.string().meta({
description: "The email address of the user to invite",
@@ -51,12 +53,21 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
"Resend the invitation email, if the user is already invited. Eg: true",
})
.optional(),
teamId: z
.string()
.meta({
description: "The team ID to invite the user to",
})
.optional(),
teamId: z.union([
z
.string()
.meta({
description: "The team ID to invite the user to",
})
.optional(),
z
.array(
z.string().meta({
description: "The team ID to invite the user to",
}),
)
.optional(),
]),
});
return createAuthEndpoint(
@@ -98,7 +109,7 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
* The team the user is
* being invited to.
*/
teamId?: string;
teamId?: string | string[];
}
: {}) &
InferAdditionalFieldsFromPluginOptions<"invitation", O, false>,
@@ -265,33 +276,48 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
"teamId" in ctx.body &&
ctx.body.teamId
) {
const teamId = ctx.body.teamId as string;
const team = await adapter.findTeamById({
teamId,
organizationId: organizationId,
includeTeamMembers: true,
});
if (!team) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
});
}
const maximumMembersPerTeam =
typeof ctx.context.orgOptions.teams.maximumMembersPerTeam ===
"function"
? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
teamId,
session: session,
organizationId: organizationId,
})
: ctx.context.orgOptions.teams.maximumMembersPerTeam;
if (team.members.length >= maximumMembersPerTeam) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
const teamIds =
typeof ctx.body.teamId === "string"
? [ctx.body.teamId as string]
: (ctx.body.teamId as string[]);
for (const teamId of teamIds) {
const team = await adapter.findTeamById({
teamId,
organizationId: organizationId,
includeTeamMembers: true,
});
if (!team) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
});
}
const maximumMembersPerTeam =
typeof ctx.context.orgOptions.teams.maximumMembersPerTeam ===
"function"
? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
teamId,
session: session,
organizationId: organizationId,
})
: ctx.context.orgOptions.teams.maximumMembersPerTeam;
if (team.members.length >= maximumMembersPerTeam) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
});
}
}
}
const teamIds: string[] =
"teamId" in ctx.body
? typeof ctx.body.teamId === "string"
? [ctx.body.teamId as string]
: (ctx.body.teamId as string[]) ?? []
: [];
const {
email: _,
role: __,
@@ -305,11 +331,7 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
role: roles,
email: ctx.body.email.toLowerCase(),
organizationId: organizationId,
...("teamId" in ctx.body
? {
teamId: ctx.body.teamId as string,
}
: {}),
teamIds,
...(additionalFields ? additionalFields : {}),
},
user: session.user,
@@ -330,6 +352,7 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
},
ctx.request,
);
return ctx.json(invitation);
},
);
@@ -378,6 +401,7 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
const invitation = await adapter.findInvitationById(
ctx.body.invitationId,
);
if (
!invitation ||
invitation.expiresAt < new Date() ||
@@ -387,12 +411,14 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
});
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
});
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const members = await adapter.listMembers({
organizationId: invitation.organizationId,
@@ -408,6 +434,7 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
invitationId: ctx.body.invitationId,
status: "accepted",
});
if (!acceptedI) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION,
@@ -417,34 +444,52 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
if (
ctx.context.orgOptions.teams &&
ctx.context.orgOptions.teams.enabled &&
typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !==
"undefined" &&
"teamId" in acceptedI &&
acceptedI.teamId
) {
const teamId = acceptedI.teamId as string;
const team = await adapter.findTeamById({
teamId,
organizationId: invitation.organizationId,
includeTeamMembers: true,
});
if (!team) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
const teamIds = (acceptedI.teamId as string).split(",");
const onlyOne = teamIds.length === 1;
for (const teamId of teamIds) {
await adapter.findOrCreateTeamMember({
teamId: teamId,
userId: session.user.id,
});
if (
typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !==
"undefined"
) {
const members = await adapter.listTeamMembers({ teamId });
const maximumMembersPerTeam =
typeof ctx.context.orgOptions.teams.maximumMembersPerTeam ===
"function"
? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
teamId,
session: session,
organizationId: invitation.organizationId,
})
: ctx.context.orgOptions.teams.maximumMembersPerTeam;
if (members.length >= maximumMembersPerTeam) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
});
}
}
}
const maximumMembersPerTeam =
typeof ctx.context.orgOptions.teams.maximumMembersPerTeam ===
"function"
? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
teamId,
session: session,
organizationId: invitation.organizationId,
})
: ctx.context.orgOptions.teams.maximumMembersPerTeam;
if (team.members.length >= maximumMembersPerTeam) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
if (onlyOne) {
const teamId = teamIds[0];
const updatedSession = await adapter.setActiveTeam(
session.session.token,
teamId,
);
await setSessionCookie(ctx, {
session: updatedSession,
user: session.user,
});
}
}
@@ -454,16 +499,13 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
userId: session.user.id,
role: invitation.role as string,
createdAt: new Date(),
...("teamId" in acceptedI
? {
teamId: acceptedI.teamId as string,
}
: {}),
});
await adapter.setActiveOrganization(
session.session.token,
invitation.organizationId,
);
if (!acceptedI) {
return ctx.json(null, {
status: 400,
@@ -472,6 +514,7 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
},
});
}
return ctx.json({
invitation: acceptedI,
member,
@@ -518,7 +561,7 @@ export const rejectInvitation = <O extends OrganizationOptions>(options: O) =>
},
async (ctx) => {
const session = ctx.context.session;
const adapter = getOrgAdapter<O>(ctx.context, options);
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const invitation = await adapter.findInvitationById(
ctx.body.invitationId,
);
@@ -537,10 +580,12 @@ export const rejectInvitation = <O extends OrganizationOptions>(options: O) =>
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
});
}
const rejectedI = await adapter.updateInvitation({
invitationId: ctx.body.invitationId,
status: "rejected",
});
return ctx.json({
invitation: rejectedI,
member: null,
@@ -153,9 +153,15 @@ export const addMember = <O extends OrganizationOptions>(option: O) => {
role: parseRoles(ctx.body.role as string | string[]),
createdAt: new Date(),
...(additionalFields ? additionalFields : {}),
...(teamId ? { teamId } : {}),
});
if (teamId) {
await adapter.findOrCreateTeamMember({
userId: user.id,
teamId,
});
}
return ctx.json(createdMember);
},
);
@@ -13,6 +13,7 @@ import type {
InferOrganization,
Member,
Team,
TeamMember,
} from "../schema";
import { hasPermission } from "../has-permission";
import {
@@ -166,6 +167,7 @@ export const createOrganization = <O extends OrganizationOptions>(
data: Record<string, any>;
}
| undefined = undefined;
if (options.organizationCreation?.beforeCreate) {
const response = await options.organizationCreation.beforeCreate(
{
@@ -189,9 +191,12 @@ export const createOrganization = <O extends OrganizationOptions>(
...(hookResponse?.data || {}),
},
});
let member:
| (Member & InferAdditionalFieldsFromPluginOptions<"member", O, false>)
| undefined;
let teamMember: TeamMember | null = null;
if (
options?.teams?.enabled &&
options.teams.defaultTeam?.enabled !== false
@@ -208,11 +213,14 @@ export const createOrganization = <O extends OrganizationOptions>(
}));
member = await adapter.createMember({
teamId: defaultTeam.id,
userId: user.id,
organizationId: organization.id,
role: ctx.context.orgOptions.creatorRole || "owner",
createdAt: new Date(),
});
teamMember = await adapter.findOrCreateTeamMember({
teamId: defaultTeam.id,
userId: user.id,
});
} else {
member = await adapter.createMember({
@@ -240,6 +248,17 @@ export const createOrganization = <O extends OrganizationOptions>(
);
}
if (
teamMember &&
ctx.context.session &&
!ctx.body.keepCurrentActiveOrganization
) {
await adapter.setActiveTeam(
ctx.context.session.session.token,
teamMember.teamId,
);
}
return ctx.json({
...organization,
metadata: ctx.body.metadata,
@@ -8,6 +8,7 @@ import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import type { OrganizationOptions } from "../types";
import { teamSchema } from "../schema";
import { hasPermission } from "../has-permission";
import { setSessionCookie } from "../../../cookies";
import {
toZodSchema,
type InferAdditionalFieldsFromPluginOptions,
@@ -232,7 +233,7 @@ export const removeTeam = <O extends OrganizationOptions>(options: O) =>
organizationId,
});
if (!member || member.teamId === ctx.body.teamId) {
if (!member || session.session?.activeTeamId === ctx.body.teamId) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM,
@@ -522,7 +523,483 @@ export const listOrganizationTeams = <O extends OrganizationOptions>(
}
const teams = await adapter.listTeams(organizationId);
return ctx.json(teams);
},
);
export const setActiveTeam = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/set-active-team",
{
method: "POST",
body: 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(),
}),
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) {
const sessionTeamId = session.session.activeTeamId;
if (!sessionTeamId) {
return ctx.json(null);
}
const updatedSession = await adapter.setActiveTeam(
session.session.token,
null,
);
await setSessionCookie(ctx, {
session: updatedSession,
user: session.user,
});
return ctx.json(null);
}
let teamId: string;
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,
});
}
const member = await adapter.findTeamMember({
teamId,
userId: session.user.id,
});
if (!member) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
});
}
const updatedSession = await adapter.setActiveTeam(
session.session.token,
team.id,
);
await setSessionCookie(ctx, {
session: updatedSession,
user: session.user,
});
return ctx.json(team);
},
);
export const listUserTeams = <O extends OrganizationOptions>(options: O) =>
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",
},
},
},
},
},
},
},
use: [orgMiddleware, orgSessionMiddleware],
},
async (ctx) => {
const session = ctx.context.session;
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const teams = await adapter.listTeamsByUser({
userId: session.user.id,
});
return ctx.json(teams);
},
);
export const listTeamMembers = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/list-team-members",
{
method: "GET",
query: 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.",
}),
),
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",
},
},
},
},
},
},
},
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,
});
}
if (!ctx.query?.teamId) {
if (!session.session.activeTeamId) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM,
});
}
const member = await adapter.findTeamMember({
userId: session.user.id,
teamId: session.session.activeTeamId,
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
});
}
const members = await adapter.listTeamMembers({
teamId: session.session.activeTeamId,
});
return ctx.json(members);
}
const member = await adapter.findTeamMember({
userId: session.user.id,
teamId: ctx.query.teamId,
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
});
}
const members = await adapter.listTeamMembers({
teamId: ctx.query.teamId,
});
return ctx.json(members);
},
);
export const addTeamMember = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/add-team-member",
{
method: "POST",
body: 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.",
}),
}),
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"],
},
},
},
},
},
},
},
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,
});
}
const canUpdateMember = hasPermission({
role: currentMember.role,
options: ctx.context.orgOptions,
permissions: {
member: ["update"],
},
});
if (!canUpdateMember) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER,
});
}
const toBeAddedMember = await adapter.findMemberByOrgId({
userId: ctx.body.userId,
organizationId: session.session.activeOrganizationId,
});
if (!toBeAddedMember) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
});
}
const teamMember = await adapter.findOrCreateTeamMember({
teamId: ctx.body.teamId,
userId: ctx.body.userId,
});
return ctx.json(teamMember);
},
);
export const removeTeamMember = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/remove-team-member",
{
method: "POST",
body: 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.",
}),
}),
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"],
},
},
},
},
},
},
},
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,
});
}
const canDeleteMember = hasPermission({
role: currentMember.role,
options: ctx.context.orgOptions,
permissions: {
member: ["delete"],
},
});
if (!canDeleteMember) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER,
});
}
const toBeAddedMember = await adapter.findMemberByOrgId({
userId: ctx.body.userId,
organizationId: session.session.activeOrganizationId,
});
if (!toBeAddedMember) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
});
}
await adapter.removeTeamMember({
teamId: ctx.body.teamId,
userId: ctx.body.userId,
});
return ctx.json({ message: "Team member removed successfully." });
},
);
@@ -27,7 +27,6 @@ export const memberSchema = z.object({
userId: z.coerce.string(),
role,
createdAt: z.date().default(() => new Date()),
teamId: z.string().optional(),
});
export const invitationSchema = z.object({
@@ -49,12 +48,21 @@ export const teamSchema = z.object({
updatedAt: z.date().optional(),
});
export const teamMemberSchema = z.object({
id: z.string().default(generateId),
teamId: z.string(),
userId: z.string(),
createdAt: z.date().default(() => new Date()),
});
export type Organization = z.infer<typeof organizationSchema>;
export type Member = z.infer<typeof memberSchema>;
export type TeamMember = z.infer<typeof teamMemberSchema>;
export type Team = z.infer<typeof teamSchema>;
export type Invitation = z.infer<typeof invitationSchema>;
export type InvitationInput = z.input<typeof invitationSchema>;
export type MemberInput = z.input<typeof memberSchema>;
export type TeamMemberInput = z.input<typeof teamMemberSchema>;
export type OrganizationInput = z.input<typeof organizationSchema>;
export type TeamInput = z.infer<typeof teamSchema>;
@@ -5,23 +5,22 @@ import { createAuthClient } from "../../client";
import { organizationClient } from "./client";
describe("team", async (it) => {
const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
await getTestInstance({
user: {
modelName: "users",
},
plugins: [
organization({
async sendInvitationEmail(data, request) {},
teams: {
enabled: true,
},
}),
],
logger: {
level: "error",
},
});
const { auth, signInWithTestUser, cookieSetter } = await getTestInstance({
user: {
modelName: "users",
},
plugins: [
organization({
async sendInvitationEmail() {},
teams: {
enabled: true,
},
}),
],
logger: {
level: "error",
},
});
const { headers } = await signInWithTestUser();
const client = createAuthClient({
@@ -50,6 +49,11 @@ describe("team", async (it) => {
name: "Invited User",
};
const signUpHeaders = new Headers();
const signUpRes = await client.signUp.email(invitedUser, {
onSuccess: cookieSetter(signUpHeaders),
});
it("should create an organization and a team", async () => {
const createOrganizationResponse = await client.organization.create({
name: "Test Organization",
@@ -119,29 +123,58 @@ describe("team", async (it) => {
teamId,
});
const newHeaders = new Headers();
const signUpRes = await client.signUp.email(invitedUser, {
onSuccess: cookieSetter(newHeaders),
});
expect(signUpRes.data?.user).toBeDefined();
const invitation = await client.organization.acceptInvitation(
{
invitationId: res.data?.id as string,
},
{
headers: newHeaders,
headers: signUpHeaders,
},
);
expect(invitation.data?.member).toMatchObject({
role: "member",
teamId,
userId: signUpRes.data?.user.id,
});
});
it("should add team to the member's list of teams", async () => {
const listUserTeamsRes = await client.organization.listUserTeams(
{},
{
headers: signUpHeaders,
},
);
expect(listUserTeamsRes.error).toBeNull();
expect(listUserTeamsRes.data).not.toBeNull();
expect(listUserTeamsRes.data).toHaveLength(1);
});
it("should be able to list team members in the current active team", async () => {
const activeTeamHeaders = new Headers();
await client.organization.setActiveTeam(
{
teamId,
},
{
headers: signUpHeaders,
onSuccess: cookieSetter(activeTeamHeaders),
},
);
const res = await client.organization.listTeamMembers(
{},
{
headers: activeTeamHeaders,
},
);
expect(res.error).toBeNull();
expect(res.data).not.toBeNull();
expect(res.data).toHaveLength(1);
});
it("should get full organization", async () => {
const organization = await client.organization.getFullOrganization({
fetchOptions: {
@@ -313,3 +346,275 @@ describe("team", async (it) => {
expect(res2.error?.code).toEqual("TEAM_MEMBER_LIMIT_REACHED");
});
});
describe("mulit team support", async (it) => {
const { auth, signInWithTestUser, cookieSetter } = await getTestInstance(
{
plugins: [
organization({
async sendInvitationEmail() {},
teams: {
enabled: true,
defaultTeam: {
enabled: true,
},
},
}),
],
logger: {
level: "error",
},
},
{
testWith: "sqlite",
},
);
const admin = await signInWithTestUser();
const invitedUser = await auth.api.signUpEmail({
body: {
name: "Invited User",
email: "invited@email.com",
password: "password",
},
returnHeaders: true,
});
let organizationId: string | null = null;
let team1Id: string | null = null;
let team2Id: string | null = null;
let team3Id: string | null = null;
let invitationId: string | null = null;
it("should create an organization to test multi team support", async () => {
const organization = await auth.api.createOrganization({
headers: admin.headers,
body: {
name: "Test Organization",
slug: "test-org",
metadata: {
test: "organization-metadata",
},
},
});
expect(organization?.id).toBeDefined();
expect(organization?.name).toBe("Test Organization");
organizationId = organization?.id as string;
});
it("should create 3 teams", async () => {
expect(organizationId).toBeDefined();
if (!organizationId) throw Error("can not run test");
const team1 = await auth.api.createTeam({
headers: admin.headers,
body: {
name: "Team One",
organizationId,
},
});
expect(team1.id).toBeDefined();
expect(team1.organizationId).toBe(organizationId);
team1Id = team1.id;
const team2 = await auth.api.createTeam({
headers: admin.headers,
body: {
name: "Team Two",
organizationId,
},
});
expect(team2.id).toBeDefined();
expect(team2.organizationId).toBe(organizationId);
team2Id = team2.id;
const team3 = await auth.api.createTeam({
headers: admin.headers,
body: {
name: "Team Three",
organizationId,
},
});
expect(team3.id).toBeDefined();
expect(team3.organizationId).toBe(organizationId);
team3Id = team3.id;
});
it("should invite user to all 3 teams", async () => {
expect(organizationId).toBeDefined();
expect(team1Id).toBeDefined();
expect(team2Id).toBeDefined();
expect(team3Id).toBeDefined();
if (!organizationId || !team1Id || !team2Id || !team3Id)
throw Error("can not run test");
const invitation = await auth.api.createInvitation({
headers: admin.headers,
body: {
email: invitedUser.response.user.email,
role: "member",
organizationId,
teamId: [team1Id, team2Id, team3Id],
},
});
expect(invitation.id).toBeDefined();
expect(invitation.teamId).toBe([team1Id, team2Id, team3Id].join(","));
invitationId = invitation.id;
});
it("should accept invite and join all 3 teams", async () => {
expect(invitationId).toBeDefined();
if (!invitationId) throw Error("can not run test");
const accept = await auth.api.acceptInvitation({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
body: {
invitationId,
},
});
expect(accept?.member).toBeDefined();
expect(accept?.invitation).toBeDefined();
});
it("should have jonied all 3 teams", async () => {
expect(invitationId).toBeDefined();
if (!invitationId) throw Error("can not run test");
const teams = await auth.api.listUserTeams({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
});
expect(teams).toHaveLength(3);
});
let activeTeamCookie: string | null = null;
it("should allow you to set one of the teams as active", async () => {
expect(team1Id).toBeDefined();
expect(organizationId).toBeDefined();
if (!team1Id || !organizationId) throw Error("can not run test");
const team = await auth.api.setActiveTeam({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
body: {
teamId: team1Id,
},
returnHeaders: true,
});
expect(team.response?.id).toBe(team1Id);
expect(team.response?.organizationId).toBe(organizationId);
activeTeamCookie = team.headers.getSetCookie()[0];
});
it("should allow you to list team members of the current active team", async () => {
expect(activeTeamCookie).toBeDefined();
if (!activeTeamCookie) throw Error("can not run test");
const members = await auth.api.listTeamMembers({
headers: { cookie: activeTeamCookie },
});
expect(members).toHaveLength(1);
expect(members.at(0)?.teamId).toBe(team1Id);
});
it("should allow user to list team members of any team the user is in", async () => {
expect(team2Id).toBeDefined();
expect(team3Id).toBeDefined();
if (!team2Id || !team3Id) throw Error("can not run test");
const team2Members = await auth.api.listTeamMembers({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
query: {
teamId: team2Id,
},
});
expect(team2Members).toHaveLength(1);
expect(team2Members.at(0)?.teamId).toBe(team2Id);
const team3Members = await auth.api.listTeamMembers({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
query: {
teamId: team3Id,
},
});
expect(team3Members).toHaveLength(1);
expect(team3Members.at(0)?.teamId).toBe(team3Id);
});
let team4Id: string | null = null;
it("should directly add a member to a team", async () => {
expect(organizationId).toBeDefined();
if (!organizationId) throw Error("can not run test");
const team = await auth.api.createTeam({
headers: admin.headers,
body: {
name: "Team Four",
organizationId,
},
});
const teamMember = await auth.api.addTeamMember({
headers: admin.headers,
body: {
userId: invitedUser.response.user.id,
teamId: team.id,
},
});
expect(teamMember.teamId).toBe(team.id);
expect(teamMember.userId).toBe(invitedUser.response.user.id);
const teams = await auth.api.listUserTeams({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
});
expect(teams).toHaveLength(4);
team4Id = team.id;
});
it("should remove a member from a team", async () => {
expect(team4Id).toBeDefined();
if (!team4Id) throw Error("can not run test");
await auth.api.removeTeamMember({
headers: admin.headers,
body: {
userId: invitedUser.response.user.id,
teamId: team4Id,
},
});
const teams = await auth.api.listUserTeams({
headers: { cookie: invitedUser.headers.getSetCookie()[0] },
});
expect(teams).toHaveLength(3);
});
});
@@ -1,7 +1,13 @@
import type { FieldAttribute } from "../../db";
import type { User, Session, AuthContext } from "../../types";
import type { AccessControl, Role } from "../access";
import type { Invitation, Member, Organization, Team } from "./schema";
import type {
Invitation,
Member,
Organization,
Team,
TeamMember,
} from "./schema";
export interface OrganizationOptions {
/**
@@ -217,6 +223,7 @@ export interface OrganizationOptions {
session?: {
fields?: {
activeOrganizationId?: string;
activeTeamId?: string;
};
};
organization?: {
@@ -256,6 +263,13 @@ export interface OrganizationOptions {
[key in string]: FieldAttribute;
};
};
teamMember?: {
modelName?: string;
fields?: {
[key in keyof Omit<TeamMember, "id">]?: string;
};
};
};
/**
* Configure how organization deletion is handled
@@ -11,6 +11,7 @@ import Database from "better-sqlite3";
import { getMigrations } from "../db";
let server = new OAuth2Server();
let port = 8005;
vi.mock("../oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
@@ -75,7 +76,7 @@ vi.mock("../oauth2", async (importOriginal) => {
expect(refreshToken).toBeDefined();
expect(options.clientId).toBe("test-client-id");
expect(options.clientSecret).toBe("test-client-secret");
expect(tokenEndpoint).toBe("http://localhost:8080/token");
expect(tokenEndpoint).toBe(`http://localhost:${port}/token`);
const data: GoogleProfile = {
email: "user@email.com",
@@ -150,8 +151,8 @@ describe("Social Providers", async (c) => {
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
await server.start(8080, "localhost");
console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
await server.start(port, "localhost");
console.log("Issuer URL:", server.issuer.url); // -> http://localhost:${port}
});
afterAll(async () => {
await server.stop().catch(console.error);
@@ -206,7 +207,7 @@ describe("Social Providers", async (c) => {
clientKey: "test-client-key",
clientSecret: "test-client-secret",
},
tokenEndpoint: "http://localhost:8080/token",
tokenEndpoint: `http://localhost:${port}/token`,
});
return tokens;
}
@@ -382,7 +383,7 @@ describe("Social Providers", async (c) => {
if (!authUrl) throw new Error("No auth url found");
const mockEndpoint = authUrl.replace(
"https://accounts.google.com/o/oauth2/auth",
"http://localhost:8080/authorize",
`http://localhost:${port}/authorize`,
);
const result = await simulateOAuthFlowRefresh(mockEndpoint, headers);
const { accessToken, refreshToken } = result;
@@ -52,7 +52,6 @@ describe("general types", async (it) => {
token: string;
ipAddress?: string | undefined | null;
userAgent?: string | undefined | null;
activeOrganizationId?: string | undefined | null;
}>();
});
});