diff --git a/package.json b/package.json index ce47b7b8a..139bc3985 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "staart-manager", - "version": "1.0.90", + "version": "1.0.91", "main": "index.js", "repository": "git@github.com:AnandChowdhary/staart.git", "author": "Anand Chowdhary ", @@ -135,5 +135,5 @@ "setup" ], "snyk": true, - "staart-version": "1.0.90" + "staart-version": "1.0.91" } \ No newline at end of file diff --git a/schema.sql b/schema.sql index 118ff32b1..7e055a8e4 100644 --- a/schema.sql +++ b/schema.sql @@ -11,7 +11,7 @@ Target Server Version : 100221 File Encoding : 65001 - Date: 18/07/2019 11:18:22 + Date: 22/07/2019 12:00:53 */ SET NAMES utf8mb4; @@ -62,6 +62,18 @@ CREATE TABLE `staart-backup-codes` ( KEY `id` (`userId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; +-- ---------------------------- +-- Table structure for staart-domains +-- ---------------------------- +DROP TABLE IF EXISTS `staart-domains`; +CREATE TABLE `staart-domains` ( + `id` int(11) NOT NULL, + `organizationId` int(11) NOT NULL, + `domain` varchar(255) COLLATE utf8mb4_bin NOT NULL, + `isVerified` int(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + -- ---------------------------- -- Table structure for staart-emails -- ---------------------------- @@ -91,7 +103,7 @@ CREATE TABLE `staart-events` ( `userAgent` text COLLATE utf8mb4_bin DEFAULT NULL, `createdAt` datetime NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; -- ---------------------------- -- Table structure for staart-memberships @@ -133,7 +145,6 @@ CREATE TABLE `staart-organizations` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `username` varchar(255) COLLATE utf8mb4_bin NOT NULL, - `invitationDomain` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `stripeCustomerId` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `ipRestrictions` text COLLATE utf8mb4_bin DEFAULT NULL, `forceTwoFactor` int(1) NOT NULL DEFAULT 0, diff --git a/src/controllers/organization.ts b/src/controllers/organization.ts index ba93cfbaf..e46924912 100644 --- a/src/controllers/organization.ts +++ b/src/controllers/organization.ts @@ -25,7 +25,12 @@ import { createApiKeyForUser, getOrganizationApiKeyForUser, updateApiKeyForUser, - deleteApiKeyForUser + deleteApiKeyForUser, + getOrganizationDomainsForUser, + createDomainForUser, + getOrganizationDomainForUser, + updateDomainForUser, + deleteDomainForUser } from "../rest/organization"; import { Get, @@ -57,8 +62,7 @@ export class OrganizationController { @Middleware( validator( { - name: Joi.string().required(), - invitationDomain: Joi.string() + name: Joi.string().required() }, "body" ) @@ -88,10 +92,7 @@ export class OrganizationController { name: Joi.string(), username: Joi.string(), forceTwoFactor: Joi.boolean(), - ipRestrictions: Joi.string(), - invitationDomain: Joi.string().regex( - /([a-z])([a-z0-9]+\.)*[a-z0-9]+\.[a-z.]+/ - ) + ipRestrictions: Joi.string() }, "body" ) @@ -634,4 +635,122 @@ export class OrganizationController { ) ); } + + @Get(":id/domains") + async getUserDomains(req: Request, res: Response) { + const id = await organizationUsernameToId(req.params.id); + joiValidate( + { id: [Joi.string().required(), Joi.number().required()] }, + { id } + ); + const domainParams = { ...req.query }; + joiValidate( + { + start: Joi.string(), + itemsPerPage: Joi.number() + }, + domainParams + ); + res.json( + await getOrganizationDomainsForUser( + localsToTokenOrKey(res), + id, + domainParams + ) + ); + } + + @Put(":id/domains") + @Middleware( + validator( + { + domain: Joi.string() + }, + "body" + ) + ) + async putUserDomains(req: Request, res: Response) { + const id = await organizationUsernameToId(req.params.id); + joiValidate( + { id: [Joi.string().required(), Joi.number().required()] }, + { id } + ); + res + .status(CREATED) + .json( + await createDomainForUser( + localsToTokenOrKey(res), + id, + req.body, + res.locals + ) + ); + } + + @Get(":id/domains/:domainId") + async getUserDomain(req: Request, res: Response) { + const id = await organizationUsernameToId(req.params.id); + const domainId = req.params.domainId; + joiValidate( + { + id: [Joi.string().required(), Joi.number().required()], + domainId: Joi.number().required() + }, + { id, domainId } + ); + res.json( + await getOrganizationDomainForUser(localsToTokenOrKey(res), id, domainId) + ); + } + + @Patch(":id/domains/:domainId") + @Middleware( + validator( + { + domain: Joi.string() + }, + "body" + ) + ) + async patchUserDomain(req: Request, res: Response) { + const id = await organizationUsernameToId(req.params.id); + const domainId = req.params.domainId; + joiValidate( + { + id: [Joi.string().required(), Joi.number().required()], + domainId: Joi.number().required() + }, + { id, domainId } + ); + res.json( + await updateDomainForUser( + localsToTokenOrKey(res), + id, + domainId, + req.body, + res.locals + ) + ); + } + + @Delete(":id/domains/:domainId") + async deleteUserDomain(req: Request, res: Response) { + const id = await organizationUsernameToId(req.params.id); + const domainId = req.params.domainId; + joiValidate( + { + id: [Joi.string().required(), Joi.number().required()], + domainId: Joi.number().required() + }, + { id, domainId } + ); + res.json( + await deleteDomainForUser( + localsToTokenOrKey(res), + id, + domainId, + res.locals + ) + ); + } } diff --git a/src/crud/organization.ts b/src/crud/organization.ts index 924fbcaed..86e099910 100644 --- a/src/crud/organization.ts +++ b/src/crud/organization.ts @@ -5,7 +5,7 @@ import { removeReadOnlyValues, tableName } from "../helpers/mysql"; -import { Organization } from "../interfaces/tables/organization"; +import { Organization, Domain } from "../interfaces/tables/organization"; import { capitalizeFirstAndLastLetter, createSlug } from "../helpers/utils"; import { KeyValue } from "../interfaces/general"; import { cachedQuery, deleteItemFromCache } from "../helpers/cache"; @@ -14,6 +14,7 @@ import { ApiKey } from "../interfaces/tables/organization"; import { getPaginatedData } from "./data"; import { apiKeyToken, invalidateToken } from "../helpers/jwt"; import { TOKEN_EXPIRY_API_KEY_MAX } from "../config"; +import { InsertResult } from "../interfaces/mysql"; /* * Create a new organization for a user @@ -200,3 +201,80 @@ export const deleteApiKey = async ( [apiKeyId, organizationId] ); }; + +/** + * Get a list of domains for an organization + */ +export const getOrganizationDomains = async ( + organizationId: number, + query: KeyValue +) => { + return await getPaginatedData({ + table: "domains", + conditions: { + organizationId + }, + ...query + }); +}; + +/** + * Get a domain + */ +export const getDomain = async (organizationId: number, domainId: number) => { + return (( + await query( + `SELECT * FROM ${tableName( + "domains" + )} WHERE id = ? AND organizationId = ? LIMIT 1`, + [domainId, organizationId] + ) + ))[0]; +}; + +/** + * Create a domain + */ +export const createDomain = async (domain: Domain): Promise => { + domain.createdAt = new Date(); + domain.updatedAt = domain.createdAt; + return await query( + `INSERT INTO ${tableName("domains")} ${tableValues(domain)}`, + Object.values(domain) + ); +}; + +/** + * Update a domain + */ +export const updateDomain = async ( + organizationId: number, + domainId: number, + data: KeyValue +) => { + data.updatedAt = new Date(); + data = removeReadOnlyValues(data); + const domain = await getDomain(organizationId, domainId); + return await query( + `UPDATE ${tableName("domains")} SET ${setValues( + data + )} WHERE id = ? AND organizationId = ?`, + [...Object.values(data), domainId, organizationId] + ); +}; + +/** + * Delete a domain + */ +export const deleteDomain = async ( + organizationId: number, + domainId: number +) => { + const currentDomain = await getDomain(organizationId, domainId); + return await query( + `DELETE FROM ${tableName( + "domains" + )} WHERE id = ? AND organizationId = ? LIMIT 1`, + [domainId, organizationId] + ); +}; diff --git a/src/crud/user.ts b/src/crud/user.ts index 2b9cb7dd5..357259466 100644 --- a/src/crud/user.ts +++ b/src/crud/user.ts @@ -5,7 +5,7 @@ import { removeReadOnlyValues, tableName } from "../helpers/mysql"; -import { User, ApprovedLocation } from "../interfaces/tables/user"; +import { User, ApprovedLocation, BackupCode } from "../interfaces/tables/user"; import { capitalizeFirstAndLastLetter, deleteSensitiveInfoUser, @@ -22,7 +22,6 @@ import { getEmail, getVerifiedEmailObject } from "./email"; import { cachedQuery, deleteItemFromCache } from "../helpers/cache"; import md5 from "md5"; import randomInt from "random-int"; -import { BackupCode } from "../interfaces/tables/backup-codes"; /** * Get a list of all ${tableName("users")} diff --git a/src/helpers/mysql.ts b/src/helpers/mysql.ts index 774d20e53..3f5834bde 100644 --- a/src/helpers/mysql.ts +++ b/src/helpers/mysql.ts @@ -7,8 +7,7 @@ import { DB_DATABASE, DB_TABLE_PREFIX } from "../config"; -import { User } from "../interfaces/tables/user"; -import { BackupCode } from "../interfaces/tables/backup-codes"; +import { User, BackupCode } from "../interfaces/tables/user"; import { Email } from "../interfaces/tables/emails"; import { Membership } from "../interfaces/tables/memberships"; import { Organization } from "../interfaces/tables/organization"; @@ -16,6 +15,7 @@ import { Event } from "../interfaces/tables/events"; import { KeyValue } from "../interfaces/general"; import { boolValues, jsonValues, dateValues, readOnlyValues } from "./utils"; import { getUserPrimaryEmailObject } from "../crud/email"; +import { InsertResult } from "../interfaces/mysql"; export const pool = createPool({ host: DB_HOST, @@ -31,7 +31,7 @@ export const pool = createPool({ export const query = ( queryString: string, values?: (string | number | boolean | Date | undefined)[] -) => +): InsertResult | any => new Promise((resolve, reject) => { pool.getConnection((error, connection) => { if (error) return reject(error); diff --git a/src/interfaces/general.ts b/src/interfaces/general.ts index 57237206b..15dc04880 100644 --- a/src/interfaces/general.ts +++ b/src/interfaces/general.ts @@ -13,3 +13,12 @@ export interface Locals { ipAddress: string; referrer?: string; } + +export interface Row { + createdAt?: Date; + updatedAt?: Date; +} + +export interface IdRow extends Row { + id?: number; +} diff --git a/src/interfaces/tables/backup-codes.ts b/src/interfaces/tables/backup-codes.ts deleted file mode 100644 index a8d48aa26..000000000 --- a/src/interfaces/tables/backup-codes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface BackupCode { - code: number; - userId: number; - used?: boolean; - createdAt?: Date; - updatedAt?: Date; -} diff --git a/src/interfaces/tables/organization.ts b/src/interfaces/tables/organization.ts index 12de16ddc..cbc5736fa 100644 --- a/src/interfaces/tables/organization.ts +++ b/src/interfaces/tables/organization.ts @@ -1,17 +1,14 @@ -export interface Organization { - id?: number; +import { IdRow } from "../general"; + +export interface Organization extends IdRow { name?: string; username?: string; forceTwoFactor?: boolean; ipRestrictions?: string; - invitationDomain?: string; stripeCustomerId?: string; - createdAt?: Date; - updatedAt?: Date; } -export interface ApiKey { - id?: number; +export interface ApiKey extends IdRow { name?: string; description?: string; jwtApiKey?: string; @@ -20,6 +17,10 @@ export interface ApiKey { ipRestrictions?: string; referrerRestrictions?: string; expiresAt?: Date; - createdAt?: Date; - updatedAt?: Date; +} + +export interface Domain extends IdRow { + organizationId: number; + domain: string; + isVerified: boolean; } diff --git a/src/interfaces/tables/user.ts b/src/interfaces/tables/user.ts index 8e3c5aa8d..204281978 100644 --- a/src/interfaces/tables/user.ts +++ b/src/interfaces/tables/user.ts @@ -47,3 +47,11 @@ export interface Notification { createdAt?: Date; updatedAt?: Date; } + +export interface BackupCode { + code: number; + userId: number; + used?: boolean; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/src/rest/organization.ts b/src/rest/organization.ts index b02aea958..bd5298139 100644 --- a/src/rest/organization.ts +++ b/src/rest/organization.ts @@ -8,7 +8,12 @@ import { getApiKey, updateApiKey, createApiKey, - deleteApiKey + deleteApiKey, + getOrganizationDomains, + getDomain, + updateDomain, + createDomain, + deleteDomain } from "../crud/organization"; import { InsertResult } from "../interfaces/mysql"; import { @@ -500,3 +505,74 @@ export const deleteApiKeyForUser = async ( } throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); }; + +export const getOrganizationDomainsForUser = async ( + userId: number | ApiKeyResponse, + organizationId: number, + query: KeyValue +) => { + if (await can(userId, Authorizations.READ, "organization", organizationId)) + return await getOrganizationDomains(organizationId, query); + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); +}; + +export const getOrganizationDomainForUser = async ( + userId: number | ApiKeyResponse, + organizationId: number, + domainId: number +) => { + if (await can(userId, Authorizations.READ, "organization", organizationId)) + return await getDomain(organizationId, domainId); + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); +}; + +export const updateDomainForUser = async ( + userId: number | ApiKeyResponse, + organizationId: number, + domainId: number, + data: KeyValue, + locals: Locals +) => { + if ( + await can(userId, Authorizations.UPDATE, "organization", organizationId) + ) { + await updateDomain(organizationId, domainId, data); + return; + } + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); +}; + +export const createDomainForUser = async ( + userId: number | ApiKeyResponse, + organizationId: number, + domain: KeyValue, + locals: Locals +) => { + if ( + await can(userId, Authorizations.CREATE, "organization", organizationId) + ) { + const key = await createDomain({ + domain: "", + organizationId, + ...domain, + isVerified: false + }); + return; + } + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); +}; + +export const deleteDomainForUser = async ( + userId: number | ApiKeyResponse, + organizationId: number, + domainId: number, + locals: Locals +) => { + if ( + await can(userId, Authorizations.DELETE, "organization", organizationId) + ) { + await deleteDomain(organizationId, domainId); + return; + } + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); +};