Skip to content

Commit a4d69ab

Browse files
committed
feat: add functionality for invites + org renaming
1 parent 0448c00 commit a4d69ab

File tree

3 files changed

+106
-11
lines changed

3 files changed

+106
-11
lines changed

controlplane/src/core/bufservices/onboarding/createOnboarding.ts

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import {
88
import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js';
99
import { OnboardingRepository } from '../../repositories/OnboardingRepository.js';
1010
import { OrganizationRepository } from '../../repositories/OrganizationRepository.js';
11+
import { UserInviteService } from '../../services/UserInviteService.js';
12+
import { AuditLogRepository } from '../../repositories/AuditLogRepository.js';
1113
import type { RouterOptions } from '../../routes.js';
1214
import { enrichLogger, getLogger, handleError } from '../../util.js';
15+
import { UnauthorizedError } from '../../errors/errors.js';
16+
import { OrganizationGroupRepository } from '../../repositories/OrganizationGroupRepository.js';
17+
import { organizationNameSchema } from '../../constants.js';
1318

1419
export function createOnboarding(
1520
opts: RouterOptions,
@@ -22,8 +27,9 @@ export function createOnboarding(
2227
const authContext = await opts.authenticator.authenticate(ctx.requestHeader);
2328
logger = enrichLogger(ctx, logger, authContext);
2429

30+
const organizationId = authContext.organizationId;
2531
const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId);
26-
const org = await orgRepo.byId(authContext.organizationId);
32+
const org = await orgRepo.byId(organizationId);
2733

2834
if (!org || org.creatorUserId !== authContext.userId) {
2935
return {
@@ -35,10 +41,69 @@ export function createOnboarding(
3541
};
3642
}
3743

38-
// TODO: handle invitation flow + organization renaming
44+
const auditLogRepo = new AuditLogRepository(opts.db);
45+
if (authContext.organizationDeactivated || !authContext.rbac.isOrganizationAdmin) {
46+
throw new UnauthorizedError();
47+
}
48+
49+
const validatedName = organizationNameSchema.safeParse(req.organizationName);
50+
if (!validatedName.success) {
51+
return {
52+
response: {
53+
code: EnumStatusCode.ERR_BAD_REQUEST,
54+
details: validatedName.error.errors[0]?.message || 'Invalid organization name',
55+
},
56+
federatedGraphsCount: 0,
57+
};
58+
}
59+
60+
const onboardingRepo = new OnboardingRepository(opts.db, organizationId);
61+
const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, organizationId);
62+
const organizationGroupRepo = new OrganizationGroupRepository(opts.db);
63+
const orgGroup = await organizationGroupRepo.byName({
64+
organizationId,
65+
name: 'organization-developer',
66+
});
3967

40-
const onboardingRepo = new OnboardingRepository(opts.db, authContext.organizationId);
41-
const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId);
68+
const service = new UserInviteService({
69+
db: opts.db,
70+
logger,
71+
keycloakRealm: opts.keycloakRealm,
72+
keycloak: opts.keycloakClient,
73+
mailer: opts.mailerClient,
74+
});
75+
76+
async function createInvitationPromise({
77+
email,
78+
organizationId,
79+
userId,
80+
groupId,
81+
}: {
82+
email: string;
83+
organizationId: string;
84+
userId: string;
85+
groupId: string;
86+
}) {
87+
await service.inviteUser({
88+
organizationId,
89+
inviterUserId: userId,
90+
email,
91+
groups: [groupId],
92+
});
93+
94+
await auditLogRepo.addAuditLog({
95+
organizationId,
96+
organizationSlug: authContext.organizationSlug,
97+
auditAction: 'organization_invitation.created',
98+
action: 'created',
99+
actorId: authContext.userId,
100+
auditableDisplayName: email,
101+
auditableType: 'user',
102+
actorDisplayName: authContext.userDisplayName,
103+
apiKeyName: authContext.apiKeyName,
104+
actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
105+
});
106+
}
42107

43108
const [onboarding, federatedGraphsCount] = await Promise.all([
44109
onboardingRepo.createOrUpdate({
@@ -47,6 +112,24 @@ export function createOnboarding(
47112
email: req.email,
48113
}),
49114
fedGraphRepo.count(),
115+
...(validatedName.data === org.name
116+
? []
117+
: [
118+
orgRepo.updateOrganizationName({
119+
id: org.id,
120+
name: validatedName.data,
121+
}),
122+
]),
123+
...(req.invititationEmails.length > 0 && orgGroup
124+
? req.invititationEmails.map((email) =>
125+
createInvitationPromise({
126+
email,
127+
organizationId,
128+
userId: authContext.userId,
129+
groupId: orgGroup!.groupId,
130+
}),
131+
)
132+
: []),
50133
]);
51134

52135
return {

controlplane/src/core/constants.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ export const organizationSlugSchema = z
4343
})
4444
.refine((value) => !['login', 'signup', 'create', 'account'].includes(value), 'This slug is a reserved keyword.');
4545

46+
export const organizationNameSchema = z
47+
.string()
48+
.trim()
49+
.min(3, {
50+
message: 'Invalid name. It must be of 3-32 characters in length.',
51+
})
52+
.max(32, { message: 'Invalid name. It must be of 3-32 characters in length.' });
53+
4654
export const organizationSchema = z.object({
47-
name: z
48-
.string()
49-
.trim()
50-
.min(3, {
51-
message: 'Invalid name. It must be of 3-32 characters in length.',
52-
})
53-
.max(32, { message: 'Invalid name. It must be of 3-32 characters in length.' }),
55+
name: organizationNameSchema,
5456
slug: organizationSlugSchema,
5557
});
5658

controlplane/src/core/repositories/OrganizationRepository.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ export class OrganizationRepository {
108108
.execute();
109109
}
110110

111+
public async updateOrganizationName(input: { id: string; name: string }) {
112+
await this.db
113+
.update(organizations)
114+
.set({
115+
name: input.name,
116+
})
117+
.where(eq(organizations.id, input.id))
118+
.execute();
119+
}
120+
111121
public async bySlug(slug: string): Promise<Omit<OrganizationDTO, 'rbac'> | null> {
112122
const org = await this.db
113123
.select({

0 commit comments

Comments
 (0)