Skip to content

Commit

Permalink
Inviting unregistered users by email (#6901)
Browse files Browse the repository at this point in the history
  • Loading branch information
klakhov authored Oct 16, 2023
1 parent 4d9af86 commit 8b0130f
Show file tree
Hide file tree
Showing 32 changed files with 1,130 additions and 150 deletions.
4 changes: 4 additions & 0 deletions changelog.d/20231009_092646_invite_users.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Invite users to organization by email
(<https://github.com/opencv/cvat/pull/6901>)
22 changes: 22 additions & 0 deletions cvat-core/src/api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,28 @@ export default function implementAPI(cvat) {
};
};

cvat.organizations.acceptInvitation.implementation = async (
username,
firstName,
lastName,
email,
password,
userConfirmations,
key,
) => {
const orgSlug = await serverProxy.organizations.acceptInvitation(
username,
firstName,
lastName,
email,
password,
userConfirmations,
key,
);

return orgSlug;
};

cvat.webhooks.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
Expand Down
13 changes: 13 additions & 0 deletions cvat-core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ function build() {
const result = await PluginRegistry.apiWrapper(cvat.organizations.deactivate);
return result;
},
async acceptInvitation(username, firstName, lastName, email, password, userConfirmations, key) {
const result = await PluginRegistry.apiWrapper(
cvat.organizations.acceptInvitation,
username,
firstName,
lastName,
email,
password,
userConfirmations,
key,
);
return result;
},
},
webhooks: {
async get(filter: any) {
Expand Down
139 changes: 112 additions & 27 deletions cvat-core/src/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: MIT

import { SerializedOrganization, SerializedOrganizationContact } from './server-response-types';
import { SerializedOrganization, SerializedOrganizationContact, SerializedUser } from './server-response-types';
import { checkObjectType, isEnum } from './common';
import config from './config';
import { MembershipRole } from './enums';
Expand All @@ -12,15 +12,82 @@ import PluginRegistry from './plugins';
import serverProxy from './server-proxy';
import User from './user';

interface Membership {
user: User;
interface SerializedInvitationData {
created_date: string;
key: string;
owner: SerializedUser;
}

interface SerializedMembershipData {
id: number;
user: SerializedUser;
is_active: boolean;
joined_date: string;
role: MembershipRole;
invitation: {
created_date: string;
owner: User;
} | null;
invitation: SerializedInvitationData | null;
}

export class Invitation {
#createdDate: string;
#owner: User | null;
#key: string;

constructor(initialData: SerializedInvitationData) {
this.#createdDate = initialData.created_date;
this.#owner = initialData.owner ? new User(initialData.owner) : null;
this.#key = initialData.key;
}

get owner(): User | null {
return this.#owner;
}

get createdDate(): string {
return this.#createdDate;
}

get key(): string {
return this.#key;
}
}

export class Membership {
#id: number;
#user: User;
#isActive: boolean;
#joinedDate: string;
#role: MembershipRole;
#invitation: Invitation | null;

constructor(initialData: SerializedMembershipData) {
this.#id = initialData.id;
this.#user = new User(initialData.user);
this.#isActive = initialData.is_active;
this.#joinedDate = initialData.joined_date;
this.#role = initialData.role;
this.#invitation = initialData.invitation ? new Invitation(initialData.invitation) : null;
}

get id(): number {
return this.#id;
}

get user(): User {
return this.#user;
}

get isActive(): boolean {
return this.#isActive;
}
get joinedDate(): string {
return this.#joinedDate;
}
get role(): MembershipRole {
return this.#role;
}
get invitation(): Invitation {
return this.#invitation;
}
}

export default class Organization {
Expand Down Expand Up @@ -193,6 +260,15 @@ export default class Organization {
);
return result;
}

public async resendInvitation(key: string): Promise<void> {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.resendInvitation,
key,
);
return result;
}
}

Object.defineProperties(Organization.prototype.save, {
Expand Down Expand Up @@ -234,27 +310,23 @@ Object.defineProperties(Organization.prototype.members, {
checkObjectType('pageSize', pageSize, 'number');

const result = await serverProxy.organizations.members(orgSlug, page, pageSize);
await Promise.all(
result.results.map((membership) => {
const { invitation } = membership;
membership.user = new User(membership.user);
if (invitation) {
return serverProxy.organizations
.invitation(invitation)
.then((invitationData) => {
membership.invitation = invitationData;
})
.catch(() => {
membership.invitation = null;
});
}

return Promise.resolve();
}),
);
const memberships = await Promise.all(result.results.map(async (rawMembership) => {
const { invitation } = rawMembership;
let rawInvitation = null;
if (invitation) {
try {
rawInvitation = await serverProxy.organizations.invitation(invitation);
// eslint-disable-next-line no-empty
} catch (e) {}
}
return new Membership({
...rawMembership,
invitation: rawInvitation,
});
}));

result.results.count = result.count;
return result.results;
memberships.count = result.count;
return memberships;
},
},
});
Expand Down Expand Up @@ -347,3 +419,16 @@ Object.defineProperties(Organization.prototype.leave, {
},
},
});

