diff --git a/app/client/ui/BillingPage.ts b/app/client/ui/BillingPage.ts index 68bd1c23..225bc0cb 100644 --- a/app/client/ui/BillingPage.ts +++ b/app/client/ui/BillingPage.ts @@ -174,7 +174,7 @@ export class BillingPage extends Disposable { ] : null, moneyPlan.amount ? [ makeSummaryFeature([`Your team site has `, `${sub.userCount}`, - ` member${sub.userCount > 1 ? 's' : ''}`]), + ` member${sub.userCount !== 1 ? 's' : ''}`]), tier ? this.buildAppSumoPlanNotes(discountName!) : null, // Currently the subtotal is misleading and scary when tiers are in effect. // In this case, for now, just report what will be invoiced. diff --git a/app/common/UserAPI.ts b/app/common/UserAPI.ts index 50840d04..00cde1e8 100644 --- a/app/common/UserAPI.ts +++ b/app/common/UserAPI.ts @@ -135,6 +135,9 @@ export interface Document extends DocumentProperties { export interface UserOptions { // Whether signing in with Google is allowed. Defaults to true if unset. allowGoogleLogin?: boolean; + // Whether user is a consultant. Consultant users can be added to sites + // without being counted for billing. Defaults to false if unset. + isConsultant?: boolean; } export interface PermissionDelta { @@ -312,6 +315,7 @@ export interface UserAPI { getUserProfile(): Promise; updateUserName(name: string): Promise; updateAllowGoogleLogin(allowGoogleLogin: boolean): Promise; + updateIsConsultant(userId: number, isConsultant: boolean): Promise; getWorker(key: string): Promise; getWorkerAPI(key: string): Promise; getBillingAPI(): BillingAPI; @@ -608,6 +612,13 @@ export class UserAPIImpl extends BaseAPI implements UserAPI { }); } + public async updateIsConsultant(userId: number, isConsultant: boolean): Promise { + await this.request(`${this._url}/api/profile/isConsultant`, { + method: 'POST', + body: JSON.stringify({userId, isConsultant}) + }); + } + public async getWorker(key: string): Promise { const json = await this.requestJson(`${this._url}/api/worker/${key}`, { method: 'GET', diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index b51253f6..0ad019b5 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -379,6 +379,25 @@ export class ApiServer { res.sendStatus(200); })); + this._app.post('/api/profile/isConsultant', expressWrap(async (req, res) => { + const userId = getAuthorizedUserId(req); + if (userId !== this._dbManager.getSupportUserId()) { + throw new ApiError('Only support user can enable/disable isConsultant', 401); + } + const isConsultant: boolean | undefined = req.body.isConsultant; + const targetUserId: number | undefined = req.body.userId; + if (isConsultant === undefined) { + throw new ApiError('Missing body param: isConsultant', 400); + } + if (targetUserId === undefined) { + throw new ApiError('Missing body param: targetUserId', 400); + } + await this._dbManager.updateUserOptions(targetUserId, { + isConsultant + }); + res.sendStatus(200); + })); + // GET /api/profile/apikey // Get user's apiKey this._app.get('/api/profile/apikey', expressWrap(async (req, res) => { diff --git a/app/gen-server/lib/HomeDBManager.ts b/app/gen-server/lib/HomeDBManager.ts index db681d6e..871efbda 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -4185,7 +4185,7 @@ function getFrom(queryBuilder: SelectQueryBuilder): string { } // Flatten a map of users per role into a simple list of users. -function removeRole(usersWithRoles: Map) { +export function removeRole(usersWithRoles: Map) { return flatten([...usersWithRoles.values()]); }