Skip to content

feat: user management public api (wip) #6722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 44 additions & 28 deletions integration-tests/testkit/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ export function createOrganization(input: CreateOrganizationInput, authToken: st
}
}
memberRoles {
id
name
locked
edges {
node {
id
name
isLocked
}
}
}
rateLimit {
retentionInDays
Expand Down Expand Up @@ -113,11 +117,13 @@ export function inviteToOrganization(input: InviteToOrganizationByEmailInput, au
mutation inviteToOrganization($input: InviteToOrganizationByEmailInput!) {
inviteToOrganizationByEmail(input: $input) {
ok {
id
createdAt
expiresAt
email
code
createdOrganizationInvitation {
id
createdAt
expiresAt
email
code
}
}
error {
message
Expand Down Expand Up @@ -204,16 +210,18 @@ export function getOrganizationMembers(selector: OrganizationSelectorInput, auth
query getOrganizationMembers($selector: OrganizationSelectorInput!) {
organization(reference: { bySelector: $selector }) {
members {
nodes {
id
user {
id
email
}
role {
edges {
node {
id
name
permissions
user {
id
email
}
role {
id
name
permissions
}
}
}
}
Expand Down Expand Up @@ -653,11 +661,15 @@ export function createMemberRole(input: CreateMemberRoleInput, authToken: string
id
slug
memberRoles {
id
name
description
locked
permissions
edges {
node {
id
name
description
isLocked
permissions
}
}
}
}
}
Expand Down Expand Up @@ -711,11 +723,15 @@ export function deleteMemberRole(input: DeleteMemberRoleInput, authToken: string
id
slug
memberRoles {
id
name
description
locked
permissions
edges {
node {
id
name
description
isLocked
permissions
}
}
}
}
}
Expand All @@ -742,7 +758,7 @@ export function updateMemberRole(input: UpdateMemberRoleInput, authToken: string
id
name
description
locked
isLocked
permissions
}
}
Expand Down
59 changes: 41 additions & 18 deletions integration-tests/testkit/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,17 @@ export function initSeed() {
async inviteMember(
email = '[email protected]',
inviteToken = ownerToken,
roleId?: string,
memberRoleId?: string,
) {
const inviteResult = await inviteToOrganization(
{
organization: {
bySelector: {
organizationSlug: organization.slug,
},
},
email,
organizationSlug: organization.slug,
roleId,
memberRoleId,
},
inviteToken,
).then(r => r.expectNoGraphQLErrors());
Expand All @@ -203,7 +207,7 @@ export function initSeed() {
ownerToken,
).then(r => r.expectNoGraphQLErrors());

const members = membersResult.organization?.members?.nodes;
const members = membersResult.organization?.members?.edges?.map(edge => edge.node);

if (!members) {
throw new Error(`Could not get members for org ${organization.slug}`);
Expand Down Expand Up @@ -814,13 +818,18 @@ export function initSeed() {

const invitationResult = await inviteToOrganization(
{
organizationSlug: organization.slug,
organization: {
bySelector: {
organizationSlug: organization.slug,
},
},
email: memberEmail,
},
inviteToken,
).then(r => r.expectNoGraphQLErrors());

const code = invitationResult.inviteToOrganizationByEmail.ok?.code;
const code =
invitationResult.inviteToOrganizationByEmail.ok?.createdOrganizationInvitation.code;

if (!code) {
throw new Error(
Expand Down Expand Up @@ -856,9 +865,17 @@ export function initSeed() {
) {
const memberRoleAssignmentResult = await assignMemberRole(
{
organizationSlug: organization.slug,
userId: input.userId,
roleId: input.roleId,
organization: {
bySelector: {
organizationSlug: organization.slug,
},
},
member: {
byId: input.userId,
},
memberRole: {
byId: input.roleId,
},
resources: input.resources ?? {
mode: GraphQLSchema.ResourceAssignmentModeType.All,
projects: [],
Expand All @@ -874,15 +891,16 @@ export function initSeed() {
return memberRoleAssignmentResult.assignMemberRole.ok?.updatedMember;
},
async deleteMemberRole(
roleId: string,
memberRoleId: string,
options: { useMemberToken?: boolean } = {
useMemberToken: false,
},
) {
const memberRoleDeletionResult = await deleteMemberRole(
{
organizationSlug: organization.slug,
roleId,
memberRole: {
byId: memberRoleId,
},
},
options.useMemberToken ? memberToken : ownerToken,
).then(r => r.expectNoGraphQLErrors());
Expand All @@ -907,7 +925,11 @@ export function initSeed() {
});
const memberRoleCreationResult = await createMemberRole(
{
organizationSlug: organization.slug,
organization: {
bySelector: {
organizationSlug: organization.slug,
},
},
name,
description: 'some description',
selectedPermissions: permissions,
Expand All @@ -931,9 +953,9 @@ export function initSeed() {
}

const createdRole =
memberRoleCreationResult.createMemberRole.ok?.updatedOrganization.memberRoles?.find(
r => r.name === name,
);
memberRoleCreationResult.createMemberRole.ok?.updatedOrganization.memberRoles?.edges.find(
e => e.node.name === name,
)?.node;

if (!createdRole) {
throw new Error(
Expand All @@ -956,8 +978,9 @@ export function initSeed() {
) {
const memberRoleUpdateResult = await updateMemberRole(
{
organizationSlug: organization.slug,
roleId: role.id,
memberRole: {
byId: role.id,
},
name: role.name,
description: role.description,
selectedPermissions: permissions,
Expand Down
6 changes: 3 additions & 3 deletions integration-tests/tests/api/oidc-integrations/crud.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ describe('restrictions', () => {
expect(refetchedOrg.organization?.oidcIntegration?.oidcUserAccessOnly).toEqual(true);

const invitation = await inviteMember('[email protected]');
const invitationCode = invitation.ok?.code;
const invitationCode = invitation.ok?.createdOrganizationInvitation.code;

if (!invitationCode) {
throw new Error('No invitation code');
Expand Down Expand Up @@ -865,7 +865,7 @@ describe('restrictions', () => {
).toEqual(false);

const invitation = await inviteMember('[email protected]');
const invitationCode = invitation.ok?.code;
const invitationCode = invitation.ok?.createdOrganizationInvitation.code;

if (!invitationCode) {
throw new Error('No invitation code');
Expand All @@ -887,7 +887,7 @@ describe('restrictions', () => {
const { organization, inviteMember, joinMemberUsingCode } = await createOrg();

const invitation = await inviteMember('[email protected]');
const invitationCode = invitation.ok?.code;
const invitationCode = invitation.ok?.createdOrganizationInvitation.code;

if (!invitationCode) {
throw new Error('No invitation code');
Expand Down
11 changes: 6 additions & 5 deletions integration-tests/tests/api/organization/members.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ test.concurrent('email invitation', async ({ expect }) => {

const inviteEmail = seed.generateEmail();
const invitationResult = await inviteMember(inviteEmail);
const inviteCode = invitationResult.ok?.code;
const inviteCode = invitationResult.ok?.createdOrganizationInvitation.code;
expect(inviteCode).toBeDefined();

const sentEmails = await history();
Expand All @@ -120,7 +120,7 @@ test.concurrent(

// Invite
const invitationResult = await inviteMember();
const inviteCode = invitationResult.ok!.code;
const inviteCode = invitationResult.ok!.createdOrganizationInvitation.code;
expect(inviteCode).toBeDefined();

// Join
Expand Down Expand Up @@ -150,9 +150,10 @@ const OrganizationInvitationsQuery = graphql(`
organization: organizationBySlug(organizationSlug: $organizationSlug) {
id
invitations {
total
nodes {
id
edges {
node {
id
}
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions integration-tests/tests/api/policy/policy-access.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('Policy Access', () => {
const { createOrg } = await initSeed().createOwner();
const { organization, createProject, inviteAndJoinMember } = await createOrg();
const { project } = await createProject(ProjectType.Single);
const adminRole = organization.memberRoles?.find(r => r.name === 'Admin');
const adminRole = organization.memberRoles?.edges.find(e => e.node.name === 'Admin')?.node;

if (!adminRole) {
throw new Error('Admin role not found');
Expand Down Expand Up @@ -183,7 +183,9 @@ describe('Policy Access', () => {
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { organization, inviteAndJoinMember } = await createOrg();
const adminRole = organization.memberRoles?.find(r => r.name === 'Admin');
const adminRole = organization.memberRoles?.edges.find(
edge => edge.node.name === 'Admin',
)?.node;

if (!adminRole) {
throw new Error('Admin role not found');
Expand Down
4 changes: 2 additions & 2 deletions integration-tests/tests/api/target/crud.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,15 @@ test.concurrent('organization member user can create a target', async ({ expect
const inviteMemberResult = await inviteMember(
orgMemberEmail,
undefined,
organization.memberRoles?.find(role => role.name === 'Admin')?.id,
organization.memberRoles?.edges?.find(edge => edge.node.name === 'Admin')?.node.id,
);

if (inviteMemberResult.ok == null) {
throw new Error('Invite did not succeed' + JSON.stringify(inviteMemberResult));
}

const joinMemberUsingCodeResult = await joinMemberUsingCode(
inviteMemberResult.ok.code,
inviteMemberResult.ok.createdOrganizationInvitation.code,
orgMemberToken,
).then(r => r.expectNoGraphQLErrors());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { MigrationExecutor } from '../pg-migrator';

const date = new Date();

const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();

const createdAt = `'${year}-${month}-${day}'`;

export default {
name: '2025.04.29T00-00-00.organization-member-pagination.ts',
noTransaction: true,
// Adds a default role to OIDC integration and set index on "oidc_integrations"."default_role_id"
run: ({ sql }) => [
{
name: 'Add "organization_member"."created_at" column',
query: sql`
ALTER TABLE "organization_member"
ADD COLUMN IF NOT EXISTS "created_at" timestamptz NOT NULL DEFAULT ${sql.literalValue(createdAt)}::timestamp
;
`,
},
{
name: 'Create pagination index ',
query: sql`
CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_pagination_idx"
ON "organization_member" (
"organization_id" DESC
, "user_id" DESC
, "created_at" DESC
)
`,
},
],
} satisfies MigrationExecutor;
3 changes: 2 additions & 1 deletion packages/migrations/src/run-pg-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
await import('./actions/2025.02.20T00-00-00.organization-access-tokens'),
await import('./actions/2025.02.14T00-00-00.schema-versions-metadata'),
await import('./actions/2025.02.21T00-00-00.schema-versions-metadata-attributes'),
await import('./actions/2025.03.20T00-00-00.dangerous_breaking'),
await import('./actions/2025.03.20T00-00-00.dangerous-breaking'),
await import('./actions/2025.04.29T00-00-00.organization-member-pagination'),
],
});
2 changes: 1 addition & 1 deletion packages/services/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export { createTaskRunner } from './modules/shared/lib/task-runner';
export { minifySchema } from './shared/schema';
export { HiveError } from './shared/errors';
export { ProjectType } from './__generated__/types';
export type { AuthProvider } from './__generated__/types';
export type { AuthProviderType } from './__generated__/types';
export { HttpClient } from './modules/shared/providers/http-client';
export { OperationsManager } from './modules/operations/providers/operations-manager';
export { OperationsReader } from './modules/operations/providers/operations-reader';
Expand Down
Loading
Loading