Object.defineProperties(Organization.prototype.resendInvitation, {
implementation: {
writable: false,
enumerable: false,
value: async function implementation(key: string) {
checkObjectType('key', key, 'string');
if (typeof this.id === 'number') {
await serverProxy.organizations.resendInvitation(key);
}
},
},
});
41 changes: 40 additions & 1 deletion cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
SerializedLabel, SerializedAnnotationFormats, ProjectsFilter,
SerializedProject, SerializedTask, TasksFilter, SerializedUser, SerializedOrganization,
SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, FunctionsResponseBody,
SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset,
SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, SerializedAcceptInvitation,
} from './server-response-types';
import { SerializedQualityReportData } from './quality-report';
import { SerializedAnalyticsReport } from './analytics-report';
Expand Down Expand Up @@ -456,6 +456,34 @@ async function resetPassword(newPassword1: string, newPassword2: string, uid: st
}
}

async function acceptOrganizationInvitation(
username: string,
firstName: string,
lastName: string,
email: string,
password: string,
confirmations: Record<string, string>,
key: string,
): Promise<SerializedAcceptInvitation> {
let response = null;
let orgSlug = null;
try {
response = await Axios.post(`${config.backendAPI}/invitations/${key}/accept`, {
username,
first_name: firstName,
last_name: lastName,
password1: password,
password2: password,
confirmations,
});
orgSlug = response.data.organization_slug;
} catch (errorData) {
throw generateError(errorData);
}

return orgSlug;
}

async function getSelf(): Promise<SerializedUser> {
const { backendAPI } = config;

Expand Down Expand Up @@ -2083,6 +2111,15 @@ async function inviteOrganizationMembers(orgId, data) {
}
}

async function resendOrganizationInvitation(key) {
const { backendAPI } = config;
try {
await Axios.post(`${backendAPI}/invitations/${key}/resend`);
} catch (errorData) {
throw generateError(errorData);
}
}

async function updateOrganizationMembership(membershipId, data) {
const { backendAPI } = config;
let response = null;
Expand Down Expand Up @@ -2505,8 +2542,10 @@ export default Object.freeze({
invitation: getMembershipInvitation,
delete: deleteOrganization,
invite: inviteOrganizationMembers,
resendInvitation: resendOrganizationInvitation,
updateMembership: updateOrganizationMembership,
deleteMembership: deleteOrganizationMembership,
acceptInvitation: acceptOrganizationInvitation,
}),

webhooks: Object.freeze({
Expand Down
5 changes: 5 additions & 0 deletions cvat-core/src/server-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface SerializedUser {
is_active?: boolean;
last_login?: string;
date_joined?: string;
email_verification_required: boolean;
}

export interface SerializedProject {
Expand Down Expand Up @@ -181,6 +182,10 @@ export interface SerializedRegister {
username: string;
}

export interface SerializedAcceptInvitation {
organization_slug: string;
}

export interface SerializedGuide {
id?: number;
task_id: number | null;
Expand Down
21 changes: 4 additions & 17 deletions cvat-core/src/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,23 @@
//
// SPDX-License-Identifier: MIT

interface RawUserData {
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
groups: string[];
last_login: string;
date_joined: string;
is_staff: boolean;
is_superuser: boolean;
is_active: boolean;
email_verification_required: boolean;
}
import { SerializedUser } from './server-response-types';

export default class User {
public readonly id: number;
public readonly username: string;
public readonly email: string;
public readonly firstName: string;
public readonly lastName: string;
public readonly groups: string[];
public readonly groups: ('user' | 'business' | 'admin')[];
public readonly lastLogin: string;
public readonly dateJoined: string;
public readonly isStaff: boolean;
public readonly isSuperuser: boolean;
public readonly isActive: boolean;
public readonly isVerified: boolean;

constructor(initialData: RawUserData) {
constructor(initialData: SerializedUser) {
const data = {
id: null,
username: null,
Expand Down Expand Up @@ -97,7 +84,7 @@ export default class User {
);
}

serialize(): RawUserData {
serialize(): Partial<SerializedUser> {
return {
id: this.id,
username: this.username,
Expand Down
Loading

0 comments on commit 8b0130f

Please sign in to comment.